@escape-game-over/atlas 0.1.37 → 0.1.39
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/docs/client-scripts.md +8 -3
- package/package.json +1 -1
- package/src/analytics/consent-storage.ts +28 -2
- package/src/analytics/google.ts +74 -38
- package/src/astro/consent-element.ts +63 -15
- package/src/astro/consent.ts +46 -26
- package/src/index.ts +6 -0
package/docs/client-scripts.md
CHANGED
|
@@ -46,8 +46,12 @@ finding them.
|
|
|
46
46
|
analytics={…}>` (`@escape-game-over/atlas/astro/consent-banner`) renders the
|
|
47
47
|
site's markup as its children, skips itself — script included — when the
|
|
48
48
|
analytics need no permission, and remembers, expires and applies the answer. The
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
answer is two, one per category: `analytics` (Google's `analytics_storage`) and
|
|
50
|
+
`marketing` (the three `ad_*` signals, and any other ad network's tag). The
|
|
51
|
+
markup marks its buttons `data-consent="grant"` and `data-consent="deny"` for
|
|
52
|
+
everything at once, and `data-consent="save"` for the answer its checkboxes
|
|
53
|
+
give — `data-consent-category="analytics"` and `"marketing"`, never ticked by
|
|
54
|
+
default. The control that brings it back is `data-consent-reopen hidden`.
|
|
51
55
|
|
|
52
56
|
`Document` ships one more: scroll restoration, on unless `restoreScroll={false}`.
|
|
53
57
|
WebKit restores a page's position only at `load`, after every image, so a page
|
|
@@ -562,7 +566,8 @@ right properties is a faithful stand-in.
|
|
|
562
566
|
- `tests/element.test.ts` stubs `customElements` to upgrade on `define`, as a
|
|
563
567
|
browser does, and pins what `connect` is handed.
|
|
564
568
|
- `tests/consent.test.ts` fakes `localStorage`, with a switch that makes it
|
|
565
|
-
throw, and the Google hook, and pins the six-month expiry to the day
|
|
569
|
+
throw, and the Google hook, and pins the six-month expiry to the day and
|
|
570
|
+
that an answer missing a category is asked again.
|
|
566
571
|
|
|
567
572
|
`carousel` has none, nor does the consent banner's element, and `element` has
|
|
568
573
|
no lifecycle test — moves and teardown. They need a real DOM and
|
package/package.json
CHANGED
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* What a visitor's consent answer is, where it is kept, and for how long it
|
|
3
|
+
* counts.
|
|
3
4
|
*
|
|
4
5
|
* Shared by the two halves that read it: the banner's module script
|
|
5
6
|
* (`astro/consent.ts`), and the inline head block Google's tags emit, which
|
|
6
7
|
* restores an answer before any tag fires. Two readers with their own copies of
|
|
7
|
-
* the key or the
|
|
8
|
+
* the key, the expiry or the categories would disagree the first time either
|
|
9
|
+
* changed.
|
|
8
10
|
*/
|
|
9
11
|
|
|
12
|
+
/** Granted or denied — the only two answers Consent Mode has. */
|
|
13
|
+
export type ConsentChoice = "granted" | "denied";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a visitor is asked about, each answered on its own.
|
|
17
|
+
*
|
|
18
|
+
* - `analytics` — measuring how the site is used. Google's `analytics_storage`.
|
|
19
|
+
* - `marketing` — advertising: measuring ad conversions and remarketing. Google's
|
|
20
|
+
* `ad_storage`, `ad_user_data` and `ad_personalization`, and any other ad
|
|
21
|
+
* network's tag a site adds.
|
|
22
|
+
*
|
|
23
|
+
* Two, because the GDPR wants a choice per purpose: a yes to analytics that also
|
|
24
|
+
* granted advertising would not have asked about advertising at all.
|
|
25
|
+
*/
|
|
26
|
+
export type ConsentCategory = "analytics" | "marketing";
|
|
27
|
+
|
|
28
|
+
export const CONSENT_CATEGORIES: readonly ConsentCategory[] = [
|
|
29
|
+
"analytics",
|
|
30
|
+
"marketing",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** One answer per category. */
|
|
34
|
+
export type ConsentChoices = Readonly<Record<ConsentCategory, ConsentChoice>>;
|
|
35
|
+
|
|
10
36
|
/**
|
|
11
37
|
* How long an answer stands before the question is asked again.
|
|
12
38
|
*
|
package/src/analytics/google.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { NonEmpty } from "../types.ts";
|
|
2
2
|
import { warn } from "../warn.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CONSENT_CATEGORIES,
|
|
5
|
+
CONSENT_KEY,
|
|
6
|
+
CONSENT_MONTHS,
|
|
7
|
+
type ConsentCategory,
|
|
8
|
+
} from "./consent-storage.ts";
|
|
4
9
|
import { type AnalyticsTags, literal, preconnect } from "./tags.ts";
|
|
5
10
|
|
|
6
11
|
/**
|
|
@@ -133,16 +138,16 @@ export interface GoogleSettings {
|
|
|
133
138
|
*/
|
|
134
139
|
readonly consent?: NonEmpty<ConsentDefaults>;
|
|
135
140
|
/**
|
|
136
|
-
*
|
|
137
|
-
* `ad_storage
|
|
141
|
+
* Carries an ad click's id (`gclid`, `gbraid`, `wbraid`) from page to page
|
|
142
|
+
* in the URL while `ad_storage` is denied. **Off unless stated.**
|
|
138
143
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
+
* Denied, the Google tag cannot keep the click id in a cookie, so a visitor
|
|
145
|
+
* who lands on one page and converts on another loses it on the way. For a
|
|
146
|
+
* site running Google Ads — where that id is what ties a lead back to the
|
|
147
|
+
* click — this keeps it on the internal links they follow. A site without
|
|
148
|
+
* ads has no click id to carry.
|
|
144
149
|
*/
|
|
145
|
-
readonly
|
|
150
|
+
readonly urlPassthrough?: true;
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
/**
|
|
@@ -192,7 +197,8 @@ const CONSENT_KEYS = {
|
|
|
192
197
|
} as const satisfies Readonly<Record<keyof ConsentDefaults, string>>;
|
|
193
198
|
|
|
194
199
|
/**
|
|
195
|
-
* The global a consent banner calls to change its mind:
|
|
200
|
+
* The global a consent banner calls to change its mind:
|
|
201
|
+
* `__consent({ analytics: "granted", marketing: "denied" })`.
|
|
196
202
|
*
|
|
197
203
|
* Emitted by lib rather than written in each project, because the *signals* it
|
|
198
204
|
* updates must be exactly the ones lib defaulted. A banner that built the
|
|
@@ -220,33 +226,45 @@ const SIGNAL_KEYS = [
|
|
|
220
226
|
"securityStorage",
|
|
221
227
|
] as const satisfies readonly (keyof ConsentDefaults)[];
|
|
222
228
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Which signals each answer moves.
|
|
231
|
+
*
|
|
232
|
+
* `functionality`, `personalization` and `security` are in neither: they are
|
|
233
|
+
* the site working, not something a visitor is asked about. A deployment that
|
|
234
|
+
* defaults one of them to denied is warned, since no answer can grant it back.
|
|
235
|
+
*/
|
|
236
|
+
const CATEGORY_SIGNALS: Readonly<
|
|
237
|
+
Record<ConsentCategory, readonly (keyof ConsentDefaults)[]>
|
|
238
|
+
> = {
|
|
239
|
+
analytics: ["analyticsStorage"],
|
|
240
|
+
marketing: ["adStorage", "adUserData", "adPersonalization"],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** The signals no answer grants — see `CATEGORY_SIGNALS`. */
|
|
244
|
+
const UNASKED = SIGNAL_KEYS.filter(
|
|
245
|
+
(key) =>
|
|
246
|
+
!CONSENT_CATEGORIES.some((category) =>
|
|
247
|
+
CATEGORY_SIGNALS[category].includes(key)
|
|
248
|
+
)
|
|
249
|
+
);
|
|
229
250
|
|
|
230
251
|
/**
|
|
231
|
-
* `window.__consent(
|
|
252
|
+
* `window.__consent(choices)`, updating precisely what was defaulted, each
|
|
253
|
+
* signal from the answer for its category.
|
|
232
254
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* which was never in force here.
|
|
255
|
+
* Only signals a default mentioned. Updating one that was never defaulted is
|
|
256
|
+
* legal and pointless — Google reads it as a change from its own implicit
|
|
257
|
+
* grant, which was never in force here.
|
|
237
258
|
*/
|
|
238
|
-
function consentUpdater(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
(advertising || !AD_KEYS.has(key)) &&
|
|
245
|
-
defaults.some((given) => given[key] !== undefined)
|
|
246
|
-
).map((key) => `${literal(CONSENT_KEYS[key])}:s`);
|
|
259
|
+
function consentUpdater(defaults: readonly ConsentDefaults[]): string {
|
|
260
|
+
const signals = CONSENT_CATEGORIES.flatMap((category) =>
|
|
261
|
+
CATEGORY_SIGNALS[category]
|
|
262
|
+
.filter((key) => defaults.some((given) => given[key] !== undefined))
|
|
263
|
+
.map((key) => `${literal(CONSENT_KEYS[key])}:c.${category}`)
|
|
264
|
+
);
|
|
247
265
|
|
|
248
266
|
if (signals.length === 0) return "";
|
|
249
|
-
return `window.${CONSENT_UPDATE_GLOBAL}=function(
|
|
267
|
+
return `window.${CONSENT_UPDATE_GLOBAL}=function(c){gtag('consent','update',{${signals.join(",")}})};`;
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
/**
|
|
@@ -258,12 +276,19 @@ function consentUpdater(
|
|
|
258
276
|
* as denied: no cookie, no returning user, no session. Measured, not supposed:
|
|
259
277
|
* `gcs=G100` on the `page_view`, `G111` only on the hits after it.
|
|
260
278
|
*
|
|
261
|
-
* The same key and
|
|
262
|
-
* take them. Anything unreadable, expired
|
|
263
|
-
*
|
|
279
|
+
* The same key, expiry and categories `astro/consent.ts` reads, from the one
|
|
280
|
+
* place both take them. Anything unreadable, expired, absent or missing a
|
|
281
|
+
* category — an answer given before that category was asked — leaves the
|
|
282
|
+
* defaults standing, and the banner will ask.
|
|
264
283
|
*/
|
|
265
284
|
function restoredConsent(): string {
|
|
266
|
-
|
|
285
|
+
const answered = CONSENT_CATEGORIES.map(
|
|
286
|
+
(category) => `(c.${category}==="granted"||c.${category}==="denied")`
|
|
287
|
+
).join("&&");
|
|
288
|
+
const choices = CONSENT_CATEGORIES.map(
|
|
289
|
+
(category) => `${category}:c.${category}`
|
|
290
|
+
).join(",");
|
|
291
|
+
return `try{var c=JSON.parse(localStorage.getItem(${literal(CONSENT_KEY)})),e=new Date(c.at);e.setMonth(e.getMonth()+${CONSENT_MONTHS});if(${answered}&&e>new Date())window.${CONSENT_UPDATE_GLOBAL}({${choices}})}catch(_){}`;
|
|
267
292
|
}
|
|
268
293
|
|
|
269
294
|
/** One `gtag('consent','default',{…})` per stated default, in order. */
|
|
@@ -375,9 +400,16 @@ export function googleScripts(
|
|
|
375
400
|
}
|
|
376
401
|
|
|
377
402
|
const consent = google.consent ?? DENIED_BY_DEFAULT;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
403
|
+
for (const key of UNASKED) {
|
|
404
|
+
if (consent.some((given) => given[key] === "denied")) {
|
|
405
|
+
warn(
|
|
406
|
+
at,
|
|
407
|
+
`consent defaults ${CONSENT_KEYS[key]} to denied, and no answer a visitor gives grants it — it is neither analytics nor marketing. Leave it to Google's default unless it is meant to stay denied.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// The way back out of the defaults, covering what they denied.
|
|
412
|
+
const updater = consentUpdater(consent);
|
|
381
413
|
const inline = [
|
|
382
414
|
"window.dataLayer=window.dataLayer||[];",
|
|
383
415
|
"function gtag(){dataLayer.push(arguments)}",
|
|
@@ -386,6 +418,10 @@ export function googleScripts(
|
|
|
386
418
|
updater,
|
|
387
419
|
// Then a remembered answer, still ahead of the first tag.
|
|
388
420
|
updater === "" ? "" : restoredConsent(),
|
|
421
|
+
// Before any `config`, which is what reads it.
|
|
422
|
+
google.urlPassthrough === true
|
|
423
|
+
? "gtag('set','url_passthrough',true);"
|
|
424
|
+
: "",
|
|
389
425
|
tags.length > 0 ? "gtag('js',new Date());" : "",
|
|
390
426
|
...tags.map((id) => `gtag('config',${literal(id)});`),
|
|
391
427
|
// Tag Manager's own loader, once per container. It appends its script
|
|
@@ -1,39 +1,87 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CONSENT_CATEGORIES } from "../analytics/consent-storage.ts";
|
|
2
|
+
import {
|
|
3
|
+
type ConsentCategory,
|
|
4
|
+
type ConsentChoices,
|
|
5
|
+
everyCategory,
|
|
6
|
+
readConsent,
|
|
7
|
+
recordConsent,
|
|
8
|
+
} from "./consent.ts";
|
|
2
9
|
import { element } from "./element.ts";
|
|
3
10
|
import { data, refs } from "./ref.ts";
|
|
4
11
|
|
|
5
|
-
/** What each button does, in the words Consent Mode records. */
|
|
6
|
-
const ACTIONS: Readonly<Record<string, ConsentChoice>> = {
|
|
7
|
-
grant: "granted",
|
|
8
|
-
deny: "denied",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
12
|
/**
|
|
12
13
|
* The banner's behaviour. Its own module, and not re-exported from `client.ts`,
|
|
13
14
|
* so the script ships only where `<ConsentBanner>` renders.
|
|
14
15
|
*
|
|
15
|
-
* A site's markup marks
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* A site's markup marks its buttons with what they do:
|
|
17
|
+
*
|
|
18
|
+
* - `data-consent="grant"` — every category granted.
|
|
19
|
+
* - `data-consent="deny"` — every category denied.
|
|
20
|
+
* - `data-consent="save"` — each category as its checkbox says:
|
|
21
|
+
* `<input type="checkbox" data-consent-category="analytics">`, and one for
|
|
22
|
+
* `marketing`.
|
|
23
|
+
*
|
|
24
|
+
* A banner with only the first two still works — one answer for everything.
|
|
25
|
+
* The checkboxes are never ticked by default; they show an answer already
|
|
26
|
+
* given, and nothing else.
|
|
27
|
+
*
|
|
28
|
+
* The control that brings it back — usually in the footer — is
|
|
29
|
+
* `data-consent-reopen hidden`: it stays hidden until there is an answer to
|
|
30
|
+
* withdraw.
|
|
19
31
|
*/
|
|
20
32
|
export const consentBanner = element("atlas-consent", ({ root, signal }) => {
|
|
33
|
+
const boxes = refs<HTMLInputElement>(root, "[data-consent-category]").map(
|
|
34
|
+
(box) => {
|
|
35
|
+
const category = data(box, "consent-category");
|
|
36
|
+
if (!CONSENT_CATEGORIES.includes(category as ConsentCategory)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`data-consent-category is "${category}", not ${CONSENT_CATEGORIES.join(" or ")}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return { box, category: category as ConsentCategory };
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const fromBoxes = (): ConsentChoices => {
|
|
46
|
+
const granted = (category: ConsentCategory) =>
|
|
47
|
+
boxes.some((each) => each.category === category && each.box.checked)
|
|
48
|
+
? "granted"
|
|
49
|
+
: "denied";
|
|
50
|
+
return {
|
|
51
|
+
analytics: granted("analytics"),
|
|
52
|
+
marketing: granted("marketing"),
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
21
56
|
const show = (): void => {
|
|
57
|
+
// Only an answer already given ticks a box.
|
|
58
|
+
const stored = readConsent();
|
|
59
|
+
for (const { box, category } of boxes) {
|
|
60
|
+
box.checked = stored?.[category] === "granted";
|
|
61
|
+
}
|
|
22
62
|
root.hidden = false;
|
|
23
63
|
};
|
|
24
64
|
|
|
65
|
+
const ACTIONS: Readonly<Record<string, () => ConsentChoices>> = {
|
|
66
|
+
grant: () => everyCategory("granted"),
|
|
67
|
+
deny: () => everyCategory("denied"),
|
|
68
|
+
save: fromBoxes,
|
|
69
|
+
};
|
|
70
|
+
|
|
25
71
|
for (const button of refs(root, "[data-consent]")) {
|
|
26
72
|
const action = data(button, "consent");
|
|
27
|
-
const
|
|
73
|
+
const choose = Object.hasOwn(ACTIONS, action)
|
|
28
74
|
? ACTIONS[action]
|
|
29
75
|
: undefined;
|
|
30
|
-
if (
|
|
31
|
-
throw new Error(
|
|
76
|
+
if (choose === undefined) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`data-consent is "${action}", not grant, deny or save`
|
|
79
|
+
);
|
|
32
80
|
}
|
|
33
81
|
button.addEventListener(
|
|
34
82
|
"click",
|
|
35
83
|
() => {
|
|
36
|
-
recordConsent(
|
|
84
|
+
recordConsent(choose());
|
|
37
85
|
root.hidden = true;
|
|
38
86
|
},
|
|
39
87
|
{ signal }
|
package/src/astro/consent.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CONSENT_KEY,
|
|
3
|
+
CONSENT_MONTHS,
|
|
4
|
+
type ConsentChoice,
|
|
5
|
+
type ConsentChoices,
|
|
6
|
+
} from "../analytics/consent-storage.ts";
|
|
2
7
|
import { CONSENT_UPDATE_GLOBAL } from "../analytics/google.ts";
|
|
3
8
|
|
|
9
|
+
export type {
|
|
10
|
+
ConsentCategory,
|
|
11
|
+
ConsentChoice,
|
|
12
|
+
ConsentChoices,
|
|
13
|
+
} from "../analytics/consent-storage.ts";
|
|
14
|
+
|
|
4
15
|
/**
|
|
5
16
|
* The browser half of consent: remembering an answer, expiring it, and handing
|
|
6
17
|
* it to Google.
|
|
@@ -18,22 +29,27 @@ import { CONSENT_UPDATE_GLOBAL } from "../analytics/google.ts";
|
|
|
18
29
|
* subtly wrong.
|
|
19
30
|
*/
|
|
20
31
|
|
|
21
|
-
/**
|
|
22
|
-
export type
|
|
23
|
-
|
|
24
|
-
/** An answer, and when it was given. */
|
|
25
|
-
export interface ConsentRecord {
|
|
26
|
-
readonly choice: ConsentChoice;
|
|
32
|
+
/** An answer for every category, and when it was given. */
|
|
33
|
+
export type ConsentRecord = ConsentChoices & {
|
|
27
34
|
/** ISO 8601, so it is legible in devtools rather than a number. */
|
|
28
35
|
readonly at: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** The same answer for every category — what "accept all" and "reject all" give. */
|
|
39
|
+
export function everyCategory(choice: ConsentChoice): ConsentChoices {
|
|
40
|
+
return { analytics: choice, marketing: choice };
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
const isChoice = (value: unknown): value is ConsentChoice =>
|
|
44
|
+
value === "granted" || value === "denied";
|
|
45
|
+
|
|
31
46
|
/**
|
|
32
47
|
* The answer this visitor gave, if it still counts.
|
|
33
48
|
*
|
|
34
|
-
* `undefined` for never asked, for an expired answer,
|
|
35
|
-
*
|
|
36
|
-
*
|
|
49
|
+
* `undefined` for never asked, for an expired answer, for anything unparseable,
|
|
50
|
+
* and for one missing a category — given before that category was asked about.
|
|
51
|
+
* All of them mean the same thing to a banner, and all of them should ask
|
|
52
|
+
* rather than assume.
|
|
37
53
|
*/
|
|
38
54
|
export function readConsent(): ConsentRecord | undefined {
|
|
39
55
|
let raw: string | null = null;
|
|
@@ -47,18 +63,17 @@ export function readConsent(): ConsentRecord | undefined {
|
|
|
47
63
|
if (raw === null) return undefined;
|
|
48
64
|
|
|
49
65
|
try {
|
|
50
|
-
const parsed = JSON.parse(raw) as Partial<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (typeof parsed.at !== "string") return undefined;
|
|
66
|
+
const parsed = JSON.parse(raw) as Partial<Record<string, unknown>>;
|
|
67
|
+
const { analytics, marketing, at } = parsed;
|
|
68
|
+
if (!isChoice(analytics) || !isChoice(marketing)) return undefined;
|
|
69
|
+
if (typeof at !== "string") return undefined;
|
|
55
70
|
|
|
56
|
-
const expiry = new Date(
|
|
71
|
+
const expiry = new Date(at);
|
|
57
72
|
if (Number.isNaN(expiry.getTime())) return undefined;
|
|
58
73
|
expiry.setMonth(expiry.getMonth() + CONSENT_MONTHS);
|
|
59
74
|
if (expiry < new Date()) return undefined;
|
|
60
75
|
|
|
61
|
-
return {
|
|
76
|
+
return { analytics, marketing, at };
|
|
62
77
|
} catch {
|
|
63
78
|
return undefined;
|
|
64
79
|
}
|
|
@@ -68,31 +83,36 @@ export function readConsent(): ConsentRecord | undefined {
|
|
|
68
83
|
* Remembers an answer, dated now, and tells Google about it — one call, so the
|
|
69
84
|
* two cannot come apart.
|
|
70
85
|
*/
|
|
71
|
-
export function recordConsent(
|
|
72
|
-
const record: ConsentRecord = {
|
|
86
|
+
export function recordConsent(choices: ConsentChoices): void {
|
|
87
|
+
const record: ConsentRecord = { ...choices, at: new Date().toISOString() };
|
|
73
88
|
try {
|
|
74
89
|
localStorage.setItem(CONSENT_KEY, JSON.stringify(record));
|
|
75
90
|
} catch {
|
|
76
91
|
// Unable to remember it, which is a worse experience and not a wrong
|
|
77
92
|
// one — the choice still applies to this page.
|
|
78
93
|
}
|
|
79
|
-
applyConsent(
|
|
94
|
+
applyConsent(choices);
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
/**
|
|
83
|
-
* Hands
|
|
98
|
+
* Hands an answer to Google, if there is a Google tag on this deployment.
|
|
84
99
|
*
|
|
85
100
|
* Reaches the head script through a global, which is the only way the two can
|
|
86
101
|
* meet: that script runs before any module exists. The name lives in one place
|
|
87
102
|
* — `CONSENT_UPDATE_GLOBAL` — and is referenced here and where it is emitted,
|
|
88
103
|
* so a project never types it.
|
|
89
104
|
*
|
|
90
|
-
*
|
|
91
|
-
* what to deny. A payload built here would be a second
|
|
105
|
+
* Which signals each category moves is decided over there, from the same
|
|
106
|
+
* settings that decided what to deny. A payload built here would be a second
|
|
107
|
+
* copy of that list.
|
|
92
108
|
*/
|
|
93
|
-
export function applyConsent(
|
|
109
|
+
export function applyConsent(choices: ConsentChoices): void {
|
|
94
110
|
const update = (
|
|
95
|
-
window as unknown as Record<
|
|
111
|
+
window as unknown as Record<
|
|
112
|
+
string,
|
|
113
|
+
((c: ConsentChoices) => void) | undefined
|
|
114
|
+
>
|
|
96
115
|
)[CONSENT_UPDATE_GLOBAL];
|
|
97
|
-
|
|
116
|
+
// Only the categories, so a record's date does not ride along.
|
|
117
|
+
update?.({ analytics: choices.analytics, marketing: choices.marketing });
|
|
98
118
|
}
|
package/src/index.ts
CHANGED