[fix] prevent duplicate verification emails on page refresh (#6167)

## Summary
- Fixed duplicate verification email issue where emails were sent every
time users returned to the root page
- Emails are now only sent automatically when coming from signup/login
flow
- Added proper toast notifications to cloud onboarding pages

## Changes
- **Conditional email sending**: Only send verification email when
`fromAuth=true` query parameter is present (from signup/login flow)
- **Auto-cleanup**: Remove `fromAuth` parameter after sending email to
prevent re-sending on page refresh
- **Toast system fix**: 
- Added `GlobalToast` component to `CloudLayoutView` for proper toast
display in onboarding pages
  - Migrated from PrimeVue `useToast()` to ComfyUI's `useToastStore()`
- **UI improvements**:
  - Better spacing and layout for email verification page
  - Added multiline support for tips and instructions
  - Improved toast messages with clearer titles and summaries

## Problem it solves
Previously, when users signed up and received a verification email,
every time they navigated back to the root page (`/`), the router guard
would redirect them to the email verification page which would
automatically send another email. This caused multiple emails to be
sent, often ending up in spam folders.

## Test plan
- [x] Sign up for a new account → Should receive ONE verification email
- [x] Navigate away and back to root → Should NOT receive another email
- [x] Click "Resend email" button → Should receive a new email
- [x] Refresh the verification page → Should NOT receive another email
- [x] Toast notifications appear correctly in all auth flows

[screen-capture
(1).webm](https://github.com/user-attachments/assets/25ffad94-d129-4051-b29e-5bdec696cd11)
This commit is contained in:
Jin Yi
2025-10-21 03:31:29 +09:00
committed by GitHub
parent fd2a52500c
commit 3a5ed57f50
6 changed files with 71 additions and 18 deletions

View File

@@ -2155,11 +2155,13 @@
"cloudVerifyEmail_failed_toast_message": "Failed to send verification email. Please contact support.",
"cloudVerifyEmail_title": "Check your email",
"cloudVerifyEmail_back": "Back",
"cloudVerifyEmail_sent": "We've sent a verification link to:",
"cloudVerifyEmail_clickToContinue": "Click the link in that email to automatically continue onto the next steps.",
"cloudVerifyEmail_sent": "A verification link was sent to:",
"cloudVerifyEmail_clickToContinue": "Click the link in that email to automatically\ncontinue onto the next steps.",
"cloudVerifyEmail_tip": "Tip: Dont forget to check your spam folder\nif you dont see it.",
"cloudVerifyEmail_didntReceive": "Didn't receive the email?",
"cloudVerifyEmail_resend": "Resend email",
"cloudVerifyEmail_toast_success": "Verification email has been sent to {email}.",
"cloudVerifyEmail_toast_title": "Email sent",
"cloudVerifyEmail_toast_summary": "Check your inbox for a new verification email.",
"cloudVerifyEmail_toast_failed": "Failed to send verification email. Please try again.",
"cloudInvite_title": "YOU'RE INVITED",
"cloudInvite_subtitle": "This invite can only be used once. Double check youre signed into the account you want to use.",

View File

@@ -112,6 +112,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import CloudSignInForm from '@/platform/onboarding/cloud/components/CloudSignInForm.vue'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { SignInData } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { translateAuthError } from '@/utils/authErrorTranslation'
@@ -122,6 +123,7 @@ const route = useRoute()
const authActions = useFirebaseAuthActions()
const isSecureContext = window.isSecureContext
const authError = ref('')
const toastStore = useToastStore()
const hasInviteCode = computed(() => !!route.query.inviteCode)
@@ -130,6 +132,11 @@ const navigateToSignup = () => {
}
const onSuccess = async () => {
toastStore.add({
severity: 'success',
summary: 'Login Completed',
life: 2000
})
// Check if there's an invite code
const inviteCode = route.query.inviteCode as string | undefined
const { isEmailVerified } = useFirebaseAuthStore()

View File

@@ -102,7 +102,9 @@ import SignUpForm from '@/components/dialog/content/signin/SignUpForm.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type { SignUpData } from '@/schemas/signInSchema'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import { translateAuthError } from '@/utils/authErrorTranslation'
import { isInChina } from '@/utils/networkUtil'
@@ -113,14 +115,32 @@ const authActions = useFirebaseAuthActions()
const isSecureContext = window.isSecureContext
const authError = ref('')
const userIsInChina = ref(false)
const toastStore = useToastStore()
const navigateToLogin = () => {
void router.push({ name: 'cloud-login', query: route.query })
}
const onSuccess = async () => {
// The invite code will be handled after the user is logged in
await router.push({ path: '/', query: route.query })
toastStore.add({
severity: 'success',
summary: 'Sign up Completed',
life: 2000
})
// Check if email verification is needed
const { isEmailVerified } = useFirebaseAuthStore()
const inviteCode = route.query.inviteCode as string | undefined
if (!isEmailVerified) {
// Redirect to email verification with fromAuth flag
await router.push({
name: 'cloud-verify-email',
query: { inviteCode, fromAuth: 'true' }
})
} else {
// The invite code will be handled after the user is logged in
await router.push({ path: '/', query: route.query })
}
}
// Custom error handler for inline display

View File

@@ -16,17 +16,24 @@
</h1>
<!-- Body copy -->
<p class="text-foreground/80 mt-6 text-base">
<p class="text-foreground/80 mt-6 mb-0 text-base">
{{ t('cloudVerifyEmail_sent') }}
</p>
<p class="mt-3 text-base font-medium">{{ authStore.userEmail }}</p>
<p class="mt-2 text-base font-medium">{{ authStore.userEmail }}</p>
<p class="text-foreground/80 mt-6 text-base">
<p class="text-foreground/80 mt-6 text-base whitespace-pre-line">
{{ t('cloudVerifyEmail_clickToContinue') }}
</p>
<p class="text-foreground/80 mt-10 text-base">
<p class="text-foreground/80 mt-6 text-base whitespace-pre-line">
{{ t('cloudVerifyEmail_tip') }}
</p>
<p class="text-foreground/80 mt-6 mb-0 text-base">
{{ t('cloudVerifyEmail_didntReceive') }}
</p>
<p class="text-foreground/80 mt-1 text-base">
<span class="cursor-pointer text-blue-400 no-underline" @click="onSend">
{{ t('cloudVerifyEmail_resend') }}</span
>
@@ -51,6 +58,7 @@ const auth = useFirebaseAuth()!
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const toastStore = useToastStore()
let intervalId: number | null = null
let timeoutId: number | null = null
@@ -114,16 +122,17 @@ async function onSend() {
useTelemetry()?.trackEmailVerification('requested')
}
useToastStore().add({
severity: 'success',
summary: t('cloudVerifyEmail_toast_success', {
email: authStore.userEmail
})
toastStore.add({
severity: 'info',
summary: t('cloudVerifyEmail_toast_title'),
detail: t('cloudVerifyEmail_toast_summary'),
life: 2000
})
} catch (e) {
useToastStore().add({
toastStore.add({
severity: 'error',
summary: t('cloudVerifyEmail_toast_failed')
summary: t('cloudVerifyEmail_toast_failed'),
life: 2000
})
}
}
@@ -140,8 +149,18 @@ onMounted(async () => {
return redirectToNextStep()
}
// Send initial verification email
await onSend()
// Only send verification email automatically if coming from signup/login flow
// Check if 'fromAuth' query parameter is present
const fromAuth = route.query.fromAuth === 'true'
if (fromAuth) {
await onSend()
// Remove fromAuth query parameter after sending email to prevent re-sending on refresh
const { fromAuth: _, ...remainingQuery } = route.query
await router.replace({
name: route.name as string,
query: remainingQuery
})
}
// Start polling to check email verification status
intervalId = window.setInterval(async () => {

View File

@@ -3,10 +3,14 @@
<!-- This will render the nested route components -->
<RouterView />
</CloudTemplate>
<!-- Global Toast for displaying notifications -->
<GlobalToast />
</template>
<script setup lang="ts">
import { RouterView } from 'vue-router'
import GlobalToast from '@/components/toast/GlobalToast.vue'
import CloudTemplate from './CloudTemplate.vue'
</script>

View File

@@ -178,6 +178,7 @@ router.beforeEach(async (to, _from, next) => {
// Check email verification first
const authStore = useFirebaseAuthStore()
if (!authStore.isEmailVerified) {
// Don't pass fromAuth here since this is from root navigation, not auth flow
return next({ name: 'cloud-verify-email' })
}