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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
22
apps/studio/lib/telemetry.tsx
Normal file
22
apps/studio/lib/telemetry.tsx
Normal 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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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}`
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
109
packages/common/consent-state.ts
Normal file
109
packages/common/consent-state.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './auth'
|
||||
export * from './consent-state'
|
||||
export * from './constants'
|
||||
export * from './database-types'
|
||||
export * from './gotrue'
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
59
packages/ui-patterns/consent.tsx
Normal file
59
packages/ui-patterns/consent.tsx
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"tsconfig": "workspace:*",
|
||||
"ui": "workspace:*",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"valtio": "*",
|
||||
"valtio": "catalog:",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -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
69
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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/**"]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user