diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ab616a9 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@wit-lab-llc:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} diff --git a/next.config.ts b/next.config.ts index dbc5789..1f71cf3 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,8 @@ import type { NextConfig } from "next"; import { BAKED_FUNNELS } from "./src/lib/funnel/bakedFunnels"; +const repoRoot = new URL("../", import.meta.url).pathname.replace(/\/$/, ""); + const buildVariant = process.env.FUNNEL_BUILD_VARIANT ?? process.env.NEXT_PUBLIC_FUNNEL_BUILD_VARIANT ?? @@ -48,6 +50,12 @@ const nextConfig: NextConfig = { DEV_LOGGER_SERVER_ENABLED: process.env.DEV_LOGGER_SERVER_ENABLED, NEXT_PUBLIC_AUTH_REDIRECT_URL: process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL, }, + + transpilePackages: ["@wit-lab-llc/frontend-shared"], + + turbopack: { + root: repoRoot, + }, async redirects() { return generateFunnelRedirects(); diff --git a/package-lock.json b/package-lock.json index ccee56a..b0ce4c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,13 +20,14 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@unleash/proxy-client-react": "^5.0.1", + "@wit-lab-llc/frontend-shared": "^1.0.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dotenv": "^17.2.2", "idb": "^8.0.3", "lucide-react": "^0.544.0", "mongoose": "^8.18.2", - "next": "15.5.7", + "next": "^15.5.9", "react": "19.1.0", "react-circular-progressbar": "^2.2.0", "react-dom": "19.1.0", @@ -1024,6 +1025,12 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fingerprintjs/fingerprintjs": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-5.0.1.tgz", + "integrity": "sha512-KbaeE/rk2WL8MfpRP6jTI4lSr42SJPjvkyrjP3QU6uUDkOMWWYC2Ts1sNSYcegHC8avzOoYTHBj+2fTqvZWQBA==", + "license": "MIT" + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", @@ -1734,9 +1741,9 @@ "license": "MIT" }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.9.tgz", + "integrity": "sha512-4GlTZ+EJM7WaW2HEZcyU317tIQDjkQIyENDLxYJfSWlfqguN+dHkZgyQTV/7ykvobU7yEH5gKvreNrH4B6QgIg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -4971,6 +4978,18 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@wit-lab-llc/frontend-shared": { + "version": "1.0.4", + "resolved": "https://npm.pkg.github.com/download/@wit-lab-llc/frontend-shared/1.0.4/c342b071bc0716511d84bc3f3c9685aa7649ea5e", + "integrity": "sha512-9EZEpjWCdz+IP4RsgkVTPiUEKhm5LqnbgD/S/GJ07eG0Y10ulj3qXjDVXc7N9gURyZULshriDt350w6nhN7Z3w==", + "license": "UNLICENSED", + "dependencies": { + "@fingerprintjs/fingerprintjs": "^5.0.1" + }, + "peerDependencies": { + "react": ">=18.0.0" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -9046,12 +9065,12 @@ "peer": true }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.9", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", + "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.9", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -10605,9 +10624,9 @@ } }, "node_modules/storybook": { - "version": "9.1.13", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.13.tgz", - "integrity": "sha512-G3KZ36EVzXyHds72B/qtWiJnhUpM0xOUeYlDcO9DSHL1bDTv15cW4+upBl+mcBZrDvU838cn7Bv4GpF+O5MCfw==", + "version": "9.1.17", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.17.tgz", + "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index baf00fd..7a93331 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "sync:funnels": "node scripts/sync-funnels-from-db.mjs", "migrate:arrow-hint": "node scripts/migrate-trial-choice-arrow-hint.mjs", "storybook": "storybook dev -p 6006 --ci", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "link:shared": "npm link @wit-lab-llc/frontend-shared", + "unlink:shared": "npm unlink @wit-lab-llc/frontend-shared && npm install" }, "dependencies": { "@hookform/resolvers": "^5.2.2", @@ -33,13 +35,14 @@ "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@unleash/proxy-client-react": "^5.0.1", + "@wit-lab-llc/frontend-shared": "^1.0.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dotenv": "^17.2.2", "idb": "^8.0.3", "lucide-react": "^0.544.0", "mongoose": "^8.18.2", - "next": "15.5.7", + "next": "^15.5.9", "react": "19.1.0", "react-circular-progressbar": "^2.2.0", "react-dom": "19.1.0", diff --git a/src/app/[funnelId]/layout.tsx b/src/app/[funnelId]/layout.tsx index e8ba441..ca69b00 100644 --- a/src/app/[funnelId]/layout.tsx +++ b/src/app/[funnelId]/layout.tsx @@ -1,10 +1,12 @@ import type { ReactNode } from "react"; import { notFound } from "next/navigation"; -import { PixelsProvider } from "@/components/providers/PixelsProvider"; +import { AnalyticsProvider } from "@/components/providers/AnalyticsProvider"; +import { WitLibInitializer } from "@/components/providers/WitLibInitializer"; import { UnleashSessionProvider } from "@/lib/funnel/unleash"; import type { FunnelDefinition } from "@/lib/funnel/types"; import { BAKED_FUNNELS } from "@/lib/funnel/bakedFunnels"; import { IS_FULL_SYSTEM_BUILD } from "@/lib/runtime/buildVariant"; +import { fetchPixelsOnServer } from "@/lib/analytics/fetchPixels"; import { PaymentPlacementProvider, TrialVariantSelectionProvider, @@ -72,21 +74,24 @@ export default async function FunnelLayout({ notFound(); } + // Prefetch analytics pixels на сервере + // Используем funnelId как source для API + const analyticsData = await fetchPixelsOnServer({ source: funnelId }); + return ( - - + - - - {children} - - - - + + + + {children} + + + + + ); } diff --git a/src/components/analytics/AnalyticsScripts.tsx b/src/components/analytics/AnalyticsScripts.tsx new file mode 100644 index 0000000..0c19fc3 --- /dev/null +++ b/src/components/analytics/AnalyticsScripts.tsx @@ -0,0 +1,115 @@ +"use client"; + +import Script from "next/script"; + +interface AnalyticsData { + facebook_pixel?: string[]; + google_analytics?: string[]; + yandex_metrica?: string[]; +} + +interface AnalyticsScriptsProps { + data: AnalyticsData | null; + externalId?: string; // visitorId from fingerprint - used for matching across Pixel and CAPI +} + +/** + * Компонент для инъекции аналитических скриптов через next/script + * + * Использует стратегию afterInteractive (default) - скрипты загружаются + * после частичной hydration страницы, что оптимально для аналитик. + * + * Преимущества над document.createElement: + * - Интеграция с Next.js lifecycle + * - Автоматическая дедупликация скриптов + * - Лучшая производительность через оптимизации Next.js + */ +export function AnalyticsScripts({ data, externalId }: AnalyticsScriptsProps) { + if (!data) return null; + + const facebookPixels = data.facebook_pixel || []; + const googleAnalyticsIds = data.google_analytics || []; + const yandexMetrikaIds = data.yandex_metrica || []; + + return ( + <> + {/* Facebook Pixel Scripts */} + {facebookPixels.map((pixelId) => ( +