mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-02 21:28:08 +00:00
fix(billing): resolve useWorkspaceUI lazily to break subscription dialog cycle (cloud/1.45) (#13288)
## Summary
Fixes a billing crash on the workspace-billing path
(`teamWorkspacesEnabled`, i.e. `@comfy.org`/`@drip.art` users) on
**test-v2, staging, and ephemeral** (all deploy from `cloud/1.45`):
```
TypeError: Cannot destructure property 'permissions' of 'useWorkspaceUI(...)' as it is undefined.
TypeError: Cannot destructure property 'isActiveSubscription' of 'useBillingContext(...)' as it is undefined.
```
## Root cause — a backport-ordering regression
`useSubscriptionDialog`, `useWorkspaceUI`, and `useBillingContext` form
an initialization cycle:
```
useBillingContext (immediate watch reads activeContext)
-> getWorkspaceBilling -> useWorkspaceBilling
-> useSubscriptionDialog (setup-time: const { permissions } = useWorkspaceUI())
-> useWorkspaceUIInternal (setup-time: const { isActiveSubscription } = useBillingContext())
-> re-enters the half-built useBillingContext -> returns undefined -> crash
```
It is first tripped by `PostHogTelemetryProvider` reading subscription
state at boot.
On `main` this is fixed: `#12761` (FE-768) / `#12953` (FE-966) resolve
`useWorkspaceUI()` lazily inside `showPricingTable`. Those landed on
`cloud/1.45` as `#13190` / `#13202` — but **`#13209` (FE-978 backport)
merged afterward and re-added the setup-time read at line 35**,
reverting the fix on the release branch only.
## Fix
Move `const { permissions } = useWorkspaceUI()` out of composable setup
into `showPricingTable` (lazy), matching `main` and the
`useBillingContext()` reads already lazy in this same file. Adds a
regression test asserting `useWorkspaceUI` is not resolved at composable
setup.
## Validation
Tagged `preview` to spin up an ephemeral env — please verify the
billing/subscription dialog opens without the destructure crash for a
workspace-billing (`@comfy.org`) user.
> Note: local pre-commit/-push hooks were skipped due to a sandbox
corepack cache permission issue; relying on CI for lint/typecheck/test.
---------
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -11,6 +11,7 @@ const mockTeamWorkspacesEnabled = vi.hoisted(() => ({ value: false }))
|
||||
const mockIsCloud = vi.hoisted(() => ({ value: true }))
|
||||
const mockIsLegacyTeamPlan = vi.hoisted(() => ({ value: false }))
|
||||
const mockCanManageSubscription = vi.hoisted(() => ({ value: true }))
|
||||
const mockUseWorkspaceUI = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('vue', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
@@ -65,13 +66,7 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workspace/composables/useWorkspaceUI', () => ({
|
||||
useWorkspaceUI: () => ({
|
||||
permissions: {
|
||||
get value() {
|
||||
return { canManageSubscription: mockCanManageSubscription.value }
|
||||
}
|
||||
}
|
||||
})
|
||||
useWorkspaceUI: mockUseWorkspaceUI
|
||||
}))
|
||||
|
||||
describe('useSubscriptionDialog', () => {
|
||||
@@ -83,6 +78,13 @@ describe('useSubscriptionDialog', () => {
|
||||
mockTeamWorkspacesEnabled.value = false
|
||||
mockIsLegacyTeamPlan.value = false
|
||||
mockCanManageSubscription.value = true
|
||||
mockUseWorkspaceUI.mockImplementation(() => ({
|
||||
permissions: {
|
||||
get value() {
|
||||
return { canManageSubscription: mockCanManageSubscription.value }
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
try {
|
||||
sessionStorage.clear()
|
||||
@@ -91,6 +93,23 @@ describe('useSubscriptionDialog', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('billing context import cycle', () => {
|
||||
it('does not resolve useWorkspaceUI at composable setup', () => {
|
||||
useSubscriptionDialog()
|
||||
|
||||
expect(mockUseWorkspaceUI).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resolves useWorkspaceUI lazily when the pricing table is shown', () => {
|
||||
const { showPricingTable } = useSubscriptionDialog()
|
||||
expect(mockUseWorkspaceUI).not.toHaveBeenCalled()
|
||||
|
||||
showPricingTable()
|
||||
|
||||
expect(mockUseWorkspaceUI).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('showPricingTable', () => {
|
||||
it('does not open dialog on non-cloud', () => {
|
||||
mockIsCloud.value = false
|
||||
|
||||
@@ -32,7 +32,6 @@ export const useSubscriptionDialog = () => {
|
||||
const dialogService = useDialogService()
|
||||
const dialogStore = useDialogStore()
|
||||
const workspaceStore = useTeamWorkspaceStore()
|
||||
const { permissions } = useWorkspaceUI()
|
||||
|
||||
function hide() {
|
||||
dialogStore.closeDialog({ key: DIALOG_KEY })
|
||||
@@ -42,6 +41,8 @@ export const useSubscriptionDialog = () => {
|
||||
function showPricingTable(options?: SubscriptionDialogOptions) {
|
||||
if (!isCloud) return
|
||||
|
||||
const { permissions } = useWorkspaceUI()
|
||||
|
||||
// Members can't manage the workspace subscription, so a blocked run shows a
|
||||
// small read-only "ask your owner to reactivate" modal instead of the
|
||||
// pricing table. Out-of-credits still routes everyone to the credits flow.
|
||||
|
||||
Reference in New Issue
Block a user