From f9b3e3e21106df92a3b5b63a9ce80306c9957b6e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 18 Aug 2026 19:29:00 -0700 Subject: [PATCH 1/3] refactor(consent): fold cookie preferences into General > Privacy The consent settings were a top-level tab of their own, which is the wrong weight for something a user opens once. They are now a sub-view of General, reached from the Privacy section that already held the telemetry toggle, and that toggle moves with them so one page owns everything Sim collects. Cookies render only on the hosted service, the only deployment that sets them; telemetry renders everywhere, so the sub-view is useful on a self-hosted deployment too. Each cookie switch commits on change rather than staging behind a Save, matching the telemetry switch directly above it -- one interaction model per page, and no unsaved-consent state. saveConsents('custom') reads selectedConsents from the store at call time and the switch's write is synchronous, so the value a toggle stages is the value it commits. The open sub-view lives in the URL, so it is linkable and Back closes it. --- .../_shell/consent/consent-preferences.tsx | 21 ++- .../settings/[section]/settings.tsx | 9 +- .../components/cookie-preferences.test.tsx | 58 +++++++ .../general/components/cookie-preferences.tsx | 59 +++++++ .../general/components/privacy-view.tsx | 71 +++++++++ .../settings/components/general/general.tsx | 46 +++--- .../components/general/search-params.ts | 17 ++ .../components/privacy/privacy.test.tsx | 146 ------------------ .../settings/components/privacy/privacy.tsx | 93 ----------- .../[workspaceId]/settings/navigation.test.ts | 2 - .../components/settings/navigation.test.ts | 1 - apps/sim/components/settings/navigation.ts | 12 -- 12 files changed, 240 insertions(+), 295 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/general/components/cookie-preferences.test.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/general/components/cookie-preferences.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/general/components/privacy-view.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/general/search-params.ts delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/privacy/privacy.test.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/settings/components/privacy/privacy.tsx diff --git a/apps/sim/app/_shell/consent/consent-preferences.tsx b/apps/sim/app/_shell/consent/consent-preferences.tsx index 36946c22814..cdff59b40e0 100644 --- a/apps/sim/app/_shell/consent/consent-preferences.tsx +++ b/apps/sim/app/_shell/consent/consent-preferences.tsx @@ -42,15 +42,23 @@ const CONSENT_CATEGORY_COPY: Record = { }, } satisfies Record +interface ConsentPreferencesProps { + /** + * Called after a switch stages its new value, for a surface that commits per + * toggle. The banner omits it and commits from its own footer instead. + */ + onChange?: () => void +} + /** * The per-category consent switches, shared by the two surfaces that offer * them: the banner's expanded state and the Privacy settings page. Both write - * to `selectedConsents`; committing is the caller's, since the banner saves - * from its own footer and settings saves from the shell's header. + * to `selectedConsents`; whether that is then committed is the caller's, via + * {@link ConsentPreferencesProps.onChange}. * - * Must be rendered inside a `ConsentManagerProvider`. + * Must be rendered inside a `ConsentStoreProvider`. */ -export function ConsentPreferences() { +export function ConsentPreferences({ onChange }: ConsentPreferencesProps) { const { consents, selectedConsents, setSelectedConsent, getDisplayedConsents } = useConsentManager() @@ -78,7 +86,10 @@ export function ConsentPreferences() { id={inputId} checked={selectedConsents[type.name] ?? consents[type.name] ?? false} disabled={type.disabled} - onCheckedChange={(checked) => setSelectedConsent(type.name, checked)} + onCheckedChange={(checked) => { + setSelectedConsent(type.name, checked) + onChange?.() + }} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx index feaccc33dd8..9ba369ef92a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx @@ -4,7 +4,6 @@ import { useEffect } from 'react' import dynamic from 'next/dynamic' import { usePostHog } from 'posthog-js/react' import { useSession } from '@/lib/auth/auth-client' -import { isHosted } from '@/lib/core/config/env-flags' import { captureEvent } from '@/lib/posthog/client' import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider' import { General } from '@/app/workspace/[workspaceId]/settings/components/general/general' @@ -105,9 +104,6 @@ const DataRetentionSettings = dynamic(() => const DataDrainsSettings = dynamic(() => import('@/ee/data-drains/components/data-drains-settings').then((m) => m.DataDrainsSettings) ) -const Privacy = dynamic(() => - import('@/app/workspace/[workspaceId]/settings/components/privacy/privacy').then((m) => m.Privacy) -) const Desktop = dynamic(() => import('@/app/workspace/[workspaceId]/settings/components/desktop/desktop').then((m) => m.Desktop) ) @@ -146,9 +142,7 @@ export function SettingsPage({ section }: SettingsPageProps) { ? 'general' : normalizedSection === 'mothership' && !sessionLoading && !isAdminRole ? 'general' - : normalizedSection === 'privacy' && !isHosted - ? 'general' - : normalizedSection + : normalizedSection const organizationId = hostContext.hostOrganizationId const meta = getSettingsSectionMeta(effectiveSection) @@ -163,7 +157,6 @@ export function SettingsPage({ section }: SettingsPageProps) { return ( {effectiveSection === 'general' && } - {effectiveSection === 'privacy' && } {effectiveSection === 'desktop' && } {effectiveSection === 'browser' && } {effectiveSection === 'terminal' && } diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/components/cookie-preferences.test.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/components/cookie-preferences.test.tsx new file mode 100644 index 00000000000..de586b01dc1 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/components/cookie-preferences.test.tsx @@ -0,0 +1,58 @@ +/** + * @vitest-environment jsdom + */ +import type { ReactNode } from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockUseConsentManager, mockSaveConsents } = vi.hoisted(() => ({ + mockUseConsentManager: vi.fn(), + mockSaveConsents: vi.fn(), +})) + +vi.mock('@sim/emcn', () => ({ toast: { success: vi.fn(), error: vi.fn() } })) +vi.mock('@c15t/nextjs/headless', () => ({ useConsentManager: mockUseConsentManager })) +vi.mock('@/app/_shell/consent/consent-store-provider', () => ({ + ConsentStoreProvider: ({ children }: { children: ReactNode }) => children, +})) +vi.mock('@/app/_shell/consent/consent-preferences', () => ({ + CONSENT_LINK_CLASS: 'link', + ConsentPreferences: ({ onChange }: { onChange?: () => void }) => ( +