From b8310dbf2323b887c331ce26b53dab1cc2685389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cemal=20K=C4=B1l=C4=B1=C3=A7?= Date: Tue, 28 Jul 2026 10:31:17 +0200 Subject: [PATCH] feat(studio): move SIWC rollout gate to ConfigCat (#48360) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Replace the deploy-gated `dashboard_auth:sign_in_with_chatgpt` AND-gate with useFlag('ShowSignInWithChatGptButton') OR'd against the existing localStorage opt-in switch, so rollout/rollback no longer requires a frontend deploy. Remove the now-dead static flag, its only consumer was this gate. ## Summary by CodeRabbit * **New Features** * ChatGPT sign-in availability now responds to either the local opt-in setting or the updated configuration flag. * GitHub sign-in continues to follow its dedicated feature setting. * **Bug Fixes** * Corrected identity provider visibility across different sign-in configuration combinations. * **Chores** * Removed the obsolete ChatGPT sign-in feature setting. --- .../useEnabledIdentityProviders.test.ts | 72 ++++++++++++++----- .../hooks/misc/useEnabledIdentityProviders.ts | 16 ++--- .../enabled-features/enabled-features.json | 1 - .../enabled-features.schema.json | 5 -- 4 files changed, 61 insertions(+), 33 deletions(-) diff --git a/apps/studio/hooks/misc/__tests__/useEnabledIdentityProviders.test.ts b/apps/studio/hooks/misc/__tests__/useEnabledIdentityProviders.test.ts index b82728b4227..38ef4892467 100644 --- a/apps/studio/hooks/misc/__tests__/useEnabledIdentityProviders.test.ts +++ b/apps/studio/hooks/misc/__tests__/useEnabledIdentityProviders.test.ts @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react' -import { describe, expect, it, vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { useEnabledIdentityProviders } from '../useEnabledIdentityProviders' import { @@ -9,6 +9,7 @@ import { const mockIsFeatureEnabled = vi.hoisted(() => vi.fn()) const mockUseLocalStorageQuery = vi.hoisted(() => vi.fn()) +const mockUseFlag = vi.hoisted(() => vi.fn()) vi.mock('../useIsFeatureEnabled', () => ({ useIsFeatureEnabled: mockIsFeatureEnabled, @@ -18,13 +19,22 @@ vi.mock('../useLocalStorage', () => ({ useLocalStorageQuery: mockUseLocalStorageQuery, })) +vi.mock('common', async (importOriginal) => ({ + ...(await importOriginal()), + useFlag: mockUseFlag, +})) + describe('useEnabledIdentityProviders', () => { + beforeEach(() => { + mockUseFlag.mockReset() + }) + it('returns every provider when all flags are enabled', () => { mockIsFeatureEnabled.mockReturnValue({ dashboardAuthSignInWithGithub: true, - dashboardAuthSignInWithChatgpt: true, }) mockUseLocalStorageQuery.mockReturnValue([true]) + mockUseFlag.mockReturnValue(true) const { result } = renderHook(() => useEnabledIdentityProviders()) @@ -34,45 +44,69 @@ describe('useEnabledIdentityProviders', () => { it('returns no providers when all flags are disabled', () => { mockIsFeatureEnabled.mockReturnValue({ dashboardAuthSignInWithGithub: false, - dashboardAuthSignInWithChatgpt: false, }) mockUseLocalStorageQuery.mockReturnValue([false]) + mockUseFlag.mockReturnValue(false) const { result } = renderHook(() => useEnabledIdentityProviders()) expect(result.current).toEqual([]) }) - it('includes ChatGPT when its flag is enabled and the local storage switch is truthy', () => { - mockIsFeatureEnabled.mockReturnValue({ - dashboardAuthSignInWithGithub: false, - dashboardAuthSignInWithChatgpt: true, - }) + it('includes ChatGPT when localStorage is true and configcat is true', () => { + mockIsFeatureEnabled.mockReturnValue({ dashboardAuthSignInWithGithub: false }) mockUseLocalStorageQuery.mockReturnValue([true]) + mockUseFlag.mockReturnValue(true) const { result } = renderHook(() => useEnabledIdentityProviders()) expect(result.current).toEqual([CHATGPT_IDENTITY_PROVIDER]) }) - it('excludes ChatGPT when its flag is enabled but the local storage switch is unset', () => { - mockIsFeatureEnabled.mockReturnValue({ - dashboardAuthSignInWithGithub: false, - dashboardAuthSignInWithChatgpt: true, - }) + it('includes ChatGPT when localStorage is true and configcat is false', () => { + mockIsFeatureEnabled.mockReturnValue({ dashboardAuthSignInWithGithub: false }) + mockUseLocalStorageQuery.mockReturnValue([true]) + mockUseFlag.mockReturnValue(false) + + const { result } = renderHook(() => useEnabledIdentityProviders()) + + expect(result.current).toEqual([CHATGPT_IDENTITY_PROVIDER]) + }) + + it('includes ChatGPT when localStorage is false and configcat is true', () => { + mockIsFeatureEnabled.mockReturnValue({ dashboardAuthSignInWithGithub: false }) mockUseLocalStorageQuery.mockReturnValue([false]) + mockUseFlag.mockReturnValue(true) + + const { result } = renderHook(() => useEnabledIdentityProviders()) + + expect(result.current).toEqual([CHATGPT_IDENTITY_PROVIDER]) + }) + + it('excludes ChatGPT when localStorage is false and configcat is false', () => { + mockIsFeatureEnabled.mockReturnValue({ dashboardAuthSignInWithGithub: false }) + mockUseLocalStorageQuery.mockReturnValue([false]) + mockUseFlag.mockReturnValue(false) const { result } = renderHook(() => useEnabledIdentityProviders()) expect(result.current).toEqual([]) }) - it('excludes ChatGPT when the local storage switch is truthy but its flag is disabled', () => { - mockIsFeatureEnabled.mockReturnValue({ - dashboardAuthSignInWithGithub: false, - dashboardAuthSignInWithChatgpt: false, - }) - mockUseLocalStorageQuery.mockReturnValue([true]) + it('includes GitHub when its feature flag is enabled', () => { + mockIsFeatureEnabled.mockReturnValue({ dashboardAuthSignInWithGithub: true }) + mockUseLocalStorageQuery.mockReturnValue([false]) + mockUseFlag.mockReturnValue(false) + + const { result } = renderHook(() => useEnabledIdentityProviders()) + + expect(result.current).toEqual([GITHUB_IDENTITY_PROVIDER]) + }) + + it('excludes GitHub when its feature flag is disabled', () => { + mockIsFeatureEnabled.mockReturnValue({ dashboardAuthSignInWithGithub: false }) + mockUseLocalStorageQuery.mockReturnValue([false]) + mockUseFlag.mockReturnValue(false) const { result } = renderHook(() => useEnabledIdentityProviders()) diff --git a/apps/studio/hooks/misc/useEnabledIdentityProviders.ts b/apps/studio/hooks/misc/useEnabledIdentityProviders.ts index 83aa7c1b18d..939a7e0e9ff 100644 --- a/apps/studio/hooks/misc/useEnabledIdentityProviders.ts +++ b/apps/studio/hooks/misc/useEnabledIdentityProviders.ts @@ -1,4 +1,4 @@ -import { LOCAL_STORAGE_KEYS } from 'common' +import { LOCAL_STORAGE_KEYS, useFlag } from 'common' import { useMemo } from 'react' import { useIsFeatureEnabled } from './useIsFeatureEnabled' @@ -14,28 +14,28 @@ import { * To add a provider: declare its config in `lib/external-identity-providers.ts`, add a * `dashboard_auth:sign_in_with_*` flag, and gate it here. * - * ChatGPT is a deliberate exception: it's also gated behind a manual, localStorage-only rollout - * switch (`LOCAL_STORAGE_KEYS.SIGN_IN_CHATGPT_ENABLED`) on top of its feature flag, since it's WIP. + * ChatGPT is a deliberate exception: it's rolled out via the `ShowSignInWithChatGptButton` + * ConfigCat flag OR'd with a manual, localStorage-only opt-in switch + * (`LOCAL_STORAGE_KEYS.SIGN_IN_CHATGPT_ENABLED`, flippable via the `?siwc-enabled=1` query param — + * see `useSiwcQueryParamOptIn`), instead of the static `dashboard_auth:sign_in_with_*` pattern. */ export function useEnabledIdentityProviders(): ExternalIdentityProviderConfig[] { const { dashboardAuthSignInWithGithub: githubEnabled } = useIsFeatureEnabled([ 'dashboard_auth:sign_in_with_github', ]) - const { dashboardAuthSignInWithChatgpt: chatgptEnabled } = useIsFeatureEnabled([ - 'dashboard_auth:sign_in_with_chatgpt', - ]) const [chatgptLocalStorageEnabled] = useLocalStorageQuery( LOCAL_STORAGE_KEYS.SIGN_IN_CHATGPT_ENABLED, false ) + const chatGptConfigCatFlagEnabled = useFlag('ShowSignInWithChatGptButton') return useMemo( () => [ githubEnabled && GITHUB_IDENTITY_PROVIDER, - chatgptEnabled && chatgptLocalStorageEnabled && CHATGPT_IDENTITY_PROVIDER, + (chatgptLocalStorageEnabled || chatGptConfigCatFlagEnabled) && CHATGPT_IDENTITY_PROVIDER, ].filter((p): p is ExternalIdentityProviderConfig => Boolean(p)), - [githubEnabled, chatgptEnabled, chatgptLocalStorageEnabled] + [githubEnabled, chatgptLocalStorageEnabled, chatGptConfigCatFlagEnabled] ) } diff --git a/packages/common/enabled-features/enabled-features.json b/packages/common/enabled-features/enabled-features.json index bdf512b1e13..7e4fcb40075 100644 --- a/packages/common/enabled-features/enabled-features.json +++ b/packages/common/enabled-features/enabled-features.json @@ -34,7 +34,6 @@ "dashboard_auth:sign_up": true, "dashboard_auth:sign_in_with_github": true, - "dashboard_auth:sign_in_with_chatgpt": true, "dashboard_auth:sign_in_with_sso": true, "dashboard_auth:sign_in_with_email": true, "dashboard_auth:show_testimonial": true, diff --git a/packages/common/enabled-features/enabled-features.schema.json b/packages/common/enabled-features/enabled-features.schema.json index 4b4694ad13f..0a63c3e2b63 100644 --- a/packages/common/enabled-features/enabled-features.schema.json +++ b/packages/common/enabled-features/enabled-features.schema.json @@ -115,10 +115,6 @@ "type": "boolean", "description": "Enable the sign in with github provider" }, - "dashboard_auth:sign_in_with_chatgpt": { - "type": "boolean", - "description": "Enable the sign in with chatgpt provider" - }, "dashboard_auth:sign_in_with_sso": { "type": "boolean", "description": "Enable the sign in with sso provider" @@ -472,7 +468,6 @@ "billing:all", "dashboard_auth:sign_up", "dashboard_auth:sign_in_with_github", - "dashboard_auth:sign_in_with_chatgpt", "dashboard_auth:sign_in_with_sso", "dashboard_auth:sign_in_with_email", "dashboard_auth:show_tos",