fix(studio): wire up siwc-enabled query param opt-in on sign-in/sign-up (#48126)

Add `useSiwcQueryParamOptIn`, which flips on the ChatGPT sign-in rollout
localStorage flag when `?siwc-enabled=1` is present, and call it from
both pages/sign-in.tsx and pages/sign-up.tsx.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for enabling the sign-in experience via
`?siwc-enabled=1`, automatically updating the stored opt-in flag on both
sign-in and sign-up pages.
* **Tests**
* Added coverage confirming the stored flag is updated only for
`siwc-enabled=1`, and not for missing, non-`1`, `0`, or repeated/array
values.
* Added assertions that the behavior is triggered consistently when
rendering the sign-in and sign-up pages.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-07-21 12:13:13 +02:00
1 parent c166bfc044
commit 0b53a1869f
6 files changed
+236

No files matched your search

@@ -0,0 +1,87 @@
import { renderHook } from '@testing-library/react'
import mockRouter from 'next-router-mock'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useSiwcQueryParamOptIn } from '../useSiwcQueryParamOptIn'
vi.mock('next/router', () => import('next-router-mock'))
// tests/vitestSetup.ts globally mocks `common`'s useParams to always return `{ ref: 'default' }`,
// which would make this hook's `siwcEnabled` lookup always undefined. Restore the real
// implementation here so useParams reflects the mocked router's query params.
vi.mock('common', async (importOriginal) => {
const actual = await importOriginal<typeof import('common')>()
return { ...actual }
})
const mockSetValue = vi.hoisted(() => vi.fn())
const mockUseLocalStorageQuery = vi.hoisted(() => vi.fn())
vi.mock('../useLocalStorage', () => ({
useLocalStorageQuery: mockUseLocalStorageQuery,
}))
describe('useSiwcQueryParamOptIn', () => {
beforeEach(() => {
mockRouter.setCurrentUrl('/sign-in')
mockSetValue.mockClear()
mockUseLocalStorageQuery.mockReturnValue([false, mockSetValue])
})
it('enables the flag when siwc-enabled=1 is present', () => {
mockRouter.setCurrentUrl('/sign-in?siwc-enabled=1')
renderHook(() => useSiwcQueryParamOptIn())
expect(mockSetValue).toHaveBeenCalledWith(true)
})
it('does nothing when the param is absent', () => {
renderHook(() => useSiwcQueryParamOptIn())
expect(mockSetValue).not.toHaveBeenCalled()
})
it('does nothing for a non-"1" value', () => {
mockRouter.setCurrentUrl('/sign-in?siwc-enabled=true')
renderHook(() => useSiwcQueryParamOptIn())
expect(mockSetValue).not.toHaveBeenCalled()
})
it('does nothing when siwc-enabled=0', () => {
mockRouter.setCurrentUrl('/sign-in?siwc-enabled=0')
renderHook(() => useSiwcQueryParamOptIn())
expect(mockSetValue).not.toHaveBeenCalled()
})
it('only considers the first value when the param is repeated (array value)', () => {
// useParams (from 'common') flattens repeated query params to their first occurrence, so
// only the first "0" here is seen by the hook, and it does nothing.
mockRouter.setCurrentUrl('/sign-in?siwc-enabled=0&siwc-enabled=1')
renderHook(() => useSiwcQueryParamOptIn())
expect(mockSetValue).not.toHaveBeenCalled()
})
it('still calls the setter when the flag is already true (idempotent no-op is the setter’s job)', () => {
mockUseLocalStorageQuery.mockReturnValue([true, mockSetValue])
mockRouter.setCurrentUrl('/sign-in?siwc-enabled=1')
renderHook(() => useSiwcQueryParamOptIn())
expect(mockSetValue).toHaveBeenCalledWith(true)
})
it('works the same way on the sign-up URL', () => {
mockRouter.setCurrentUrl('/sign-up?siwc-enabled=1')
renderHook(() => useSiwcQueryParamOptIn())
expect(mockSetValue).toHaveBeenCalledWith(true)
})
})
@@ -0,0 +1,27 @@
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { useEffect } from 'react'
import { useLocalStorageQuery } from './useLocalStorage'
/**
* Lets a shareable link (e.g. `/sign-in?siwc-enabled=1`) flip on the manual ChatGPT sign-in
* rollout switch (`LOCAL_STORAGE_KEYS.SIGN_IN_CHATGPT_ENABLED`, read by
* `useEnabledIdentityProviders`) for this browser, instead of requiring a devtools localStorage
* edit. Only ever meaningful on `/sign-in` and `/sign-up`, where this param would be linked to.
*
* Only the exact string `'1'` opts in; the flag is never cleared based on the param's absence, so
* it persists once set.
*/
export function useSiwcQueryParamOptIn() {
const { siwcEnabled } = useParams()
const [, setChatgptLocalStorageEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SIGN_IN_CHATGPT_ENABLED,
false
)
useEffect(() => {
if (siwcEnabled === '1') {
setChatgptLocalStorageEnabled(true)
}
}, [siwcEnabled, setChatgptLocalStorageEnabled])
}
+3
View File
@@ -14,11 +14,14 @@ import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
import { useEnabledIdentityProviders } from '@/hooks/misc/useEnabledIdentityProviders'
import { useInboundBranding } from '@/hooks/misc/useInboundBranding'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSiwcQueryParamOptIn } from '@/hooks/misc/useSiwcQueryParamOptIn'
import { IS_PLATFORM } from '@/lib/constants'
import type { ExternalIdentityProviderConfig } from '@/lib/external-identity-providers'
import type { NextPageWithLayout } from '@/types'
const SignInPage: NextPageWithLayout = () => {
useSiwcQueryParamOptIn()
const router = useRouter()
const [showOtherOptions, setShowOtherOptions] = useState(false)
+3
View File
@@ -9,10 +9,13 @@ import { UnknownInterface } from '@/components/ui/UnknownInterface'
import { useEnabledIdentityProviders } from '@/hooks/misc/useEnabledIdentityProviders'
import { useInboundBranding } from '@/hooks/misc/useInboundBranding'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { useSiwcQueryParamOptIn } from '@/hooks/misc/useSiwcQueryParamOptIn'
import type { ExternalIdentityProviderConfig } from '@/lib/external-identity-providers'
import type { NextPageWithLayout } from '@/types'
const SignUpPage: NextPageWithLayout = () => {
useSiwcQueryParamOptIn()
const [showOtherOptions, setShowOtherOptions] = useState(false)
const { dashboardAuthSignUp: signUpEnabled } = useIsFeatureEnabled(['dashboard_auth:sign_up'])
+70
View File
@@ -0,0 +1,70 @@
import { render } from '@testing-library/react'
import type { ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import SignInPage from '@/pages/sign-in'
const { mockUseSiwcQueryParamOptIn, mockUseIsFeatureEnabled } = vi.hoisted(() => ({
mockUseSiwcQueryParamOptIn: vi.fn(),
mockUseIsFeatureEnabled: vi.fn(),
}))
vi.mock('next/router', () => ({
useRouter: () => ({ query: {}, replace: vi.fn() }),
}))
vi.mock('@/hooks/misc/useSiwcQueryParamOptIn', () => ({
useSiwcQueryParamOptIn: mockUseSiwcQueryParamOptIn,
}))
vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
useIsFeatureEnabled: mockUseIsFeatureEnabled,
}))
vi.mock('@/hooks/custom-content/useCustomContent', () => ({
useCustomContent: () => ({
dashboardAuthCustomProvider: undefined,
dashboardAuthCustomProviders: undefined,
}),
}))
vi.mock('@/hooks/misc/useEnabledIdentityProviders', () => ({
useEnabledIdentityProviders: () => [],
}))
vi.mock('@/hooks/misc/useInboundBranding', () => ({
useInboundBranding: () => ({ focusProvider: undefined }),
}))
vi.mock('@/components/interfaces/SignIn/LastSignInWrapper', () => ({
LastSignInWrapper: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}))
vi.mock('@/components/interfaces/SignIn/SignInForm', () => ({
SignInForm: () => <div>SignInForm</div>,
}))
vi.mock('@/components/interfaces/SignIn/SignInWithCustom', () => ({
SignInWithCustom: () => <div>SignInWithCustom</div>,
}))
vi.mock('@/components/interfaces/SignIn/SignInWithExternalProvider', () => ({
SignInWithExternalProvider: () => <div>SignInWithExternalProvider</div>,
}))
describe('/sign-in', () => {
beforeEach(() => {
mockUseSiwcQueryParamOptIn.mockClear()
mockUseIsFeatureEnabled.mockReturnValue({
dashboardAuthSignInWithSso: false,
dashboardAuthSignInWithEmail: false,
dashboardAuthSignUp: false,
})
})
it('calls useSiwcQueryParamOptIn so a shareable ?siwc-enabled=1 link can opt this browser in', () => {
render(<SignInPage dehydratedState={{}} />)
expect(mockUseSiwcQueryParamOptIn).toHaveBeenCalled()
})
})
+46
View File
@@ -0,0 +1,46 @@
import { render } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import SignUpPage from '@/pages/sign-up'
const { mockUseSiwcQueryParamOptIn, mockUseIsFeatureEnabled } = vi.hoisted(() => ({
mockUseSiwcQueryParamOptIn: vi.fn(),
mockUseIsFeatureEnabled: vi.fn(),
}))
vi.mock('@/hooks/misc/useSiwcQueryParamOptIn', () => ({
useSiwcQueryParamOptIn: mockUseSiwcQueryParamOptIn,
}))
vi.mock('@/hooks/misc/useIsFeatureEnabled', () => ({
useIsFeatureEnabled: mockUseIsFeatureEnabled,
}))
vi.mock('@/hooks/misc/useEnabledIdentityProviders', () => ({
useEnabledIdentityProviders: () => [],
}))
vi.mock('@/hooks/misc/useInboundBranding', () => ({
useInboundBranding: () => ({ focusProvider: undefined }),
}))
vi.mock('@/components/interfaces/SignIn/SignInWithExternalProvider', () => ({
SignInWithExternalProvider: () => <div>SignInWithExternalProvider</div>,
}))
vi.mock('@/components/interfaces/SignIn/SignUpForm', () => ({
SignUpForm: () => <div>SignUpForm</div>,
}))
describe('/sign-up', () => {
beforeEach(() => {
mockUseSiwcQueryParamOptIn.mockClear()
mockUseIsFeatureEnabled.mockReturnValue({ dashboardAuthSignUp: true })
})
it('calls useSiwcQueryParamOptIn so a shareable ?siwc-enabled=1 link can opt this browser in', () => {
render(<SignUpPage dehydratedState={{}} />)
expect(mockUseSiwcQueryParamOptIn).toHaveBeenCalled()
})
})