chore: add usercentrics for consent management (#35384)

* chore: add usercentrics for consent management

* client component to make next.js happy

* address feedback

* move consent state to common

* fix import

* ensure page events are sent correctly

* add feature flag provider to ui library site

* fix ui lib 500 error

* skip in test env

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Jordi Enric <jordi.err@gmail.com>
This commit is contained in:
Alaister Young
2025-05-05 13:18:48 +08:00
committed by GitHub
parent 3c3fe9b5f1
commit 2b419c70a1
34 changed files with 443 additions and 362 deletions

View File

@@ -1,15 +0,0 @@
'use client'
import { useEffect } from 'react'
import { useConsent } from 'ui-patterns/ConsentToast'
/**
* Helper client component to trigger the consent toast. It has to be a client component.
*/
export const TriggerConsentToast = () => {
const { triggerConsentToast } = useConsent()
useEffect(() => {
setTimeout(() => triggerConsentToast(), 1000)
}, [triggerConsentToast])
return null
}

View File

@@ -1,11 +1,11 @@
'use client'
import { IS_PLATFORM, PageTelemetry as PageTelemetryImpl } from 'common'
import { useConsent } from 'ui-patterns/ConsentToast'
import { useConsentToast } from 'ui-patterns/consent'
import { API_URL } from '~/lib/constants'
const PageTelemetry = () => {
const { hasAcceptedConsent } = useConsent()
const { hasAcceptedConsent } = useConsentToast()
return (
<PageTelemetryImpl

View File

@@ -109,7 +109,7 @@
"unist-util-filter": "^4.0.1",
"unist-util-visit": "^4.1.2",
"uuid": "^9.0.1",
"valtio": "^1.12.0",
"valtio": "catalog:",
"yaml": "^2.4.5",
"zod": "^3.22.4"
},

View File

@@ -1,24 +1,27 @@
import { Toggle } from 'ui'
import { useConsentState } from 'common'
import Panel from 'components/ui/Panel'
import { useSendResetMutation } from 'data/telemetry/send-reset-mutation'
import { useAppStateSnapshot } from 'state/app-state'
export const AnalyticsSettings = () => {
const snap = useAppStateSnapshot()
const { hasAccepted, acceptAll, denyAll } = useConsentState()
const { mutate: sendReset } = useSendResetMutation()
const onToggleOptIn = () => {
const value = !snap.isOptedInTelemetry ? 'true' : 'false'
snap.setIsOptedInTelemetry(value === 'true')
if (value === 'false') sendReset()
if (hasAccepted) {
denyAll()
sendReset()
} else {
acceptAll()
}
}
return (
<Panel title={<h5 key="panel-title">Analytics</h5>}>
<Panel.Content>
<Toggle
checked={snap.isOptedInTelemetry}
checked={hasAccepted}
onChange={onToggleOptIn}
label="Opt-in to send telemetry data from the dashboard"
descriptionText="By opting into sending telemetry data, Supabase can improve the overall dashboard user experience"

View File

@@ -9,7 +9,6 @@ import { useSendGroupsResetMutation } from 'data/telemetry/send-groups-reset-mut
import { usePrevious } from 'hooks/deprecated'
import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
const getAnonId = async (id: string) => {
const encoder = new TextEncoder()
@@ -29,7 +28,6 @@ const GroupsTelemetry = ({ hasAcceptedConsent }: { hasAcceptedConsent: boolean }
const user = useUser()
const router = useRouter()
const { ref, slug } = useParams()
const snap = useAppStateSnapshot()
const organization = useSelectedOrganization()
const previousPathname = usePrevious(router.pathname)
@@ -41,13 +39,6 @@ const GroupsTelemetry = ({ hasAcceptedConsent }: { hasAcceptedConsent: boolean }
const referrer = typeof document !== 'undefined' ? document?.referrer : ''
useTelemetryCookie({ hasAcceptedConsent, title, referrer })
useEffect(() => {
if (hasAcceptedConsent) {
snap.setIsOptedInTelemetry(hasAcceptedConsent)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasAcceptedConsent])
useEffect(() => {
// don't set the sentry user id if the user hasn't logged in (so that Sentry errors show null user id instead of anonymous id)
if (!user?.id) {

View File

@@ -1,7 +1,7 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { components } from 'api-types'
import { LOCAL_STORAGE_KEYS } from 'common'
import { hasConsented } from 'common'
import { handleError, post } from 'data/fetchers'
import { IS_PLATFORM } from 'lib/constants'
import type { ResponseError } from 'types'
@@ -9,10 +9,7 @@ import type { ResponseError } from 'types'
export type SendGroupsIdentifyVariables = components['schemas']['TelemetryGroupsIdentityBody']
export async function sendGroupsIdentify({ body }: { body: SendGroupsIdentifyVariables }) {
const consent =
(typeof window !== 'undefined'
? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
: null) === 'true'
const consent = hasConsented()
if (!consent || !IS_PLATFORM) return undefined

View File

@@ -1,7 +1,7 @@
import { useMutation, UseMutationOptions } from '@tanstack/react-query'
import { components } from 'api-types'
import { LOCAL_STORAGE_KEYS } from 'common'
import { hasConsented } from 'common'
import { handleError, post } from 'data/fetchers'
import { IS_PLATFORM } from 'lib/constants'
import type { ResponseError } from 'types'
@@ -9,10 +9,7 @@ import type { ResponseError } from 'types'
export type SendGroupsResetVariables = components['schemas']['TelemetryGroupsResetBody']
export async function sendGroupsReset({ body }: { body: SendGroupsResetVariables }) {
const consent =
(typeof window !== 'undefined'
? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
: null) === 'true'
const consent = hasConsented()
if (!consent || !IS_PLATFORM) return undefined

View File

@@ -1,15 +1,12 @@
import { components } from 'api-types'
import { LOCAL_STORAGE_KEYS } from 'common'
import { hasConsented } from 'common'
import { handleError, post } from 'data/fetchers'
import { IS_PLATFORM } from './constants'
type TrackFeatureFlagVariables = components['schemas']['TelemetryFeatureFlagBodyDto']
export async function trackFeatureFlag(body: TrackFeatureFlagVariables) {
const consent =
(typeof window !== 'undefined'
? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
: null) === 'true'
const consent = hasConsented()
if (!consent || !IS_PLATFORM) return undefined
const { data, error } = await post(`/platform/telemetry/feature-flags/track`, { body })

View File

@@ -0,0 +1,22 @@
import { PageTelemetry } from 'common'
import GroupsTelemetry from 'components/ui/GroupsTelemetry'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { useConsentToast } from 'ui-patterns/consent'
export function Telemetry() {
// Although this is "technically" breaking the rules of hooks
// IS_PLATFORM never changes within a session, so this won't cause any issues
// eslint-disable-next-line react-hooks/rules-of-hooks
const { hasAcceptedConsent } = IS_PLATFORM ? useConsentToast() : { hasAcceptedConsent: true }
return (
<>
<PageTelemetry
API_URL={API_URL}
hasAcceptedConsent={hasAcceptedConsent}
enabled={IS_PLATFORM}
/>
<GroupsTelemetry hasAcceptedConsent={hasAcceptedConsent} />
</>
)
}

View File

@@ -56,14 +56,17 @@ const SUPABASE_ASSETS_URL =
? 'https://frontend-assets.supabase.green'
: 'https://frontend-assets.supabase.com'
const USERCENTRICS_URLS = 'https://*.usercentrics.eu'
const USERCENTRICS_APP_URL = 'https://app.usercentrics.eu'
// used by vercel live preview
const PUSHER_URL = 'https://*.pusher.com'
const PUSHER_URL_WS = 'wss://*.pusher.com'
const DEFAULT_SRC_URLS = `${API_URL} ${SUPABASE_URL} ${GOTRUE_URL} ${SUPABASE_LOCAL_PROJECTS_URL_WS} ${SUPABASE_PROJECTS_URL} ${SUPABASE_PROJECTS_URL_WS} ${HCAPTCHA_SUBDOMAINS_URL} ${CONFIGCAT_URL} ${STRIPE_SUBDOMAINS_URL} ${STRIPE_NETWORK_URL} ${CLOUDFLARE_URL} ${ONE_ONE_ONE_ONE_URL} ${VERCEL_INSIGHTS_URL} ${GITHUB_API_URL} ${GITHUB_USER_CONTENT_URL} ${SUPABASE_ASSETS_URL}`
const DEFAULT_SRC_URLS = `${API_URL} ${SUPABASE_URL} ${GOTRUE_URL} ${SUPABASE_LOCAL_PROJECTS_URL_WS} ${SUPABASE_PROJECTS_URL} ${SUPABASE_PROJECTS_URL_WS} ${HCAPTCHA_SUBDOMAINS_URL} ${CONFIGCAT_URL} ${STRIPE_SUBDOMAINS_URL} ${STRIPE_NETWORK_URL} ${CLOUDFLARE_URL} ${ONE_ONE_ONE_ONE_URL} ${VERCEL_INSIGHTS_URL} ${GITHUB_API_URL} ${GITHUB_USER_CONTENT_URL} ${SUPABASE_ASSETS_URL} ${USERCENTRICS_URLS}`
const SCRIPT_SRC_URLS = `${CLOUDFLARE_CDN_URL} ${HCAPTCHA_JS_URL} ${STRIPE_JS_URL} ${SUPABASE_ASSETS_URL}`
const FRAME_SRC_URLS = `${HCAPTCHA_ASSET_URL} ${STRIPE_JS_URL}`
const IMG_SRC_URLS = `${SUPABASE_URL} ${SUPABASE_COM_URL} ${SUPABASE_PROJECTS_URL} ${GITHUB_USER_AVATAR_URL} ${GOOGLE_USER_AVATAR_URL} ${SUPABASE_ASSETS_URL}`
const IMG_SRC_URLS = `${SUPABASE_URL} ${SUPABASE_COM_URL} ${SUPABASE_PROJECTS_URL} ${GITHUB_USER_AVATAR_URL} ${GOOGLE_USER_AVATAR_URL} ${SUPABASE_ASSETS_URL} ${USERCENTRICS_APP_URL}`
const STYLE_SRC_URLS = `${CLOUDFLARE_CDN_URL} ${SUPABASE_ASSETS_URL}`
const FONT_SRC_URLS = `${CLOUDFLARE_CDN_URL} ${SUPABASE_ASSETS_URL}`

View File

@@ -125,7 +125,7 @@
"ui-patterns": "workspace:*",
"use-debounce": "^7.0.1",
"uuid": "^9.0.1",
"valtio": "^1.12.0",
"valtio": "catalog:",
"yup": "^1.4.0",
"yup-password": "^0.3.0",
"zod": "^3.22.4",

View File

@@ -31,7 +31,7 @@ import { NuqsAdapter } from 'nuqs/adapters/next/pages'
import { ErrorInfo } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { FeatureFlagProvider, PageTelemetry, ThemeProvider, useThemeSandbox } from 'common'
import { FeatureFlagProvider, ThemeProvider, useThemeSandbox } from 'common'
import MetaFaviconsPagesRouter from 'common/MetaFavicons/pages-router'
import { RouteValidationWrapper } from 'components/interfaces/App'
import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext'
@@ -41,18 +41,17 @@ import FeaturePreviewModal from 'components/interfaces/App/FeaturePreview/Featur
import { MonacoThemeProvider } from 'components/interfaces/App/MonacoThemeProvider'
import { GenerateSql } from 'components/interfaces/SqlGenerator/SqlGenerator'
import { ErrorBoundaryState } from 'components/ui/ErrorBoundaryState'
import GroupsTelemetry from 'components/ui/GroupsTelemetry'
import { useRootQueryClient } from 'data/query-client'
import { customFont, sourceCodePro } from 'fonts'
import { AuthProvider } from 'lib/auth'
import { getFlags as getConfigCatFlags } from 'lib/configcat'
import { API_URL, BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { ProfileProvider } from 'lib/profile'
import { Telemetry } from 'lib/telemetry'
import HCaptchaLoadedStore from 'stores/hcaptcha-loaded-store'
import { AppPropsWithLayout } from 'types'
import { SonnerToaster, TooltipProvider } from 'ui'
import { CommandProvider } from 'ui-patterns/CommandMenu'
import { useConsent } from 'ui-patterns/ConsentToast'
dayjs.extend(customParseFormat)
dayjs.extend(utc)
@@ -93,11 +92,6 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
useThemeSandbox()
// Although this is "technically" breaking the rules of hooks
// IS_PLATFORM never changes within a session, so this won't cause any issues
// eslint-disable-next-line react-hooks/rules-of-hooks
const { hasAcceptedConsent } = IS_PLATFORM ? useConsent() : { hasAcceptedConsent: true }
const isTestEnv = process.env.NEXT_PUBLIC_NODE_ENV === 'test'
return (
@@ -146,12 +140,7 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
</ThemeProvider>
</RouteValidationWrapper>
</TooltipProvider>
<PageTelemetry
API_URL={API_URL}
hasAcceptedConsent={hasAcceptedConsent}
enabled={IS_PLATFORM}
/>
<GroupsTelemetry hasAcceptedConsent={hasAcceptedConsent} />
<Telemetry />
{!isTestEnv && <HCaptchaLoadedStore />}
{!isTestEnv && (
<ReactQueryDevtools initialIsOpen={false} position="bottom-right" />

View File

@@ -3,8 +3,8 @@
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs'
import { hasConsented } from 'common'
import { IS_PLATFORM } from 'common/constants/environment'
import { LOCAL_STORAGE_KEYS } from 'common/constants/local-storage'
import { match } from 'path-to-regexp'
// This is a workaround to ignore hCaptcha related errors.
@@ -26,12 +26,9 @@ Sentry.init({
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
beforeSend(event, hint) {
const consent =
typeof window !== 'undefined'
? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
: null
const consent = hasConsented()
if (IS_PLATFORM && consent === 'true') {
if (IS_PLATFORM && consent) {
// Ignore invalid URL events for 99% of the time because it's using up a lot of quota.
const isInvalidUrlEvent = (hint.originalException as any)?.message?.includes(
`Failed to construct 'URL': Invalid URL`

View File

@@ -1,6 +1,5 @@
import { proxy, snapshot, subscribe, useSnapshot } from 'valtio'
import { LOCAL_STORAGE_KEYS as COMMON_LOCAL_STORAGE_KEYS } from 'common'
import { SQL_TEMPLATES } from 'components/interfaces/SQLEditor/SQLEditor.queries'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
@@ -52,7 +51,6 @@ const getInitialState = () => {
activeDocsSection: ['introduction'],
docsLanguage: 'js',
showProjectApiDocs: false,
isOptedInTelemetry: false,
showEnableBranchingModal: false,
showFeaturePreviewModal: false,
selectedFeaturePreview: '',
@@ -84,7 +82,6 @@ const getInitialState = () => {
activeDocsSection: ['introduction'],
docsLanguage: 'js',
showProjectApiDocs: false,
isOptedInTelemetry: false,
showEnableBranchingModal: false,
showFeaturePreviewModal: false,
selectedFeaturePreview: '',
@@ -124,14 +121,6 @@ export const appState = proxy({
appState.docsLanguage = value
},
isOptedInTelemetry: false,
setIsOptedInTelemetry: (value: boolean | null) => {
appState.isOptedInTelemetry = value === null ? false : value
if (typeof window !== 'undefined' && value !== null) {
localStorage.setItem(COMMON_LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT, value.toString())
}
},
showEnableBranchingModal: false,
setShowEnableBranchingModal: (value: boolean) => {
appState.showEnableBranchingModal = value

View File

@@ -2,10 +2,10 @@
import { API_URL } from '@/lib/constants'
import { IS_PLATFORM, PageTelemetry } from 'common'
import { useConsent } from 'ui-patterns/ConsentToast'
import { useConsentToast } from 'ui-patterns/consent'
export const TelemetryWrapper = () => {
const { hasAcceptedConsent } = useConsent()
const { hasAcceptedConsent } = useConsentToast()
return (
<PageTelemetry
API_URL={API_URL}

View File

@@ -8,7 +8,6 @@ import { FrameworkProvider } from '@/context/framework-context'
import { MobileMenuProvider } from '@/context/mobile-menu-context'
import { AuthProvider } from 'common'
import { TooltipProvider } from 'ui'
import { useConsent } from 'ui-patterns/ConsentToast'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (

View File

@@ -1,6 +1,8 @@
import type { Metadata } from 'next'
import '@/styles/globals.css'
import { API_URL } from '@/lib/constants'
import { FeatureFlagProvider } from 'common'
import { genFaviconData } from 'common/MetaFavicons/app-router'
import { Inter } from 'next/font/google'
import { ThemeProvider } from './Providers'
@@ -41,14 +43,16 @@ export default async function Layout({ children }: RootLayoutProps) {
<html lang="en" suppressHydrationWarning>
<head />
<body className={`${inter.className} antialiased`}>
<ThemeProvider
themes={['dark', 'light', 'classic-dark']}
defaultTheme="system"
enableSystem
>
{children}
<SonnerToaster />
</ThemeProvider>
<FeatureFlagProvider API_URL={API_URL}>
<ThemeProvider
themes={['dark', 'light', 'classic-dark']}
defaultTheme="system"
enableSystem
>
{children}
<SonnerToaster />
</ThemeProvider>
</FeatureFlagProvider>
</body>
</html>
)

View File

@@ -16,7 +16,7 @@ import Head from 'next/head'
import { useRouter } from 'next/router'
import { SonnerToaster, themes, TooltipProvider } from 'ui'
import { CommandProvider } from 'ui-patterns/CommandMenu'
import { useConsent } from 'ui-patterns/ConsentToast'
import { useConsentToast } from 'ui-patterns/consent'
import MetaFaviconsPagesRouter, {
DEFAULT_FAVICON_ROUTE,
@@ -28,7 +28,7 @@ import useDarkLaunchWeeks from '../hooks/useDarkLaunchWeeks'
export default function App({ Component, pageProps }: AppProps) {
const router = useRouter()
const { hasAcceptedConsent } = useConsent()
const { hasAcceptedConsent } = useConsentToast()
useThemeSandbox()

View File

@@ -0,0 +1,109 @@
import type Usercentrics from '@usercentrics/cmp-browser-sdk'
import type { BaseCategory, UserDecision } from '@usercentrics/cmp-browser-sdk'
import { proxy, snapshot, useSnapshot } from 'valtio'
import { LOCAL_STORAGE_KEYS } from './constants'
export const consentState = proxy({
// Usercentrics state
UC: null as Usercentrics | null,
categories: null as BaseCategory[] | null,
// Our state
showConsentToast: false,
hasConsented: false,
acceptAll: () => {
if (!consentState.UC) return
const previousConsentValue = consentState.hasConsented
consentState.hasConsented = true
consentState.showConsentToast = false
consentState.UC.acceptAllServices()
.then(() => {
consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null
})
.catch(() => {
consentState.hasConsented = previousConsentValue
consentState.showConsentToast = true
})
},
denyAll: () => {
if (!consentState.UC) return
const previousConsentValue = consentState.hasConsented
consentState.hasConsented = false
consentState.showConsentToast = false
consentState.UC.denyAllServices()
.then(() => {
consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null
})
.catch(() => {
consentState.showConsentToast = previousConsentValue
})
},
updateServices: (decisions: UserDecision[]) => {
if (!consentState.UC) return
consentState.showConsentToast = false
consentState.UC.updateServices(decisions)
.then(() => {
consentState.hasConsented = consentState.UC?.areAllConsentsAccepted() ?? false
consentState.categories = consentState.UC?.getCategoriesBaseInfo() ?? null
})
.catch(() => {
consentState.showConsentToast = true
})
},
})
async function initUserCentrics() {
if (process.env.NODE_ENV === 'test') return
const { default: Usercentrics } = await import('@usercentrics/cmp-browser-sdk')
const UC = new Usercentrics(process.env.NEXT_PUBLIC_USERCENTRICS_RULESET_ID!, {
rulesetId: process.env.NEXT_PUBLIC_USERCENTRICS_RULESET_ID,
useRulesetId: true,
})
const initialUIValues = await UC.init()
consentState.UC = UC
const hasConsented = UC.areAllConsentsAccepted()
// 0 = first layer, aka show consent toast
consentState.showConsentToast = initialUIValues.initialLayer === 0
consentState.hasConsented = hasConsented
consentState.categories = UC.getCategoriesBaseInfo()
// If the user has previously consented (before usercentrics), accept all services
if (!hasConsented && localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) === 'true') {
consentState.acceptAll()
localStorage.removeItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
}
}
// Usercentrics is not available on the server
if (typeof window !== 'undefined') {
initUserCentrics()
}
// Public API for consent
export function hasConsented() {
return snapshot(consentState).hasConsented
}
export function useConsentState() {
const snap = useSnapshot(consentState)
return {
hasAccepted: snap.hasConsented,
categories: snap.categories as BaseCategory[] | null,
acceptAll: snap.acceptAll,
denyAll: snap.denyAll,
updateServices: snap.updateServices,
}
}

View File

@@ -1,4 +1,5 @@
export const LOCAL_STORAGE_KEYS = {
/** @deprecated we're using usercentrics instead to handle telemetry consent */
TELEMETRY_CONSENT: 'supabase-consent-ph',
TELEMETRY_DATA: 'supabase-telemetry-data',
HIDE_PROMO_TOAST: 'supabase-hide-promo-toast-lw13-d1',

View File

@@ -5,7 +5,7 @@ import { createContext, PropsWithChildren, useContext, useEffect, useState } fro
import { components } from 'api-types'
import { useUser } from './auth'
import { LOCAL_STORAGE_KEYS } from './constants'
import { hasConsented } from './consent-state'
import { get, post } from './fetchWrappers'
import { ensurePlatformSuffix } from './helpers'
@@ -19,10 +19,7 @@ export async function getFeatureFlags(API_URL: string) {
}
export async function trackFeatureFlag(API_URL: string, body: TrackFeatureFlagVariables) {
const consent =
(typeof window !== 'undefined'
? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
: null) === 'true'
const consent = hasConsented()
if (!consent) return undefined
await post(`${ensurePlatformSuffix(API_URL)}/telemetry/feature-flags/track`, { body })
@@ -83,15 +80,21 @@ export const FeatureFlagProvider = ({
let flagStore: FeatureFlagContextType = { configcat: {}, posthog: {} }
// Load PH flags
const flags = await getFeatureFlags(API_URL)
// Run both async operations in parallel
const [flags, flagValues] = await Promise.all([
getFeatureFlags(API_URL),
typeof getConfigCatFlags === 'function'
? getConfigCatFlags(user?.email)
: Promise.resolve([]),
])
// Process PostHog flags
if (flags) {
flagStore.posthog = flags
}
// Load ConfigCat flags
// Process ConfigCat flags
if (typeof getConfigCatFlags === 'function') {
const flagValues = await getConfigCatFlags(user?.email)
let overridesCookieValue: Record<string, boolean> = {}
try {
const cookies = getCookies()

View File

@@ -1,4 +1,5 @@
export * from './auth'
export * from './consent-state'
export * from './constants'
export * from './database-types'
export * from './gotrue'

View File

@@ -12,13 +12,15 @@
},
"dependencies": {
"@types/dat.gui": "^0.7.12",
"@usercentrics/cmp-browser-sdk": "^4.42.0",
"@vercel/flags": "^2.6.0",
"api-types": "workspace:*",
"config": "workspace:*",
"dat.gui": "^0.7.9",
"lodash": "^4.17.21",
"next-themes": "^0.3.0",
"react-use": "^17.4.0"
"react-use": "^17.4.0",
"valtio": "catalog:"
},
"devDependencies": {
"@types/lodash": "4.17.5",

View File

@@ -4,15 +4,19 @@ import { components } from 'api-types'
import { useRouter } from 'next/compat/router'
import { usePathname } from 'next/navigation'
import { useCallback, useEffect, useRef } from 'react'
import { useLatest } from 'react-use'
import { useUser } from './auth'
import { hasConsented } from './consent-state'
import { LOCAL_STORAGE_KEYS } from './constants'
import { useFeatureFlags } from './feature-flags'
import { post } from './fetchWrappers'
import { ensurePlatformSuffix, isBrowser } from './helpers'
import { useTelemetryCookie } from './hooks'
import { TelemetryEvent } from './telemetry-constants'
import { useUser } from './auth'
import { getSharedTelemetryData } from './telemetry-utils'
const { TELEMETRY_DATA } = LOCAL_STORAGE_KEYS
//---
// PAGE TELEMETRY
//---
@@ -74,51 +78,67 @@ export const PageTelemetry = ({
const referrer = typeof document !== 'undefined' ? document?.referrer : ''
useTelemetryCookie({ hasAcceptedConsent, title, referrer })
const pathname =
pagesPathname ?? appPathname ?? (isBrowser ? window.location.pathname : undefined)
const pathnameRef = useLatest(pathname)
const featureFlagsRef = useLatest(featureFlags.posthog)
const sendPageTelemetry = useCallback(() => {
if (!(enabled && hasAcceptedConsent)) return Promise.resolve()
return handlePageTelemetry(
API_URL,
pagesPathname ?? appPathname ?? undefined,
featureFlags.posthog
).catch((e) => {
return handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current).catch((e) => {
console.error('Problem sending telemetry page:', e)
})
}, [API_URL, pagesPathname, appPathname, hasAcceptedConsent, featureFlags.posthog])
}, [API_URL, enabled, hasAcceptedConsent])
const sendPageLeaveTelemetry = useCallback(() => {
if (!(enabled && hasAcceptedConsent)) return Promise.resolve()
return handlePageTelemetry(
API_URL,
pagesPathname ?? appPathname ?? undefined,
featureFlags.posthog
).catch((e) => {
return handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current).catch((e) => {
console.error('Problem sending telemetry page-leave:', e)
})
}, [pagesPathname, appPathname, hasAcceptedConsent, featureFlags.posthog])
}, [API_URL, enabled, hasAcceptedConsent])
// Handle initial page telemetry event
const hasSentInitialPageTelemetryRef = useRef(false)
useEffect(() => {
// Send page telemetry on first page load
// Waiting for router ready before sending page_view
// if not the path will be dynamic route instead of the browser url
if (
(router?.isReady ?? true) &&
hasAcceptedConsent &&
featureFlags.hasLoaded &&
!hasSentInitialPageTelemetryRef.current
) {
sendPageTelemetry()
const cookies = document.cookie.split(';')
const telemetryCookie = cookies.find((cookie) => cookie.trim().startsWith(TELEMETRY_DATA))
if (telemetryCookie) {
try {
const encodedData = telemetryCookie.split('=')[1]
const telemetryData = JSON.parse(decodeURIComponent(encodedData))
handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current, telemetryData)
// remove the telemetry cookie
document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
} catch (error) {
console.error('Invalid telemetry data:', error)
}
} else {
handlePageTelemetry(API_URL, pathnameRef.current, featureFlagsRef.current)
}
hasSentInitialPageTelemetryRef.current = true
}
}, [router?.isReady, featureFlags.hasLoaded])
}, [router?.isReady, hasAcceptedConsent, featureFlags.hasLoaded])
useEffect(() => {
// For pages router
if (router === null) return
function handleRouteChange() {
// Wait until we've sent the initial page telemetry event
if (!hasSentInitialPageTelemetryRef.current) return
sendPageTelemetry()
}
@@ -134,7 +154,8 @@ export const PageTelemetry = ({
// For app router
if (router !== null) return
if (appPathname) {
// Wait until we've sent the initial page telemetry event
if (appPathname && !hasSentInitialPageTelemetryRef.current) {
sendPageTelemetry()
}
}, [appPathname, router, sendPageTelemetry])
@@ -161,11 +182,7 @@ export const PageTelemetry = ({
type EventBody = components['schemas']['TelemetryEventBodyV2Dto']
export function sendTelemetryEvent(API_URL: string, event: TelemetryEvent, pathname?: string) {
const consent =
(typeof window !== 'undefined'
? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
: null) === 'true'
const consent = hasConsented()
if (!consent) return
const body: EventBody = {
@@ -194,10 +211,7 @@ export function sendTelemetryEvent(API_URL: string, event: TelemetryEvent, pathn
type IdentifyBody = components['schemas']['TelemetryIdentifyBodyV2']
export function sendTelemetryIdentify(API_URL: string, body: IdentifyBody) {
const consent =
(typeof window !== 'undefined'
? localStorage.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
: null) === 'true'
const consent = hasConsented()
if (!consent) return Promise.resolve()

View File

@@ -17,7 +17,7 @@ export const ConsentToast = ({ onAccept = noop, onOptOut = noop }: ConsentToastP
<div className="py-1 flex flex-col gap-y-3 w-full">
<div>
<p className="text-sm text-foreground">
We use first-party cookies to improve our services.{' '}
We use cookies to collect data and improve our services.{' '}
<a
target="_blank"
rel="noreferrer noopener"
@@ -67,5 +67,3 @@ export const ConsentToast = ({ onAccept = noop, onOptOut = noop }: ConsentToastP
</div>
)
}
export { useConsent, useConsentValue } from '../shared/consent'

View File

@@ -1,40 +1,45 @@
'use client'
import { handleResetTelemetry, LOCAL_STORAGE_KEYS } from 'common'
import { useConsentState } from 'common'
import Link from 'next/link'
import { PropsWithChildren, useEffect, useState } from 'react'
import { PropsWithChildren, useState } from 'react'
import { Modal, Toggle } from 'ui'
import { useConsentValue } from '../shared/consent'
interface PrivacySettingsProps {
className?: string
}
export const PrivacySettings = ({
children,
...props
}: PropsWithChildren<{ className?: string }>) => {
}: PropsWithChildren<PrivacySettingsProps>) => {
const [isOpen, setIsOpen] = useState(false)
const { hasAccepted, handleConsent } = useConsentValue(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT)
const [telemetryValue, setTelemetryValue] = useState(hasAccepted)
const { categories, updateServices } = useConsentState()
// Every time the modal opens, sync state with localStorage
useEffect(() => {
setTelemetryValue(localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) === 'true')
}, [isOpen])
const [serviceConsentMap, setServiceConsentMap] = useState(() => new Map<string, boolean>())
function handleServicesChange(services: { id: string; status: boolean }[]) {
let newServiceConsentMap = new Map(serviceConsentMap)
services.forEach((service) => {
newServiceConsentMap.set(service.id, service.status)
})
setServiceConsentMap(newServiceConsentMap)
}
const handleConfirmPreferences = () => {
handleConsent && handleConsent(telemetryValue ? 'true' : 'false')
const services = Array.from(serviceConsentMap.entries()).map(([id, status]) => ({
serviceId: id,
status,
}))
updateServices(services)
setIsOpen(false)
}
const handleCancel = () => {
setTelemetryValue(hasAccepted)
setIsOpen(false)
}
const handleOptOutTelemetry = async () => {
// remove telemetry data from cookies
document.cookie = `${LOCAL_STORAGE_KEYS.TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
handleResetTelemetry(process.env.NEXT_PUBLIC_API_URL!)
}
return (
<>
<button {...props} onClick={() => setIsOpen(true)}>
@@ -58,56 +63,76 @@ export const PrivacySettings = ({
className="max-w-[calc(100vw-4rem)]"
size="medium"
>
<div className="pt-6 pb-3 space-y-4">
<Modal.Content>
<Toggle
checked={true}
disabled
onChange={() => null}
label="Strictly necessary cookies"
descriptionText={
<>
These cookies are necessary for Supabase to function.{' '}
<Link
href="https://supabase.com/privacy#8-cookies-and-similar-technologies-used-on-our-european-services"
className="underline"
>
Learn more
</Link>
</>
}
/>
</Modal.Content>
<Modal.Separator />
<Modal.Content>
<Toggle
checked={telemetryValue}
onChange={() => {
if (telemetryValue) {
// [Joshen] Will be toggle off, so trigger reset event
handleOptOutTelemetry()
}
setTelemetryValue((prev) => !prev)
}}
label="Telemetry"
descriptionText={
<>
By opting in to sending telemetry data, Supabase can improve the overall user
experience.{' '}
<a
target="_blank"
rel="noreferrer noopener"
className="underline"
href="https://supabase.com/privacy#8-cookies-and-similar-technologies-used-on-our-european-services"
>
Learn more
</a>
</>
}
/>
</Modal.Content>
<div className="pt-3 divide-y divide-border">
{categories
?.toReversed()
.map((category) => (
<Category
key={category.slug}
category={category}
handleServicesChange={handleServicesChange}
/>
))}
</div>
</Modal>
</>
)
}
function Category({
category,
handleServicesChange,
}: {
category: {
slug: string
label: string
description: string
isEssential: boolean
services: readonly {
id: string
consent: {
status: boolean
}
}[]
}
handleServicesChange: (services: { id: string; status: boolean }[]) => void
}) {
const [isChecked, setIsChecked] = useState(() =>
category.services.every((service) => service.consent.status)
)
function handleChange() {
setIsChecked(!isChecked)
handleServicesChange(
category.services.map((service) => ({
id: service.id,
status: !isChecked,
}))
)
}
return (
<Modal.Content key={category.slug}>
<Toggle
checked={isChecked}
defaultChecked={isChecked}
disabled={category.isEssential}
onChange={handleChange}
label={category.label}
descriptionText={
<>
{category.description}
<br />
<Link
href="https://supabase.com/privacy#8-cookies-and-similar-technologies-used-on-our-european-services"
className="underline"
>
Learn more
</Link>
</>
}
/>
</Modal.Content>
)
}

View File

@@ -1,11 +1,11 @@
'use client'
// import Image from 'next/image'
import { hasConsented, LOCAL_STORAGE_KEYS } from 'common'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { cn } from 'ui/src/lib/utils/cn'
import { Button } from 'ui/src/components/Button/Button'
import { LOCAL_STORAGE_KEYS } from 'common'
import { cn } from 'ui/src/lib/utils/cn'
// import { useTheme } from 'next-themes'
import announcement from '../Banners/data.json'
import './styles.css'
@@ -16,8 +16,7 @@ const PromoToast = () => {
useEffect(() => {
const shouldHide =
!localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) ||
localStorage?.getItem(LOCAL_STORAGE_KEYS.HIDE_PROMO_TOAST) === 'true'
!hasConsented() || localStorage?.getItem(LOCAL_STORAGE_KEYS.HIDE_PROMO_TOAST) === 'true'
if (!shouldHide) {
setVisible(true)

View File

@@ -0,0 +1,59 @@
'use client'
import { consentState, isBrowser, LOCAL_STORAGE_KEYS } from 'common'
import { useCallback, useEffect, useRef } from 'react'
import { toast } from 'sonner'
import { cn } from 'ui'
import { useSnapshot } from 'valtio'
import { ConsentToast } from './ConsentToast'
const { TELEMETRY_DATA } = LOCAL_STORAGE_KEYS
export const useConsentToast = () => {
const consentToastId = useRef<string | number>()
const snap = useSnapshot(consentState)
const acceptAll = useCallback(() => {
if (!isBrowser) return
snap.acceptAll()
if (consentToastId.current) {
toast.dismiss(consentToastId.current)
}
}, [snap.acceptAll])
const denyAll = useCallback(() => {
if (!isBrowser) return
snap.denyAll()
// remove the telemetry cookie
document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
if (consentToastId.current) {
toast.dismiss(consentToastId.current)
}
}, [snap.denyAll])
useEffect(() => {
if (isBrowser && snap.showConsentToast) {
consentToastId.current = toast(<ConsentToast onAccept={acceptAll} onOptOut={denyAll} />, {
id: 'consent-toast',
position: 'bottom-right',
duration: Infinity,
closeButton: false,
dismissible: false,
className: cn(
'!w-screen !fixed !border-t !h-auto !left-0 !bottom-0 !top-auto !right-0 !rounded-none !max-w-none !bg-overlay !text',
'sm:!w-full sm:!max-w-[356px] sm:!left-auto sm:!right-8 sm:!bottom-8 sm:!rounded-lg sm:border'
),
})
} else if (consentToastId.current) {
toast.dismiss(consentToastId.current)
}
}, [snap.showConsentToast])
return {
hasAcceptedConsent: snap.hasConsented,
}
}

View File

@@ -3,7 +3,7 @@
* import directly from here.
*/
export * from './CommandMenu'
export * from './ConsentToast'
export * from './consent'
export * from './CountdownWidget'
export * from './ExpandableVideo'
export * from './GlassPanel'

View File

@@ -48,7 +48,7 @@
"tsconfig": "workspace:*",
"ui": "workspace:*",
"unist-util-visit": "^5.0.0",
"valtio": "*",
"valtio": "catalog:",
"zod": "^3.22.4"
},
"devDependencies": {

View File

@@ -1,134 +0,0 @@
import { LOCAL_STORAGE_KEYS, handlePageTelemetry, isBrowser } from 'common'
import { useCallback, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { proxy, useSnapshot } from 'valtio'
import { useRouter } from 'next/compat/router'
import { usePathname } from 'next/navigation'
import { useFeatureFlags } from 'common'
import { cn } from 'ui'
import { ConsentToast } from '../ConsentToast'
const consentState = proxy({
consentValue: (isBrowser ? localStorage?.getItem(LOCAL_STORAGE_KEYS.TELEMETRY_CONSENT) : null) as
| string
| null,
setConsentValue: (value: string | null) => {
consentState.consentValue = value
},
})
export const useConsent = () => {
const { TELEMETRY_CONSENT, TELEMETRY_DATA } = LOCAL_STORAGE_KEYS
const consentToastId = useRef<string | number>()
const router = useRouter()
const appRouterPathname = usePathname()
const featureFlags = useFeatureFlags()
const { consentValue } = useSnapshot(consentState)
const pathname =
router?.pathname ?? appRouterPathname ?? (isBrowser ? window.location.pathname : '')
const handleConsent = (value: 'true' | 'false') => {
if (!isBrowser) return
if (value === 'true') {
const cookies = document.cookie.split(';')
const telemetryCookie = cookies.find((cookie) => cookie.trim().startsWith(TELEMETRY_DATA))
if (telemetryCookie) {
try {
const encodedData = telemetryCookie.split('=')[1]
const telemetryData = JSON.parse(decodeURIComponent(encodedData))
handlePageTelemetry(
process.env.NEXT_PUBLIC_API_URL!,
pathname,
featureFlags.posthog,
telemetryData
)
// remove the telemetry cookie
document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
} catch (error) {
console.error('Invalid telemetry data:', error)
}
} else {
handlePageTelemetry(process.env.NEXT_PUBLIC_API_URL!, pathname, featureFlags.posthog)
}
} else {
// remove the telemetry cookie
document.cookie = `${TELEMETRY_DATA}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`
}
consentState.setConsentValue(value)
localStorage.setItem(TELEMETRY_CONSENT, value)
if (consentToastId.current) {
toast.dismiss(consentToastId.current)
}
}
const triggerConsentToast = useCallback(() => {
if (isBrowser && consentValue === null) {
consentToastId.current = toast(
<ConsentToast
onAccept={() => handleConsent('true')}
onOptOut={() => handleConsent('false')}
/>,
{
id: 'consent-toast',
position: 'bottom-right',
duration: Infinity,
closeButton: false,
dismissible: false,
className: cn(
'!w-screen !fixed !border-t !h-auto !left-0 !bottom-0 !top-auto !right-0 !rounded-none !max-w-none !bg-overlay !text',
'sm:!w-full sm:!max-w-[356px] sm:!left-auto sm:!right-8 sm:!bottom-8 sm:!rounded-lg sm:border'
),
}
)
}
}, [])
useEffect(() => {
const handleSetLocalStorage = () => {
if (localStorage?.getItem(TELEMETRY_CONSENT)) toast.dismiss(consentToastId.current)
}
if (isBrowser) {
window.addEventListener('storage', handleSetLocalStorage)
return window.removeEventListener('storage', () => null)
}
}, [])
useEffect(() => {
setTimeout(() => {
consentValue === null && triggerConsentToast()
}, 300)
}, [consentValue])
return {
consentValue,
hasAcceptedConsent: consentValue === 'true',
triggerConsentToast,
}
}
export const useConsentValue = (KEY_NAME: string) => {
const initialValue = isBrowser ? localStorage?.getItem(KEY_NAME) : null
const [consentValue, setConsentValue] = useState<string | null>(initialValue)
const handleConsent = (value: 'true' | 'false') => {
if (!isBrowser) return
setConsentValue(value)
localStorage.setItem(KEY_NAME, value)
window.dispatchEvent(new Event('storage'))
if (value === 'true') {
handlePageTelemetry(process.env.NEXT_PUBLIC_API_URL!, location.pathname)
}
}
return {
consentValue,
setConsentValue,
hasAccepted: consentValue === 'true',
handleConsent,
}
}

69
pnpm-lock.yaml generated
View File

@@ -33,6 +33,9 @@ catalogs:
react-dom:
specifier: ^18.3.0
version: 18.3.1
valtio:
specifier: ^1.12.0
version: 1.12.0
overrides:
'@supabase/supabase-js>@supabase/auth-js': 2.69.0-rc.3
@@ -439,7 +442,7 @@ importers:
specifier: ^9.0.1
version: 9.0.1
valtio:
specifier: ^1.12.0
specifier: 'catalog:'
version: 1.12.0(@types/react@18.3.3)(react@18.3.1)
yaml:
specifier: ^2.4.5
@@ -875,7 +878,7 @@ importers:
specifier: ^9.0.1
version: 9.0.1
valtio:
specifier: ^1.12.0
specifier: 'catalog:'
version: 1.12.0(@types/react@18.3.3)(react@18.3.1)
yup:
specifier: ^1.4.0
@@ -1592,9 +1595,12 @@ importers:
'@types/dat.gui':
specifier: ^0.7.12
version: 0.7.12
'@usercentrics/cmp-browser-sdk':
specifier: ^4.42.0
version: 4.42.0
'@vercel/flags':
specifier: ^2.6.0
version: 2.6.0(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 2.6.0(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
api-types:
specifier: workspace:*
version: link:../api-types
@@ -1609,7 +1615,7 @@ importers:
version: 4.17.21
next:
specifier: 'catalog:'
version: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)
version: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1622,6 +1628,9 @@ importers:
react-use:
specifier: ^17.4.0
version: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
valtio:
specifier: 'catalog:'
version: 1.12.0(@types/react@18.3.3)(react@18.3.1)
devDependencies:
'@types/lodash':
specifier: 4.17.5
@@ -2044,7 +2053,7 @@ importers:
version: 0.52.2
next:
specifier: '*'
version: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)
version: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)
next-themes:
specifier: '*'
version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -2112,7 +2121,7 @@ importers:
specifier: ^5.0.0
version: 5.0.0
valtio:
specifier: '*'
specifier: 'catalog:'
version: 1.12.0(@types/react@18.3.3)(react@18.3.1)
zod:
specifier: ^3.22.4
@@ -2153,7 +2162,7 @@ importers:
version: link:../api-types
next-router-mock:
specifier: ^0.9.13
version: 0.9.13(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react@18.3.1)
version: 0.9.13(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react@18.3.1)
typescript:
specifier: ~5.5.0
version: 5.5.2
@@ -6678,6 +6687,9 @@ packages:
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/d3-array@3.0.8':
resolution: {integrity: sha512-2xAVyAUgaXHX9fubjcCbGAUOqYfRJN1em1EKR2HfzWBpObZhwfnZKvofTN4TplMqJdFQao61I+NVSai/vnBvDQ==}
@@ -6867,6 +6879,10 @@ packages:
'@types/lodash@4.17.5':
resolution: {integrity: sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==}
'@types/lz-string@1.5.0':
resolution: {integrity: sha512-s84fKOrzqqNCAPljhVyC5TjAo6BH4jKHw9NRNFNiRUY5QSgZCmVm5XILlWbisiKl+0OcS7eWihmKGS5akc2iQw==}
deprecated: This is a stub types definition. lz-string provides its own type definitions, so you do not need this installed.
'@types/markdown-table@3.0.0':
resolution: {integrity: sha512-vX5zya26rnLLHdAKA3E9ZOT07CJslYp2gZfAZiXKUY5vd2RRiJyOQKrSlmUjyJ+eNoDAPkHflebG3PsfqC32aA==}
deprecated: This is a stub types definition. markdown-table provides its own type definitions, so you do not need this installed.
@@ -7082,6 +7098,9 @@ packages:
'@ungap/structured-clone@1.2.0':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
'@usercentrics/cmp-browser-sdk@4.42.0':
resolution: {integrity: sha512-/cik01TdeiYUV1+EasK83Ip05TDqmowM5tmWGfRDdaOrneSic5BedBdJHKya4pP2vwiYAtzDYVJe4BwPt2M16g==}
'@vercel/flags@2.6.0':
resolution: {integrity: sha512-GvLX0CK/OsIqq672OBvcCeu0k3tb3QdE0lRn1P5zulMQJIPTLlUDEH0y9TeAyC7TMWlpTTT5Bealqu0rmncyXQ==}
deprecated: This package was renamed to flags, which offers the same functionality. https://vercel.com/changelog/npm-i-flags
@@ -8760,6 +8779,9 @@ packages:
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
dynamic-import-polyfill@0.1.1:
resolution: {integrity: sha512-m953zv0w5oDagTItWm6Auhmk/pY7EiejaqiVbnzSS3HIjh1FCUeK7WzuaVtWPNs58A+/xpIE+/dVk6pKsrua8g==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -21555,6 +21577,8 @@ snapshots:
'@types/cookie@0.6.0': {}
'@types/crypto-js@4.2.2': {}
'@types/d3-array@3.0.8': {}
'@types/d3-axis@3.0.6':
@@ -21760,6 +21784,10 @@ snapshots:
'@types/lodash@4.17.5': {}
'@types/lz-string@1.5.0':
dependencies:
lz-string: 1.5.0
'@types/markdown-table@3.0.0':
dependencies:
markdown-table: 3.0.3
@@ -22018,13 +22046,15 @@ snapshots:
'@ungap/structured-clone@1.2.0': {}
'@vercel/flags@2.6.0(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@usercentrics/cmp-browser-sdk@4.42.0':
dependencies:
jose: 5.2.1
optionalDependencies:
next: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@types/crypto-js': 4.2.2
'@types/lz-string': 1.5.0
'@types/uuid': 8.3.4
crypto-js: 4.2.0
dynamic-import-polyfill: 0.1.1
lz-string: 1.5.0
uuid: 9.0.1
'@vercel/flags@2.6.0(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
@@ -23903,6 +23933,8 @@ snapshots:
duplexer@0.1.2: {}
dynamic-import-polyfill@0.1.1: {}
eastasianwidth@0.2.0: {}
ee-first@1.1.1: {}
@@ -24265,7 +24297,7 @@ snapshots:
debug: 4.4.0(supports-color@8.1.1)
enhanced-resolve: 5.17.1
eslint: 8.57.0(supports-color@8.1.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
fast-glob: 3.3.2
get-tsconfig: 4.7.2
@@ -24277,7 +24309,7 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1):
eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1):
dependencies:
debug: 3.2.7(supports-color@8.1.1)
optionalDependencies:
@@ -24304,7 +24336,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.0(supports-color@8.1.1)
eslint-import-resolver-node: 0.3.9(supports-color@8.1.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.5.2))(eslint-import-resolver-node@0.3.9(supports-color@8.1.1))(eslint-plugin-import@2.29.1)(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@8.57.0(supports-color@8.1.1))(supports-color@8.1.1)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@@ -27980,11 +28012,6 @@ snapshots:
dependencies:
js-yaml-loader: 1.2.2
next-router-mock@0.9.13(next@14.2.26(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react@18.3.1):
dependencies:
next: 14.2.26(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)
react: 18.3.1
next-router-mock@0.9.13(next@15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0))(react@18.3.1):
dependencies:
next: 15.3.1(@babel/core@7.26.10(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.72.0)

View File

@@ -13,3 +13,4 @@ catalog:
'react-dom': '^18.3.0'
'@types/react': '^18.3.0'
'@types/react-dom': '^18.3.0'
'valtio': '^1.12.0'

View File

@@ -49,6 +49,7 @@
"NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY",
"NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL",
"NEXT_PUBLIC_VERCEL_ENV",
"NEXT_PUBLIC_USERCENTRICS_RULESET_ID",
// These envs are technically passthrough env vars because they're only used on the server side of Nextjs
"PLATFORM_PG_META_URL",
"STUDIO_PG_META_URL",
@@ -105,6 +106,7 @@
"NEXT_PUBLIC_BASE_PATH",
"NEXT_PUBLIC_EXAMPLES_SUPABASE_ANON_KEY",
"NEXT_PUBLIC_EXAMPLES_SUPABASE_URL",
"NEXT_PUBLIC_USERCENTRICS_RULESET_ID",
// These envs are technically passthrough env vars because they're only used on the server side of Nextjs
"LIVE_SUPABASE_COM_SERVICE_ROLE_KEY",
"GITHUB_CHANGELOG_APP_ID",
@@ -144,7 +146,8 @@
"NEXT_PUBLIC_AUTH_NAVIGATOR_LOCK_KEY",
"NEXT_PUBLIC_AUTH_DETECT_SESSION_IN_URL",
"NEXT_PUBLIC_GOTRUE_URL",
"NEXT_PUBLIC_SUPABASE_ANON_KEY"
"NEXT_PUBLIC_SUPABASE_ANON_KEY",
"NEXT_PUBLIC_USERCENTRICS_RULESET_ID"
],
"outputs": [".next/**", "!.next/cache/**", ".contentlayer/**"]
},