diff --git a/src/api/auth.ts b/src/api/auth.ts index 2eafdd0cf..ce0aa9104 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -88,10 +88,10 @@ export async function getUserCloudStatus(): Promise { export async function getInviteCodeStatus( inviteCode: string -): Promise<{ expired: boolean }> { +): Promise<{ claimed: boolean; expired: boolean }> { try { const response = await api.fetchApi( - `/invite/${encodeURIComponent(inviteCode)}/status`, + `/invite_code/${encodeURIComponent(inviteCode)}/status`, { method: 'GET', headers: { @@ -105,22 +105,22 @@ export async function getInviteCodeStatus( ) captureApiError( error, - '/invite/{code}/status', + '/invite_code/{code}/status', 'http_error', response.status, undefined, { api: { method: 'GET', - endpoint: `/invite/${inviteCode}/status`, + endpoint: `/invite_code/${inviteCode}/status`, status_code: response.status, status_text: response.statusText }, extra: { invite_code_length: inviteCode.length }, - route_template: '/invite/{code}/status', - route_actual: `/invite/${inviteCode}/status` + route_template: '/invite_code/{code}/status', + route_actual: `/invite_code/${inviteCode}/status` } ) throw error @@ -132,13 +132,13 @@ export async function getInviteCodeStatus( if (!isHttpError(error, 'Failed to get invite code status:')) { captureApiError( error as Error, - '/invite/{code}/status', + '/invite_code/{code}/status', 'network_error', undefined, undefined, { - route_template: '/invite/{code}/status', - route_actual: `/invite/${inviteCode}/status` + route_template: '/invite_code/{code}/status', + route_actual: `/invite_code/${inviteCode}/status` } ) } @@ -293,7 +293,9 @@ export async function submitSurvey( } } -export async function claimInvite(code: string): Promise { +export async function claimInvite( + code: string +): Promise<{ success: boolean; message: string }> { try { Sentry.addBreadcrumb({ category: 'auth', @@ -305,7 +307,7 @@ export async function claimInvite(code: string): Promise { }) const res = await api.fetchApi( - `/invite/${encodeURIComponent(code)}/claim`, + `/invite_code/${encodeURIComponent(code)}/claim`, { method: 'POST' } @@ -317,7 +319,7 @@ export async function claimInvite(code: string): Promise { ) captureApiError( error, - '/invite/{code}/claim', + '/invite_code/{code}/claim', 'http_error', res.status, 'claim_invite', @@ -327,8 +329,8 @@ export async function claimInvite(code: string): Promise { status_code: res.status, status_text: res.statusText }, - route_template: '/invite/{code}/claim', - route_actual: `/invite/${encodeURIComponent(code)}/claim` + route_template: '/invite_code/{code}/claim', + route_actual: `/invite_code/${encodeURIComponent(code)}/claim` } ) throw error @@ -340,18 +342,20 @@ export async function claimInvite(code: string): Promise { message: 'Invite claimed successfully', level: 'info' }) + + return res.json() } catch (error) { // Only capture network errors (not HTTP errors we already captured) if (!isHttpError(error, 'Failed to claim invite:')) { captureApiError( error as Error, - '/invite/{code}/claim', + '/invite_code/{code}/claim', 'network_error', undefined, 'claim_invite', { - route_template: '/invite/{code}/claim', - route_actual: `/invite/${encodeURIComponent(code)}/claim` + route_template: '/invite_code/{code}/claim', + route_actual: `/invite_code/${encodeURIComponent(code)}/claim` } ) } diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 9d5f1ae84..5504948f8 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1827,6 +1827,10 @@ "cloudStart_learnAboutButton": "Learn about Cloud", "cloudStart_wantToRun": "Want to run comfyUI locally instead?", "cloudStart_download": "Download ComfyUI", + "cloudStart_invited": "YOU'RE INVITED", + "cloudStart_invited_signin": "Sign in to continue onto Cloud.", + "cloudStart_invited_signup_title": "Donโ€™t have an account yet?", + "cloudStart_invited_signup_description": "Sign up instead", "cloudWaitlist_titleLine1": "YOU'RE ON THE", "cloudWaitlist_titleLine2": "WAITLIST ๐ŸŽ‰", "cloudWaitlist_message": "You have been added to the waitlist. We will notify you when access is available.", @@ -1835,7 +1839,6 @@ "cloudClaimInvite_processingTitle": "Processing Invite Code...", "cloudClaimInvite_claimButton": "Claim Invite", "cloudSorryContactSupport_title": "Sorry, contact support", - "cloudVerifyEmail_title": "Email Verification", "cloudPrivateBeta_title": "Cloud is currently in private beta", "cloudPrivateBeta_desc": "Sign in to join the waitlist. We'll notify you when it's your turn. Already been notified? Sign in start using Cloud.", "cloudForgotPassword_title": "Forgot Password", @@ -1851,5 +1854,28 @@ "cloudSurvey_steps_familiarity": "How familiar are you with ComfyUI?", "cloudSurvey_steps_purpose": "What will you primarily use ComfyUI for?", "cloudSurvey_steps_industry": "What's your primary industry?", - "cloudSurvey_steps_making": "What do you plan on making?" + "cloudSurvey_steps_making": "What do you plan on making?", + "cloudVerifyEmail_toast_message": "We've sent a verification email to {email}. Please check your inbox and click the link to verify your email address.", + "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_didntReceive": "Didn't receive the email?", + "cloudVerifyEmail_resend": "Resend email", + "cloudVerifyEmail_toast_success": "Verification email has been sent to {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 youโ€™re signed into the account you want to use.", + "cloudInvite_switchAccounts": "Switch accounts", + "cloudInvite_signedInAs": "Signed in as:", + "cloudInvite_acceptButton": "Accept invite", + "cloudInvite_placeholderEmail": "email@email.com", + "cloudInvite_processing": "Processing...", + "cloudInvite_alreadyClaimed_prefix": "It looks like this invite has already been claimed by", + "cloudInvite_expired_prefix": "It looks like this invite is expired.", + "cloudInvite_unknownEmail": "this account", + "cloudInvite_expired": "This invite has expired.", + "cloudInvite_contactLink": "Contact us here", + "cloudInvite_contactLink_suffix": "for questions." } diff --git a/src/locales/ko/main.json b/src/locales/ko/main.json index 29f5135fc..039538509 100644 --- a/src/locales/ko/main.json +++ b/src/locales/ko/main.json @@ -1709,23 +1709,23 @@ "zoomToFit": "ํ™”๋ฉด์— ๋งž๊ฒŒ ํ™•๋Œ€" }, "cloudFooter_needHelp": "๋„์›€์ด ํ•„์š”ํ•˜์‹ ๊ฐ€์š”๏ผŸ", - "cloudStart_title": "๋ฐ”๋กœ ์ฐฝ์ž‘, ๋‹จ ๋ช‡ ์ดˆ๋ฉด ์ถฉ๋ถ„", - "cloudStart_desc": "์„ค์ • ๋ถˆํ•„์š”. ๋ชจ๋“  ๊ธฐ๊ธฐ์—์„œ ์ž‘๋™.", - "cloudStart_explain": "๋‹ค์ˆ˜์˜ ๊ฒฐ๊ณผ๋ฅผ ๋™์‹œ ์ƒ์„ฑ. ํ”„๋กœ์ ํŠธ๋ฅผ ์‰ฝ๊ฒŒ ๊ณต์œ .", + "cloudStart_title": "์ฐฝ์ž‘์€ ๋‹จ ๋ช‡ ์ดˆ๋ฉด ์ถฉ๋ถ„ํ•ด์š”", + "cloudStart_desc": "์„ค์ •์—†์ด๋„, ๊ธฐ๊ธฐ์˜ ์ œ์•ฝ์—†์ด", + "cloudStart_explain": "ํ•œ๋ฒˆ์— ๋‹ค์ˆ˜์˜ ๊ฒฐ๊ณผ๋ฌผ์„, ์›Œํฌํ”Œ๋กœ์šฐ๋กœ ์‰ฝ๊ฒŒ ๊ณต์œ ํ•ด์š”.", "cloudStart_learnAboutButton": "Cloud์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ธฐ", - "cloudStart_wantToRun": "๋Œ€์‹  ComfyUI๋ฅผ ๋กœ์ปฌ์—์„œ ์‹คํ–‰ํ•˜๊ณ  ์‹ถ์œผ์‹ ๊ฐ€์š”๏ผŸ", + "cloudStart_wantToRun": "ComfyUI๋ฅผ ๋กœ์ปฌ์—์„œ ์‹คํ–‰ํ•ด๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด๏ผŸ", "cloudStart_download": "ComfyUI ๋‹ค์šด๋กœ๋“œ", - "cloudWaitlist_titleLine1": "๋‹น์‹ ์€", - "cloudWaitlist_titleLine2": "๋Œ€๊ธฐ ๋ชฉ๋ก์— ์žˆ์Šต๋‹ˆ๋‹ค ๐ŸŽ‰", - "cloudWaitlist_message": "๋Œ€๊ธฐ ๋ชฉ๋ก์— ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฒ ํƒ€ ์•ก์„ธ์Šค๊ฐ€ ๊ฐ€๋Šฅํ•  ๋•Œ ์•Œ๋ ค๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.", - "cloudWaitlist_questionsText": "๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ์‹ ๊ฐ€์š”? ๋ฌธ์˜ํ•˜๊ธฐ", - "cloudWaitlist_contactLink": "์—ฌ๊ธฐ", - "cloudClaimInvite_processingTitle": "์ดˆ๋Œ€ ์ฝ”๋“œ ์ฒ˜๋ฆฌ ์ค‘...", - "cloudClaimInvite_claimButton": "์ดˆ๋Œ€ ์‹ ์ฒญ", - "cloudSorryContactSupport_title": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค, ์ง€์›ํŒ€์— ๋ฌธ์˜ํ•˜์„ธ์š”", + "cloudWaitlist_titleLine1": "๋ฐฉ๊ธˆ", + "cloudWaitlist_titleLine2": "๋Œ€๊ธฐ์ž ๋ช…๋‹จ์— ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค ๐ŸŽ‰", + "cloudWaitlist_message": "๋Œ€๊ธฐ์ž ๋ช…๋‹จ์— ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฒ ํƒ€ ๋ฒ„์ „์ด ์˜คํ”ˆ๋˜๋ฉด ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”.", + "cloudWaitlist_questionsText": "๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์œผ์‹ ๊ฐ€์š”? ๋ฌธ์˜๋Š”", + "cloudWaitlist_contactLink": "์—ฌ๊ธฐ๋กœ", + "cloudClaimInvite_processingTitle": "์ดˆ๋Œ€ ์ฝ”๋“œ ํ™•์ธ์ค‘...", + "cloudClaimInvite_claimButton": "์ดˆ๋Œ€ ์š”์ฒญํ•˜๊ธฐ", + "cloudSorryContactSupport_title": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค, ์ง€์›ํŒ€์— ๋ฌธ์˜ํ•ด์ฃผ์„ธ์š”", "cloudVerifyEmail_title": "์ด๋ฉ”์ผ ํ™•์ธ", - "cloudPrivateBeta_title": "Cloud๋Š” ํ˜„์žฌ ๋น„๊ณต๊ฐœ ๋ฒ ํƒ€์ž…๋‹ˆ๋‹ค", - "cloudPrivateBeta_desc": "๋กœ๊ทธ์ธํ•˜์—ฌ ๋Œ€๊ธฐ ๋ชฉ๋ก์— ๋“ฑ๋กํ•˜์„ธ์š”. ๋ฒ ํƒ€ ์•ก์„ธ์Šค๊ฐ€ ๊ฐ€๋Šฅํ•  ๋•Œ ์•Œ๋ ค๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ์•Œ๋ฆผ์„ ๋ฐ›์œผ์…จ๋‚˜์š”? ๋กœ๊ทธ์ธํ•˜์—ฌ Comfy Cloud๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”.", + "cloudPrivateBeta_title": "Cloud๋Š” ํ˜„์žฌ ๋น„๊ณต๊ฐœ ๋ฒ ํƒ€ ๋ฒ„์ „์ž…๋‹ˆ๋‹ค", + "cloudPrivateBeta_desc": "๋กœ๊ทธ์ธํ•˜์—ฌ ๋Œ€๊ธฐ์ž ๋ช…๋‹จ์— ๋“ฑ๋กํ•˜์„ธ์š”. ๋ฒ ํƒ€ ๋ฒ„์ „์ด ์˜คํ”ˆ๋  ๋•Œ ์•Œ๋ ค๋“œ๋ฆด๊ฒŒ์š”. ์ด๋ฏธ ์•Œ๋ฆผ์„ ๋ฐ›์œผ์…จ๋‹ค๋ฉด? ๋กœ๊ทธ์ธํ•˜์—ฌ Comfy Cloud๋ฅผ ์‹œ์ž‘ํ•ด๋ณด์„ธ์š”.", "cloudForgotPassword_title": "๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ", "cloudForgotPassword_instructions": "์ด๋ฉ”์ผ ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์‹œ๋ฉด ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๋งํฌ๋ฅผ ๋ณด๋‚ด๋“œ๋ฆฌ๊ฒ ์Šต๋‹ˆ๋‹ค.", "cloudForgotPassword_emailLabel": "์ด๋ฉ”์ผ", @@ -1736,8 +1736,8 @@ "cloudForgotPassword_emailRequired": "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค", "cloudForgotPassword_passwordResetSent": "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •์ด ์ „์†ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค", "cloudForgotPassword_passwordResetError": "๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ด๋ฉ”์ผ ์ „์†ก์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค", - "cloudSurvey_steps_familiarity": "ComfyUI ๊ฒฝํ—˜ ์ˆ˜์ค€์€ ์–ด๋–ป๊ฒŒ ๋˜์‹œ๋‚˜์š”?", - "cloudSurvey_steps_purpose": "ComfyUI์˜ ์ฃผ์š” ์šฉ๋„๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”?", - "cloudSurvey_steps_industry": "์ฃผ์š” ์—…๊ณ„๋Š” ๋ฌด์—‡์ธ๊ฐ€์š”๏ผŸ", + "cloudSurvey_steps_familiarity": "ComfyUI ๊ฒฝํ—˜๋„๋Š” ์–ด๋–ป๊ฒŒ ๋˜์‹œ๋‚˜์š”?", + "cloudSurvey_steps_purpose": "ComfyUI์˜ ์ฃผ๋œ ๋ชฉ์ ์€ ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?", + "cloudSurvey_steps_industry": "์–ด๋–ค ์—…๊ณ„/์—…์ข…์— ๊ทผ๋ฌดํ•˜์‹œ๋‚˜์š”๏ผŸ", "cloudSurvey_steps_making": "์–ด๋–ค ์ข…๋ฅ˜์˜ ์ฝ˜ํ…์ธ ๋ฅผ ๋งŒ๋“ค ๊ณ„ํš์ด์‹ ๊ฐ€์š”?" } diff --git a/src/onboardingCloudRoutes.ts b/src/onboardingCloudRoutes.ts index a1111767a..77ed5428b 100644 --- a/src/onboardingCloudRoutes.ts +++ b/src/onboardingCloudRoutes.ts @@ -45,6 +45,13 @@ export const cloudOnboardingRoutes: RouteRecordRaw[] = [ import('@/platform/onboarding/cloud/UserCheckView.vue'), meta: { requiresAuth: true } }, + { + path: 'code/:code', + name: 'cloud-invite-code', + component: () => + import('@/platform/onboarding/cloud/CloudInviteEntryView.vue'), + meta: { requiresAuth: true } + }, { path: 'invite-check', name: 'cloud-invite-check', diff --git a/src/platform/onboarding/cloud/CloudClaimInviteView.vue b/src/platform/onboarding/cloud/CloudClaimInviteView.vue index d9fc45b07..52374a42f 100644 --- a/src/platform/onboarding/cloud/CloudClaimInviteView.vue +++ b/src/platform/onboarding/cloud/CloudClaimInviteView.vue @@ -1,27 +1,159 @@ diff --git a/src/platform/onboarding/cloud/CloudInviteEntryView.vue b/src/platform/onboarding/cloud/CloudInviteEntryView.vue index 5cd8131b4..e66620300 100644 --- a/src/platform/onboarding/cloud/CloudInviteEntryView.vue +++ b/src/platform/onboarding/cloud/CloudInviteEntryView.vue @@ -12,7 +12,7 @@ const route = useRoute() const router = useRouter() onMounted(async () => { - const inviteCode = route.params.inviteCode + const inviteCode = route.params.code await router.push({ name: 'cloud-login', query: { diff --git a/src/platform/onboarding/cloud/CloudLoginView.vue b/src/platform/onboarding/cloud/CloudLoginView.vue index fcbf09df1..4da2634f6 100644 --- a/src/platform/onboarding/cloud/CloudLoginView.vue +++ b/src/platform/onboarding/cloud/CloudLoginView.vue @@ -1,29 +1,44 @@ @@ -80,13 +104,14 @@ import Button from 'primevue/button' import Divider from 'primevue/divider' import Message from 'primevue/message' -import { ref } from 'vue' +import { computed, onMounted, ref } from 'vue' import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import CloudSignInForm from '@/platform/onboarding/cloud/components/CloudSignInForm.vue' import { type SignInData } from '@/schemas/signInSchema' +import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { translateAuthError } from '@/utils/authErrorTranslation' const { t } = useI18n() @@ -96,23 +121,29 @@ const authActions = useFirebaseAuthActions() const isSecureContext = window.isSecureContext const authError = ref('') +const hasInviteCode = computed(() => !!route.query.inviteCode) + const navigateToSignup = () => { void router.push({ name: 'cloud-signup', query: route.query }) } const onSuccess = async () => { // Check if there's an invite code - const inviteCode = route.query.inviteCode as string - - if (inviteCode) { - // Handle invite code flow - go to invite check - await router.push({ - name: 'cloud-invite-check', - query: { inviteCode } - }) + const inviteCode = route.query.inviteCode as string | undefined + const { isEmailVerified } = useFirebaseAuthStore() + if (!isEmailVerified) { + await router.push({ name: 'cloud-verify-email', query: { inviteCode } }) } else { - // Normal login flow - go to user check - await router.push({ name: 'cloud-user-check' }) + if (inviteCode) { + // Handle invite code flow - go to invite check + await router.push({ + name: 'cloud-invite-check', + query: { inviteCode } + }) + } else { + // Normal login flow - go to user check + await router.push({ name: 'cloud-user-check' }) + } } } @@ -148,4 +179,8 @@ const signInWithEmail = async (values: SignInData) => { await onSuccess() } } + +onMounted(async () => { + await authActions.logout() +}) diff --git a/src/platform/onboarding/cloud/CloudSignupView.vue b/src/platform/onboarding/cloud/CloudSignupView.vue index e229c5744..35e36ce2a 100644 --- a/src/platform/onboarding/cloud/CloudSignupView.vue +++ b/src/platform/onboarding/cloud/CloudSignupView.vue @@ -117,9 +117,7 @@ const navigateToLogin = () => { } const onSuccess = async () => { - // After successful signup, always go to user check - // The user check will handle routing based on their status - await router.push({ name: 'cloud-user-check' }) + await router.push({ name: 'cloud-login', query: route.query }) } // Custom error handler for inline display diff --git a/src/platform/onboarding/cloud/CloudSurveyView.vue b/src/platform/onboarding/cloud/CloudSurveyView.vue index a87c285b3..e1a1c6f2a 100644 --- a/src/platform/onboarding/cloud/CloudSurveyView.vue +++ b/src/platform/onboarding/cloud/CloudSurveyView.vue @@ -46,7 +46,7 @@