diff --git a/src/composables/useFeatureFlags.test.ts b/src/composables/useFeatureFlags.test.ts index 5e7b79defaf..6406c0544c6 100644 --- a/src/composables/useFeatureFlags.test.ts +++ b/src/composables/useFeatureFlags.test.ts @@ -7,6 +7,7 @@ import { } from '@/composables/useFeatureFlags' import * as distributionTypes from '@/platform/distribution/types' import { + cachedBillingControlEnabled, cachedConsolidatedBillingEnabled, cachedTeamWorkspacesEnabled, remoteConfig, @@ -226,6 +227,14 @@ describe('useFeatureFlags', () => { expect(flags.teamWorkspacesEnabled).toBe(true) }) + it('billingControlEnabled override bypasses isCloud and isAuthenticatedConfigLoaded guards', () => { + vi.mocked(distributionTypes).isCloud = false + localStorage.setItem('ff:billing_control_enabled', 'true') + + const { flags } = useFeatureFlags() + expect(flags.billingControlEnabled).toBe(true) + }) + it('consolidatedBillingEnabled override bypasses isCloud and isAuthenticatedConfigLoaded guards', () => { vi.mocked(distributionTypes).isCloud = false localStorage.setItem('ff:consolidated_billing_enabled', 'true') @@ -234,10 +243,11 @@ describe('useFeatureFlags', () => { expect(flags.consolidatedBillingEnabled).toBe(true) }) - it('consolidatedBillingEnabled is false off-cloud even without an override', () => { + it('billingControlEnabled is false off-cloud even without an override', () => { vi.mocked(distributionTypes).isCloud = false const { flags } = useFeatureFlags() + expect(flags.billingControlEnabled).toBe(false) expect(flags.consolidatedBillingEnabled).toBe(false) }) }) @@ -249,6 +259,7 @@ describe('useFeatureFlags', () => { remoteConfig.value = {} cachedTeamWorkspacesEnabled.value = undefined cachedConsolidatedBillingEnabled.value = undefined + cachedBillingControlEnabled.value = undefined localStorage.clear() }) @@ -258,35 +269,41 @@ describe('useFeatureFlags', () => { remoteConfig.value = {} cachedTeamWorkspacesEnabled.value = undefined cachedConsolidatedBillingEnabled.value = undefined + cachedBillingControlEnabled.value = undefined localStorage.clear() }) it('returns the cached session value during the auth window', () => { cachedTeamWorkspacesEnabled.value = false cachedConsolidatedBillingEnabled.value = true + cachedBillingControlEnabled.value = true const { flags } = useFeatureFlags() expect(flags.teamWorkspacesEnabled).toBe(false) expect(flags.consolidatedBillingEnabled).toBe(true) + expect(flags.billingControlEnabled).toBe(true) }) it('defaults to false during the auth window when nothing is cached', () => { const { flags } = useFeatureFlags() expect(flags.teamWorkspacesEnabled).toBe(false) expect(flags.consolidatedBillingEnabled).toBe(false) + expect(flags.billingControlEnabled).toBe(false) }) it('prefers authenticated remoteConfig over the server feature fallback', () => { remoteConfigState.value = 'authenticated' remoteConfig.value = { team_workspaces_enabled: true, - consolidated_billing_enabled: true + consolidated_billing_enabled: true, + billing_control_enabled: false } vi.mocked(api.getServerFeature).mockReturnValue(false) const { flags } = useFeatureFlags() expect(flags.teamWorkspacesEnabled).toBe(true) expect(flags.consolidatedBillingEnabled).toBe(true) + expect(flags.billingControlEnabled).toBe(false) }) it('falls back to api.getServerFeature when authenticated config omits the flag', () => { @@ -297,6 +314,7 @@ describe('useFeatureFlags', () => { if (path === ServerFeatureFlag.TEAM_WORKSPACES_ENABLED) return true if (path === ServerFeatureFlag.CONSOLIDATED_BILLING_ENABLED) return true + if (path === ServerFeatureFlag.BILLING_CONTROL_ENABLED) return true return defaultValue } ) @@ -304,6 +322,7 @@ describe('useFeatureFlags', () => { const { flags } = useFeatureFlags() expect(flags.teamWorkspacesEnabled).toBe(true) expect(flags.consolidatedBillingEnabled).toBe(true) + expect(flags.billingControlEnabled).toBe(true) }) }) diff --git a/src/composables/useFeatureFlags.ts b/src/composables/useFeatureFlags.ts index 10829ab4a98..c39ce8a0ab2 100644 --- a/src/composables/useFeatureFlags.ts +++ b/src/composables/useFeatureFlags.ts @@ -3,6 +3,7 @@ import type { Ref } from 'vue' import { isCloud, isNightly } from '@/platform/distribution/types' import { + cachedBillingControlEnabled, cachedConsolidatedBillingEnabled, cachedTeamWorkspacesEnabled, isAuthenticatedConfigLoaded, @@ -33,6 +34,7 @@ export enum ServerFeatureFlag { SHOW_SIGNIN_BUTTON = 'show_signin_button', UNIFIED_CLOUD_AUTH = 'unified_cloud_auth', CONSOLIDATED_BILLING_ENABLED = 'consolidated_billing_enabled', + BILLING_CONTROL_ENABLED = 'billing_control_enabled', FREE_TIER_JOB_ALLOWANCE_ENABLED = 'free_tier_job_allowance_enabled', SIGNUP_TURNSTILE = 'signup_turnstile' } @@ -203,6 +205,13 @@ export function useFeatureFlags() { cachedConsolidatedBillingEnabled ) }, + get billingControlEnabled() { + return resolveAuthGatedFlag( + ServerFeatureFlag.BILLING_CONTROL_ENABLED, + remoteConfig.value.billing_control_enabled, + cachedBillingControlEnabled + ) + }, get freeTierJobAllowanceEnabled() { const config = remoteConfig.value as typeof remoteConfig.value & { free_tier_job_allowance_enabled?: boolean diff --git a/src/platform/remoteConfig/refreshRemoteConfig.ts b/src/platform/remoteConfig/refreshRemoteConfig.ts index e1b2c9e4766..ba5533764dc 100644 --- a/src/platform/remoteConfig/refreshRemoteConfig.ts +++ b/src/platform/remoteConfig/refreshRemoteConfig.ts @@ -1,4 +1,5 @@ import { + cachedBillingControlEnabled, cachedConsolidatedBillingEnabled, cachedTeamWorkspacesEnabled, remoteConfig, @@ -63,6 +64,9 @@ export async function refreshRemoteConfig( cachedConsolidatedBillingEnabled.value = Boolean( config.consolidated_billing_enabled ) + cachedBillingControlEnabled.value = Boolean( + config.billing_control_enabled + ) } return } diff --git a/src/platform/remoteConfig/remoteConfig.ts b/src/platform/remoteConfig/remoteConfig.ts index 09f4eb46fab..cf119203087 100644 --- a/src/platform/remoteConfig/remoteConfig.ts +++ b/src/platform/remoteConfig/remoteConfig.ts @@ -64,3 +64,8 @@ export const cachedConsolidatedBillingEnabled = useStorage( 'consolidated_billing_enabled' satisfies `${ServerFeatureFlag.CONSOLIDATED_BILLING_ENABLED}`, undefined ) + +export const cachedBillingControlEnabled = useStorage( + 'billing_control_enabled' satisfies `${ServerFeatureFlag.BILLING_CONTROL_ENABLED}`, + undefined +) diff --git a/src/platform/remoteConfig/types.ts b/src/platform/remoteConfig/types.ts index d4479fe09e2..cca7c8e6982 100644 --- a/src/platform/remoteConfig/types.ts +++ b/src/platform/remoteConfig/types.ts @@ -121,6 +121,7 @@ export type RemoteConfig = { comfyhub_profile_gate_enabled?: boolean unified_cloud_auth?: boolean consolidated_billing_enabled?: boolean + billing_control_enabled?: boolean sentry_dsn?: string turnstile_sitekey?: string // Raw, unvalidated wire value (a server typo like 'enfroce' is possible). diff --git a/src/storybook/mocks/useFeatureFlags.ts b/src/storybook/mocks/useFeatureFlags.ts index ed04ad22ab2..f5e4139031d 100644 --- a/src/storybook/mocks/useFeatureFlags.ts +++ b/src/storybook/mocks/useFeatureFlags.ts @@ -26,14 +26,16 @@ export enum ServerFeatureFlag { COMFYHUB_PROFILE_GATE_ENABLED = 'comfyhub_profile_gate_enabled', SHOW_SIGNIN_BUTTON = 'show_signin_button', UNIFIED_CLOUD_AUTH = 'unified_cloud_auth', - CONSOLIDATED_BILLING_ENABLED = 'consolidated_billing_enabled' + CONSOLIDATED_BILLING_ENABLED = 'consolidated_billing_enabled', + BILLING_CONTROL_ENABLED = 'billing_control_enabled' } export function useFeatureFlags() { return { flags: { teamWorkspacesEnabled: true, - consolidatedBillingEnabled: true + consolidatedBillingEnabled: true, + billingControlEnabled: true } } }