mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
[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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user