mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
## Summary Discussion here: https://comfy-organization.slack.com/archives/C0A0XANFJRE/p1764899027465379 Implement: Subscription tier query parameter for direct checkout flow Example button link: `/cloud/subscribe?tier=standard` `tier` could be `standard`, `creator` or `pro` `cycle` could be `monthly` or `yearly`. it is optional, and `monthly` by default. <!-- One sentence describing what changed and why. --> ## Changes - **What**: <!-- Core functionality added/modified --> - Add a landing page called `CloudSubscriptionRedirectView.vue` to handling the subscription tier button link parameter - Extract subscription handling logic from `PriceTable.vue` - **Breaking**: <!-- Any breaking changes (if none, remove this line) --> - Code change touched `PriceTable.vue` - **Dependencies**: <!-- New dependencies (if none, remove this line) --> ## Review Focus - link will redirect to login url, when cloud app not login - after login, the cloud app will redirect to CloudSubscriptionRedirect page - wait for several seconds, the cloud app will be redirected to checkout page <!-- Critical design decisions or edge cases that need attention --> <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots (if applicable)  <!-- Add screenshots or video recording to help explain your changes --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7553-feat-handling-subscription-tier-button-link-parameter-2cb6d73d365081ee9580e89090248300) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
211 lines
6.1 KiB
TypeScript
211 lines
6.1 KiB
TypeScript
import { until } from '@vueuse/core'
|
|
import { storeToRefs } from 'pinia'
|
|
import {
|
|
createRouter,
|
|
createWebHashHistory,
|
|
createWebHistory
|
|
} from 'vue-router'
|
|
import type { RouteLocationNormalized } from 'vue-router'
|
|
|
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
import { isCloud } from '@/platform/distribution/types'
|
|
import { useDialogService } from '@/services/dialogService'
|
|
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
|
import { useUserStore } from '@/stores/userStore'
|
|
import { isElectron } from '@/utils/envUtil'
|
|
import LayoutDefault from '@/views/layouts/LayoutDefault.vue'
|
|
|
|
import { installPreservedQueryTracker } from '@/platform/navigation/preservedQueryTracker'
|
|
import { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'
|
|
import { cloudOnboardingRoutes } from './platform/cloud/onboarding/onboardingCloudRoutes'
|
|
|
|
const isFileProtocol = window.location.protocol === 'file:'
|
|
|
|
/**
|
|
* Determine base path for the router.
|
|
* - Electron: always root
|
|
* - Cloud: use Vite's BASE_URL (configured at build time)
|
|
* - Standard web (including reverse proxy subpaths): use window.location.pathname
|
|
* to support deployments like http://mysite.com/ComfyUI/
|
|
*/
|
|
function getBasePath(): string {
|
|
if (isElectron()) return '/'
|
|
if (isCloud) return import.meta.env?.BASE_URL || '/'
|
|
return window.location.pathname
|
|
}
|
|
|
|
const basePath = getBasePath()
|
|
|
|
const router = createRouter({
|
|
history: isFileProtocol
|
|
? createWebHashHistory()
|
|
: // Base path must be specified to ensure correct relative paths
|
|
// Example: For URL 'http://localhost:7801/ComfyBackendDirect',
|
|
// we need this base path or assets will incorrectly resolve from 'http://localhost:7801/'
|
|
createWebHistory(basePath),
|
|
routes: [
|
|
...(isCloud ? cloudOnboardingRoutes : []),
|
|
{
|
|
path: '/',
|
|
component: LayoutDefault,
|
|
children: [
|
|
{
|
|
path: '',
|
|
name: 'GraphView',
|
|
component: () => import('@/views/GraphView.vue'),
|
|
beforeEnter: async (_to, _from, next) => {
|
|
// Then check user store
|
|
const userStore = useUserStore()
|
|
await userStore.initialize()
|
|
if (userStore.needsLogin) {
|
|
next('/user-select')
|
|
} else {
|
|
next()
|
|
}
|
|
}
|
|
},
|
|
{
|
|
path: 'user-select',
|
|
name: 'UserSelectView',
|
|
component: () => import('@/views/UserSelectView.vue')
|
|
}
|
|
]
|
|
}
|
|
],
|
|
|
|
scrollBehavior(_to, _from, savedPosition) {
|
|
if (savedPosition) {
|
|
return savedPosition
|
|
} else {
|
|
return { top: 0 }
|
|
}
|
|
}
|
|
})
|
|
|
|
installPreservedQueryTracker(router, [
|
|
{
|
|
namespace: PRESERVED_QUERY_NAMESPACES.TEMPLATE,
|
|
keys: ['template', 'source', 'mode']
|
|
}
|
|
])
|
|
|
|
if (isCloud) {
|
|
const { flags } = useFeatureFlags()
|
|
const PUBLIC_ROUTE_NAMES = new Set([
|
|
'cloud-login',
|
|
'cloud-signup',
|
|
'cloud-forgot-password',
|
|
'cloud-sorry-contact-support'
|
|
])
|
|
const PUBLIC_ROUTE_PATHS = new Set([
|
|
'/cloud/login',
|
|
'/cloud/signup',
|
|
'/cloud/forgot-password',
|
|
'/cloud/sorry-contact-support'
|
|
])
|
|
|
|
function isPublicRoute(to: RouteLocationNormalized) {
|
|
const name = String(to.name)
|
|
if (PUBLIC_ROUTE_NAMES.has(name)) return true
|
|
const path = to.path
|
|
return PUBLIC_ROUTE_PATHS.has(path)
|
|
}
|
|
// Global authentication guard
|
|
router.beforeEach(async (to, _from, next) => {
|
|
const authStore = useFirebaseAuthStore()
|
|
|
|
// Wait for Firebase auth to initialize
|
|
// Timeout after 16 seconds
|
|
if (!authStore.isInitialized) {
|
|
try {
|
|
const { isInitialized } = storeToRefs(authStore)
|
|
await until(isInitialized).toBe(true, { timeout: 16_000 })
|
|
} catch (error) {
|
|
console.error('Auth initialization failed:', error)
|
|
return next({ name: 'cloud-auth-timeout' })
|
|
}
|
|
}
|
|
|
|
// Pass authenticated users
|
|
const authHeader = await authStore.getAuthHeader()
|
|
const isLoggedIn = !!authHeader
|
|
|
|
// Allow public routes
|
|
if (isPublicRoute(to)) {
|
|
return next()
|
|
}
|
|
|
|
// Special handling for user-check
|
|
// These routes need auth but handle their own routing logic
|
|
if (to.name === 'cloud-user-check') {
|
|
if (to.meta.requiresAuth && !isLoggedIn) {
|
|
return next({ name: 'cloud-login' })
|
|
}
|
|
return next()
|
|
}
|
|
|
|
// Prevent redirect loop when coming from user-check
|
|
if (_from.name === 'cloud-user-check' && to.path === '/') {
|
|
return next()
|
|
}
|
|
|
|
const query =
|
|
to.fullPath === '/'
|
|
? undefined
|
|
: { previousFullPath: encodeURIComponent(to.fullPath) }
|
|
|
|
// Check if route requires authentication
|
|
if (to.meta.requiresAuth && !isLoggedIn) {
|
|
return next({
|
|
name: 'cloud-login',
|
|
query
|
|
})
|
|
}
|
|
|
|
// Handle other protected routes
|
|
if (!isLoggedIn) {
|
|
// For Electron, use dialog
|
|
if (isElectron()) {
|
|
const dialogService = useDialogService()
|
|
const loginSuccess = await dialogService.showSignInDialog()
|
|
return loginSuccess ? next() : next(false)
|
|
}
|
|
|
|
// For web, redirect to login
|
|
return next({
|
|
name: 'cloud-login',
|
|
query
|
|
})
|
|
}
|
|
|
|
// User is logged in - check if they need onboarding (when enabled)
|
|
// For root path, check actual user status to handle waitlisted users
|
|
if (!isElectron() && isLoggedIn && to.path === '/') {
|
|
if (!flags.onboardingSurveyEnabled) {
|
|
return next()
|
|
}
|
|
// Import auth functions dynamically to avoid circular dependency
|
|
const { getSurveyCompletedStatus } =
|
|
await import('@/platform/cloud/onboarding/auth')
|
|
try {
|
|
// Check user's actual status
|
|
const surveyCompleted = await getSurveyCompletedStatus()
|
|
|
|
// Survey is required for all users (when feature flag enabled)
|
|
if (!surveyCompleted) {
|
|
return next({ name: 'cloud-survey' })
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to check user status:', error)
|
|
// On error, redirect to user-check as fallback
|
|
return next({ name: 'cloud-user-check' })
|
|
}
|
|
}
|
|
|
|
// User is logged in and accessing protected route
|
|
return next()
|
|
})
|
|
}
|
|
|
|
export default router
|