mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
feat: add error handling and timeout recovery to cloud onboarding (#5573)
Implements robust error handling and authentication timeout recovery for the cloud onboarding flow: - Enhanced UserCheckView with VueUse useAsyncState for declarative error handling - Added parallel API calls for better performance using Promise.all - Implemented loading states with skeleton views and user-friendly error messages - Added authentication timeout handling (16s) with recovery options - Created CloudAuthTimeoutView with "Sign Out & Try Again" functionality - Added comprehensive i18n support for error states
This commit is contained in:
@@ -1806,6 +1806,14 @@
|
||||
"learnAboutButton": "Learn about Cloud",
|
||||
"wantToRun": "Want to run comfyUI locally instead?",
|
||||
"download": "Download ComfyUI"
|
||||
},
|
||||
"checkingStatus": "Checking your account status...",
|
||||
"retrying": "Retrying...",
|
||||
"retry": "Try Again",
|
||||
"authTimeout": {
|
||||
"title": "Connection Taking Too Long",
|
||||
"message": "We're having trouble connecting to ComfyUI Cloud. This could be due to a slow connection or temporary service issue.",
|
||||
"restart": "Sign Out & Try Again"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,12 @@ export const cloudOnboardingRoutes: RouteRecordRaw[] = [
|
||||
name: 'cloud-sorry-contact-support',
|
||||
component: () =>
|
||||
import('@/platform/onboarding/cloud/CloudSorryContactSupportView.vue')
|
||||
},
|
||||
{
|
||||
path: 'auth-timeout',
|
||||
name: 'cloud-auth-timeout',
|
||||
component: () =>
|
||||
import('@/platform/onboarding/cloud/CloudAuthTimeoutView.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
35
src/platform/onboarding/cloud/CloudAuthTimeoutView.vue
Normal file
35
src/platform/onboarding/cloud/CloudAuthTimeoutView.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center p-8">
|
||||
<div class="w-96 text-center">
|
||||
<h2 class="text-xl mb-4">
|
||||
{{ $t('cloudOnboarding.authTimeout.title') }}
|
||||
</h2>
|
||||
<p class="mb-6 text-gray-600">
|
||||
{{ $t('cloudOnboarding.authTimeout.message') }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<Button
|
||||
:label="$t('cloudOnboarding.authTimeout.restart')"
|
||||
class="w-full"
|
||||
@click="handleRestart"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
|
||||
const router = useRouter()
|
||||
const { logout } = useFirebaseAuthActions()
|
||||
|
||||
const handleRestart = async () => {
|
||||
await logout()
|
||||
await router.replace({ name: 'cloud-login' })
|
||||
}
|
||||
</script>
|
||||
@@ -2,67 +2,103 @@
|
||||
<CloudLoginViewSkeleton v-if="skeletonType === 'login'" />
|
||||
<CloudSurveyViewSkeleton v-else-if="skeletonType === 'survey'" />
|
||||
<CloudWaitlistViewSkeleton v-else-if="skeletonType === 'waitlist'" />
|
||||
<div v-else-if="error" class="h-full flex items-center justify-center p-8">
|
||||
<div class="w-96 p-2 text-center">
|
||||
<p class="text-red-500 mb-4">{{ errorMessage }}</p>
|
||||
<Button
|
||||
:label="
|
||||
isRetrying
|
||||
? $t('cloudOnboarding.retrying')
|
||||
: $t('cloudOnboarding.retry')
|
||||
"
|
||||
:loading="isRetrying"
|
||||
class="w-full"
|
||||
@click="handleRetry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex items-center justify-center min-h-screen">
|
||||
<div class="animate-pulse text-gray-500">{{ $t('g.loading') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { useAsyncState } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { getSurveyCompletedStatus, getUserCloudStatus } from '@/api/auth'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
|
||||
import CloudLoginViewSkeleton from './skeletons/CloudLoginViewSkeleton.vue'
|
||||
import CloudSurveyViewSkeleton from './skeletons/CloudSurveyViewSkeleton.vue'
|
||||
import CloudWaitlistViewSkeleton from './skeletons/CloudWaitlistViewSkeleton.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const isNavigating = ref(false)
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const skeletonType = ref<'login' | 'survey' | 'waitlist' | 'loading'>('loading')
|
||||
|
||||
onMounted(async () => {
|
||||
// Prevent multiple executions
|
||||
if (isNavigating.value) {
|
||||
return
|
||||
}
|
||||
isNavigating.value = true
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
execute: checkUserStatus
|
||||
} = useAsyncState(
|
||||
wrapWithErrorHandlingAsync(async () => {
|
||||
await nextTick()
|
||||
|
||||
// Wait for next tick to ensure component is fully mounted
|
||||
await nextTick()
|
||||
|
||||
try {
|
||||
const cloudUserStats = await getUserCloudStatus()
|
||||
const [cloudUserStats, surveyStatus] = await Promise.all([
|
||||
getUserCloudStatus(),
|
||||
getSurveyCompletedStatus()
|
||||
])
|
||||
|
||||
// Navigate based on user status
|
||||
if (!cloudUserStats) {
|
||||
skeletonType.value = 'login'
|
||||
await router.replace({ name: 'cloud-login' })
|
||||
return
|
||||
}
|
||||
|
||||
// We know user exists, now check survey status - show survey skeleton while loading
|
||||
skeletonType.value = 'survey'
|
||||
const surveyStatus = await getSurveyCompletedStatus()
|
||||
|
||||
// Check onboarding status and redirect accordingly
|
||||
if (!surveyStatus) {
|
||||
// User hasn't completed survey
|
||||
skeletonType.value = 'survey'
|
||||
await router.replace({ name: 'cloud-survey' })
|
||||
} else {
|
||||
// Survey is done, now check if waitlisted - show waitlist skeleton while loading
|
||||
skeletonType.value = 'waitlist'
|
||||
if (cloudUserStats.status !== 'active') {
|
||||
// User completed survey but not whitelisted
|
||||
await router.replace({ name: 'cloud-waitlist' })
|
||||
} else {
|
||||
// User is fully onboarded - just reload the page to bypass router issues
|
||||
window.location.href = '/'
|
||||
}
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
// On error, fallback to page reload
|
||||
skeletonType.value = 'login'
|
||||
await router.push({ name: 'cloud-login' })
|
||||
|
||||
if (cloudUserStats.status !== 'active') {
|
||||
skeletonType.value = 'waitlist'
|
||||
await router.replace({ name: 'cloud-waitlist' })
|
||||
return
|
||||
}
|
||||
|
||||
// User is fully onboarded
|
||||
window.location.href = '/'
|
||||
}),
|
||||
null,
|
||||
{ resetOnExecute: false }
|
||||
)
|
||||
|
||||
const errorMessage = computed(() => {
|
||||
if (!error.value) return ''
|
||||
|
||||
// Provide user-friendly error messages
|
||||
const errorStr = error.value.toString().toLowerCase()
|
||||
|
||||
if (errorStr.includes('network') || errorStr.includes('fetch')) {
|
||||
return 'Connection problem. Please check your internet connection.'
|
||||
}
|
||||
|
||||
if (errorStr.includes('timeout')) {
|
||||
return 'Request timed out. Please try again.'
|
||||
}
|
||||
|
||||
return 'Unable to check account status. Please try again.'
|
||||
})
|
||||
|
||||
const isRetrying = computed(() => isLoading.value && !!error.value)
|
||||
|
||||
const handleRetry = async () => {
|
||||
await checkUserStatus()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -181,16 +181,28 @@ const router = createRouter({
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
|
||||
// Wait for Firebase auth to initialize
|
||||
// Wait for Firebase auth to initialize with timeout
|
||||
if (!authStore.isInitialized) {
|
||||
await new Promise<void>((resolve) => {
|
||||
const unwatch = authStore.$subscribe((_, state) => {
|
||||
if (state.isInitialized) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
unwatch()
|
||||
resolve()
|
||||
}
|
||||
reject(new Error('Authentication initialization timeout'))
|
||||
}, 16000) // 16 second timeout
|
||||
|
||||
const unwatch = authStore.$subscribe((_, state) => {
|
||||
if (state.isInitialized) {
|
||||
clearTimeout(timeout)
|
||||
unwatch()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Auth initialization failed:', error)
|
||||
// Navigate to auth timeout recovery page
|
||||
return next({ name: 'cloud-auth-timeout' })
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
|
||||
Reference in New Issue
Block a user