consentlayer 0.0.0 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -1
  3. package/dist/adapters/google-consent-mode.d.ts +20 -0
  4. package/dist/adapters/google-consent-mode.js +18 -0
  5. package/dist/adapters/google-consent-mode.js.map +1 -0
  6. package/dist/adapters/shopify.d.ts +49 -0
  7. package/dist/adapters/shopify.js +85 -0
  8. package/dist/adapters/shopify.js.map +1 -0
  9. package/dist/adapters/tcf.d.ts +20 -0
  10. package/dist/adapters/tcf.js +64 -0
  11. package/dist/adapters/tcf.js.map +1 -0
  12. package/dist/analytics-provider-07mxUgIK.d.ts +36 -0
  13. package/dist/angular.d.ts +56 -0
  14. package/dist/angular.js +120 -0
  15. package/dist/angular.js.map +1 -0
  16. package/dist/auto-tracking.d.ts +11 -0
  17. package/dist/auto-tracking.js +72 -0
  18. package/dist/auto-tracking.js.map +1 -0
  19. package/dist/chunk-7XLGT6U3.js +34 -0
  20. package/dist/chunk-7XLGT6U3.js.map +1 -0
  21. package/dist/chunk-BWPWDN4G.js +61 -0
  22. package/dist/chunk-BWPWDN4G.js.map +1 -0
  23. package/dist/chunk-DK45XPEY.js +77 -0
  24. package/dist/chunk-DK45XPEY.js.map +1 -0
  25. package/dist/chunk-ETU64ZV3.js +98 -0
  26. package/dist/chunk-ETU64ZV3.js.map +1 -0
  27. package/dist/chunk-FDJMIJFV.js +367 -0
  28. package/dist/chunk-FDJMIJFV.js.map +1 -0
  29. package/dist/chunk-HJGBVDPM.js +47 -0
  30. package/dist/chunk-HJGBVDPM.js.map +1 -0
  31. package/dist/chunk-J3KUPF4R.js +69 -0
  32. package/dist/chunk-J3KUPF4R.js.map +1 -0
  33. package/dist/chunk-LBEIEAJX.js +55 -0
  34. package/dist/chunk-LBEIEAJX.js.map +1 -0
  35. package/dist/chunk-LEAQJXJU.js +582 -0
  36. package/dist/chunk-LEAQJXJU.js.map +1 -0
  37. package/dist/chunk-R6I7OYQB.js +69 -0
  38. package/dist/chunk-R6I7OYQB.js.map +1 -0
  39. package/dist/chunk-RK2M57GW.js +54 -0
  40. package/dist/chunk-RK2M57GW.js.map +1 -0
  41. package/dist/chunk-RZ4TYRNY.js +128 -0
  42. package/dist/chunk-RZ4TYRNY.js.map +1 -0
  43. package/dist/chunk-TVSZJLRC.js +112 -0
  44. package/dist/chunk-TVSZJLRC.js.map +1 -0
  45. package/dist/chunk-TYOQYV45.js +79 -0
  46. package/dist/chunk-TYOQYV45.js.map +1 -0
  47. package/dist/chunk-UAIZU5WD.js +52 -0
  48. package/dist/chunk-UAIZU5WD.js.map +1 -0
  49. package/dist/chunk-VIIGBALD.js +14 -0
  50. package/dist/chunk-VIIGBALD.js.map +1 -0
  51. package/dist/consent-controller-DyHPNfyT.d.ts +47 -0
  52. package/dist/consentlayer-banner.global.js +20 -0
  53. package/dist/consentlayer.global.js +9 -0
  54. package/dist/create-analytics-Bzvm1UpE.d.ts +44 -0
  55. package/dist/ecommerce.d.ts +41 -0
  56. package/dist/ecommerce.js +43 -0
  57. package/dist/ecommerce.js.map +1 -0
  58. package/dist/index.d.ts +58 -0
  59. package/dist/index.js +42 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/next.d.ts +16 -0
  62. package/dist/next.js +27 -0
  63. package/dist/next.js.map +1 -0
  64. package/dist/nuxt/config.d.ts +7 -0
  65. package/dist/nuxt/config.js +6 -0
  66. package/dist/nuxt/config.js.map +1 -0
  67. package/dist/nuxt/index.d.ts +11 -0
  68. package/dist/nuxt/index.js +35 -0
  69. package/dist/nuxt/index.js.map +1 -0
  70. package/dist/nuxt/plugin.d.ts +5 -0
  71. package/dist/nuxt/plugin.js +24 -0
  72. package/dist/nuxt/plugin.js.map +1 -0
  73. package/dist/providers/contentsquare.d.ts +27 -0
  74. package/dist/providers/contentsquare.js +58 -0
  75. package/dist/providers/contentsquare.js.map +1 -0
  76. package/dist/providers/ga4.d.ts +46 -0
  77. package/dist/providers/ga4.js +3 -0
  78. package/dist/providers/ga4.js.map +1 -0
  79. package/dist/providers/gtm.d.ts +28 -0
  80. package/dist/providers/gtm.js +83 -0
  81. package/dist/providers/gtm.js.map +1 -0
  82. package/dist/providers/hotjar.d.ts +38 -0
  83. package/dist/providers/hotjar.js +68 -0
  84. package/dist/providers/hotjar.js.map +1 -0
  85. package/dist/providers/meta.d.ts +42 -0
  86. package/dist/providers/meta.js +65 -0
  87. package/dist/providers/meta.js.map +1 -0
  88. package/dist/providers/posthog.d.ts +42 -0
  89. package/dist/providers/posthog.js +91 -0
  90. package/dist/providers/posthog.js.map +1 -0
  91. package/dist/providers/sentry.d.ts +38 -0
  92. package/dist/providers/sentry.js +82 -0
  93. package/dist/providers/sentry.js.map +1 -0
  94. package/dist/providers/tiktok.d.ts +46 -0
  95. package/dist/providers/tiktok.js +72 -0
  96. package/dist/providers/tiktok.js.map +1 -0
  97. package/dist/providers/vercel.d.ts +14 -0
  98. package/dist/providers/vercel.js +17 -0
  99. package/dist/providers/vercel.js.map +1 -0
  100. package/dist/react/index.d.ts +78 -0
  101. package/dist/react/index.js +79 -0
  102. package/dist/react/index.js.map +1 -0
  103. package/dist/sentry/next.d.ts +1 -0
  104. package/dist/sentry/next.js +3 -0
  105. package/dist/sentry/next.js.map +1 -0
  106. package/dist/sentry/server.d.ts +24 -0
  107. package/dist/sentry/server.js +28 -0
  108. package/dist/sentry/server.js.map +1 -0
  109. package/dist/sentry-replay-HSFVEFCM.js +3 -0
  110. package/dist/sentry-replay-HSFVEFCM.js.map +1 -0
  111. package/dist/sentry-sdk-YQOGKGWD.js +3 -0
  112. package/dist/sentry-sdk-YQOGKGWD.js.map +1 -0
  113. package/dist/sentry-tracing-KWT7ITHF.js +3 -0
  114. package/dist/sentry-tracing-KWT7ITHF.js.map +1 -0
  115. package/dist/server/ga4.d.ts +15 -0
  116. package/dist/server/ga4.js +29 -0
  117. package/dist/server/ga4.js.map +1 -0
  118. package/dist/server/index.d.ts +116 -0
  119. package/dist/server/index.js +206 -0
  120. package/dist/server/index.js.map +1 -0
  121. package/dist/server/meta.d.ts +14 -0
  122. package/dist/server/meta.js +63 -0
  123. package/dist/server/meta.js.map +1 -0
  124. package/dist/server/sgtm.d.ts +18 -0
  125. package/dist/server/sgtm.js +34 -0
  126. package/dist/server/sgtm.js.map +1 -0
  127. package/dist/server/tiktok.d.ts +15 -0
  128. package/dist/server/tiktok.js +64 -0
  129. package/dist/server/tiktok.js.map +1 -0
  130. package/dist/solid.d.ts +59 -0
  131. package/dist/solid.js +3 -0
  132. package/dist/solid.js.map +1 -0
  133. package/dist/solidstart.d.ts +16 -0
  134. package/dist/solidstart.js +22 -0
  135. package/dist/solidstart.js.map +1 -0
  136. package/dist/svelte/GtmNoscript.svelte +18 -0
  137. package/dist/svelte/MetaPixelNoscript.svelte +18 -0
  138. package/dist/svelte.d.ts +55 -0
  139. package/dist/svelte.js +3 -0
  140. package/dist/svelte.js.map +1 -0
  141. package/dist/sveltekit.d.ts +18 -0
  142. package/dist/sveltekit.js +16 -0
  143. package/dist/sveltekit.js.map +1 -0
  144. package/dist/testing.d.ts +11 -0
  145. package/dist/testing.js +50 -0
  146. package/dist/testing.js.map +1 -0
  147. package/dist/types-BV_AFZKQ.d.ts +407 -0
  148. package/dist/vue.d.ts +92 -0
  149. package/dist/vue.js +97 -0
  150. package/dist/vue.js.map +1 -0
  151. package/package.json +278 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nicolopadovan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,89 @@
