Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions core/app/[locale]/(default)/checkout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,33 @@ import { graphql } from '~/client/graphql';
import { redirect } from '~/i18n/routing';
import { getVisitIdCookie, getVisitorIdCookie } from '~/lib/analytics/bigcommerce';
import { getCartId } from '~/lib/cart';
import { getConsentCookie } from '~/lib/consent-manager/cookies/server';

const CheckoutRedirectMutation = graphql(`
mutation CheckoutRedirectMutation($cartId: String!, $visitId: UUID, $visitorId: UUID) {
mutation CheckoutRedirectMutation(
$cartId: String!
$visitId: String!
$visitorId: String!
$referer: URL!
$userAgent: String!
$analyticsConsent: Boolean!
$functionalConsent: Boolean!
$targetingConsent: Boolean!
) {
cart {
createCartRedirectUrls(
input: { cartEntityId: $cartId, visitId: $visitId, visitorId: $visitorId }
input: {
cartEntityId: $cartId
analytics: {
initiator: { visitId: $visitId, visitorId: $visitorId }
request: { url: $referer, userAgent: $userAgent }
consent: {
analytics: $analyticsConsent
functional: $functionalConsent
targeting: $targetingConsent
}
}
}
) {
errors {
... on NotFoundError {
Expand All @@ -41,11 +62,21 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ loca

const visitId = await getVisitIdCookie();
const visitorId = await getVisitorIdCookie();
const consent = await getConsentCookie();

try {
const { data } = await client.fetch({
document: CheckoutRedirectMutation,
variables: { cartId, visitId, visitorId },
variables: {
cartId,
visitId: visitId ?? '',
visitorId: visitorId ?? '',
analyticsConsent: consent?.preferences.measurement ?? false,
functionalConsent: consent?.preferences.functionality ?? false,
targetingConsent: consent?.preferences.marketing ?? false,
referer: req.headers.get('referer') ?? '',
userAgent: req.headers.get('user-agent') ?? '',
},
fetchOptions: { cache: 'no-store' },
customerAccessToken,
channelId,
Expand Down
60 changes: 25 additions & 35 deletions core/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,22 @@ import { cache, PropsWithChildren } from 'react';

import '../../globals.css';

import { Streamable } from '@/vibes/soul/lib/streamable';
import { fonts } from '~/app/fonts';
import { CookieNotifications } from '~/app/notifications';
import { Providers } from '~/app/providers';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { WebAnalyticsFragment } from '~/components/analytics/fragment';
import { StreamableAnalyticsProvider } from '~/components/analytics/streamable-provider';
import { ConsentManagerDialog } from '~/components/consent-manager/consent-manager-dialog';
import { CookieBanner } from '~/components/consent-manager/cookie-banner';
import { AnalyticsProvider } from '~/components/analytics/provider';
import { ConsentManager } from '~/components/consent-manager';
import { ScriptsFragment } from '~/components/consent-manager/scripts-fragment';
import { ContainerQueryPolyfill } from '~/components/polyfills/container-query';
import { ScriptManagerScripts, ScriptsFragment } from '~/components/scripts';
import { scriptsTransformer } from '~/data-transformers/scripts-transformer';
import { routing } from '~/i18n/routing';
import { ConsentManagerProvider } from '~/lib/consent-manager';
import { SiteTheme } from '~/lib/makeswift/components/site-theme';
import { MakeswiftProvider } from '~/lib/makeswift/provider';

import { getToastNotification } from '../../lib/server-toast';
import { getToastNotification } from '~/lib/server-toast';

import '~/lib/makeswift/components';

Expand All @@ -38,6 +35,9 @@ const RootLayoutMetadataQuery = graphql(
query RootLayoutMetadataQuery {
site {
settings {
privacy {
cookieConsentEnabled
}
storeName
seo {
pageTitle
Expand Down Expand Up @@ -122,47 +122,37 @@ export default async function RootLayout({ params, children }: Props) {
// https://next-intl-docs.vercel.app/docs/getting-started/app-router#add-setRequestLocale-to-all-layouts-and-pages
setRequestLocale(locale);

const streamableAnalyticsData = Streamable.from(async () => {
const { data } = await fetchRootLayoutMetadata();

return {
channelId: data.channel.entityId,
settings: data.site.settings,
};
});
const scripts = scriptsTransformer(rootData.data.site.content.scripts);
const isCookieConsentEnabled =
rootData.data.site.settings?.privacy?.cookieConsentEnabled ?? false;

return (
<MakeswiftProvider siteVersion={siteVersion}>
<html className={clsx(fonts.map((f) => f.variable))} lang={locale}>
<head>
<SiteTheme />
<ScriptManagerScripts
scripts={rootData.data.site.content.headerScripts}
strategy="afterInteractive"
/>
</head>
<body className="flex min-h-screen flex-col">
<NextIntlClientProvider>
<ConsentManagerProvider>
<ConsentManager isCookieConsentEnabled={isCookieConsentEnabled} scripts={scripts}>
<NuqsAdapter>
<StreamableAnalyticsProvider data={streamableAnalyticsData} />
<Providers>
{toastNotificationCookieData && (
<CookieNotifications {...toastNotificationCookieData} />
)}
<ConsentManagerDialog />
<CookieBanner />
{children}
</Providers>
<AnalyticsProvider
channelId={rootData.data.channel.entityId}
isCookieConsentEnabled={isCookieConsentEnabled}
settings={rootData.data.site.settings}
>
<Providers>
{toastNotificationCookieData && (
<CookieNotifications {...toastNotificationCookieData} />
)}
{children}
</Providers>
</AnalyticsProvider>
</NuqsAdapter>
</ConsentManagerProvider>
</ConsentManager>
</NextIntlClientProvider>
<VercelComponents />
<ContainerQueryPolyfill />
<ScriptManagerScripts
scripts={rootData.data.site.content.footerScripts}
strategy="lazyOnload"
/>
</body>
</html>
</MakeswiftProvider>
Expand Down
90 changes: 90 additions & 0 deletions core/components/analytics/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use client';

import { useConsentManager } from '@c15t/nextjs';
import { PropsWithChildren, useEffect, useRef } from 'react';

import { FragmentOf } from '~/client/graphql';
import { Analytics } from '~/lib/analytics';
import { GoogleAnalyticsProvider } from '~/lib/analytics/providers/google-analytics';
import { AnalyticsProvider as AnalyticsProviderLib } from '~/lib/analytics/react';
import { getConsentCookie } from '~/lib/consent-manager/cookies/client';

import { WebAnalyticsFragment } from './fragment';

interface Props {
channelId: number;
isCookieConsentEnabled: boolean;
settings?: FragmentOf<typeof WebAnalyticsFragment> | null;
}

const getConsent = () => {
const consentCookie = getConsentCookie();

if (!consentCookie) {
return null;
}

return {
functionality: consentCookie.preferences.functionality ?? false,
marketing: consentCookie.preferences.marketing ?? false,
measurement: consentCookie.preferences.measurement ?? false,
necessary: consentCookie.preferences.necessary ?? false,
};
};

const getAnalytics = ({ channelId, isCookieConsentEnabled, settings }: Props): Analytics | null => {
if (settings?.webAnalytics?.ga4?.tagId && channelId) {
const googleAnalytics = new GoogleAnalyticsProvider({
gaId: settings.webAnalytics.ga4.tagId,
consentModeEnabled: isCookieConsentEnabled,
developerId: 'dMjk3Nj',
getConsent,
});

return new Analytics({
channelId,
providers: [googleAnalytics],
});
}

return null;
};

export function AnalyticsProvider({
channelId,
isCookieConsentEnabled,
settings,
children,
}: PropsWithChildren<Props>) {
const { consents } = useConsentManager();
const prevConsentsRef = useRef<Record<string, boolean> | null>(null);

const analytics = getAnalytics({
channelId,
isCookieConsentEnabled,
settings,
});

// Update consent when user changes preferences
useEffect(() => {
if (!isCookieConsentEnabled || !analytics) {
return;
}

const currentConsents = consents;
const prevConsents = prevConsentsRef.current;

// Check if consents have changed
if (prevConsents && JSON.stringify(currentConsents) !== JSON.stringify(prevConsents)) {
const consentState = getConsent();

if (consentState) {
analytics.consent.consentUpdated(consentState);
}
}

prevConsentsRef.current = currentConsents;
}, [isCookieConsentEnabled, analytics, consents]);

return <AnalyticsProviderLib analytics={analytics ?? null}>{children}</AnalyticsProviderLib>;
}
53 changes: 0 additions & 53 deletions core/components/analytics/streamable-provider.tsx

This file was deleted.

38 changes: 38 additions & 0 deletions core/components/consent-manager/consent-providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { ConsentManagerProvider as C15TConsentManagerProvider } from '@c15t/nextjs';
import { ClientSideOptionsProvider } from '@c15t/nextjs/client';
import type { ComponentProps, PropsWithChildren } from 'react';

import { setConsent, showConsentBanner, verifyConsent } from '~/lib/consent-manager/handlers';

export type C15tScripts = NonNullable<ComponentProps<typeof ClientSideOptionsProvider>['scripts']>;

interface ConsentManagerProviderProps extends PropsWithChildren {
scripts: C15tScripts;
isCookieConsentEnabled: boolean;
}

export function ConsentManagerProvider({
children,
scripts,
isCookieConsentEnabled,
}: ConsentManagerProviderProps) {
return (
<C15TConsentManagerProvider
options={{
mode: 'custom',
consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],

// @ts-expect-error endpointHandlers type is not yet exposed by the package
endpointHandlers: {
showConsentBanner: () => showConsentBanner(isCookieConsentEnabled),
setConsent,
verifyConsent,
},
}}
>
<ClientSideOptionsProvider scripts={scripts}>{children}</ClientSideOptionsProvider>
</C15TConsentManagerProvider>
);
}
22 changes: 22 additions & 0 deletions core/components/consent-manager/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use client';

import type { PropsWithChildren } from 'react';

import { ConsentManagerDialog } from './consent-manager-dialog';
import { type C15tScripts, ConsentManagerProvider } from './consent-providers';
import { CookieBanner } from './cookie-banner';

interface ConsentManagerProps extends PropsWithChildren {
scripts: C15tScripts;
isCookieConsentEnabled: boolean;
}

export function ConsentManager({ children, scripts, isCookieConsentEnabled }: ConsentManagerProps) {
return (
<ConsentManagerProvider isCookieConsentEnabled={isCookieConsentEnabled} scripts={scripts}>
<ConsentManagerDialog />
<CookieBanner />
{children}
</ConsentManagerProvider>
);
}
25 changes: 25 additions & 0 deletions core/components/consent-manager/scripts-fragment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { graphql } from '~/client/graphql';

export const ScriptsFragment = graphql(`
fragment ScriptsFragment on Content {
scripts(first: 50, filters: { visibilities: [ALL_PAGES, STOREFRONT] }) {
edges {
node {
__typename
integrityHashes {
hash
}
entityId
consentCategory
visibility
... on InlineScript {
scriptTag
}
... on SrcScript {
src
}
}
}
}
}
`);
Loading