diff --git a/.gitignore b/.gitignore index 355a1f06..ee49134f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ pnpm-debug.log* .DS_Store .superpowers/ +.env.local diff --git a/astro.config.mjs b/astro.config.mjs index 26fbd2c1..8323703a 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -92,6 +92,7 @@ export default defineConfig({ "/home-new/", "/endorsements-new/", "/e4p/pledge-new/", + "/growthbook-test/", ]; return !exclude.some((path) => page.endsWith(path)); }, diff --git a/package.json b/package.json index 79d2b982..104a39b0 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@emotion/styled": "^11.14.1", "@fontsource/fraunces": "^5.2.9", "@fontsource/outfit": "^5.2.8", + "@growthbook/growthbook-react": "^1.7.0", "@iconify-json/simple-icons": "^1.2.54", "@mui/icons-material": "^6.5.0", "@mui/material": "^6.5.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28df680b..2deddead 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: '@fontsource/outfit': specifier: ^5.2.8 version: 5.2.8 + '@growthbook/growthbook-react': + specifier: ^1.7.0 + version: 1.7.0(react@19.2.0) '@iconify-json/simple-icons': specifier: ^1.2.54 version: 1.2.54 @@ -975,6 +978,16 @@ packages: '@fontsource/outfit@5.2.8': resolution: {integrity: sha512-rXC6g0MqD7cOBjht0bMqc43qM6lRqDLML9KXsmg9uykz0wLQhy8Z/ajrMG6iyoT3NJR+MYgU+OEHp7uHoTb+Yw==} + '@growthbook/growthbook-react@1.7.0': + resolution: {integrity: sha512-7TU0Yjmkl2Me8LKpG5cjqIsLSK+sz9T/1vocJAm49KsBeTvse6QwR9544KB9sU7eJF9qYVGr4GekW/r9bqmaWg==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0 + + '@growthbook/growthbook@1.7.0': + resolution: {integrity: sha512-Vmad3fWRh1XaFH4q+AS62ofX6Q3IxSTD7sBGuhEHCaad8VyZerxrVdojucjzlnJDUocfrnZFUSpEkR6R/wCV+A==} + engines: {node: '>=10'} + '@iconify-json/bi@1.2.6': resolution: {integrity: sha512-fWfLr1/+DJDe8+rIUXxMwvmWBZFlxRtM59sYnrezJ2xX87QKyXVw3QuforJ4kF2Orrz85J+JTRG6305vaJ7flA==} @@ -2335,6 +2348,10 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dom-mutator@0.6.0: + resolution: {integrity: sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg==} + engines: {node: '>=10'} + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -4956,6 +4973,15 @@ snapshots: '@fontsource/outfit@5.2.8': {} + '@growthbook/growthbook-react@1.7.0(react@19.2.0)': + dependencies: + '@growthbook/growthbook': 1.7.0 + react: 19.2.0 + + '@growthbook/growthbook@1.7.0': + dependencies: + dom-mutator: 0.6.0 + '@iconify-json/bi@1.2.6': dependencies: '@iconify/types': 2.0.0 @@ -6494,6 +6520,8 @@ snapshots: '@babel/runtime': 7.28.4 csstype: 3.1.3 + dom-mutator@0.6.0: {} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 diff --git a/src/components/GrowthBookProvider.tsx b/src/components/GrowthBookProvider.tsx new file mode 100644 index 00000000..dd73d8d3 --- /dev/null +++ b/src/components/GrowthBookProvider.tsx @@ -0,0 +1,14 @@ +import { GrowthBookProvider as GBProvider } from "@growthbook/growthbook-react"; +import type { ReactNode } from "react"; +import { growthbook } from "../growthbook"; + +interface Props { + children: ReactNode; +} + +// Wrap a client:only="react" island in this to read GrowthBook flags with +// useFeatureIsOn / useFeatureValue. Each island mounts its own React root, so +// there's no single app entry point to wrap once — use this per island instead. +export default function GrowthBookProvider({ children }: Props) { + return {children}; +} diff --git a/src/components/GrowthBookTestBanner.tsx b/src/components/GrowthBookTestBanner.tsx new file mode 100644 index 00000000..319340d3 --- /dev/null +++ b/src/components/GrowthBookTestBanner.tsx @@ -0,0 +1,21 @@ +import { useFeatureIsOn } from "@growthbook/growthbook-react"; +import GrowthBookProvider from "./GrowthBookProvider"; + +// Throwaway banner to confirm the GrowthBook SDK is wired end to end. +// Safe to delete once the connection shows as Connected in the dashboard. +function Banner() { + const enabled = useFeatureIsOn("growthbook-test"); + return ( +
+ growthbook-test flag is {enabled ? "ON" : "OFF"} +
+ ); +} + +export default function GrowthBookTestBanner() { + return ( + + + + ); +} diff --git a/src/growthbook.ts b/src/growthbook.ts new file mode 100644 index 00000000..4f7e7e5b --- /dev/null +++ b/src/growthbook.ts @@ -0,0 +1,22 @@ +import { GrowthBook } from "@growthbook/growthbook-react"; + +const clientKey = import.meta.env.PUBLIC_GROWTHBOOK_CLIENT_KEY; + +if (!clientKey) { + // Fail loudly. A missing key makes every flag return its default instead, + // which looks like working code that simply never turns anything on. + throw new Error( + "Missing PUBLIC_GROWTHBOOK_CLIENT_KEY — GrowthBook flags will not load without it.", + ); +} + +export const growthbook = new GrowthBook({ + apiHost: "https://cdn.growthbook.io", + clientKey, + enableDevMode: true, +}); + +// streaming: false — fetch the payload once on load. Flag changes then land on the +// next page load. Streaming holds an SSE connection open per client; opt in only +// if you want live updates. +export const ready = growthbook.init({ streaming: false }); diff --git a/src/middleware/csp.ts b/src/middleware/csp.ts index 5556cd3e..da7f675f 100644 --- a/src/middleware/csp.ts +++ b/src/middleware/csp.ts @@ -27,7 +27,7 @@ export const csp = defineMiddleware(async (context, next) => { `style-src 'nonce-${nonce}' 'self' https://fonts.googleapis.com https://secure.qgiv.com`, "font-src 'self' https://fonts.gstatic.com https://gallery.eo.page", "img-src 'self' data: https:", - "connect-src 'self' https://plausible.io https://pal-chat.net https://eomail4.com https://www.google.com https://1k0gztb8b2.execute-api.us-east-2.amazonaws.com https://www.charitystack.com https://www.donation.charitystack.com https://*.ingest.sentry.io https://*.ingest.de.sentry.io", + "connect-src 'self' https://plausible.io https://pal-chat.net https://eomail4.com https://www.google.com https://1k0gztb8b2.execute-api.us-east-2.amazonaws.com https://www.charitystack.com https://www.donation.charitystack.com https://*.ingest.sentry.io https://*.ingest.de.sentry.io https://cdn.growthbook.io", "frame-src https://secure.qgiv.com https://calendly.com https://www.youtube.com https://www.youtube-nocookie.com https://www.google.com https://validaid.org https://www.charitystack.com", "object-src 'none'", "base-uri 'self'", diff --git a/src/pages/growthbook-test.astro b/src/pages/growthbook-test.astro new file mode 100644 index 00000000..46a659e6 --- /dev/null +++ b/src/pages/growthbook-test.astro @@ -0,0 +1,10 @@ +--- +import Layout from "../layouts/Layout.astro"; +import GrowthBookTestBanner from "../components/GrowthBookTestBanner"; +--- + + +
+ +
+