Skip to content

Commit bc0b5cf

Browse files
committed
feat: route personal workspaces on billing_control_enabled
Replaces consolidated_billing_enabled as the flag that decides whether a personal workspace uses workspace-scoped billing. Team workspaces are unaffected — they are always workspace-scoped. This matters for the banner: isTeamPlan requires the workspace billing rail, so a personal workspace holding a team plan only surfaces billing state once its routing flag is on. Leaving routing on consolidated_billing_enabled while the subscribe path unblocks under billing_control_enabled would let the two diverge — a personal workspace could buy a team plan and then be routed to legacy, which has no billing_status at all, blanking the banner. consolidated_billing_enabled had exactly one consumer (this routing check), so it is replaced rather than left alongside. Cloud registers the new flag in Comfy-Org/cloud#5091. Renames the whole chain: remote config field, cached session ref, ServerFeatureFlag member, and the flags accessor.
1 parent 6c62edc commit bc0b5cf

12 files changed

Lines changed: 76 additions & 79 deletions

File tree

src/composables/billing/useBillingContext.test.ts

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ const DEFAULT_BILLING_STATUS: BillingStatusResponse = {
1919

2020
const {
2121
mockTeamWorkspacesEnabled,
22-
mockConsolidatedBillingEnabled,
22+
mockBillingControlEnabled,
2323
mockIsPersonal,
2424
mockPlans,
2525
mockPurchaseCredits,
2626
mockUpdateActiveWorkspace,
2727
mockBillingStatus
2828
} = vi.hoisted(() => ({
2929
mockTeamWorkspacesEnabled: { value: false },
30-
mockConsolidatedBillingEnabled: { value: false },
30+
mockBillingControlEnabled: { value: false },
3131
mockIsPersonal: { value: true },
3232
mockPlans: { value: [] as Plan[] },
3333
mockPurchaseCredits: vi.fn(),
@@ -59,13 +59,11 @@ vi.mock('@/composables/useFeatureFlags', async () => {
5959
teamWorkspacesEnabledRef.value = value
6060
}
6161
})
62-
const consolidatedBillingEnabledRef = ref(
63-
mockConsolidatedBillingEnabled.value
64-
)
65-
Object.defineProperty(mockConsolidatedBillingEnabled, 'value', {
66-
get: () => consolidatedBillingEnabledRef.value,
62+
const billingControlEnabledRef = ref(mockBillingControlEnabled.value)
63+
Object.defineProperty(mockBillingControlEnabled, 'value', {
64+
get: () => billingControlEnabledRef.value,
6765
set: (value: boolean) => {
68-
consolidatedBillingEnabledRef.value = value
66+
billingControlEnabledRef.value = value
6967
}
7068
})
7169
return {
@@ -74,8 +72,8 @@ vi.mock('@/composables/useFeatureFlags', async () => {
7472
get teamWorkspacesEnabled() {
7573
return mockTeamWorkspacesEnabled.value
7674
},
77-
get consolidatedBillingEnabled() {
78-
return mockConsolidatedBillingEnabled.value
75+
get billingControlEnabled() {
76+
return mockBillingControlEnabled.value
7977
}
8078
}
8179
})
@@ -165,7 +163,7 @@ describe('useBillingContext', () => {
165163
setActivePinia(createPinia())
166164
vi.clearAllMocks()
167165
mockTeamWorkspacesEnabled.value = false
168-
mockConsolidatedBillingEnabled.value = false
166+
mockBillingControlEnabled.value = false
169167
mockIsPersonal.value = true
170168
mockPlans.value = []
171169
mockBillingStatus.value = { ...DEFAULT_BILLING_STATUS }
@@ -177,27 +175,27 @@ describe('useBillingContext', () => {
177175
expect(type.value).toBe('legacy')
178176
})
179177

180-
it('keeps personal on legacy when consolidated billing is disabled', () => {
178+
it('keeps personal on legacy when billing control is disabled', () => {
181179
mockTeamWorkspacesEnabled.value = true
182-
mockConsolidatedBillingEnabled.value = false
180+
mockBillingControlEnabled.value = false
183181
mockIsPersonal.value = true
184182

185183
const { type } = useBillingContext()
186184
expect(type.value).toBe('legacy')
187185
})
188186

189-
it('selects workspace type for personal when consolidated billing is enabled', () => {
187+
it('selects workspace type for personal when billing control is enabled', () => {
190188
mockTeamWorkspacesEnabled.value = true
191-
mockConsolidatedBillingEnabled.value = true
189+
mockBillingControlEnabled.value = true
192190
mockIsPersonal.value = true
193191

194192
const { type } = useBillingContext()
195193
expect(type.value).toBe('workspace')
196194
})
197195

198-
it('selects workspace type for team regardless of consolidated billing', () => {
196+
it('selects workspace type for team regardless of billing control', () => {
199197
mockTeamWorkspacesEnabled.value = true
200-
mockConsolidatedBillingEnabled.value = false
198+
mockBillingControlEnabled.value = false
201199
mockIsPersonal.value = false
202200

203201
const { type } = useBillingContext()
@@ -298,7 +296,7 @@ describe('useBillingContext', () => {
298296
expect(workspaceApi.getBillingStatus).not.toHaveBeenCalled()
299297

300298
// Authenticated remote config resolves the flag on for the same workspace
301-
mockConsolidatedBillingEnabled.value = true
299+
mockBillingControlEnabled.value = true
302300
mockTeamWorkspacesEnabled.value = true
303301

304302
await vi.waitFor(() => {
@@ -307,16 +305,16 @@ describe('useBillingContext', () => {
307305
})
308306
})
309307

310-
it('moves a personal workspace to workspace billing when consolidated billing flips on', async () => {
308+
it('moves a personal workspace to workspace billing when billing control flips on', async () => {
311309
mockTeamWorkspacesEnabled.value = true
312-
mockConsolidatedBillingEnabled.value = false
310+
mockBillingControlEnabled.value = false
313311
mockIsPersonal.value = true
314312

315313
const { type } = useBillingContext()
316314
await nextTick()
317315
expect(type.value).toBe('legacy')
318316

319-
mockConsolidatedBillingEnabled.value = true
317+
mockBillingControlEnabled.value = true
320318

321319
await vi.waitFor(() => {
322320
expect(type.value).toBe('workspace')
@@ -325,9 +323,9 @@ describe('useBillingContext', () => {
325323
})
326324

327325
describe('subscription mirror to workspace store', () => {
328-
it('mirrors subscription for personal workspaces on the consolidated billing flow', async () => {
326+
it('mirrors subscription for personal workspaces on the billing control flow', async () => {
329327
mockTeamWorkspacesEnabled.value = true
330-
mockConsolidatedBillingEnabled.value = true
328+
mockBillingControlEnabled.value = true
331329
mockIsPersonal.value = true
332330

333331
const { initialize } = useBillingContext()

src/composables/billing/useBillingContext.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ const LEGACY_TEAM_PLAN_SLUG_PREFIX = 'team-'
3535
*
3636
* - Team workspaces disabled (OSS/Desktop): legacy billing via /customers/*
3737
* - Team workspaces enabled: workspace billing via /api/billing/* for team
38-
* workspaces, and for personal workspaces once consolidated billing is
39-
* enabled; personal workspaces otherwise stay on legacy billing
38+
* workspaces, and for personal workspaces once billing control is enabled;
39+
* personal workspaces otherwise stay on legacy billing
4040
*
4141
* The context automatically initializes when the workspace changes and provides
4242
* a unified interface for subscription status, balance, and billing actions.
@@ -206,9 +206,9 @@ function useBillingContextInternal(): BillingContext {
206206
error.value = null
207207
}
208208

209-
// type flips when the team-workspaces or consolidated-billing flag resolves
210-
// from authenticated config, swapping the active backend. Reset then reinit
211-
// on every workspace-id or type change.
209+
// type flips when the team-workspaces or billing-control flag resolves from
210+
// authenticated config, swapping the active backend. Reset then reinit on
211+
// every workspace-id or type change.
212212
watch(
213213
[() => store.activeWorkspace?.id, () => type.value],
214214
async ([newWorkspaceId]) => {

src/composables/billing/useBillingRouting.test.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useBillingRouting } from './useBillingRouting'
55
const { mockFlags, mockActiveWorkspace } = vi.hoisted(() => ({
66
mockFlags: {
77
teamWorkspacesEnabled: false,
8-
consolidatedBillingEnabled: false
8+
billingControlEnabled: false
99
},
1010
mockActiveWorkspace: {
1111
value: null as { id: string; type: 'personal' | 'team' } | null
@@ -30,7 +30,7 @@ const team = { id: 'w-team', type: 'team' as const }
3030
describe('useBillingRouting', () => {
3131
beforeEach(() => {
3232
mockFlags.teamWorkspacesEnabled = false
33-
mockFlags.consolidatedBillingEnabled = false
33+
mockFlags.billingControlEnabled = false
3434
mockActiveWorkspace.value = personal
3535
})
3636

@@ -44,19 +44,19 @@ describe('useBillingRouting', () => {
4444
expect(shouldUseWorkspaceBilling.value).toBe(false)
4545
})
4646

47-
it('keeps personal on legacy when consolidated billing is disabled', () => {
47+
it('keeps personal on legacy when billing control is disabled', () => {
4848
mockFlags.teamWorkspacesEnabled = true
49-
mockFlags.consolidatedBillingEnabled = false
49+
mockFlags.billingControlEnabled = false
5050
mockActiveWorkspace.value = personal
5151

5252
const { type } = useBillingRouting()
5353

5454
expect(type.value).toBe('legacy')
5555
})
5656

57-
it('moves personal to workspace billing when consolidated billing is enabled', () => {
57+
it('moves personal to workspace billing when billing control is enabled', () => {
5858
mockFlags.teamWorkspacesEnabled = true
59-
mockFlags.consolidatedBillingEnabled = true
59+
mockFlags.billingControlEnabled = true
6060
mockActiveWorkspace.value = personal
6161

6262
const { type, shouldUseWorkspaceBilling } = useBillingRouting()
@@ -65,9 +65,9 @@ describe('useBillingRouting', () => {
6565
expect(shouldUseWorkspaceBilling.value).toBe(true)
6666
})
6767

68-
it('uses workspace billing for team workspaces regardless of consolidated billing', () => {
68+
it('uses workspace billing for team workspaces regardless of billing control', () => {
6969
mockFlags.teamWorkspacesEnabled = true
70-
mockFlags.consolidatedBillingEnabled = false
70+
mockFlags.billingControlEnabled = false
7171
mockActiveWorkspace.value = team
7272

7373
const { type, shouldUseWorkspaceBilling } = useBillingRouting()
@@ -76,9 +76,9 @@ describe('useBillingRouting', () => {
7676
expect(shouldUseWorkspaceBilling.value).toBe(true)
7777
})
7878

79-
it('uses workspace billing for team workspaces with consolidated billing enabled', () => {
79+
it('uses workspace billing for team workspaces with billing control enabled', () => {
8080
mockFlags.teamWorkspacesEnabled = true
81-
mockFlags.consolidatedBillingEnabled = true
81+
mockFlags.billingControlEnabled = true
8282
mockActiveWorkspace.value = team
8383

8484
const { type, shouldUseWorkspaceBilling } = useBillingRouting()
@@ -89,7 +89,7 @@ describe('useBillingRouting', () => {
8989

9090
it('defaults to legacy while the workspace has not loaded', () => {
9191
mockFlags.teamWorkspacesEnabled = true
92-
mockFlags.consolidatedBillingEnabled = true
92+
mockFlags.billingControlEnabled = true
9393
mockActiveWorkspace.value = null
9494

9595
const { type } = useBillingRouting()

src/composables/billing/useBillingRouting.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { BillingType } from './types'
88
/**
99
* Selects the billing backend for the active workspace: legacy user-scoped
1010
* (`/customers/*`) or workspace-scoped (`/api/billing/*`). Personal workspaces
11-
* stay legacy until `consolidatedBillingEnabled`; team workspaces are always
11+
* stay legacy until `billingControlEnabled`; team workspaces are always
1212
* workspace-scoped. The routing matrix is covered in useBillingRouting.test.ts.
1313
*/
1414
export function useBillingRouting() {
@@ -23,7 +23,7 @@ export function useBillingRouting() {
2323
const workspaceType = workspaceStore.activeWorkspace?.type
2424
if (!workspaceType) return 'legacy'
2525

26-
if (workspaceType === 'personal' && !flags.consolidatedBillingEnabled) {
26+
if (workspaceType === 'personal' && !flags.billingControlEnabled) {
2727
return 'legacy'
2828
}
2929

src/composables/useFeatureFlags.test.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '@/composables/useFeatureFlags'
88
import * as distributionTypes from '@/platform/distribution/types'
99
import {
10-
cachedConsolidatedBillingEnabled,
10+
cachedBillingControlEnabled,
1111
cachedTeamWorkspacesEnabled,
1212
remoteConfig,
1313
remoteConfigState
@@ -226,19 +226,19 @@ describe('useFeatureFlags', () => {
226226
expect(flags.teamWorkspacesEnabled).toBe(true)
227227
})
228228

229-
it('consolidatedBillingEnabled override bypasses isCloud and isAuthenticatedConfigLoaded guards', () => {
229+
it('billingControlEnabled override bypasses isCloud and isAuthenticatedConfigLoaded guards', () => {
230230
vi.mocked(distributionTypes).isCloud = false
231-
localStorage.setItem('ff:consolidated_billing_enabled', 'true')
231+
localStorage.setItem('ff:billing_control_enabled', 'true')
232232

233233
const { flags } = useFeatureFlags()
234-
expect(flags.consolidatedBillingEnabled).toBe(true)
234+
expect(flags.billingControlEnabled).toBe(true)
235235
})
236236

237-
it('consolidatedBillingEnabled is false off-cloud even without an override', () => {
237+
it('billingControlEnabled is false off-cloud even without an override', () => {
238238
vi.mocked(distributionTypes).isCloud = false
239239

240240
const { flags } = useFeatureFlags()
241-
expect(flags.consolidatedBillingEnabled).toBe(false)
241+
expect(flags.billingControlEnabled).toBe(false)
242242
})
243243
})
244244

@@ -248,7 +248,7 @@ describe('useFeatureFlags', () => {
248248
remoteConfigState.value = 'unloaded'
249249
remoteConfig.value = {}
250250
cachedTeamWorkspacesEnabled.value = undefined
251-
cachedConsolidatedBillingEnabled.value = undefined
251+
cachedBillingControlEnabled.value = undefined
252252
localStorage.clear()
253253
})
254254

@@ -257,36 +257,36 @@ describe('useFeatureFlags', () => {
257257
remoteConfigState.value = 'unloaded'
258258
remoteConfig.value = {}
259259
cachedTeamWorkspacesEnabled.value = undefined
260-
cachedConsolidatedBillingEnabled.value = undefined
260+
cachedBillingControlEnabled.value = undefined
261261
localStorage.clear()
262262
})
263263

264264
it('returns the cached session value during the auth window', () => {
265265
cachedTeamWorkspacesEnabled.value = false
266-
cachedConsolidatedBillingEnabled.value = true
266+
cachedBillingControlEnabled.value = true
267267

268268
const { flags } = useFeatureFlags()
269269
expect(flags.teamWorkspacesEnabled).toBe(false)
270-
expect(flags.consolidatedBillingEnabled).toBe(true)
270+
expect(flags.billingControlEnabled).toBe(true)
271271
})
272272

273273
it('defaults to false during the auth window when nothing is cached', () => {
274274
const { flags } = useFeatureFlags()
275275
expect(flags.teamWorkspacesEnabled).toBe(false)
276-
expect(flags.consolidatedBillingEnabled).toBe(false)
276+
expect(flags.billingControlEnabled).toBe(false)
277277
})
278278

279279
it('prefers authenticated remoteConfig over the server feature fallback', () => {
280280
remoteConfigState.value = 'authenticated'
281281
remoteConfig.value = {
282282
team_workspaces_enabled: true,
283-
consolidated_billing_enabled: true
283+
billing_control_enabled: true
284284
}
285285
vi.mocked(api.getServerFeature).mockReturnValue(false)
286286

287287
const { flags } = useFeatureFlags()
288288
expect(flags.teamWorkspacesEnabled).toBe(true)
289-
expect(flags.consolidatedBillingEnabled).toBe(true)
289+
expect(flags.billingControlEnabled).toBe(true)
290290
})
291291

292292
it('falls back to api.getServerFeature when authenticated config omits the flag', () => {
@@ -295,15 +295,14 @@ describe('useFeatureFlags', () => {
295295
vi.mocked(api.getServerFeature).mockImplementation(
296296
(path, defaultValue) => {
297297
if (path === ServerFeatureFlag.TEAM_WORKSPACES_ENABLED) return true
298-
if (path === ServerFeatureFlag.CONSOLIDATED_BILLING_ENABLED)
299-
return true
298+
if (path === ServerFeatureFlag.BILLING_CONTROL_ENABLED) return true
300299
return defaultValue
301300
}
302301
)
303302

304303
const { flags } = useFeatureFlags()
305304
expect(flags.teamWorkspacesEnabled).toBe(true)
306-
expect(flags.consolidatedBillingEnabled).toBe(true)
305+
expect(flags.billingControlEnabled).toBe(true)
307306
})
308307
})
309308

src/composables/useFeatureFlags.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Ref } from 'vue'
33

44
import { isCloud, isNightly } from '@/platform/distribution/types'
55
import {
6-
cachedConsolidatedBillingEnabled,
6+
cachedBillingControlEnabled,
77
cachedTeamWorkspacesEnabled,
88
isAuthenticatedConfigLoaded,
99
remoteConfig
@@ -32,7 +32,7 @@ export enum ServerFeatureFlag {
3232
COMFYHUB_PROFILE_GATE_ENABLED = 'comfyhub_profile_gate_enabled',
3333
SHOW_SIGNIN_BUTTON = 'show_signin_button',
3434
UNIFIED_CLOUD_AUTH = 'unified_cloud_auth',
35-
CONSOLIDATED_BILLING_ENABLED = 'consolidated_billing_enabled',
35+
BILLING_CONTROL_ENABLED = 'billing_control_enabled',
3636
SIGNUP_TURNSTILE = 'signup_turnstile'
3737
}
3838

@@ -191,15 +191,15 @@ export function useFeatureFlags() {
191191
)
192192
},
193193
/**
194-
* Whether personal workspaces use the consolidated (workspace-scoped)
195-
* billing flow. While false (default), personal workspaces stay on the
196-
* legacy per-user billing flow; team workspaces are unaffected.
194+
* Whether personal workspaces use the workspace-scoped billing flow. While
195+
* false (default), personal workspaces stay on the legacy per-user billing
196+
* flow; team workspaces are unaffected.
197197
*/
198-
get consolidatedBillingEnabled() {
198+
get billingControlEnabled() {
199199
return resolveAuthGatedFlag(
200-
ServerFeatureFlag.CONSOLIDATED_BILLING_ENABLED,
201-
remoteConfig.value.consolidated_billing_enabled,
202-
cachedConsolidatedBillingEnabled
200+
ServerFeatureFlag.BILLING_CONTROL_ENABLED,
201+
remoteConfig.value.billing_control_enabled,
202+
cachedBillingControlEnabled
203203
)
204204
},
205205
get signupTurnstileMode() {

0 commit comments

Comments
 (0)