1
1
  # consentlayer
2
2
 
3
- Placeholder for an upcoming release of consent-first analytics. Nothing to use yet.
3
+ Consent-first analytics for the browser and the server. You call one API;
4
+ ConsentLayer decides, from the visitor's consent, which providers load, what
5
+ each one receives and when, and translates your events into each provider's
6
+ format.
7
+
8
+ - **Providers:** GA4, Google Tag Manager, Meta Pixel, TikTok Pixel, PostHog,
9
+ Sentry, Hotjar, Contentsquare, Vercel Analytics, and your own plugins.
10
+ - **Consent:** categories `preferences`, `analytics` and `marketing`, plus
11
+ your own. A provider loads only once its category is granted, or in its
12
+ vendor's consent mode, and stops on withdrawal. Global Privacy Control is
13
+ honored. Consent adapters read an IAB TCF CMP, sync Shopify's Customer
14
+ Privacy API and send Google Consent Mode signals.
15
+ - **Light:** each provider is its own entry point, and vendor SDKs and heavy
16
+ features (Replay, tracing, session recording) are chunks loaded only when
17
+ used.
18
+ - **Frameworks:** bindings for React, Next.js, Vue, Nuxt, Svelte, SvelteKit,
19
+ Solid, SolidStart and Angular, plus a script-tag build with an optional
20
+ banner. On npm you build your own banner on `useConsentController()`.
21
+ - **Server:** forward browser events to the Meta Conversions API, the TikTok
22
+ Events API or a server-side GTM container, with shared event IDs for
23
+ deduplication, and keep signed consent records.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install consentlayer
29
+ ```
30
+
31
+ Then install the SDK of each provider that needs one (`posthog-js`,
32
+ `@sentry/browser`).
33
+
34
+ ## Quick start
35
+
36
+ ```ts
37
+ // lib/analytics.ts
38
+ import { createAnalytics } from 'consentlayer'
39
+ import { ga4 } from 'consentlayer/providers/ga4'
40
+ import { posthog } from 'consentlayer/providers/posthog'
41
+
42
+ export const analytics = createAnalytics({
43
+ providers: [
44
+ ga4({ measurementId: 'G-XXXXXXX' }),
45
+ posthog({ apiKey: 'phc_…' }),
46
+ ],
47
+ })
48
+ ```
49
+
50
+ In the Next.js App Router, `AnalyticsProvider` from `consentlayer/next` sends a
51
+ page view on every navigation. Elsewhere, use the one from `consentlayer/react`
52
+ and pass a `routeKey` that changes on navigation.
53
+
54
+ ```tsx
55
+ 'use client'
56
+ import { AnalyticsProvider } from 'consentlayer/next'
57
+ import { ConsentBanner } from './consent-banner' // the Consent UI recipe in the docs
58
+ import { analytics } from '../lib/analytics'
59
+
60
+ export function Shell({ children }: { children: React.ReactNode }) {
61
+ return (
62
+ <AnalyticsProvider analytics={analytics}>
63
+ <ConsentBanner />
64
+ {children}
65
+ </AnalyticsProvider>
66
+ )
67
+ }
68
+ ```
69
+
70
+ ```ts
71
+ analytics.track('sign_up', { method: 'google' })
72
+ analytics.identify('user_123', { email: 'ada@example.com' })
73
+ ```
74
+
75
+ ## Documentation
76
+
77
+ **[consentlayer.nicolopadovan.com](https://consentlayer.nicolopadovan.com)**: a setup guide
78
+ for each framework, every provider and option, and the server.
79
+
80
+ ## Legal notice
81
+
82
+ ConsentLayer has not been legally reviewed and is not legal advice. Whether
83
+ your implementation complies with the law where you and your visitors are is
84
+ your responsibility; see the
85
+ [legal notice](https://consentlayer.nicolopadovan.com/guides/legal/).
86
+
87
+ ## License
88
+
89
+ [MIT](./LICENSE)
@@ -0,0 +1,20 @@
1
+ import { C as ConsentIntegrationAdapter } from '../types-BV_AFZKQ.js';
2
+
3
+ type GoogleConsentModeOptions = {
4
+ /** Categories that count as granted before a choice in the listed ISO 3166-2 regions, e.g. `'US'` or `'US-CA'`. */
5
+ regions?: GoogleConsentModeRegion[];
6
+ /** Milliseconds Google tags wait for a consent update before sending. */
7
+ waitForUpdate?: number;
8
+ /** Pass ad click IDs through URLs while `ad_storage` is denied. */
9
+ urlPassthrough?: boolean;
10
+ /** Redact ad click data while `ad_storage` is denied. */
11
+ adsDataRedaction?: boolean;
12
+ };
13
+ type GoogleConsentModeRegion = {
14
+ region: string[];
15
+ } & Partial<Record<'preferences' | 'analytics' | 'marketing', boolean>>;
16
+
17
+ /** Sends the visitor's choice to Google tags (GA4, Google Ads, Tag Manager) as Consent Mode v2 signals. */
18
+ declare function googleConsentMode(options?: GoogleConsentModeOptions): ConsentIntegrationAdapter;
19
+
20
+ export { type GoogleConsentModeOptions, type GoogleConsentModeRegion, googleConsentMode };
@@ -0,0 +1,18 @@
1
+ import { consentModeSignals } from '../chunk-BWPWDN4G.js';
2
+ import { setFlag } from '../chunk-R6I7OYQB.js';
3
+
4
+ // src/adapters/google-consent-mode.ts
5
+ function googleConsentMode(options = {}) {
6
+ const observe = consentModeSignals(options);
7
+ return {
8
+ id: "google-consent-mode",
9
+ observe(consent) {
10
+ setFlag("GoogleConsentMode", true);
11
+ observe(consent);
12
+ }
13
+ };
14
+ }
15
+
16
+ export { googleConsentMode };
17
+ //# sourceMappingURL=google-consent-mode.js.map
18
+ //# sourceMappingURL=google-consent-mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/google-consent-mode.ts"],"names":[],"mappings":";;;;AAOO,SAAS,iBAAA,CAAkB,OAAA,GAAoC,EAAC,EAA8B;AACnG,EAAA,MAAM,OAAA,GAAU,mBAAmB,OAAO,CAAA;AAC1C,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,qBAAA;AAAA,IACJ,QAAQ,OAAA,EAAS;AACf,MAAA,OAAA,CAAQ,qBAAqB,IAAI,CAAA;AACjC,MAAA,OAAA,CAAQ,OAAO,CAAA;AAAA,IACjB;AAAA,GACF;AACF","file":"google-consent-mode.js","sourcesContent":["import { consentModeSignals, type GoogleConsentModeOptions } from '../core/consent-mode'\nimport { setFlag } from '../core/plugin'\nimport type { ConsentIntegrationAdapter } from '../core/types'\n\nexport type { GoogleConsentModeOptions, GoogleConsentModeRegion } from '../core/consent-mode'\n\n/** Sends the visitor's choice to Google tags (GA4, Google Ads, Tag Manager) as Consent Mode v2 signals. */\nexport function googleConsentMode(options: GoogleConsentModeOptions = {}): ConsentIntegrationAdapter {\n const observe = consentModeSignals(options)\n return {\n id: 'google-consent-mode',\n observe(consent) {\n setFlag('GoogleConsentMode', true)\n observe(consent)\n },\n }\n}\n"]}
@@ -0,0 +1,49 @@
1
+ import { C as ConsentIntegrationAdapter } from '../types-BV_AFZKQ.js';
2
+
3
+ declare global {
4
+ interface Window {
5
+ Shopify?: {
6
+ customerPrivacy?: ShopifyCustomerPrivacy;
7
+ loadFeatures?: (features: Array<{
8
+ name: string;
9
+ version: string;
10
+ }>, callback: (error?: unknown) => void) => void;
11
+ };
12
+ }
13
+ }
14
+ type ShopifyCustomerPrivacy = {
15
+ setTrackingConsent?: (args: boolean | {
16
+ marketing?: boolean;
17
+ analytics?: boolean;
18
+ preferences?: boolean;
19
+ sale_of_data?: boolean;
20
+ headlessStorefront?: boolean;
21
+ checkoutRootDomain?: string;
22
+ storefrontRootDomain?: string;
23
+ storefrontAccessToken?: string;
24
+ }, callback?: () => void) => void;
25
+ currentVisitorConsent?: () => {
26
+ marketing?: string;
27
+ analytics?: string;
28
+ preferences?: string;
29
+ sale_of_data?: string;
30
+ };
31
+ };
32
+ /** Options for `shopifyCustomerPrivacy`. `headlessStorefront` is needed off Shopify-hosted themes, and applies only once `storefrontAccessToken` is set. */
33
+ type ShopifyCustomerPrivacyOptions = {
34
+ canBootstrap?: () => boolean;
35
+ headlessStorefront?: {
36
+ checkoutRootDomain?: string;
37
+ storefrontRootDomain?: string;
38
+ storefrontAccessToken?: string;
39
+ };
40
+ loadFeature?: boolean;
41
+ document?: Document;
42
+ window?: Window;
43
+ };
44
+ /** Syncs consent with Shopify's Customer Privacy API, so Shopify and the store analytics agree. */
45
+ declare function shopifyCustomerPrivacy(options?: ShopifyCustomerPrivacyOptions): ConsentIntegrationAdapter;
46
+ /** URL of Shopify's Customer Privacy script, for headless storefronts that load it themselves. */
47
+ declare function getShopifyCustomerPrivacyScriptUrl(): string;
48
+
49
+ export { type ShopifyCustomerPrivacyOptions, getShopifyCustomerPrivacyScriptUrl, shopifyCustomerPrivacy };
@@ -0,0 +1,85 @@
1
+ import { loadScript } from '../chunk-7XLGT6U3.js';
2
+
3
+ // src/adapters/shopify.ts
4
+ function coerceShopifyConsent(value, fallback = false) {
5
+ if (typeof value === "boolean") return value;
6
+ if (typeof value === "string") {
7
+ const normalized = value.toLowerCase();
8
+ if (["granted", "allow", "accepted", "yes", "true"].includes(normalized)) {
9
+ return true;
10
+ }
11
+ if (["denied", "reject", "refused", "no", "false"].includes(normalized)) {
12
+ return false;
13
+ }
14
+ }
15
+ return fallback;
16
+ }
17
+ async function ensureCustomerPrivacyFeature(win, loadFeature, headless) {
18
+ if (!loadFeature) return;
19
+ if (headless && !win.Shopify?.customerPrivacy) await loadScript(getShopifyCustomerPrivacyScriptUrl());
20
+ if (!win.Shopify?.loadFeatures) return;
21
+ await new Promise((resolve) => {
22
+ win.Shopify?.loadFeatures?.([{ name: "consent-tracking-api", version: "0.1" }], () => resolve());
23
+ });
24
+ }
25
+ function shopifyCustomerPrivacy(options = {}) {
26
+ const doc = options.document ?? (typeof document !== "undefined" ? document : void 0);
27
+ const win = options.window ?? (typeof window !== "undefined" ? window : void 0);
28
+ const headless = Boolean(options.headlessStorefront?.storefrontAccessToken);
29
+ return {
30
+ id: "shopify-customer-privacy",
31
+ async bootstrap() {
32
+ if (!win || !doc) return null;
33
+ if (options.canBootstrap && !options.canBootstrap()) return null;
34
+ await ensureCustomerPrivacyFeature(win, options.loadFeature ?? true, headless);
35
+ const explicit = win.Shopify?.customerPrivacy?.currentVisitorConsent?.();
36
+ if (!explicit) return null;
37
+ const hasAnyExplicit = Object.values(explicit).some((value) => typeof value === "string" && value.length > 0);
38
+ if (!hasAnyExplicit) return null;
39
+ return {
40
+ preferences: coerceShopifyConsent(explicit.preferences),
41
+ analytics: coerceShopifyConsent(explicit.analytics),
42
+ marketing: coerceShopifyConsent(explicit.marketing),
43
+ ...explicit.sale_of_data ? { sale_of_data: coerceShopifyConsent(explicit.sale_of_data) } : {}
44
+ };
45
+ },
46
+ async apply(consent) {
47
+ if (!win) return;
48
+ await ensureCustomerPrivacyFeature(win, options.loadFeature ?? true, headless);
49
+ const args = {
50
+ preferences: consent.preferences,
51
+ analytics: consent.analytics,
52
+ marketing: consent.marketing,
53
+ ...typeof consent.sale_of_data === "boolean" ? { sale_of_data: consent.sale_of_data } : {},
54
+ ...headless ? {
55
+ headlessStorefront: true,
56
+ ...options.headlessStorefront
57
+ } : {}
58
+ };
59
+ const setTrackingConsent = win.Shopify?.customerPrivacy?.setTrackingConsent;
60
+ if (setTrackingConsent) await new Promise((resolve) => setTrackingConsent(args, resolve));
61
+ },
62
+ subscribe(onConsent) {
63
+ if (!doc) return () => void 0;
64
+ const handler = (event) => {
65
+ const detail = event.detail;
66
+ const next = {
67
+ marketing: Boolean(detail?.marketingAllowed),
68
+ analytics: Boolean(detail?.analyticsAllowed),
69
+ preferences: Boolean(detail?.preferencesAllowed),
70
+ ...typeof detail?.saleOfDataAllowed === "boolean" ? { sale_of_data: detail.saleOfDataAllowed } : {}
71
+ };
72
+ onConsent(next);
73
+ };
74
+ doc.addEventListener("visitorConsentCollected", handler);
75
+ return () => doc.removeEventListener("visitorConsentCollected", handler);
76
+ }
77
+ };
78
+ }
79
+ function getShopifyCustomerPrivacyScriptUrl() {
80
+ return "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js";
81
+ }
82
+
83
+ export { getShopifyCustomerPrivacyScriptUrl, shopifyCustomerPrivacy };
84
+ //# sourceMappingURL=shopify.js.map
85
+ //# sourceMappingURL=shopify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/shopify.ts"],"names":[],"mappings":"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","file":"shopify.js","sourcesContent":["import { loadScript } from '../core/plugin'\nimport type { ConsentIntegrationAdapter, ConsentState } from '../core/types'\n\ndeclare global {\n interface Window {\n Shopify?: {\n customerPrivacy?: ShopifyCustomerPrivacy\n loadFeatures?: (features: Array<{ name: string; version: string }>, callback: (error?: unknown) => void) => void\n }\n }\n}\n\ntype ShopifyCustomerPrivacy = {\n setTrackingConsent?: (\n args:\n | boolean\n | {\n marketing?: boolean\n analytics?: boolean\n preferences?: boolean\n sale_of_data?: boolean\n headlessStorefront?: boolean\n checkoutRootDomain?: string\n storefrontRootDomain?: string\n storefrontAccessToken?: string\n },\n callback?: () => void,\n ) => void\n currentVisitorConsent?: () => {\n marketing?: string\n analytics?: string\n preferences?: string\n sale_of_data?: string\n }\n}\n\n/** Options for `shopifyCustomerPrivacy`. `headlessStorefront` is needed off Shopify-hosted themes, and applies only once `storefrontAccessToken` is set. */\nexport type ShopifyCustomerPrivacyOptions = {\n canBootstrap?: () => boolean\n headlessStorefront?: {\n checkoutRootDomain?: string\n storefrontRootDomain?: string\n storefrontAccessToken?: string\n }\n loadFeature?: boolean\n document?: Document\n window?: Window\n}\n\nfunction coerceShopifyConsent(value: unknown, fallback = false): boolean {\n if (typeof value === 'boolean') return value\n if (typeof value === 'string') {\n const normalized = value.toLowerCase()\n if (['granted', 'allow', 'accepted', 'yes', 'true'].includes(normalized)) {\n return true\n }\n if (['denied', 'reject', 'refused', 'no', 'false'].includes(normalized)) {\n return false\n }\n }\n return fallback\n}\n\nasync function ensureCustomerPrivacyFeature(win: Window, loadFeature: boolean, headless: boolean): Promise<void> {\n if (!loadFeature) return\n if (headless && !win.Shopify?.customerPrivacy) await loadScript(getShopifyCustomerPrivacyScriptUrl())\n if (!win.Shopify?.loadFeatures) return\n await new Promise<void>((resolve) => {\n win.Shopify?.loadFeatures?.([{ name: 'consent-tracking-api', version: '0.1' }], () => resolve())\n })\n}\n\n/** Syncs consent with Shopify's Customer Privacy API, so Shopify and the store analytics agree. */\nexport function shopifyCustomerPrivacy(options: ShopifyCustomerPrivacyOptions = {}): ConsentIntegrationAdapter {\n const doc = options.document ?? (typeof document !== 'undefined' ? document : undefined)\n const win = options.window ?? (typeof window !== 'undefined' ? window : undefined)\n const headless = Boolean(options.headlessStorefront?.storefrontAccessToken)\n\n return {\n id: 'shopify-customer-privacy',\n async bootstrap() {\n if (!win || !doc) return null\n if (options.canBootstrap && !options.canBootstrap()) return null\n await ensureCustomerPrivacyFeature(win, options.loadFeature ?? true, headless)\n const explicit = win.Shopify?.customerPrivacy?.currentVisitorConsent?.()\n if (!explicit) return null\n const hasAnyExplicit = Object.values(explicit).some((value) => typeof value === 'string' && value.length > 0)\n if (!hasAnyExplicit) return null\n return {\n preferences: coerceShopifyConsent(explicit.preferences),\n analytics: coerceShopifyConsent(explicit.analytics),\n marketing: coerceShopifyConsent(explicit.marketing),\n ...(explicit.sale_of_data ? { sale_of_data: coerceShopifyConsent(explicit.sale_of_data) } : {}),\n }\n },\n async apply(consent) {\n if (!win) return\n await ensureCustomerPrivacyFeature(win, options.loadFeature ?? true, headless)\n const args = {\n preferences: consent.preferences,\n analytics: consent.analytics,\n marketing: consent.marketing,\n ...(typeof consent.sale_of_data === 'boolean' ? { sale_of_data: consent.sale_of_data } : {}),\n ...(headless\n ? {\n headlessStorefront: true,\n ...options.headlessStorefront,\n }\n : {}),\n }\n const setTrackingConsent = win.Shopify?.customerPrivacy?.setTrackingConsent\n if (setTrackingConsent) await new Promise<void>((resolve) => setTrackingConsent(args, resolve))\n },\n subscribe(onConsent) {\n if (!doc) return () => undefined\n const handler = (event: Event) => {\n const detail = (event as CustomEvent).detail as\n | {\n marketingAllowed?: boolean\n analyticsAllowed?: boolean\n preferencesAllowed?: boolean\n saleOfDataAllowed?: boolean\n }\n | undefined\n const next: ConsentState = {\n marketing: Boolean(detail?.marketingAllowed),\n analytics: Boolean(detail?.analyticsAllowed),\n preferences: Boolean(detail?.preferencesAllowed),\n ...(typeof detail?.saleOfDataAllowed === 'boolean' ? { sale_of_data: detail.saleOfDataAllowed } : {}),\n }\n onConsent(next)\n }\n doc.addEventListener('visitorConsentCollected', handler)\n return () => doc.removeEventListener('visitorConsentCollected', handler)\n },\n }\n}\n\n/** URL of Shopify's Customer Privacy script, for headless storefronts that load it themselves. */\nexport function getShopifyCustomerPrivacyScriptUrl(): string {\n return 'https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js'\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { k as ConsentCategory, C as ConsentIntegrationAdapter } from '../types-BV_AFZKQ.js';
2
+
3
+ type TcfApi = (command: string, version: number, callback: (...args: never[]) => void, parameter?: unknown) => void;
4
+ declare global {
5
+ interface Window {
6
+ __tcfapi?: TcfApi;
7
+ }
8
+ }
9
+ /** Options for `tcf`. */
10
+ type TcfOptions = {
11
+ /** TCF purpose IDs per category, merged over the defaults. A category is granted when every listed purpose has consent. */
12
+ purposes?: Partial<Record<ConsentCategory, number[]>>;
13
+ /** Milliseconds bootstrap waits for the CMP before it fails and ConsentLayer falls back to the stored choice. Defaults to `5000`. */
14
+ timeout?: number;
15
+ window?: Window;
16
+ };
17
+ /** Reads consent from the IAB TCF v2 CMP on the page through `__tcfapi`. Read-only: visitors change their choice in the CMP. */
18
+ declare function tcf(options?: TcfOptions): ConsentIntegrationAdapter;
19
+
20
+ export { type TcfOptions, tcf };
@@ -0,0 +1,64 @@
1
+ // src/adapters/tcf.ts
2
+ var defaultPurposes = {
3
+ preferences: [1],
4
+ analytics: [1, 8],
5
+ marketing: [1, 2, 3, 4, 7]
6
+ };
7
+ function listen(api, onData) {
8
+ let listenerId;
9
+ let stopped = false;
10
+ const remove = () => api("removeEventListener", 2, () => void 0, listenerId);
11
+ api("addEventListener", 2, (tcData, success) => {
12
+ if (success && tcData) listenerId = tcData.listenerId;
13
+ if (!stopped && onData(success && tcData ? tcData : null)) stopped = true;
14
+ if (stopped && listenerId !== void 0) remove();
15
+ });
16
+ return () => {
17
+ stopped = true;
18
+ if (listenerId !== void 0) remove();
19
+ };
20
+ }
21
+ function tcf(options = {}) {
22
+ const win = options.window ?? (typeof window !== "undefined" ? window : void 0);
23
+ const purposes = { ...defaultPurposes, ...options.purposes };
24
+ const timeout = options.timeout ?? 5e3;
25
+ if (!(timeout >= 0 && Number.isFinite(timeout))) {
26
+ throw new Error(`[consentlayer] tcf timeout must be a non-negative number, received ${timeout}`);
27
+ }
28
+ const toConsent = (tcData) => Object.fromEntries(
29
+ Object.entries(purposes).map(([category, ids]) => [
30
+ category,
31
+ ids.length > 0 && ids.every((id) => tcData.purpose?.consents?.[id] === true)
32
+ ])
33
+ );
34
+ return {
35
+ id: "tcf",
36
+ authoritative: true,
37
+ bootstrap() {
38
+ const api = win?.__tcfapi;
39
+ if (!api) return null;
40
+ return new Promise((resolve, reject) => {
41
+ const timer = setTimeout(() => {
42
+ stop();
43
+ reject(new Error(`[consentlayer] The TCF CMP did not answer within ${timeout} ms`));
44
+ }, timeout);
45
+ const stop = listen(api, (tcData) => {
46
+ clearTimeout(timer);
47
+ resolve(tcData?.gdprApplies === true ? toConsent(tcData) : null);
48
+ return true;
49
+ });
50
+ });
51
+ },
52
+ subscribe(onConsent) {
53
+ const api = win?.__tcfapi;
54
+ if (!api) return;
55
+ return listen(api, (tcData) => {
56
+ if (tcData?.gdprApplies === true) onConsent(toConsent(tcData));
57
+ });
58
+ }
59
+ };
60
+ }
61
+
62
+ export { tcf };
63
+ //# sourceMappingURL=tcf.js.map
64
+ //# sourceMappingURL=tcf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/tcf.ts"],"names":[],"mappings":"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","file":"tcf.js","sourcesContent":["import type { ConsentCategory, ConsentIntegrationAdapter, ConsentState } from '../core/types'\n\ntype TcData = {\n gdprApplies?: boolean\n eventStatus?: 'tcloaded' | 'cmpuishown' | 'useractioncomplete'\n listenerId?: number\n purpose?: { consents?: Record<number, boolean> }\n}\n\ntype TcfApi = (command: string, version: number, callback: (...args: never[]) => void, parameter?: unknown) => void\n\ndeclare global {\n interface Window {\n __tcfapi?: TcfApi\n }\n}\n\n/** Options for `tcf`. */\nexport type TcfOptions = {\n /** TCF purpose IDs per category, merged over the defaults. A category is granted when every listed purpose has consent. */\n purposes?: Partial<Record<ConsentCategory, number[]>>\n /** Milliseconds bootstrap waits for the CMP before it fails and ConsentLayer falls back to the stored choice. Defaults to `5000`. */\n timeout?: number\n window?: Window\n}\n\nconst defaultPurposes: Partial<Record<ConsentCategory, number[]>> = {\n preferences: [1],\n analytics: [1, 8],\n marketing: [1, 2, 3, 4, 7],\n}\n\nfunction listen(api: TcfApi, onData: (tcData: TcData | null) => boolean | void): () => void {\n let listenerId: number | undefined\n let stopped = false\n const remove = () => api('removeEventListener', 2, () => undefined, listenerId)\n api('addEventListener', 2, (tcData: TcData | null, success: boolean) => {\n if (success && tcData) listenerId = tcData.listenerId\n if (!stopped && onData(success && tcData ? tcData : null)) stopped = true\n if (stopped && listenerId !== undefined) remove()\n })\n return () => {\n stopped = true\n if (listenerId !== undefined) remove()\n }\n}\n\n/** Reads consent from the IAB TCF v2 CMP on the page through `__tcfapi`. Read-only: visitors change their choice in the CMP. */\nexport function tcf(options: TcfOptions = {}): ConsentIntegrationAdapter {\n const win = options.window ?? (typeof window !== 'undefined' ? window : undefined)\n const purposes = { ...defaultPurposes, ...options.purposes }\n const timeout = options.timeout ?? 5000\n if (!(timeout >= 0 && Number.isFinite(timeout))) {\n throw new Error(`[consentlayer] tcf timeout must be a non-negative number, received ${timeout}`)\n }\n const toConsent = (tcData: TcData) =>\n Object.fromEntries(\n Object.entries(purposes).map(([category, ids]) => [\n category,\n ids.length > 0 && ids.every((id) => tcData.purpose?.consents?.[id] === true),\n ]),\n ) as ConsentState\n\n return {\n id: 'tcf',\n authoritative: true,\n bootstrap() {\n const api = win?.__tcfapi\n if (!api) return null\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n stop()\n reject(new Error(`[consentlayer] The TCF CMP did not answer within ${timeout} ms`))\n }, timeout)\n const stop = listen(api, (tcData) => {\n clearTimeout(timer)\n resolve(tcData?.gdprApplies === true ? toConsent(tcData) : null)\n return true\n })\n })\n },\n subscribe(onConsent) {\n const api = win?.__tcfapi\n if (!api) return\n return listen(api, (tcData) => {\n if (tcData?.gdprApplies === true) onConsent(toConsent(tcData))\n })\n },\n }\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as react from 'react';
3
+ import { ReactNode } from 'react';
4
+ import { m as Analytics, n as PageViewPayload, o as LazyBootConfig, p as ConsentState, q as ConsentApi } from './types-BV_AFZKQ.js';
5
+ import { C as ConsentControllerState, a as ConsentControllerActions } from './consent-controller-DyHPNfyT.js';
6
+
7
+ declare const AnalyticsContext: react.Context<Analytics | undefined>;
8
+ type ConsentControllerValue = ConsentControllerState & ConsentControllerActions;
9
+ /** Props for `<AnalyticsProvider>`. */
10
+ type AnalyticsProviderProps = {
11
+ analytics: Analytics;
12
+ children: ReactNode;
13
+ /** Changes on every navigation; a page view is sent once per key. `null` holds page views until the route is known. */
14
+ routeKey?: string | null;
15
+ page?: PageViewPayload;
16
+ /** When providers load. Defaults to `{ mode: 'immediate' }`. */
17
+ boot?: LazyBootConfig;
18
+ autoShowBanner?: boolean;
19
+ /** Defaults to the configured name (plus `ACM_openConsent` with `names.legacy`). */
20
+ openConsentGlobalKey?: string;
21
+ };
22
+ /** Boots analytics, sends page views on navigation and provides the state for your consent UI. */
23
+ declare function AnalyticsProvider({ analytics, children, routeKey, page, boot, autoShowBanner, openConsentGlobalKey, }: AnalyticsProviderProps): react_jsx_runtime.JSX.Element;
24
+ /** The instance from `<AnalyticsProvider>`, else the one `createAnalytics()` made. */
25
+ declare function useAnalytics(): Analytics;
26
+ /** Current consent, re-rendering when it changes. */
27
+ declare function useConsent(): {
28
+ consent: ConsentState | null;
29
+ engine: ConsentApi;
30
+ setConsent: (consent: ConsentState) => Promise<void>;
31
+ update: (partial: Partial<ConsentState>) => Promise<void>;
32
+ };
33
+ /** State and actions for building your own consent UI. */
34
+ declare function useConsentController(): ConsentControllerValue;
35
+
36
+ export { type AnalyticsProviderProps as A, AnalyticsContext as a, AnalyticsProvider as b, useConsent as c, useConsentController as d, useAnalytics as u };
@@ -0,0 +1,56 @@
1
+ import { Signal, EnvironmentProviders } from '@angular/core';
2
+ import { C as ConsentControllerState, a as ConsentControllerActions, b as ConsentControllerOptions } from './consent-controller-DyHPNfyT.js';
3
+ import { m as Analytics, n as PageViewPayload, p as ConsentState, q as ConsentApi } from './types-BV_AFZKQ.js';
4
+
5
+ /** Options for `provideConsentLayer()`. */
6
+ type ConsentLayerOptions = ConsentControllerOptions & {
7
+ analytics: Analytics;
8
+ page?: PageViewPayload;
9
+ /** Reports errors that reach Angular's `ErrorHandler` through `captureError`. Defaults to `true`. */
10
+ captureErrors?: boolean;
11
+ };
12
+ /** What `injectConsentController()` returns: the UI state as signals, and the actions. */
13
+ type ConsentController = {
14
+ readonly [Key in keyof ConsentControllerState]: Signal<ConsentControllerState[Key]>;
15
+ } & ConsentControllerActions;
16
+ /**
17
+ * Provides the instance and the consent UI state. In the browser, boots
18
+ * analytics once the application is stable, sends a page view per Angular
19
+ * Router navigation, and reports errors from Angular's `ErrorHandler`.
20
+ */
21
+ declare function provideConsentLayer(options: ConsentLayerOptions): EnvironmentProviders;
22
+ /** The instance from `provideConsentLayer()`, else the one `createAnalytics()` made. */
23
+ declare function injectAnalytics(): Analytics;
24
+ /** Current consent as a signal, `null` before a choice. */
25
+ declare function injectConsent(): {
26
+ consent: Signal<ConsentState | null>;
27
+ engine: ConsentApi;
28
+ setConsent: (consent: ConsentState) => Promise<void>;
29
+ update: (partial: Partial<ConsentState>) => Promise<void>;
30
+ };
31
+ /** State and actions for building your own consent UI. */
32
+ declare function injectConsentController(): ConsentController;
33
+ /**
34
+ * Adds Meta's `<noscript>` PageView pixel to the end of `<body>` in HTML
35
+ * rendered on the server. It fires only for visitors without JavaScript, who
36
+ * never see the consent banner, so it tracks them without consent. Add it only
37
+ * where that is lawful for you. Adds nothing without `enabled` or while
38
+ * `pixelId` is unset. Other options become attributes of the `<img>`.
39
+ */
40
+ declare function provideMetaPixelNoscript({ pixelId, enabled, ...attrs }: {
41
+ pixelId: string | undefined;
42
+ enabled?: boolean;
43
+ } & Record<string, unknown>): EnvironmentProviders;
44
+ /**
45
+ * Adds Google Tag Manager's `<noscript>` iframe to the start of `<body>` in
46
+ * HTML rendered on the server. It loads the container only for visitors without
47
+ * JavaScript, who never see the consent banner, so its tags run without
48
+ * consent. Add it only where that is lawful for you. Adds nothing without
49
+ * `enabled` or while `containerId` is unset. Other options become attributes of the `<iframe>`.
50
+ */
51
+ declare function provideGtmNoscript({ containerId, enabled, ...attrs }: {
52
+ containerId: string | undefined;
53
+ enabled?: boolean;
54
+ } & Record<string, unknown>): EnvironmentProviders;
55
+
56
+ export { type ConsentController, type ConsentLayerOptions, injectAnalytics, injectConsent, injectConsentController, provideConsentLayer, provideGtmNoscript, provideMetaPixelNoscript };
@@ -0,0 +1,120 @@
1
+ import { metaNoscriptImg, gtmNoscriptIframe } from './chunk-VIIGBALD.js';
2
+ import { createConsentController, latestAnalytics } from './chunk-FDJMIJFV.js';
3
+ import { InjectionToken, makeEnvironmentProviders, ErrorHandler, signal, inject, ApplicationRef, DestroyRef, provideEnvironmentInitializer, DOCUMENT, computed, PLATFORM_ID } from '@angular/core';
4
+ import { ROUTES, Router, NavigationEnd } from '@angular/router';
5
+
6
+ var ANALYTICS = new InjectionToken("consentlayer analytics");
7
+ var CONTROLLER = new InjectionToken("consentlayer controller");
8
+ var isBrowser = () => inject(PLATFORM_ID) === "browser";
9
+ function provideConsentLayer(options) {
10
+ const { analytics, page, captureErrors = true, ...controllerOptions } = options;
11
+ return makeEnvironmentProviders([
12
+ { provide: ANALYTICS, useValue: analytics },
13
+ {
14
+ provide: CONTROLLER,
15
+ useFactory: () => {
16
+ const controller = createConsentController(analytics, controllerOptions);
17
+ const state = signal(controller.getSnapshot());
18
+ if (isBrowser()) {
19
+ const routes = inject(ROUTES, { optional: true });
20
+ const router = routes && inject(Router);
21
+ const navigations = router?.events.subscribe((event) => {
22
+ if (event instanceof NavigationEnd)
23
+ queueMicrotask(() => controller.setRoute(event.urlAfterRedirects.split("#")[0] ?? "/", page));
24
+ });
25
+ if (!router) controller.setRoute("default", page);
26
+ const unsubscribe = controller.subscribe(() => state.set(controller.getSnapshot()));
27
+ let stop;
28
+ let destroyed = false;
29
+ void inject(ApplicationRef).whenStable().then(() => {
30
+ if (!destroyed) stop = controller.start();
31
+ });
32
+ inject(DestroyRef).onDestroy(() => {
33
+ destroyed = true;
34
+ stop?.();
35
+ unsubscribe();
36
+ navigations?.unsubscribe();
37
+ });
38
+ }
39
+ const field = (key) => computed(() => state()[key]);
40
+ return {
41
+ ready: field("ready"),
42
+ error: field("error"),
43
+ bannerVisible: field("bannerVisible"),
44
+ dialogVisible: field("dialogVisible"),
45
+ categories: field("categories"),
46
+ gpc: field("gpc"),
47
+ prefs: field("prefs"),
48
+ ...controller.actions
49
+ };
50
+ }
51
+ },
52
+ provideEnvironmentInitializer(() => void inject(CONTROLLER)),
53
+ captureErrors ? {
54
+ provide: ErrorHandler,
55
+ useFactory: () => {
56
+ const fallback = new ErrorHandler();
57
+ const browser = isBrowser();
58
+ return {
59
+ handleError(error) {
60
+ if (browser) void analytics.captureError(error, { source: "boundary" });
61
+ fallback.handleError(error);
62
+ }
63
+ };
64
+ }
65
+ } : []
66
+ ]);
67
+ }
68
+ function injectAnalytics() {
69
+ const analytics = inject(ANALYTICS, { optional: true }) ?? latestAnalytics();
70
+ if (!analytics) throw new Error("injectAnalytics needs createAnalytics() to have run, or provideConsentLayer()");
71
+ return analytics;
72
+ }
73
+ function injectConsent() {
74
+ const analytics = injectAnalytics();
75
+ const consent = signal(analytics.consent.getConsent());
76
+ if (isBrowser()) {
77
+ inject(DestroyRef).onDestroy(analytics.consent.subscribe(() => consent.set(analytics.consent.getConsent())));
78
+ }
79
+ return {
80
+ consent: consent.asReadonly(),
81
+ engine: analytics.consent,
82
+ setConsent: (next) => analytics.consent.setConsent(next),
83
+ update: (partial) => analytics.consent.update(partial)
84
+ };
85
+ }
86
+ function injectConsentController() {
87
+ const controller = inject(CONTROLLER, { optional: true });
88
+ if (!controller) throw new Error("injectConsentController needs provideConsentLayer()");
89
+ return controller;
90
+ }
91
+ function provideMetaPixelNoscript({
92
+ pixelId,
93
+ enabled,
94
+ ...attrs
95
+ }) {
96
+ return provideEnvironmentInitializer(() => {
97
+ if (!enabled || !pixelId || isBrowser()) return;
98
+ const document = inject(DOCUMENT);
99
+ const noscript = document.createElement("noscript");
100
+ noscript.innerHTML = metaNoscriptImg(pixelId, attrs);
101
+ document.body.appendChild(noscript);
102
+ });
103
+ }
104
+ function provideGtmNoscript({
105
+ containerId,
106
+ enabled,
107
+ ...attrs
108
+ }) {
109
+ return provideEnvironmentInitializer(() => {
110
+ if (!enabled || !containerId || isBrowser()) return;
111
+ const document = inject(DOCUMENT);
112
+ const noscript = document.createElement("noscript");
113
+ noscript.innerHTML = gtmNoscriptIframe(containerId, attrs);
114
+ document.body.insertBefore(noscript, document.body.firstChild);
115
+ });
116
+ }
117
+
118
+ export { injectAnalytics, injectConsent, injectConsentController, provideConsentLayer, provideGtmNoscript, provideMetaPixelNoscript };
119
+ //# sourceMappingURL=angular.js.map
120
+ //# sourceMappingURL=angular.js.map