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:
Hunter
2026-06-30 03:13:58 -04:00
committed by GitHub
parent 8172788e27
commit bfa534fc0a
2 changed files with 28 additions and 8 deletions

View File

@@ -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

View File

@@ -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.