@buildinams/core 0.3.0 → 0.4.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.
- package/README.md +24 -1
- package/dist/analytics/google/index.d.ts +158 -0
- package/dist/analytics/google/index.js +274 -0
- package/dist/analytics/google/index.js.map +1 -0
- package/dist/analytics/index.d.ts +101 -0
- package/dist/analytics/index.js +384 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/shopify/index.d.ts +186 -0
- package/dist/analytics/shopify/index.js +175 -0
- package/dist/analytics/shopify/index.js.map +1 -0
- package/dist/{chunk-IAAQVL6M.js → chunk-FJ7SNQQI.js} +24 -5
- package/dist/chunk-FJ7SNQQI.js.map +1 -0
- package/dist/chunk-G2BKC4BU.js +8 -0
- package/dist/chunk-G2BKC4BU.js.map +1 -0
- package/dist/chunk-GQVN2ECX.js +14 -0
- package/dist/chunk-GQVN2ECX.js.map +1 -0
- package/dist/chunk-LL3VTFNY.js +22 -0
- package/dist/chunk-LL3VTFNY.js.map +1 -0
- package/dist/chunk-M3HNM5HC.js +48 -0
- package/dist/chunk-M3HNM5HC.js.map +1 -0
- package/dist/chunk-MHFGMH3G.js +11 -0
- package/dist/chunk-MHFGMH3G.js.map +1 -0
- package/dist/chunk-OS2T6P2T.js +50 -0
- package/dist/chunk-OS2T6P2T.js.map +1 -0
- package/dist/chunk-QIJGG2F4.js +51 -0
- package/dist/chunk-QIJGG2F4.js.map +1 -0
- package/dist/consent/cookiebot/index.d.ts +16 -0
- package/dist/consent/cookiebot/index.js +27 -0
- package/dist/consent/cookiebot/index.js.map +1 -0
- package/dist/consent/google/index.d.ts +23 -0
- package/dist/consent/google/index.js +48 -0
- package/dist/consent/google/index.js.map +1 -0
- package/dist/consent/index.d.ts +1 -0
- package/dist/consent/index.js +3 -0
- package/dist/consent/index.js.map +1 -0
- package/dist/consent/onetrust/index.d.ts +23 -0
- package/dist/consent/onetrust/index.js +39 -0
- package/dist/consent/onetrust/index.js.map +1 -0
- package/dist/consent/shopify/index.d.ts +44 -0
- package/dist/consent/shopify/index.js +84 -0
- package/dist/consent/shopify/index.js.map +1 -0
- package/dist/cookies/index.d.ts +230 -0
- package/dist/cookies/index.js +188 -0
- package/dist/cookies/index.js.map +1 -0
- package/dist/define-events-CxZjoUlG.d.ts +255 -0
- package/dist/destination-5KJ5PeR3.d.ts +122 -0
- package/dist/index-Zef1nnTP.d.ts +36 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +64 -1
- package/dist/index.js.map +1 -1
- package/dist/listeners-DkU-vBPa.d.ts +15 -0
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.js +1 -1
- package/dist/schema/index.d.ts +16 -9
- package/dist/schema/index.js +1 -1
- package/dist/schema-CfQvhbuU.d.ts +87 -0
- package/dist/store/index.js +1 -1
- package/dist/tracking/cart/index.d.ts +74 -0
- package/dist/tracking/cart/index.js +118 -0
- package/dist/tracking/cart/index.js.map +1 -0
- package/dist/tracking/dom/index.d.ts +320 -0
- package/dist/tracking/dom/index.js +1196 -0
- package/dist/tracking/dom/index.js.map +1 -0
- package/docs/analytics.md +883 -0
- package/docs/cookies.md +354 -0
- package/docs/decisions/001-analytics-project-events.md +31 -0
- package/docs/decisions/002-tracking-dom-opt-in.md +29 -0
- package/docs/decisions/003-tracking-element-data.md +27 -0
- package/docs/decisions/004-cookies-explicit-store.md +26 -0
- package/docs/decisions/005-cookies-invalid-reads-fall-back.md +27 -0
- package/docs/decisions/006-cookies-server-only-types.md +19 -0
- package/docs/decisions/007-cookies-options-on-definition.md +25 -0
- package/docs/decisions/008-cookies-in-vertical-slices.md +24 -0
- package/docs/decisions/009-analytics-consent-targets.md +29 -0
- package/docs/decisions/010-analytics-destination-contract.md +29 -0
- package/docs/decisions/011-tracking-decoupled.md +30 -0
- package/docs/tracking.md +221 -0
- package/package.json +84 -3
- package/dist/chunk-IAAQVL6M.js.map +0 -1
- package/dist/chunk-SIU7SKJK.js +0 -139
- package/dist/chunk-SIU7SKJK.js.map +0 -1
- package/dist/schema-iy92gbTC.d.ts +0 -36
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/consent/google/google.ts
|
|
2
|
+
var GRANTED = "granted";
|
|
3
|
+
var DENIED = "denied";
|
|
4
|
+
var toConsentModeState = (consent) => {
|
|
5
|
+
const state = (allowed) => allowed ? GRANTED : DENIED;
|
|
6
|
+
return {
|
|
7
|
+
functionality_storage: state(consent.preferences),
|
|
8
|
+
personalization_storage: state(consent.preferences),
|
|
9
|
+
analytics_storage: state(consent.analytics),
|
|
10
|
+
ad_storage: state(consent.marketing),
|
|
11
|
+
ad_user_data: state(consent.marketing),
|
|
12
|
+
ad_personalization: state(consent.marketing),
|
|
13
|
+
security_storage: GRANTED
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
var googleConsentMode = ({
|
|
17
|
+
dataLayerName = "dataLayer",
|
|
18
|
+
waitForUpdate = 500
|
|
19
|
+
} = {}) => {
|
|
20
|
+
const dataLayer = () => {
|
|
21
|
+
const target = window;
|
|
22
|
+
return target[dataLayerName] ??= [];
|
|
23
|
+
};
|
|
24
|
+
function gtag(..._args) {
|
|
25
|
+
dataLayer().push(arguments);
|
|
26
|
+
}
|
|
27
|
+
const isDefaultCommand = (entry) => Object.prototype.toString.call(entry) === "[object Arguments]" && entry[0] === "consent" && entry[1] === "default";
|
|
28
|
+
if (typeof window !== "undefined" && !dataLayer().some(isDefaultCommand)) {
|
|
29
|
+
gtag("consent", "default", {
|
|
30
|
+
...toConsentModeState({
|
|
31
|
+
preferences: false,
|
|
32
|
+
analytics: false,
|
|
33
|
+
marketing: false
|
|
34
|
+
}),
|
|
35
|
+
wait_for_update: waitForUpdate
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
name: "google-consent-mode",
|
|
40
|
+
apply: (consent) => {
|
|
41
|
+
gtag("consent", "update", toConsentModeState(consent));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export { googleConsentMode };
|
|
47
|
+
//# sourceMappingURL=index.js.map
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/consent/google/google.ts"],"names":[],"mappings":";AAEA,IAAM,OAAA,GAAU,SAAA;AAChB,IAAM,MAAA,GAAS,QAAA;AAGR,IAAM,kBAAA,GAAqB,CAAC,OAAA,KAAqB;AACvD,EAAA,MAAM,KAAA,GAAQ,CAAC,OAAA,KAAsB,OAAA,GAAU,OAAA,GAAU,MAAA;AACzD,EAAA,OAAO;AAAA,IACN,qBAAA,EAAuB,KAAA,CAAM,OAAA,CAAQ,WAAW,CAAA;AAAA,IAChD,uBAAA,EAAyB,KAAA,CAAM,OAAA,CAAQ,WAAW,CAAA;AAAA,IAClD,iBAAA,EAAmB,KAAA,CAAM,OAAA,CAAQ,SAAS,CAAA;AAAA,IAC1C,UAAA,EAAY,KAAA,CAAM,OAAA,CAAQ,SAAS,CAAA;AAAA,IACnC,YAAA,EAAc,KAAA,CAAM,OAAA,CAAQ,SAAS,CAAA;AAAA,IACrC,kBAAA,EAAoB,KAAA,CAAM,OAAA,CAAQ,SAAS,CAAA;AAAA,IAC3C,gBAAA,EAAkB;AAAA,GACnB;AACD,CAAA;AAqBO,IAAM,oBAAoB,CAAC;AAAA,EACjC,aAAA,GAAgB,WAAA;AAAA,EAChB,aAAA,GAAgB;AACjB,CAAA,GAA8B,EAAC,KAAqB;AACnD,EAAA,MAAM,YAAY,MAAM;AACvB,IAAA,MAAM,MAAA,GAAS,MAAA;AACf,IAAA,OAAQ,MAAA,CAAO,aAAa,CAAA,KAAM,EAAC;AAAA,EACpC,CAAA;AAEA,EAAA,SAAS,QAAQ,KAAA,EAAkB;AAElC,IAAA,SAAA,EAAU,CAAE,KAAK,SAAS,CAAA;AAAA,EAC3B;AACA,EAAA,MAAM,mBAAmB,CAAC,KAAA,KACzB,MAAA,CAAO,SAAA,CAAU,SAAS,IAAA,CAAK,KAAK,CAAA,KAAM,oBAAA,IACzC,MAA6B,CAAC,CAAA,KAAM,SAAA,IACpC,KAAA,CAA6B,CAAC,CAAA,KAAM,SAAA;AAGtC,EAAA,IAAI,OAAO,WAAW,WAAA,IAAe,CAAC,WAAU,CAAE,IAAA,CAAK,gBAAgB,CAAA,EAAG;AACzE,IAAA,IAAA,CAAK,WAAW,SAAA,EAAW;AAAA,MAC1B,GAAG,kBAAA,CAAmB;AAAA,QACrB,WAAA,EAAa,KAAA;AAAA,QACb,SAAA,EAAW,KAAA;AAAA,QACX,SAAA,EAAW;AAAA,OACX,CAAA;AAAA,MACD,eAAA,EAAiB;AAAA,KACjB,CAAA;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,qBAAA;AAAA,IACN,KAAA,EAAO,CAAC,OAAA,KAAY;AACnB,MAAA,IAAA,CAAK,SAAA,EAAW,QAAA,EAAU,kBAAA,CAAmB,OAAO,CAAC,CAAA;AAAA,IACtD;AAAA,GACD;AACD","file":"index.js","sourcesContent":["import type { Consent, ConsentTarget } from \"../consent\";\n\nconst GRANTED = \"granted\";\nconst DENIED = \"denied\";\n\n/** Google Consent Mode v2 signals for an answer. */\nexport const toConsentModeState = (consent: Consent) => {\n\tconst state = (allowed: boolean) => (allowed ? GRANTED : DENIED);\n\treturn {\n\t\tfunctionality_storage: state(consent.preferences),\n\t\tpersonalization_storage: state(consent.preferences),\n\t\tanalytics_storage: state(consent.analytics),\n\t\tad_storage: state(consent.marketing),\n\t\tad_user_data: state(consent.marketing),\n\t\tad_personalization: state(consent.marketing),\n\t\tsecurity_storage: GRANTED,\n\t};\n};\n\nexport type GoogleConsentModeOptions = {\n\t/** Default `\"dataLayer\"`. */\n\tdataLayerName?: string;\n\t/** Milliseconds tags wait for the first answer before firing under the denied defaults. Default 500. */\n\twaitForUpdate?: number;\n};\n\n/**\n * Sends Google Consent Mode v2 signals through the dataLayer: every category denied when created, then an update for\n * every answer. Only for consent platforms without a Consent Mode integration, like Shopify's. Create it before GTM or\n * gtag.js loads, so the defaults come first.\n *\n * @see https://developers.google.com/tag-platform/security/guides/consent\n *\n * @example\n * ```ts\n * export const analytics = createAnalytics({ consent: shopifyConsent(), consentTargets: [googleConsentMode()] });\n * ```\n */\nexport const googleConsentMode = ({\n\tdataLayerName = \"dataLayer\",\n\twaitForUpdate = 500,\n}: GoogleConsentModeOptions = {}): ConsentTarget => {\n\tconst dataLayer = () => {\n\t\tconst target = window as unknown as Record<string, unknown[] | undefined>;\n\t\treturn (target[dataLayerName] ??= []);\n\t};\n\t// GTM only treats an `arguments` object as a gtag command, not an array.\n\tfunction gtag(..._args: unknown[]) {\n\t\t// eslint-disable-next-line prefer-rest-params\n\t\tdataLayer().push(arguments);\n\t}\n\tconst isDefaultCommand = (entry: unknown) =>\n\t\tObject.prototype.toString.call(entry) === \"[object Arguments]\" &&\n\t\t(entry as ArrayLike<unknown>)[0] === \"consent\" &&\n\t\t(entry as ArrayLike<unknown>)[1] === \"default\";\n\n\t// Once per dataLayer, so another instance, e.g. after a hot reload, doesn't reset earlier updates.\n\tif (typeof window !== \"undefined\" && !dataLayer().some(isDefaultCommand)) {\n\t\tgtag(\"consent\", \"default\", {\n\t\t\t...toConsentModeState({\n\t\t\t\tpreferences: false,\n\t\t\t\tanalytics: false,\n\t\t\t\tmarketing: false,\n\t\t\t}),\n\t\t\twait_for_update: waitForUpdate,\n\t\t});\n\t}\n\n\treturn {\n\t\tname: \"google-consent-mode\",\n\t\tapply: (consent) => {\n\t\t\tgtag(\"consent\", \"update\", toConsentModeState(consent));\n\t\t},\n\t};\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { e as CONSENT_CATEGORIES, d as Consent, a as ConsentSource, b as ConsentTarget } from '../index-Zef1nnTP.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { C as ConsentCategory, a as ConsentSource } from '../../index-Zef1nnTP.js';
|
|
2
|
+
|
|
3
|
+
type OneTrustConsentOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* OneTrust group ids per category, all of which must be active. Defaults to OneTrust's standard groups:
|
|
6
|
+
* `C0003` (functional), `C0002` (performance) and `C0004` (targeting).
|
|
7
|
+
*/
|
|
8
|
+
groups?: Partial<Record<ConsentCategory, readonly string[]>>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Consent from OneTrust. Answered once the visitor closes the banner, or straight away where OneTrust shows none.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* export const analytics = createAnalytics({
|
|
16
|
+
* consent: oneTrustConsent({ groups: { marketing: ["C0004", "C0005"] } }),
|
|
17
|
+
* destinations: [gtmDestination()],
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare const oneTrustConsent: ({ groups: customGroups, }?: OneTrustConsentOptions) => ConsentSource;
|
|
22
|
+
|
|
23
|
+
export { type OneTrustConsentOptions, oneTrustConsent };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { eventConsentSource } from '../../chunk-QIJGG2F4.js';
|
|
2
|
+
|
|
3
|
+
// src/consent/onetrust/onetrust.ts
|
|
4
|
+
var DEFAULT_GROUPS = {
|
|
5
|
+
preferences: ["C0003"],
|
|
6
|
+
analytics: ["C0002"],
|
|
7
|
+
marketing: ["C0004"]
|
|
8
|
+
};
|
|
9
|
+
var oneTrustConsent = ({
|
|
10
|
+
groups: customGroups
|
|
11
|
+
} = {}) => {
|
|
12
|
+
const groups = { ...DEFAULT_GROUPS, ...customGroups };
|
|
13
|
+
return eventConsentSource({
|
|
14
|
+
name: "onetrust",
|
|
15
|
+
read: () => {
|
|
16
|
+
const { OneTrust, OnetrustActiveGroups = "" } = window;
|
|
17
|
+
if (typeof OneTrust?.IsAlertBoxClosed !== "function" || typeof OneTrust.GetDomainData !== "function") {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (!OneTrust.IsAlertBoxClosed() && OneTrust.GetDomainData().ShowAlertNotice !== false) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const active = new Set(
|
|
24
|
+
OnetrustActiveGroups.split(",").map((id) => id.trim())
|
|
25
|
+
);
|
|
26
|
+
const granted = (category) => groups[category].every((id) => active.has(id));
|
|
27
|
+
return {
|
|
28
|
+
preferences: granted("preferences"),
|
|
29
|
+
analytics: granted("analytics"),
|
|
30
|
+
marketing: granted("marketing")
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
events: ["OneTrustGroupsUpdated"]
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export { oneTrustConsent };
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
39
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/consent/onetrust/onetrust.ts"],"names":[],"mappings":";;;AAsBA,IAAM,cAAA,GAA6D;AAAA,EAClE,WAAA,EAAa,CAAC,OAAO,CAAA;AAAA,EACrB,SAAA,EAAW,CAAC,OAAO,CAAA;AAAA,EACnB,SAAA,EAAW,CAAC,OAAO;AACpB,CAAA;AAaO,IAAM,kBAAkB,CAAC;AAAA,EAC/B,MAAA,EAAQ;AACT,CAAA,GAA4B,EAAC,KAAqB;AACjD,EAAA,MAAM,MAAA,GAAS,EAAE,GAAG,cAAA,EAAgB,GAAG,YAAA,EAAa;AAEpD,EAAA,OAAO,kBAAA,CAAmB;AAAA,IACzB,IAAA,EAAM,UAAA;AAAA,IACN,MAAM,MAAM;AACX,MAAA,MAAM,EAAE,QAAA,EAAU,oBAAA,GAAuB,EAAA,EAAG,GAAI,MAAA;AAEhD,MAAA,IACC,OAAO,QAAA,EAAU,gBAAA,KAAqB,cACtC,OAAO,QAAA,CAAS,kBAAkB,UAAA,EACjC;AACD,QAAA,OAAO,IAAA;AAAA,MACR;AACA,MAAA,IACC,CAAC,SAAS,gBAAA,EAAiB,IAC3B,SAAS,aAAA,EAAc,CAAE,oBAAoB,KAAA,EAC5C;AACD,QAAA,OAAO,IAAA;AAAA,MACR;AAEA,MAAA,MAAM,SAAS,IAAI,GAAA;AAAA,QAClB,oBAAA,CAAqB,MAAM,GAAG,CAAA,CAAE,IAAI,CAAC,EAAA,KAAO,EAAA,CAAG,IAAA,EAAM;AAAA,OACtD;AACA,MAAA,MAAM,OAAA,GAAU,CAAC,QAAA,KAChB,MAAA,CAAO,QAAQ,CAAA,CAAE,KAAA,CAAM,CAAC,EAAA,KAAO,MAAA,CAAO,GAAA,CAAI,EAAE,CAAC,CAAA;AAE9C,MAAA,OAAO;AAAA,QACN,WAAA,EAAa,QAAQ,aAAa,CAAA;AAAA,QAClC,SAAA,EAAW,QAAQ,WAAW,CAAA;AAAA,QAC9B,SAAA,EAAW,QAAQ,WAAW;AAAA,OAC/B;AAAA,IACD,CAAA;AAAA,IACA,MAAA,EAAQ,CAAC,uBAAuB;AAAA,GAChC,CAAA;AACF","file":"index.js","sourcesContent":["import type { ConsentCategory, ConsentSource } from \"../consent\";\nimport { eventConsentSource } from \"../event-consent-source\";\n\ntype OneTrustGlobal = {\n\tIsAlertBoxClosed?: () => boolean;\n\tGetDomainData?: () => { ShowAlertNotice?: boolean };\n};\n\ntype OneTrustWindow = {\n\tOneTrust?: OneTrustGlobal;\n\t/** Comma-separated active group ids, e.g. `\",C0001,C0002,\"`. */\n\tOnetrustActiveGroups?: string;\n};\n\nexport type OneTrustConsentOptions = {\n\t/**\n\t * OneTrust group ids per category, all of which must be active. Defaults to OneTrust's standard groups:\n\t * `C0003` (functional), `C0002` (performance) and `C0004` (targeting).\n\t */\n\tgroups?: Partial<Record<ConsentCategory, readonly string[]>>;\n};\n\nconst DEFAULT_GROUPS: Record<ConsentCategory, readonly string[]> = {\n\tpreferences: [\"C0003\"],\n\tanalytics: [\"C0002\"],\n\tmarketing: [\"C0004\"],\n};\n\n/**\n * Consent from OneTrust. Answered once the visitor closes the banner, or straight away where OneTrust shows none.\n *\n * @example\n * ```ts\n * export const analytics = createAnalytics({\n * consent: oneTrustConsent({ groups: { marketing: [\"C0004\", \"C0005\"] } }),\n * destinations: [gtmDestination()],\n * });\n * ```\n */\nexport const oneTrustConsent = ({\n\tgroups: customGroups,\n}: OneTrustConsentOptions = {}): ConsentSource => {\n\tconst groups = { ...DEFAULT_GROUPS, ...customGroups };\n\n\treturn eventConsentSource({\n\t\tname: \"onetrust\",\n\t\tread: () => {\n\t\t\tconst { OneTrust, OnetrustActiveGroups = \"\" } = window as OneTrustWindow;\n\t\t\t// `window.OneTrust` can exist as a partial object before the SDK has loaded.\n\t\t\tif (\n\t\t\t\ttypeof OneTrust?.IsAlertBoxClosed !== \"function\" ||\n\t\t\t\ttypeof OneTrust.GetDomainData !== \"function\"\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (\n\t\t\t\t!OneTrust.IsAlertBoxClosed() &&\n\t\t\t\tOneTrust.GetDomainData().ShowAlertNotice !== false\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst active = new Set(\n\t\t\t\tOnetrustActiveGroups.split(\",\").map((id) => id.trim()),\n\t\t\t);\n\t\t\tconst granted = (category: ConsentCategory) =>\n\t\t\t\tgroups[category].every((id) => active.has(id));\n\n\t\t\treturn {\n\t\t\t\tpreferences: granted(\"preferences\"),\n\t\t\t\tanalytics: granted(\"analytics\"),\n\t\t\t\tmarketing: granted(\"marketing\"),\n\t\t\t};\n\t\t},\n\t\tevents: [\"OneTrustGroupsUpdated\"],\n\t});\n};\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { a as ConsentSource, b as ConsentTarget, d as Consent } from '../../index-Zef1nnTP.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Consent from Shopify's Customer Privacy API, e.g. collected by Shopify's privacy banner through
|
|
5
|
+
* `<ShopifyScripts consent={{ mode: "default-banner" }} />`. Answered once the visitor responds, or straight away where
|
|
6
|
+
* Shopify shows no banner. `marketing` needs both `marketing` and `sale_of_data`, so an opt-out of data sale also stops
|
|
7
|
+
* third-party ad tags, as Shopify does for pixels that count as a sale.
|
|
8
|
+
*
|
|
9
|
+
* @see https://shopify.dev/docs/api/customer-privacy
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* export const analytics = createAnalytics({ consent: shopifyConsent(), destinations: [shopifyDestination()] });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
declare const shopifyConsent: () => ConsentSource;
|
|
17
|
+
type ShopifyCustomerPrivacyOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* What to set `sale_of_data` to, which no category covers. Default `false`. Pass `(consent) => consent.marketing` to
|
|
20
|
+
* have it follow marketing.
|
|
21
|
+
*/
|
|
22
|
+
saleOfData?: boolean | ((consent: Consent) => boolean);
|
|
23
|
+
};
|
|
24
|
+
type ShopifyCustomerPrivacy = ConsentTarget & {
|
|
25
|
+
/**
|
|
26
|
+
* Pass to `<ShopifyScripts consent={{ mode: "custom-banner", setup }} />`. Resolves once the visitor's answer has
|
|
27
|
+
* reached the Customer Privacy API, which releases Hydrogen's analytics bus.
|
|
28
|
+
*/
|
|
29
|
+
syncConsent: () => Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Tells Shopify's Customer Privacy API the visitor's consent, so Shopify's scripts and checkout follow it: `preferences`,
|
|
33
|
+
* `analytics` and `marketing` as they are, `sale_of_data` from `saleOfData`. Skipped when `shopifyConsent()` is the
|
|
34
|
+
* source.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* export const customerPrivacy = shopifyCustomerPrivacy();
|
|
39
|
+
* export const analytics = createAnalytics({ consent: cookiebotConsent(), consentTargets: [customerPrivacy] });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare const shopifyCustomerPrivacy: ({ saleOfData, }?: ShopifyCustomerPrivacyOptions) => ShopifyCustomerPrivacy;
|
|
43
|
+
|
|
44
|
+
export { type ShopifyCustomerPrivacy, type ShopifyCustomerPrivacyOptions, shopifyConsent, shopifyCustomerPrivacy };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { eventConsentSource } from '../../chunk-QIJGG2F4.js';
|
|
2
|
+
import { IS_DEVELOPMENT } from '../../chunk-GQVN2ECX.js';
|
|
3
|
+
|
|
4
|
+
// src/consent/shopify/shopify.ts
|
|
5
|
+
var NAME = "shopify";
|
|
6
|
+
var SYNC_WARNING_DELAY = 1e4;
|
|
7
|
+
var getCustomerPrivacy = () => window.Shopify?.customerPrivacy;
|
|
8
|
+
var hasAnswered = (privacy) => {
|
|
9
|
+
if (privacy.shouldShowBanner?.() === false) return true;
|
|
10
|
+
const { analytics, marketing, preferences } = privacy.currentVisitorConsent?.() ?? {};
|
|
11
|
+
return [analytics, marketing, preferences].some((value) => !!value);
|
|
12
|
+
};
|
|
13
|
+
var shopifyConsent = () => eventConsentSource({
|
|
14
|
+
name: NAME,
|
|
15
|
+
read: () => {
|
|
16
|
+
const privacy = getCustomerPrivacy();
|
|
17
|
+
if (typeof privacy?.analyticsProcessingAllowed !== "function" || privacy.consentStatus !== void 0 && privacy.consentStatus !== "loaded") {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const api = privacy;
|
|
21
|
+
if (!hasAnswered(api)) return null;
|
|
22
|
+
return {
|
|
23
|
+
preferences: api.preferencesProcessingAllowed(),
|
|
24
|
+
analytics: api.analyticsProcessingAllowed(),
|
|
25
|
+
marketing: api.marketingAllowed() && api.saleOfDataAllowed()
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
events: ["consentTrackingApiLoaded", "visitorConsentCollected"],
|
|
29
|
+
target: () => document
|
|
30
|
+
});
|
|
31
|
+
var shopifyCustomerPrivacy = ({
|
|
32
|
+
saleOfData = false
|
|
33
|
+
} = {}) => {
|
|
34
|
+
let latest;
|
|
35
|
+
let resolveAnswered;
|
|
36
|
+
const answered = new Promise((resolve) => {
|
|
37
|
+
resolveAnswered = resolve;
|
|
38
|
+
});
|
|
39
|
+
const setTrackingConsent = (consent, required = false) => new Promise((resolve, reject) => {
|
|
40
|
+
const privacy = getCustomerPrivacy();
|
|
41
|
+
if (!privacy?.setTrackingConsent) {
|
|
42
|
+
if (required) {
|
|
43
|
+
reject(new Error("Shopify's Customer Privacy API hasn't loaded."));
|
|
44
|
+
} else {
|
|
45
|
+
resolve();
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
privacy.setTrackingConsent(
|
|
50
|
+
{
|
|
51
|
+
analytics: consent.analytics,
|
|
52
|
+
marketing: consent.marketing,
|
|
53
|
+
preferences: consent.preferences,
|
|
54
|
+
sale_of_data: typeof saleOfData === "function" ? saleOfData(consent) : saleOfData
|
|
55
|
+
},
|
|
56
|
+
(result) => {
|
|
57
|
+
if (result?.error) reject(new Error(result.error));
|
|
58
|
+
else resolve();
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
name: NAME,
|
|
64
|
+
apply: (consent) => {
|
|
65
|
+
latest = consent;
|
|
66
|
+
resolveAnswered();
|
|
67
|
+
return setTrackingConsent(consent);
|
|
68
|
+
},
|
|
69
|
+
syncConsent: async () => {
|
|
70
|
+
const warning = IS_DEVELOPMENT && latest === void 0 ? setTimeout(() => {
|
|
71
|
+
console.warn(
|
|
72
|
+
'[consent] shopifyCustomerPrivacy().syncConsent is still waiting for an answer. It never gets one when shopifyConsent() is the source: use <ShopifyScripts consent={{ mode: "default-banner" }} /> then.'
|
|
73
|
+
);
|
|
74
|
+
}, SYNC_WARNING_DELAY) : void 0;
|
|
75
|
+
await answered;
|
|
76
|
+
clearTimeout(warning);
|
|
77
|
+
await setTrackingConsent(latest, true);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export { shopifyConsent, shopifyCustomerPrivacy };
|
|
83
|
+
//# sourceMappingURL=index.js.map
|
|
84
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/consent/shopify/shopify.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { IS_DEVELOPMENT } from \"../../config\";\nimport type { Consent, ConsentSource, ConsentTarget } from \"../consent\";\nimport { eventConsentSource } from \"../event-consent-source\";\n\nconst NAME = \"shopify\";\n\n// Long enough for most visitors to answer the banner, so the warning points at a setup error.\nconst SYNC_WARNING_DELAY = 10_000;\n\ntype VisitorConsentValue = \"yes\" | \"no\" | \"\";\n\ntype CustomerPrivacy = {\n\t/** Set by Hydrogen's `<ShopifyScripts>`, `\"loaded\"` once the visitor's consent has been fetched. */\n\tconsentStatus?: string;\n\tanalyticsProcessingAllowed: () => boolean;\n\tmarketingAllowed: () => boolean;\n\tpreferencesProcessingAllowed: () => boolean;\n\tsaleOfDataAllowed: () => boolean;\n\tshouldShowBanner?: () => boolean;\n\tcurrentVisitorConsent?: () => Partial<\n\t\tRecord<\n\t\t\t\"analytics\" | \"marketing\" | \"preferences\" | \"sale_of_data\",\n\t\t\tVisitorConsentValue\n\t\t>\n\t>;\n\tsetTrackingConsent?: (\n\t\tconsent: Record<\n\t\t\t\"analytics\" | \"marketing\" | \"preferences\" | \"sale_of_data\",\n\t\t\tboolean\n\t\t>,\n\t\tcallback: (result?: { error?: string }) => void,\n\t) => void;\n};\n\nconst getCustomerPrivacy = () =>\n\t(window as { Shopify?: { customerPrivacy?: Partial<CustomerPrivacy> } })\n\t\t.Shopify?.customerPrivacy;\n\nconst hasAnswered = (privacy: CustomerPrivacy) => {\n\tif (privacy.shouldShowBanner?.() === false) return true;\n\tconst { analytics, marketing, preferences } =\n\t\tprivacy.currentVisitorConsent?.() ?? {};\n\treturn [analytics, marketing, preferences].some((value) => !!value);\n};\n\n/**\n * Consent from Shopify's Customer Privacy API, e.g. collected by Shopify's privacy banner through\n * `<ShopifyScripts consent={{ mode: \"default-banner\" }} />`. Answered once the visitor responds, or straight away where\n * Shopify shows no banner. `marketing` needs both `marketing` and `sale_of_data`, so an opt-out of data sale also stops\n * third-party ad tags, as Shopify does for pixels that count as a sale.\n *\n * @see https://shopify.dev/docs/api/customer-privacy\n *\n * @example\n * ```ts\n * export const analytics = createAnalytics({ consent: shopifyConsent(), destinations: [shopifyDestination()] });\n * ```\n */\nexport const shopifyConsent = (): ConsentSource =>\n\teventConsentSource({\n\t\tname: NAME,\n\t\tread: () => {\n\t\t\tconst privacy = getCustomerPrivacy();\n\t\t\tif (\n\t\t\t\ttypeof privacy?.analyticsProcessingAllowed !== \"function\" ||\n\t\t\t\t(privacy.consentStatus !== undefined &&\n\t\t\t\t\tprivacy.consentStatus !== \"loaded\")\n\t\t\t) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst api = privacy as CustomerPrivacy;\n\t\t\tif (!hasAnswered(api)) return null;\n\t\t\treturn {\n\t\t\t\tpreferences: api.preferencesProcessingAllowed(),\n\t\t\t\tanalytics: api.analyticsProcessingAllowed(),\n\t\t\t\tmarketing: api.marketingAllowed() && api.saleOfDataAllowed(),\n\t\t\t};\n\t\t},\n\t\tevents: [\"consentTrackingApiLoaded\", \"visitorConsentCollected\"],\n\t\ttarget: () => document,\n\t});\n\nexport type ShopifyCustomerPrivacyOptions = {\n\t/**\n\t * What to set `sale_of_data` to, which no category covers. Default `false`. Pass `(consent) => consent.marketing` to\n\t * have it follow marketing.\n\t */\n\tsaleOfData?: boolean | ((consent: Consent) => boolean);\n};\n\nexport type ShopifyCustomerPrivacy = ConsentTarget & {\n\t/**\n\t * Pass to `<ShopifyScripts consent={{ mode: \"custom-banner\", setup }} />`. Resolves once the visitor's answer has\n\t * reached the Customer Privacy API, which releases Hydrogen's analytics bus.\n\t */\n\tsyncConsent: () => Promise<void>;\n};\n\n/**\n * Tells Shopify's Customer Privacy API the visitor's consent, so Shopify's scripts and checkout follow it: `preferences`,\n * `analytics` and `marketing` as they are, `sale_of_data` from `saleOfData`. Skipped when `shopifyConsent()` is the\n * source.\n *\n * @example\n * ```ts\n * export const customerPrivacy = shopifyCustomerPrivacy();\n * export const analytics = createAnalytics({ consent: cookiebotConsent(), consentTargets: [customerPrivacy] });\n * ```\n */\nexport const shopifyCustomerPrivacy = ({\n\tsaleOfData = false,\n}: ShopifyCustomerPrivacyOptions = {}): ShopifyCustomerPrivacy => {\n\tlet latest: Consent | undefined;\n\tlet resolveAnswered: () => void;\n\tconst answered = new Promise<void>((resolve) => {\n\t\tresolveAnswered = resolve;\n\t});\n\n\t// Without the API, skipped unless `required`: `syncConsent` sends the latest answer once it has loaded.\n\tconst setTrackingConsent = (consent: Consent, required = false) =>\n\t\tnew Promise<void>((resolve, reject) => {\n\t\t\tconst privacy = getCustomerPrivacy();\n\t\t\tif (!privacy?.setTrackingConsent) {\n\t\t\t\tif (required) {\n\t\t\t\t\treject(new Error(\"Shopify's Customer Privacy API hasn't loaded.\"));\n\t\t\t\t} else {\n\t\t\t\t\tresolve();\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tprivacy.setTrackingConsent(\n\t\t\t\t{\n\t\t\t\t\tanalytics: consent.analytics,\n\t\t\t\t\tmarketing: consent.marketing,\n\t\t\t\t\tpreferences: consent.preferences,\n\t\t\t\t\tsale_of_data:\n\t\t\t\t\t\ttypeof saleOfData === \"function\" ? saleOfData(consent) : saleOfData,\n\t\t\t\t},\n\t\t\t\t(result) => {\n\t\t\t\t\tif (result?.error) reject(new Error(result.error));\n\t\t\t\t\telse resolve();\n\t\t\t\t},\n\t\t\t);\n\t\t});\n\n\treturn {\n\t\tname: NAME,\n\t\tapply: (consent) => {\n\t\t\tlatest = consent;\n\t\t\tresolveAnswered();\n\t\t\treturn setTrackingConsent(consent);\n\t\t},\n\t\tsyncConsent: async () => {\n\t\t\tconst warning =\n\t\t\t\tIS_DEVELOPMENT && latest === undefined\n\t\t\t\t\t? setTimeout(() => {\n\t\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t\t'[consent] shopifyCustomerPrivacy().syncConsent is still waiting for an answer. It never gets one when shopifyConsent() is the source: use <ShopifyScripts consent={{ mode: \"default-banner\" }} /> then.',\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}, SYNC_WARNING_DELAY)\n\t\t\t\t\t: undefined;\n\t\t\tawait answered;\n\t\t\tclearTimeout(warning);\n\t\t\tawait setTrackingConsent(latest!, true);\n\t\t},\n\t};\n};\n"]}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every cookie attribute a store can write. Mirrors the options of Next.js' `cookies().set()`.
|
|
3
|
+
*/
|
|
4
|
+
type CookieAttributes = {
|
|
5
|
+
/** @default "/" */
|
|
6
|
+
path?: string;
|
|
7
|
+
domain?: string;
|
|
8
|
+
/** Lifetime in seconds, relative to the moment the cookie is written. */
|
|
9
|
+
maxAge?: number;
|
|
10
|
+
/** Absolute expiry. A `number` is a unix timestamp in milliseconds. */
|
|
11
|
+
expires?: Date | number;
|
|
12
|
+
secure?: boolean;
|
|
13
|
+
/** `"none"` requires `secure: true`, browsers reject the cookie otherwise. */
|
|
14
|
+
sameSite?: "lax" | "strict" | "none";
|
|
15
|
+
/** Hides the cookie from client-side JavaScript. Can only be written on the server. */
|
|
16
|
+
httpOnly?: boolean;
|
|
17
|
+
partitioned?: boolean;
|
|
18
|
+
priority?: "low" | "medium" | "high";
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Anything cookies can be read from.
|
|
22
|
+
*
|
|
23
|
+
* Satisfied by:
|
|
24
|
+
* - `await cookies()` from `next/headers` (server components, server actions, route handlers)
|
|
25
|
+
* - `request.cookies` in Next.js proxy / middleware (`NextRequest`)
|
|
26
|
+
* - `response.cookies` in Next.js proxy / middleware / route handlers (`NextResponse`)
|
|
27
|
+
* - `browserCookies` from `@buildinams/core/cookies` (client)
|
|
28
|
+
*/
|
|
29
|
+
type CookieReader = {
|
|
30
|
+
get(name: string): {
|
|
31
|
+
value: string;
|
|
32
|
+
} | undefined;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Anything cookies can be written to.
|
|
36
|
+
*
|
|
37
|
+
* Satisfied by:
|
|
38
|
+
* - `await cookies()` from `next/headers`, in server actions and route handlers only (Next.js throws in server components)
|
|
39
|
+
* - `response.cookies` in Next.js proxy / middleware / route handlers (`NextResponse`)
|
|
40
|
+
* - `browserCookies` from `@buildinams/core/cookies` (client)
|
|
41
|
+
*/
|
|
42
|
+
type CookieWriter = {
|
|
43
|
+
set(name: string, value: string, attributes?: CookieAttributes): unknown;
|
|
44
|
+
};
|
|
45
|
+
declare const BROWSER_STORE: unique symbol;
|
|
46
|
+
/** Marks `browserCookies`, so server-only cookies can reject it at compile time. */
|
|
47
|
+
type BrowserBrand = {
|
|
48
|
+
readonly [BROWSER_STORE]: true;
|
|
49
|
+
};
|
|
50
|
+
/** A `CookieReader` that isn't `browserCookies`. Every Next.js cookie store qualifies. */
|
|
51
|
+
type ServerCookieReader = CookieReader & {
|
|
52
|
+
readonly [BROWSER_STORE]?: never;
|
|
53
|
+
};
|
|
54
|
+
/** A `CookieWriter` that isn't `browserCookies`. Every writable Next.js cookie store qualifies. */
|
|
55
|
+
type ServerCookieWriter = CookieWriter & {
|
|
56
|
+
readonly [BROWSER_STORE]?: never;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type Listener = () => void;
|
|
60
|
+
type BrowserCookieStore = CookieReader & CookieWriter & BrowserBrand & {
|
|
61
|
+
/** Calls `listener` whenever cookies may have changed. Returns an unsubscribe function. */
|
|
62
|
+
subscribe(listener: Listener): () => void;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Client-side cookie store backed by `document.cookie`. Implements `CookieReader` and `CookieWriter`, so it can be
|
|
66
|
+
* passed to any cookie definition: `themeCookie.get(browserCookies)`.
|
|
67
|
+
*
|
|
68
|
+
* Reads and writes are synchronous. Values are URL-encoded the same way Next.js does, so cookies written here can be
|
|
69
|
+
* read on the server and vice versa.
|
|
70
|
+
*/
|
|
71
|
+
declare const browserCookies: BrowserCookieStore;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Converts between a typed value and the raw string stored in the cookie.
|
|
75
|
+
*
|
|
76
|
+
* - `parse` receives the decoded cookie string and must **throw** when the value is invalid.
|
|
77
|
+
* Cookies are user-controlled input, so an invalid value is treated as "cookie not set".
|
|
78
|
+
* - `serialize` turns a typed value into a string. URL-encoding is handled by the cookie store, don't encode here.
|
|
79
|
+
*
|
|
80
|
+
* Structurally compatible with `Schema<Value, string>` from `@buildinams/core/schema`.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const dateSchema: CookieSchema<Date> = {
|
|
85
|
+
* parse: (raw) => {
|
|
86
|
+
* const date = new Date(raw);
|
|
87
|
+
* if (Number.isNaN(date.getTime())) throw new Error("Invalid date");
|
|
88
|
+
* return date;
|
|
89
|
+
* },
|
|
90
|
+
* serialize: (value) => value.toISOString(),
|
|
91
|
+
* };
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
type CookieSchema<Value> = {
|
|
95
|
+
parse: (raw: string) => Value;
|
|
96
|
+
serialize: (value: Value) => string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Ready-made `CookieSchema`s for common value types.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* cookieSchema.string(); // string
|
|
104
|
+
* cookieSchema.number(); // number
|
|
105
|
+
* cookieSchema.boolean(); // boolean
|
|
106
|
+
* cookieSchema.enum(["light", "dark"]); // "light" | "dark"
|
|
107
|
+
* cookieSchema.json(ConsentSchema.parse); // z.infer<typeof ConsentSchema>
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
declare const cookieSchema: {
|
|
111
|
+
string: () => CookieSchema<string>;
|
|
112
|
+
/** Finite numbers only. */
|
|
113
|
+
number: () => CookieSchema<number>;
|
|
114
|
+
/** Stored as `"true"` / `"false"`. */
|
|
115
|
+
boolean: () => CookieSchema<boolean>;
|
|
116
|
+
/** One of a fixed set of string literals. */
|
|
117
|
+
enum: <const Values extends readonly [string, ...string[]]>(values: Values) => CookieSchema<Values[number]>;
|
|
118
|
+
/**
|
|
119
|
+
* JSON-encoded value. `validate` receives the result of `JSON.parse` and must return the typed value or throw.
|
|
120
|
+
* A zod/valibot/etc. `parse` function fits directly.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* cookieSchema.json(z.object({ analytics: z.boolean() }).parse);
|
|
125
|
+
*
|
|
126
|
+
* // without a validation library
|
|
127
|
+
* cookieSchema.json((input) => {
|
|
128
|
+
* if (typeof input !== "object" || input === null || !("analytics" in input)) throw new Error();
|
|
129
|
+
* return { analytics: input.analytics === true };
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
json: <Value>(validate: (input: unknown) => Value) => CookieSchema<Value>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Attributes fixed on a cookie definition, applied to every write and to `delete()`.
|
|
138
|
+
*/
|
|
139
|
+
type CookieOptions = Omit<CookieAttributes, "expires">;
|
|
140
|
+
/**
|
|
141
|
+
* Per-write lifetime overrides. Path, domain etc. stay on the definition so `delete()` always hits the same cookie.
|
|
142
|
+
*/
|
|
143
|
+
type CookieWriteOptions = Pick<CookieAttributes, "maxAge" | "expires">;
|
|
144
|
+
type ReadValue<Value, HasDefault extends boolean> = HasDefault extends true ? Value : Value | null;
|
|
145
|
+
/**
|
|
146
|
+
* A typed cookie readable and writable on the server and in the browser. Create with `defineCookie()`.
|
|
147
|
+
*
|
|
148
|
+
* - `Value`: the type written and read.
|
|
149
|
+
* - `HasDefault`: `true` when a `defaultValue` is defined. Reads return `Value`, otherwise `Value | null`.
|
|
150
|
+
*/
|
|
151
|
+
type Cookie<Value, HasDefault extends boolean = boolean> = {
|
|
152
|
+
readonly name: string;
|
|
153
|
+
readonly httpOnly: false;
|
|
154
|
+
/** Reads and parses the cookie. Returns the default (or `null`) when it is missing or invalid. */
|
|
155
|
+
get(store: CookieReader): ReadValue<Value, HasDefault>;
|
|
156
|
+
/** Serializes and writes the cookie with the definition's options. */
|
|
157
|
+
set(store: CookieWriter, value: Value, options?: CookieWriteOptions): void;
|
|
158
|
+
/** Expires the cookie. */
|
|
159
|
+
delete(store: CookieWriter): void;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* A typed httpOnly cookie. Same API as `Cookie`, but only accepts server stores: passing `browserCookies` is a
|
|
163
|
+
* compile error, since the browser can't read or write it.
|
|
164
|
+
*/
|
|
165
|
+
type ServerCookie<Value, HasDefault extends boolean = boolean> = {
|
|
166
|
+
readonly name: string;
|
|
167
|
+
readonly httpOnly: true;
|
|
168
|
+
get(store: ServerCookieReader): ReadValue<Value, HasDefault>;
|
|
169
|
+
set(store: ServerCookieWriter, value: Value, options?: CookieWriteOptions): void;
|
|
170
|
+
delete(store: ServerCookieWriter): void;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* The type written to a cookie.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* type Theme = CookieWriteValue<typeof themeCookie>; // "light" | "dark"
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
type CookieWriteValue<T extends Cookie<any> | ServerCookie<any>> = Parameters<T["set"]>[1];
|
|
181
|
+
/**
|
|
182
|
+
* The type read from a cookie: `CookieWriteValue | null` when there is no `defaultValue`.
|
|
183
|
+
*/
|
|
184
|
+
type CookieReadValue<T extends Cookie<any> | ServerCookie<any>> = ReturnType<T["get"]>;
|
|
185
|
+
type CookieConfig<Value, HttpOnly extends boolean> = {
|
|
186
|
+
/** Cookie name, as stored in the browser. */
|
|
187
|
+
name: string;
|
|
188
|
+
/** How to convert between the typed value and the stored string. See `cookieSchema`. */
|
|
189
|
+
schema: CookieSchema<Value>;
|
|
190
|
+
/** Attributes used for every write. `path` defaults to `"/"`. */
|
|
191
|
+
options?: Omit<CookieOptions, "httpOnly"> & {
|
|
192
|
+
httpOnly?: HttpOnly;
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
type DefinedCookie<Value, HasDefault extends boolean, HttpOnly extends boolean> = HttpOnly extends true ? ServerCookie<Value, HasDefault> : Cookie<Value, HasDefault>;
|
|
196
|
+
/**
|
|
197
|
+
* Defines a typed cookie that can be read and written with the same API on the server (Next.js) and on the client.
|
|
198
|
+
* With `options.httpOnly: true` it returns a `ServerCookie`, which rejects `browserCookies` at compile time.
|
|
199
|
+
*
|
|
200
|
+
* Define it next to the code it belongs to (e.g. `src/features/auth/cookies.ts`), in a module without
|
|
201
|
+
* `"use client"` / `"use server"`, so both environments can import it.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* import { cookieSchema, defineCookie } from "@buildinams/core/cookies";
|
|
206
|
+
*
|
|
207
|
+
* export const themeCookie = defineCookie({
|
|
208
|
+
* name: "theme",
|
|
209
|
+
* schema: cookieSchema.enum(["light", "dark"]),
|
|
210
|
+
* defaultValue: "light",
|
|
211
|
+
* options: { maxAge: 60 * 60 * 24 * 365, sameSite: "lax" },
|
|
212
|
+
* });
|
|
213
|
+
*
|
|
214
|
+
* // server component / server action / route handler
|
|
215
|
+
* const theme = themeCookie.get(await cookies()); // "light" | "dark"
|
|
216
|
+
* themeCookie.set(await cookies(), "dark"); // server action / route handler only
|
|
217
|
+
*
|
|
218
|
+
* // client
|
|
219
|
+
* themeCookie.set(browserCookies, "dark");
|
|
220
|
+
* const theme = useCookieValue(themeCookie); // @buildinams/react/cookies
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
declare function defineCookie<Value, HttpOnly extends boolean = false>(config: CookieConfig<Value, HttpOnly> & {
|
|
224
|
+
defaultValue: NoInfer<Value>;
|
|
225
|
+
}): DefinedCookie<Value, true, HttpOnly>;
|
|
226
|
+
declare function defineCookie<Value, HttpOnly extends boolean = false>(config: CookieConfig<Value, HttpOnly> & {
|
|
227
|
+
defaultValue?: undefined;
|
|
228
|
+
}): DefinedCookie<Value, false, HttpOnly>;
|
|
229
|
+
|
|
230
|
+
export { type Cookie, type CookieAttributes, type CookieOptions, type CookieReadValue, type CookieReader, type CookieSchema, type CookieWriteOptions, type CookieWriteValue, type CookieWriter, type ServerCookie, type ServerCookieReader, type ServerCookieWriter, browserCookies, cookieSchema, defineCookie };
|