@replohq/sdk 0.1.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/LICENSE +11 -0
- package/_vendor/replo-utils/lib/crypto.d.ts +30 -0
- package/_vendor/replo-utils/lib/errors.d.ts +83 -0
- package/_vendor/replo-utils/lib/json.d.ts +6 -0
- package/_vendor/replo-utils/lib/json.mjs +43 -0
- package/_vendor/replo-utils/lib/misc.d.ts +145 -0
- package/_vendor/replo-utils/lib/object.d.ts +31 -0
- package/_vendor/schemas/errors.d.ts +459 -0
- package/_vendor/schemas/generated/consent.d.ts +55 -0
- package/_vendor/schemas/generated/errors.d.ts +61 -0
- package/_vendor/schemas/loaderKeys.d.ts +1 -0
- package/_vendor/schemas/loaderKeys.mjs +27 -0
- package/_vendor/schemas/money.d.ts +60 -0
- package/_vendor/schemas/money.mjs +2051 -0
- package/analytics/analytics-provider.d.ts +16 -0
- package/analytics/analytics-provider.js +113 -0
- package/analytics/analytics-provider.js.map +7 -0
- package/analytics/analytics-sink.d.ts +26 -0
- package/analytics/analytics-sink.js +1 -0
- package/analytics/analytics-sink.js.map +7 -0
- package/analytics/get-analytics-sinks.d.ts +27 -0
- package/analytics/get-analytics-sinks.js +52 -0
- package/analytics/get-analytics-sinks.js.map +7 -0
- package/analytics/hooks/use-product-viewed-analytics.d.ts +23 -0
- package/analytics/hooks/use-product-viewed-analytics.js +34 -0
- package/analytics/hooks/use-product-viewed-analytics.js.map +7 -0
- package/analytics/replo-first-party-pixel.d.ts +12 -0
- package/analytics/replo-first-party-pixel.js +31 -0
- package/analytics/replo-first-party-pixel.js.map +7 -0
- package/analytics/replo-pixel-script.d.ts +13 -0
- package/analytics/replo-pixel-script.js +30 -0
- package/analytics/replo-pixel-script.js.map +7 -0
- package/analytics/sinks/ga4-sink.d.ts +15 -0
- package/analytics/sinks/ga4-sink.js +60 -0
- package/analytics/sinks/ga4-sink.js.map +7 -0
- package/analytics/sinks/meta-sink.d.ts +14 -0
- package/analytics/sinks/meta-sink.js +56 -0
- package/analytics/sinks/meta-sink.js.map +7 -0
- package/analytics/sinks/pinterest-sink.d.ts +14 -0
- package/analytics/sinks/pinterest-sink.js +76 -0
- package/analytics/sinks/pinterest-sink.js.map +7 -0
- package/analytics/sinks/reddit-sink.d.ts +14 -0
- package/analytics/sinks/reddit-sink.js +80 -0
- package/analytics/sinks/reddit-sink.js.map +7 -0
- package/analytics/sinks/replo-sink.d.ts +14 -0
- package/analytics/sinks/replo-sink.js +57 -0
- package/analytics/sinks/replo-sink.js.map +7 -0
- package/analytics/sinks/snapchat-sink.d.ts +14 -0
- package/analytics/sinks/snapchat-sink.js +67 -0
- package/analytics/sinks/snapchat-sink.js.map +7 -0
- package/analytics/sinks/tiktok-sink.d.ts +14 -0
- package/analytics/sinks/tiktok-sink.js +79 -0
- package/analytics/sinks/tiktok-sink.js.map +7 -0
- package/analytics/sinks/window-sink.d.ts +18 -0
- package/analytics/sinks/window-sink.js +46 -0
- package/analytics/sinks/window-sink.js.map +7 -0
- package/analytics/utils/analytics-utils.d.ts +7 -0
- package/analytics/utils/analytics-utils.js +130 -0
- package/analytics/utils/analytics-utils.js.map +7 -0
- package/analytics/utils/navigation-tracker.d.ts +16 -0
- package/analytics/utils/navigation-tracker.js +44 -0
- package/analytics/utils/navigation-tracker.js.map +7 -0
- package/analytics/utils/shopify-funnel-gate.d.ts +8 -0
- package/analytics/utils/shopify-funnel-gate.js +22 -0
- package/analytics/utils/shopify-funnel-gate.js.map +7 -0
- package/cart/buy-now-action.d.ts +21 -0
- package/cart/buy-now-action.js +104 -0
- package/cart/buy-now-action.js.map +7 -0
- package/cart/cart-actions.d.ts +63 -0
- package/cart/cart-actions.js +90 -0
- package/cart/cart-actions.js.map +7 -0
- package/cart/cart-loader.d.ts +9 -0
- package/cart/cart-loader.js +11 -0
- package/cart/cart-loader.js.map +7 -0
- package/cart/cart-provider.d.ts +85 -0
- package/cart/cart-provider.js +372 -0
- package/cart/cart-provider.js.map +7 -0
- package/cart/cart-schema.d.ts +74 -0
- package/cart/cart-schema.js +69 -0
- package/cart/cart-schema.js.map +7 -0
- package/cart/cart-types.d.ts +132 -0
- package/cart/cart-types.js +8 -0
- package/cart/cart-types.js.map +7 -0
- package/cart/gateways/cart-constants.d.ts +1 -0
- package/cart/gateways/cart-constants.js +5 -0
- package/cart/gateways/cart-constants.js.map +7 -0
- package/cart/gateways/cart-gateway.d.ts +10 -0
- package/cart/gateways/cart-gateway.js +19 -0
- package/cart/gateways/cart-gateway.js.map +7 -0
- package/cart/gateways/cookie-cart-gateway.d.ts +28 -0
- package/cart/gateways/cookie-cart-gateway.js +109 -0
- package/cart/gateways/cookie-cart-gateway.js.map +7 -0
- package/cart/gateways/default-cart-gateway.d.ts +19 -0
- package/cart/gateways/default-cart-gateway.js +30 -0
- package/cart/gateways/default-cart-gateway.js.map +7 -0
- package/cart/gateways/resolver-cart-gateway.d.ts +2 -0
- package/cart/gateways/resolver-cart-gateway.js +109 -0
- package/cart/gateways/resolver-cart-gateway.js.map +7 -0
- package/cart/gateways/stripe-cart-gateway.d.ts +19 -0
- package/cart/gateways/stripe-cart-gateway.js +28 -0
- package/cart/gateways/stripe-cart-gateway.js.map +7 -0
- package/cart/hooks/use-add-to-cart.d.ts +63 -0
- package/cart/hooks/use-add-to-cart.js +124 -0
- package/cart/hooks/use-add-to-cart.js.map +7 -0
- package/cart/hooks/use-buy-now.d.ts +49 -0
- package/cart/hooks/use-buy-now.js +62 -0
- package/cart/hooks/use-buy-now.js.map +7 -0
- package/cart/utils/cart-utils.d.ts +43 -0
- package/cart/utils/cart-utils.js +46 -0
- package/cart/utils/cart-utils.js.map +7 -0
- package/cart/utils/cookie-cart-persistence.d.ts +55 -0
- package/cart/utils/cookie-cart-persistence.js +166 -0
- package/cart/utils/cookie-cart-persistence.js.map +7 -0
- package/cart/utils/variant-to-cart-line.d.ts +74 -0
- package/cart/utils/variant-to-cart-line.js +162 -0
- package/cart/utils/variant-to-cart-line.js.map +7 -0
- package/chunk-UJCSKKID.mjs +27 -0
- package/consent/consent-action.d.ts +9 -0
- package/consent/consent-action.js +25 -0
- package/consent/consent-action.js.map +7 -0
- package/consent/consent-actions.d.ts +28 -0
- package/consent/consent-actions.js +36 -0
- package/consent/consent-actions.js.map +7 -0
- package/consent/consent-store.d.ts +69 -0
- package/consent/consent-store.js +194 -0
- package/consent/consent-store.js.map +7 -0
- package/consent/cookie.d.ts +36 -0
- package/consent/cookie.js +82 -0
- package/consent/cookie.js.map +7 -0
- package/consent/replo-scripts.d.ts +23 -0
- package/consent/replo-scripts.js +102 -0
- package/consent/replo-scripts.js.map +7 -0
- package/consent/script-registration-store.d.ts +25 -0
- package/consent/script-registration-store.js +61 -0
- package/consent/script-registration-store.js.map +7 -0
- package/consent/script-snippets.d.ts +24 -0
- package/consent/script-snippets.js +122 -0
- package/consent/script-snippets.js.map +7 -0
- package/consent/types.d.ts +34 -0
- package/consent/types.js +1 -0
- package/consent/types.js.map +7 -0
- package/consent/window-api.d.ts +41 -0
- package/consent/window-api.js +73 -0
- package/consent/window-api.js.map +7 -0
- package/env.d.ts +9 -0
- package/env.js +42 -0
- package/env.js.map +7 -0
- package/gateways/canopy-api.d.ts +83 -0
- package/gateways/canopy-api.js +197 -0
- package/gateways/canopy-api.js.map +7 -0
- package/hooks/use-client-value.d.ts +10 -0
- package/hooks/use-client-value.js +12 -0
- package/hooks/use-client-value.js.map +7 -0
- package/hooks/use-formatted-price.d.ts +15 -0
- package/hooks/use-formatted-price.js +20 -0
- package/hooks/use-formatted-price.js.map +7 -0
- package/lib/canopy-error.d.ts +25 -0
- package/lib/canopy-error.js +24 -0
- package/lib/canopy-error.js.map +7 -0
- package/lib/get-query-client.d.ts +2 -0
- package/lib/get-query-client.js +34 -0
- package/lib/get-query-client.js.map +7 -0
- package/lib/integration-utils.d.ts +2 -0
- package/lib/integration-utils.js +9 -0
- package/lib/integration-utils.js.map +7 -0
- package/lib/shopify-utils.d.ts +10 -0
- package/lib/shopify-utils.js +7 -0
- package/lib/shopify-utils.js.map +7 -0
- package/lib/url-utils.d.ts +4 -0
- package/lib/url-utils.js +8 -0
- package/lib/url-utils.js.map +7 -0
- package/loaders/collection-loader.d.ts +11 -0
- package/loaders/collection-loader.js +55 -0
- package/loaders/collection-loader.js.map +7 -0
- package/loaders/collection-products-loader.d.ts +12 -0
- package/loaders/collection-products-loader.js +58 -0
- package/loaders/collection-products-loader.js.map +7 -0
- package/loaders/contentful-entries-loader.d.ts +17 -0
- package/loaders/contentful-entries-loader.js +78 -0
- package/loaders/contentful-entries-loader.js.map +7 -0
- package/loaders/contentful-entry-loader.d.ts +14 -0
- package/loaders/contentful-entry-loader.js +60 -0
- package/loaders/contentful-entry-loader.js.map +7 -0
- package/loaders/invoke-loader-action.d.ts +11 -0
- package/loaders/invoke-loader-action.js +14 -0
- package/loaders/invoke-loader-action.js.map +7 -0
- package/loaders/invoke-loader-server.d.ts +18 -0
- package/loaders/invoke-loader-server.js +40 -0
- package/loaders/invoke-loader-server.js.map +7 -0
- package/loaders/invoke-loader.d.ts +16 -0
- package/loaders/invoke-loader.js +37 -0
- package/loaders/invoke-loader.js.map +7 -0
- package/loaders/levanta-product-loader.d.ts +11 -0
- package/loaders/levanta-product-loader.js +53 -0
- package/loaders/levanta-product-loader.js.map +7 -0
- package/loaders/loader-error-boundary.d.ts +25 -0
- package/loaders/loader-error-boundary.js +24 -0
- package/loaders/loader-error-boundary.js.map +7 -0
- package/loaders/loader-keys.d.ts +34 -0
- package/loaders/loader-keys.js +26 -0
- package/loaders/loader-keys.js.map +7 -0
- package/loaders/loader-schemas.d.ts +946 -0
- package/loaders/loader-schemas.js +353 -0
- package/loaders/loader-schemas.js.map +7 -0
- package/loaders/metaobject-loader.d.ts +12 -0
- package/loaders/metaobject-loader.js +54 -0
- package/loaders/metaobject-loader.js.map +7 -0
- package/loaders/metaobjects-loader.d.ts +14 -0
- package/loaders/metaobjects-loader.js +60 -0
- package/loaders/metaobjects-loader.js.map +7 -0
- package/loaders/okendo-review-aggregate-loader.d.ts +17 -0
- package/loaders/okendo-review-aggregate-loader.js +56 -0
- package/loaders/okendo-review-aggregate-loader.js.map +7 -0
- package/loaders/okendo-reviews-loader.d.ts +23 -0
- package/loaders/okendo-reviews-loader.js +54 -0
- package/loaders/okendo-reviews-loader.js.map +7 -0
- package/loaders/prefetch-loaders.d.ts +33 -0
- package/loaders/prefetch-loaders.js +46 -0
- package/loaders/prefetch-loaders.js.map +7 -0
- package/loaders/preview-placeholder.d.ts +25 -0
- package/loaders/preview-placeholder.js +222 -0
- package/loaders/preview-placeholder.js.map +7 -0
- package/loaders/product-loader-args.d.ts +21 -0
- package/loaders/product-loader-args.js +17 -0
- package/loaders/product-loader-args.js.map +7 -0
- package/loaders/product-loader.d.ts +10 -0
- package/loaders/product-loader.js +52 -0
- package/loaders/product-loader.js.map +7 -0
- package/loaders/rebuy-recommendations-loader.d.ts +165 -0
- package/loaders/rebuy-recommendations-loader.js +57 -0
- package/loaders/rebuy-recommendations-loader.js.map +7 -0
- package/loaders/reviews-io-review-aggregate-loader.d.ts +12 -0
- package/loaders/reviews-io-review-aggregate-loader.js +43 -0
- package/loaders/reviews-io-review-aggregate-loader.js.map +7 -0
- package/loaders/reviews-io-reviews-loader.d.ts +14 -0
- package/loaders/reviews-io-reviews-loader.js +65 -0
- package/loaders/reviews-io-reviews-loader.js.map +7 -0
- package/loaders/smile-earning-rules-loader.d.ts +19 -0
- package/loaders/smile-earning-rules-loader.js +44 -0
- package/loaders/smile-earning-rules-loader.js.map +7 -0
- package/loaders/smile-rewards-loader.d.ts +19 -0
- package/loaders/smile-rewards-loader.js +44 -0
- package/loaders/smile-rewards-loader.js.map +7 -0
- package/loaders/smile-vip-tiers-loader.d.ts +18 -0
- package/loaders/smile-vip-tiers-loader.js +43 -0
- package/loaders/smile-vip-tiers-loader.js.map +7 -0
- package/loaders/statsig-experiment-loader.d.ts +20 -0
- package/loaders/statsig-experiment-loader.js +81 -0
- package/loaders/statsig-experiment-loader.js.map +7 -0
- package/loaders/yotpo-review-aggregate-loader.d.ts +12 -0
- package/loaders/yotpo-review-aggregate-loader.js +50 -0
- package/loaders/yotpo-review-aggregate-loader.js.map +7 -0
- package/loaders/yotpo-reviews-loader.d.ts +14 -0
- package/loaders/yotpo-reviews-loader.js +65 -0
- package/loaders/yotpo-reviews-loader.js.map +7 -0
- package/money.d.ts +9 -0
- package/money.js +13 -0
- package/money.js.map +7 -0
- package/next/config.d.ts +4 -0
- package/next/config.js +91 -0
- package/next/config.js.map +7 -0
- package/next/image-loader.d.ts +2 -0
- package/next/image-loader.js +52 -0
- package/next/image-loader.js.map +7 -0
- package/next/open-next.d.ts +2 -0
- package/next/open-next.js +24 -0
- package/next/open-next.js.map +7 -0
- package/orders/order-status-schema.d.ts +161 -0
- package/orders/order-status-schema.js +83 -0
- package/orders/order-status-schema.js.map +7 -0
- package/orders/order-status.d.ts +11 -0
- package/orders/order-status.js +41 -0
- package/orders/order-status.js.map +7 -0
- package/package.json +216 -0
- package/providers/query-provider.d.ts +1 -0
- package/providers/query-provider.js +12 -0
- package/providers/query-provider.js.map +7 -0
- package/providers/replo-provider.d.ts +1 -0
- package/providers/replo-provider.js +22 -0
- package/providers/replo-provider.js.map +7 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { deriveConsentAction } from "./consent-action";
|
|
4
|
+
import { recordConsentAction } from "./consent-actions";
|
|
5
|
+
import {
|
|
6
|
+
CONSENT_COOKIE_MAX_AGE_SECONDS,
|
|
7
|
+
CONSENT_COOKIE_NAME,
|
|
8
|
+
DENIED_CATEGORIES,
|
|
9
|
+
GRANTED_CATEGORIES,
|
|
10
|
+
parseConsentCookie,
|
|
11
|
+
serializeConsentCookieValue
|
|
12
|
+
} from "./cookie";
|
|
13
|
+
const TOGGLEABLE_CATEGORIES = [
|
|
14
|
+
"analytics",
|
|
15
|
+
"marketing",
|
|
16
|
+
"preferences",
|
|
17
|
+
"sale_of_data"
|
|
18
|
+
];
|
|
19
|
+
const SERVER_SNAPSHOT = Object.freeze({
|
|
20
|
+
mode: "off",
|
|
21
|
+
categories: DENIED_CATEGORIES,
|
|
22
|
+
decidedAt: null
|
|
23
|
+
});
|
|
24
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
25
|
+
let snapshot = null;
|
|
26
|
+
function isConsentMode(value) {
|
|
27
|
+
return value === "off" || value === "simple" || value === "per-category";
|
|
28
|
+
}
|
|
29
|
+
function resolveMode() {
|
|
30
|
+
if (typeof window === "undefined") {
|
|
31
|
+
return "off";
|
|
32
|
+
}
|
|
33
|
+
const attribute = document.documentElement.dataset.reploConsentMode;
|
|
34
|
+
return isConsentMode(attribute) ? attribute : "off";
|
|
35
|
+
}
|
|
36
|
+
const DEFAULT_CONSENT_POLICY_VERSION = 1;
|
|
37
|
+
function computeSnapshot() {
|
|
38
|
+
if (typeof window === "undefined") {
|
|
39
|
+
return SERVER_SNAPSHOT;
|
|
40
|
+
}
|
|
41
|
+
const mode = resolveMode();
|
|
42
|
+
const fromCookie = parseConsentCookie(document.cookie);
|
|
43
|
+
if (fromCookie) {
|
|
44
|
+
return {
|
|
45
|
+
mode,
|
|
46
|
+
categories: {
|
|
47
|
+
necessary: true,
|
|
48
|
+
analytics: fromCookie.categories.analytics,
|
|
49
|
+
marketing: fromCookie.categories.marketing,
|
|
50
|
+
preferences: fromCookie.categories.preferences,
|
|
51
|
+
sale_of_data: fromCookie.categories.sale_of_data
|
|
52
|
+
},
|
|
53
|
+
decidedAt: fromCookie.decidedAt,
|
|
54
|
+
consentId: fromCookie.consentId
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { mode, categories: DENIED_CATEGORIES, decidedAt: null };
|
|
58
|
+
}
|
|
59
|
+
function writeCookie(categories, decidedAt, consentId) {
|
|
60
|
+
if (typeof document === "undefined") {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const value = serializeConsentCookieValue({
|
|
64
|
+
categories,
|
|
65
|
+
decidedAt,
|
|
66
|
+
consentId
|
|
67
|
+
});
|
|
68
|
+
document.cookie = `${CONSENT_COOKIE_NAME}=${value}; path=/; max-age=${CONSENT_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
|
|
69
|
+
}
|
|
70
|
+
function notify() {
|
|
71
|
+
for (const listener of listeners) {
|
|
72
|
+
listener();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function commit({
|
|
76
|
+
categories,
|
|
77
|
+
policyVersion = DEFAULT_CONSENT_POLICY_VERSION
|
|
78
|
+
}) {
|
|
79
|
+
const previous = consentStore.getSnapshot();
|
|
80
|
+
const decidedAt = Date.now();
|
|
81
|
+
const consentId = previous.consentId ?? crypto.randomUUID();
|
|
82
|
+
const resultingCategories = {
|
|
83
|
+
...categories,
|
|
84
|
+
necessary: true
|
|
85
|
+
};
|
|
86
|
+
writeCookie(categories, decidedAt, consentId);
|
|
87
|
+
snapshot = {
|
|
88
|
+
mode: previous.mode,
|
|
89
|
+
categories: resultingCategories,
|
|
90
|
+
decidedAt,
|
|
91
|
+
consentId
|
|
92
|
+
};
|
|
93
|
+
notify();
|
|
94
|
+
const wasDowngrade = previous.decidedAt !== null && TOGGLEABLE_CATEGORIES.some((category) => {
|
|
95
|
+
return previous.categories[category] && !categories[category];
|
|
96
|
+
});
|
|
97
|
+
const reloadIfDowngraded = () => {
|
|
98
|
+
if (wasDowngrade && typeof window !== "undefined") {
|
|
99
|
+
window.location.reload();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
if (previous.mode !== "off") {
|
|
103
|
+
void recordConsentAction({
|
|
104
|
+
consentId,
|
|
105
|
+
action: deriveConsentAction({ previous, next: resultingCategories }),
|
|
106
|
+
categories: resultingCategories,
|
|
107
|
+
consentMode: previous.mode,
|
|
108
|
+
decidedAt,
|
|
109
|
+
policyVersion
|
|
110
|
+
}).finally(reloadIfDowngraded);
|
|
111
|
+
} else {
|
|
112
|
+
reloadIfDowngraded();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const consentStore = {
|
|
116
|
+
getSnapshot() {
|
|
117
|
+
if (!snapshot) {
|
|
118
|
+
snapshot = computeSnapshot();
|
|
119
|
+
}
|
|
120
|
+
return snapshot;
|
|
121
|
+
},
|
|
122
|
+
getServerSnapshot() {
|
|
123
|
+
return SERVER_SNAPSHOT;
|
|
124
|
+
},
|
|
125
|
+
subscribe(listener) {
|
|
126
|
+
listeners.add(listener);
|
|
127
|
+
return () => {
|
|
128
|
+
listeners.delete(listener);
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
/** Grant every consent category. */
|
|
132
|
+
accept(options = {}) {
|
|
133
|
+
commit({
|
|
134
|
+
categories: GRANTED_CATEGORIES,
|
|
135
|
+
policyVersion: options.policyVersion
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
/** Deny every non-necessary category. */
|
|
139
|
+
reject(options = {}) {
|
|
140
|
+
commit({
|
|
141
|
+
categories: DENIED_CATEGORIES,
|
|
142
|
+
policyVersion: options.policyVersion
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
/** Set a subset of categories, leaving the rest at their current value. */
|
|
146
|
+
updateCategories({
|
|
147
|
+
next,
|
|
148
|
+
policyVersion
|
|
149
|
+
}) {
|
|
150
|
+
const current = consentStore.getSnapshot().categories;
|
|
151
|
+
commit({
|
|
152
|
+
categories: { ...current, ...next, necessary: true },
|
|
153
|
+
policyVersion
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
/** Test-only: reset cached state so the next read recomputes. */
|
|
157
|
+
reset() {
|
|
158
|
+
snapshot = null;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
function useConsent(options = {}) {
|
|
162
|
+
const policyVersion = options.version;
|
|
163
|
+
const state = useSyncExternalStore(
|
|
164
|
+
consentStore.subscribe,
|
|
165
|
+
consentStore.getSnapshot,
|
|
166
|
+
consentStore.getServerSnapshot
|
|
167
|
+
);
|
|
168
|
+
return {
|
|
169
|
+
...state,
|
|
170
|
+
accept: () => consentStore.accept({ policyVersion }),
|
|
171
|
+
reject: () => consentStore.reject({ policyVersion }),
|
|
172
|
+
updateCategories: (next) => {
|
|
173
|
+
consentStore.updateCategories({ next, policyVersion });
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function isConsentAllowed({
|
|
178
|
+
state,
|
|
179
|
+
requiredConsent
|
|
180
|
+
}) {
|
|
181
|
+
if (state.mode === "off") {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
if (requiredConsent.length === 0) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return requiredConsent.every((category) => state.categories[category]);
|
|
188
|
+
}
|
|
189
|
+
export {
|
|
190
|
+
consentStore,
|
|
191
|
+
isConsentAllowed,
|
|
192
|
+
useConsent
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=consent-store.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../consent/consent-store.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport type { ConsentCategory, ConsentMode } from \"schemas/generated/consent\";\nimport type { ConsentCategories } from \"./cookie\";\n\nimport { useSyncExternalStore } from \"react\";\n\nimport { deriveConsentAction } from \"./consent-action\";\nimport { recordConsentAction } from \"./consent-actions\";\nimport {\n CONSENT_COOKIE_MAX_AGE_SECONDS,\n CONSENT_COOKIE_NAME,\n DENIED_CATEGORIES,\n GRANTED_CATEGORIES,\n parseConsentCookie,\n serializeConsentCookieValue,\n} from \"./cookie\";\n\n/**\n * Framework-agnostic, cookie-backed consent store. Lives as a single module\n * singleton so `ReploScripts`, the site's consent banner, and `AnalyticsProvider`\n * all share the same state regardless of where they sit in the React tree \u2014 no\n * provider-ancestor relationship is required.\n *\n * Consent-dependent work (script injection, banner visibility) happens on the\n * client (effects / post-mount), so there is no SSR/hydration mismatch. The\n * client reads the (non-httpOnly) consent cookie directly, so no server seed is\n * needed. The whole-site `mode` comes from the `data-replo-consent-mode`\n * attribute on `<html>`.\n */\n\nexport interface ConsentState {\n mode: ConsentMode;\n categories: ConsentCategories;\n /** Epoch ms when the visitor last made a choice, or null if undecided. */\n decidedAt: number | null;\n /**\n * Persistent pseudonymous per-browser id linking a visitor's consent history.\n * Absent until the first `commit()` (and on legacy pre-id cookies, until the\n * next interaction lazily backfills it).\n */\n consentId?: string;\n}\n\n// The non-necessary categories a visitor can grant or revoke. `necessary` is\n// always granted and never toggled.\nconst TOGGLEABLE_CATEGORIES: Exclude<ConsentCategory, \"necessary\">[] = [\n \"analytics\",\n \"marketing\",\n \"preferences\",\n \"sale_of_data\",\n];\n\n// A stable reference for SSR / first hydration render. Consent-dependent DOM is\n// only produced on the client, so this constant never causes a mismatch.\nconst SERVER_SNAPSHOT: ConsentState = Object.freeze({\n mode: \"off\",\n categories: DENIED_CATEGORIES,\n decidedAt: null,\n});\n\nconst listeners = new Set<() => void>();\nlet snapshot: ConsentState | null = null;\n\nfunction isConsentMode(value: string | null | undefined): value is ConsentMode {\n return value === \"off\" || value === \"simple\" || value === \"per-category\";\n}\n\nfunction resolveMode(): ConsentMode {\n if (typeof window === \"undefined\") {\n return \"off\";\n }\n const attribute = document.documentElement.dataset.reploConsentMode;\n return isConsentMode(attribute) ? attribute : \"off\";\n}\n\n// Used when the consent caller doesn't declare a policy version via\n// `useConsent({ version })` or the store method options.\nconst DEFAULT_CONSENT_POLICY_VERSION = 1;\n\nfunction computeSnapshot(): ConsentState {\n if (typeof window === \"undefined\") {\n return SERVER_SNAPSHOT;\n }\n\n const mode = resolveMode();\n\n const fromCookie = parseConsentCookie(document.cookie);\n if (fromCookie) {\n return {\n mode,\n categories: {\n necessary: true,\n analytics: fromCookie.categories.analytics,\n marketing: fromCookie.categories.marketing,\n preferences: fromCookie.categories.preferences,\n sale_of_data: fromCookie.categories.sale_of_data,\n },\n decidedAt: fromCookie.decidedAt,\n consentId: fromCookie.consentId,\n };\n }\n\n return { mode, categories: DENIED_CATEGORIES, decidedAt: null };\n}\n\nfunction writeCookie(\n categories: ConsentCategories,\n decidedAt: number,\n consentId: string,\n) {\n if (typeof document === \"undefined\") {\n return;\n }\n const value = serializeConsentCookieValue({\n categories,\n decidedAt,\n consentId,\n });\n document.cookie = `${CONSENT_COOKIE_NAME}=${value}; path=/; max-age=${CONSENT_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;\n}\n\nfunction notify() {\n for (const listener of listeners) {\n listener();\n }\n}\n\nfunction commit({\n categories,\n policyVersion = DEFAULT_CONSENT_POLICY_VERSION,\n}: {\n categories: ConsentCategories;\n policyVersion?: number;\n}) {\n const previous = consentStore.getSnapshot();\n const decidedAt = Date.now();\n // Reuse the visitor's existing id, or mint one on the first decision (also\n // lazily backfills legacy pre-id cookies). `crypto.randomUUID` is available in\n // every browser we target and on the server (where commit() never runs).\n const consentId = previous.consentId ?? crypto.randomUUID();\n const resultingCategories: ConsentCategories = {\n ...categories,\n necessary: true,\n };\n writeCookie(categories, decidedAt, consentId);\n snapshot = {\n mode: previous.mode,\n categories: resultingCategories,\n decidedAt,\n consentId,\n };\n notify();\n\n // Revocation cannot unload an already-loaded vendor. If the visitor had\n // previously decided and is now turning a granted category off, reload so the\n // page renders without those scripts. Deferred until after the audit record is\n // sent so the reload doesn't cancel the in-flight server action.\n const wasDowngrade =\n previous.decidedAt !== null &&\n TOGGLEABLE_CATEGORIES.some((category) => {\n return previous.categories[category] && !categories[category];\n });\n const reloadIfDowngraded = () => {\n if (wasDowngrade && typeof window !== \"undefined\") {\n window.location.reload();\n }\n };\n\n // Proof-of-consent audit record (ungated by consent, but only on managed\n // sites). On `mode === \"off\"` there is no banner/consent UX, so a\n // `setTrackingConsent()` call from a third-party script would only produce\n // meaningless `consent_mode: \"off\"` noise \u2014 skip it. Egress runs through a\n // server action (like the cart) so the client never needs the project id or\n // analytics-fire URL.\n if (previous.mode !== \"off\") {\n void recordConsentAction({\n consentId,\n action: deriveConsentAction({ previous, next: resultingCategories }),\n categories: resultingCategories,\n consentMode: previous.mode,\n decidedAt,\n policyVersion,\n }).finally(reloadIfDowngraded);\n } else {\n reloadIfDowngraded();\n }\n}\n\nexport const consentStore = {\n getSnapshot(): ConsentState {\n if (!snapshot) {\n snapshot = computeSnapshot();\n }\n return snapshot;\n },\n getServerSnapshot(): ConsentState {\n return SERVER_SNAPSHOT;\n },\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n /** Grant every consent category. */\n accept(options: { policyVersion?: number } = {}) {\n commit({\n categories: GRANTED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Deny every non-necessary category. */\n reject(options: { policyVersion?: number } = {}) {\n commit({\n categories: DENIED_CATEGORIES,\n policyVersion: options.policyVersion,\n });\n },\n /** Set a subset of categories, leaving the rest at their current value. */\n updateCategories({\n next,\n policyVersion,\n }: {\n next: Partial<ConsentCategories>;\n policyVersion?: number;\n }) {\n const current = consentStore.getSnapshot().categories;\n commit({\n categories: { ...current, ...next, necessary: true },\n policyVersion,\n });\n },\n /** Test-only: reset cached state so the next read recomputes. */\n reset() {\n snapshot = null;\n },\n};\n\nexport interface UseConsentOptions {\n /**\n * Site-owned consent policy version, stamped onto each proof-of-consent\n * record so a decision can be tied to the exact banner the visitor saw.\n * Bump it whenever the banner copy or category set changes. Defaults to 1.\n */\n version?: number;\n}\n\nexport interface UseConsentResult extends ConsentState {\n accept(): void;\n reject(): void;\n updateCategories(next: Partial<ConsentCategories>): void;\n}\n\nexport function useConsent(options: UseConsentOptions = {}): UseConsentResult {\n const policyVersion = options.version;\n const state = useSyncExternalStore(\n consentStore.subscribe,\n consentStore.getSnapshot,\n consentStore.getServerSnapshot,\n );\n return {\n ...state,\n accept: () => consentStore.accept({ policyVersion }),\n reject: () => consentStore.reject({ policyVersion }),\n updateCategories: (next) => {\n consentStore.updateCategories({ next, policyVersion });\n },\n };\n}\n\n/**\n * Whether a script requiring `requiredConsent` is allowed to load right now.\n * `mode === \"off\"` always allows; otherwise every required category must be\n * granted. An empty requirement list is always allowed.\n */\nexport function isConsentAllowed({\n state,\n requiredConsent,\n}: {\n state: ConsentState;\n requiredConsent: ConsentCategory[];\n}): boolean {\n if (state.mode === \"off\") {\n return true;\n }\n if (requiredConsent.length === 0) {\n return true;\n }\n return requiredConsent.every((category) => state.categories[category]);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ConsentCategory } from "../_vendor/schemas/generated/consent";
|
|
2
|
+
/**
|
|
3
|
+
* Pure (non-React, framework-agnostic) consent cookie helpers used by the client
|
|
4
|
+
* store (`consent-store.ts`).
|
|
5
|
+
*/
|
|
6
|
+
export declare const CONSENT_COOKIE_NAME = "replo_consent";
|
|
7
|
+
export declare const CONSENT_COOKIE_VERSION = 1;
|
|
8
|
+
export declare const CONSENT_COOKIE_MAX_AGE_SECONDS: number;
|
|
9
|
+
export type ConsentCategories = Record<ConsentCategory, boolean>;
|
|
10
|
+
/** Persisted cookie payload. `necessary` is always granted and never stored. */
|
|
11
|
+
export interface ConsentCookiePayload {
|
|
12
|
+
version: number;
|
|
13
|
+
decidedAt: number;
|
|
14
|
+
/** Persistent pseudonymous per-browser id; absent on pre-id (legacy) cookies. */
|
|
15
|
+
consentId?: string;
|
|
16
|
+
categories: {
|
|
17
|
+
analytics: boolean;
|
|
18
|
+
marketing: boolean;
|
|
19
|
+
preferences: boolean;
|
|
20
|
+
sale_of_data: boolean;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare const DENIED_CATEGORIES: ConsentCategories;
|
|
24
|
+
export declare const GRANTED_CATEGORIES: ConsentCategories;
|
|
25
|
+
/**
|
|
26
|
+
* Parse a raw `document.cookie` (or a single cookie value) into a payload.
|
|
27
|
+
* Returns null for missing, malformed, or stale-version cookies so callers
|
|
28
|
+
* treat the visitor as undecided and re-prompt.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseConsentCookie(rawCookie: string | undefined): ConsentCookiePayload | null;
|
|
31
|
+
/** Serialize categories into the JSON-encoded cookie value (not the full Set-Cookie line). */
|
|
32
|
+
export declare function serializeConsentCookieValue({ categories, decidedAt, consentId, }: {
|
|
33
|
+
categories: ConsentCategories;
|
|
34
|
+
decidedAt: number;
|
|
35
|
+
consentId?: string;
|
|
36
|
+
}): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { parseJsonOrNull } from "../_vendor/replo-utils/lib/json.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const CONSENT_COOKIE_NAME = "replo_consent";
|
|
4
|
+
const CONSENT_COOKIE_VERSION = 1;
|
|
5
|
+
const CONSENT_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 180;
|
|
6
|
+
const persistedCookieSchema = z.object({
|
|
7
|
+
version: z.literal(CONSENT_COOKIE_VERSION),
|
|
8
|
+
decidedAt: z.number().optional(),
|
|
9
|
+
consentId: z.string().optional(),
|
|
10
|
+
categories: z.object({
|
|
11
|
+
analytics: z.boolean(),
|
|
12
|
+
marketing: z.boolean(),
|
|
13
|
+
preferences: z.boolean(),
|
|
14
|
+
sale_of_data: z.boolean()
|
|
15
|
+
})
|
|
16
|
+
});
|
|
17
|
+
const DENIED_CATEGORIES = {
|
|
18
|
+
necessary: true,
|
|
19
|
+
analytics: false,
|
|
20
|
+
marketing: false,
|
|
21
|
+
preferences: false,
|
|
22
|
+
sale_of_data: false
|
|
23
|
+
};
|
|
24
|
+
const GRANTED_CATEGORIES = {
|
|
25
|
+
necessary: true,
|
|
26
|
+
analytics: true,
|
|
27
|
+
marketing: true,
|
|
28
|
+
preferences: true,
|
|
29
|
+
sale_of_data: true
|
|
30
|
+
};
|
|
31
|
+
function parseConsentCookie(rawCookie) {
|
|
32
|
+
if (!rawCookie) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const match = rawCookie.split("; ").find((entry) => entry.startsWith(`${CONSENT_COOKIE_NAME}=`));
|
|
36
|
+
const encoded = match ? match.slice(CONSENT_COOKIE_NAME.length + 1) : rawCookie;
|
|
37
|
+
let decoded;
|
|
38
|
+
try {
|
|
39
|
+
decoded = decodeURIComponent(encoded);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const result = persistedCookieSchema.safeParse(parseJsonOrNull(decoded));
|
|
44
|
+
if (!result.success) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const { decidedAt, categories, consentId } = result.data;
|
|
48
|
+
return {
|
|
49
|
+
version: CONSENT_COOKIE_VERSION,
|
|
50
|
+
decidedAt: decidedAt ?? Date.now(),
|
|
51
|
+
consentId,
|
|
52
|
+
categories
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function serializeConsentCookieValue({
|
|
56
|
+
categories,
|
|
57
|
+
decidedAt,
|
|
58
|
+
consentId
|
|
59
|
+
}) {
|
|
60
|
+
const payload = {
|
|
61
|
+
version: CONSENT_COOKIE_VERSION,
|
|
62
|
+
decidedAt,
|
|
63
|
+
consentId,
|
|
64
|
+
categories: {
|
|
65
|
+
analytics: categories.analytics,
|
|
66
|
+
marketing: categories.marketing,
|
|
67
|
+
preferences: categories.preferences,
|
|
68
|
+
sale_of_data: categories.sale_of_data
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
return encodeURIComponent(JSON.stringify(payload));
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
CONSENT_COOKIE_MAX_AGE_SECONDS,
|
|
75
|
+
CONSENT_COOKIE_NAME,
|
|
76
|
+
CONSENT_COOKIE_VERSION,
|
|
77
|
+
DENIED_CATEGORIES,
|
|
78
|
+
GRANTED_CATEGORIES,
|
|
79
|
+
parseConsentCookie,
|
|
80
|
+
serializeConsentCookieValue
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=cookie.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../consent/cookie.ts"],
|
|
4
|
+
"sourcesContent": ["import type { ConsentCategory } from \"schemas/generated/consent\";\n\nimport { parseJsonOrNull } from \"replo-utils/lib/json\";\nimport { z } from \"zod\";\n\n/**\n * Pure (non-React, framework-agnostic) consent cookie helpers used by the client\n * store (`consent-store.ts`).\n */\n\nexport const CONSENT_COOKIE_NAME = \"replo_consent\";\nexport const CONSENT_COOKIE_VERSION = 1;\nexport const CONSENT_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 180; // 180 days\n\n// Validates the untrusted JSON cookie payload at the parse boundary. A stale\n// version (or any shape mismatch) fails `safeParse`, so the caller treats the\n// visitor as undecided and re-prompts.\n//\n// `consentId` is OPTIONAL on purpose: it was added after v1 shipped, and we must\n// NOT bump `CONSENT_COOKIE_VERSION` to introduce it. Bumping would fail this\n// `safeParse` for every existing cookie, re-showing the banner and dropping the\n// visitor's prior decision. Optional keeps v1 cookies valid; `commit()` lazily\n// backfills the id on the next interaction.\nconst persistedCookieSchema = z.object({\n version: z.literal(CONSENT_COOKIE_VERSION),\n decidedAt: z.number().optional(),\n consentId: z.string().optional(),\n categories: z.object({\n analytics: z.boolean(),\n marketing: z.boolean(),\n preferences: z.boolean(),\n sale_of_data: z.boolean(),\n }),\n});\n\nexport type ConsentCategories = Record<ConsentCategory, boolean>;\n\n/** Persisted cookie payload. `necessary` is always granted and never stored. */\nexport interface ConsentCookiePayload {\n version: number;\n decidedAt: number;\n /** Persistent pseudonymous per-browser id; absent on pre-id (legacy) cookies. */\n consentId?: string;\n categories: {\n analytics: boolean;\n marketing: boolean;\n preferences: boolean;\n sale_of_data: boolean;\n };\n}\n\nexport const DENIED_CATEGORIES: ConsentCategories = {\n necessary: true,\n analytics: false,\n marketing: false,\n preferences: false,\n sale_of_data: false,\n};\n\nexport const GRANTED_CATEGORIES: ConsentCategories = {\n necessary: true,\n analytics: true,\n marketing: true,\n preferences: true,\n sale_of_data: true,\n};\n\n/**\n * Parse a raw `document.cookie` (or a single cookie value) into a payload.\n * Returns null for missing, malformed, or stale-version cookies so callers\n * treat the visitor as undecided and re-prompt.\n */\nexport function parseConsentCookie(\n rawCookie: string | undefined,\n): ConsentCookiePayload | null {\n if (!rawCookie) {\n return null;\n }\n const match = rawCookie\n .split(\"; \")\n .find((entry) => entry.startsWith(`${CONSENT_COOKIE_NAME}=`));\n const encoded = match\n ? match.slice(CONSENT_COOKIE_NAME.length + 1)\n : rawCookie;\n\n // A tampered/corrupted cookie (bad percent-encoding) must not throw \u2014 that\n // would crash the client snapshot or the server seed read. Treat as undecided.\n let decoded: string;\n try {\n decoded = decodeURIComponent(encoded);\n } catch {\n return null;\n }\n\n const result = persistedCookieSchema.safeParse(parseJsonOrNull(decoded));\n if (!result.success) {\n return null;\n }\n const { decidedAt, categories, consentId } = result.data;\n\n return {\n version: CONSENT_COOKIE_VERSION,\n decidedAt: decidedAt ?? Date.now(),\n consentId,\n categories,\n };\n}\n\n/** Serialize categories into the JSON-encoded cookie value (not the full Set-Cookie line). */\nexport function serializeConsentCookieValue({\n categories,\n decidedAt,\n consentId,\n}: {\n categories: ConsentCategories;\n decidedAt: number;\n consentId?: string;\n}): string {\n const payload: ConsentCookiePayload = {\n version: CONSENT_COOKIE_VERSION,\n decidedAt,\n consentId,\n categories: {\n analytics: categories.analytics,\n marketing: categories.marketing,\n preferences: categories.preferences,\n sale_of_data: categories.sale_of_data,\n },\n };\n return encodeURIComponent(JSON.stringify(payload));\n}\n"],
|
|
5
|
+
"mappings": "AAEA,SAAS,uBAAuB;AAChC,SAAS,SAAS;AAOX,MAAM,sBAAsB;AAC5B,MAAM,yBAAyB;AAC/B,MAAM,iCAAiC,KAAK,KAAK,KAAK;AAW7D,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,SAAS,EAAE,QAAQ,sBAAsB;AAAA,EACzC,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,YAAY,EAAE,OAAO;AAAA,IACnB,WAAW,EAAE,QAAQ;AAAA,IACrB,WAAW,EAAE,QAAQ;AAAA,IACrB,aAAa,EAAE,QAAQ;AAAA,IACvB,cAAc,EAAE,QAAQ;AAAA,EAC1B,CAAC;AACH,CAAC;AAkBM,MAAM,oBAAuC;AAAA,EAClD,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,cAAc;AAChB;AAEO,MAAM,qBAAwC;AAAA,EACnD,WAAW;AAAA,EACX,WAAW;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,cAAc;AAChB;AAOO,SAAS,mBACd,WAC6B;AAC7B,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,UACX,MAAM,IAAI,EACV,KAAK,CAAC,UAAU,MAAM,WAAW,GAAG,mBAAmB,GAAG,CAAC;AAC9D,QAAM,UAAU,QACZ,MAAM,MAAM,oBAAoB,SAAS,CAAC,IAC1C;AAIJ,MAAI;AACJ,MAAI;AACF,cAAU,mBAAmB,OAAO;AAAA,EACtC,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,sBAAsB,UAAU,gBAAgB,OAAO,CAAC;AACvE,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO;AAAA,EACT;AACA,QAAM,EAAE,WAAW,YAAY,UAAU,IAAI,OAAO;AAEpD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,WAAW,aAAa,KAAK,IAAI;AAAA,IACjC;AAAA,IACA;AAAA,EACF;AACF;AAGO,SAAS,4BAA4B;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AACF,GAIW;AACT,QAAM,UAAgC;AAAA,IACpC,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA,YAAY;AAAA,MACV,WAAW,WAAW;AAAA,MACtB,WAAW,WAAW;AAAA,MACtB,aAAa,WAAW;AAAA,MACxB,cAAc,WAAW;AAAA,IAC3B;AAAA,EACF;AACA,SAAO,mBAAmB,KAAK,UAAU,OAAO,CAAC;AACnD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReploScriptEntry, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
2
|
+
/**
|
|
3
|
+
* Injects the resolved tags into `document.head` once a caller has decided
|
|
4
|
+
* consent allows it. Reused both for author-managed `ReploScripts` entries and
|
|
5
|
+
* for the implicit Replo first-party pixel.
|
|
6
|
+
*
|
|
7
|
+
* DOM insertion (not JSX) is required because inline `<script>` bodies set via
|
|
8
|
+
* React's `dangerouslySetInnerHTML` never execute. Each node is tagged with
|
|
9
|
+
* `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict
|
|
10
|
+
* Mode double-invokes do not double-inject.
|
|
11
|
+
*/
|
|
12
|
+
export declare function InjectedScript({ baseId, descriptors, }: {
|
|
13
|
+
baseId: string;
|
|
14
|
+
descriptors: ScriptTagDescriptor[];
|
|
15
|
+
}): null;
|
|
16
|
+
/**
|
|
17
|
+
* The single registry component for all managed tracking scripts on the site.
|
|
18
|
+
* Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`
|
|
19
|
+
* array. Each entry registers itself and is gated + injected per consent.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ReploScripts({ scripts }: {
|
|
22
|
+
scripts: ReploScriptEntry[];
|
|
23
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { isConsentAllowed, useConsent } from "./consent-store";
|
|
5
|
+
import { registerScript, unregisterScript } from "./script-registration-store";
|
|
6
|
+
import { buildScriptTags, defaultConsentFor } from "./script-snippets";
|
|
7
|
+
import { installConsentWindowApi } from "./window-api";
|
|
8
|
+
function scriptId(entry) {
|
|
9
|
+
if (entry.type === "custom") {
|
|
10
|
+
return `custom:${entry.id}`;
|
|
11
|
+
}
|
|
12
|
+
if (entry.type === "snippet") {
|
|
13
|
+
return `snippet:${entry.id}`;
|
|
14
|
+
}
|
|
15
|
+
return `${entry.type}:${entry.identifier}`;
|
|
16
|
+
}
|
|
17
|
+
function requiredConsentFor(entry) {
|
|
18
|
+
if (entry.type === "custom" || entry.type === "snippet") {
|
|
19
|
+
return entry.requiredConsent;
|
|
20
|
+
}
|
|
21
|
+
return entry.requiredConsent ?? defaultConsentFor(entry.type);
|
|
22
|
+
}
|
|
23
|
+
function descriptorsFor(entry) {
|
|
24
|
+
if (entry.type === "custom") {
|
|
25
|
+
if (entry.src) {
|
|
26
|
+
return [{ kind: "external", src: entry.src }];
|
|
27
|
+
}
|
|
28
|
+
if (entry.body) {
|
|
29
|
+
return [{ kind: "inline", body: entry.body }];
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
if (entry.type === "snippet") {
|
|
34
|
+
return [{ kind: "inline", body: entry.body }];
|
|
35
|
+
}
|
|
36
|
+
return buildScriptTags({ type: entry.type, identifier: entry.identifier });
|
|
37
|
+
}
|
|
38
|
+
function InjectedScript({
|
|
39
|
+
baseId,
|
|
40
|
+
descriptors
|
|
41
|
+
}) {
|
|
42
|
+
const injectionKey = `${baseId}|${JSON.stringify(descriptors)}`;
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (typeof document === "undefined") {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const createdNodes = [];
|
|
48
|
+
descriptors.forEach((descriptor, index) => {
|
|
49
|
+
const nodeId = `${baseId}#${index}`;
|
|
50
|
+
const existing = document.querySelector(
|
|
51
|
+
`script[data-replo-script-id="${nodeId}"]`
|
|
52
|
+
);
|
|
53
|
+
if (existing) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const scriptElement = document.createElement("script");
|
|
57
|
+
scriptElement.dataset.reploScriptId = nodeId;
|
|
58
|
+
if (descriptor.kind === "external") {
|
|
59
|
+
scriptElement.src = descriptor.src;
|
|
60
|
+
scriptElement.async = true;
|
|
61
|
+
for (const [attributeName, attributeValue] of Object.entries(
|
|
62
|
+
descriptor.attributes ?? {}
|
|
63
|
+
)) {
|
|
64
|
+
scriptElement.setAttribute(attributeName, attributeValue);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
scriptElement.textContent = descriptor.body;
|
|
68
|
+
}
|
|
69
|
+
document.head.append(scriptElement);
|
|
70
|
+
createdNodes.push(scriptElement);
|
|
71
|
+
});
|
|
72
|
+
return () => {
|
|
73
|
+
for (const node of createdNodes) {
|
|
74
|
+
node.remove();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}, [injectionKey]);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function ManagedScript({ entry }) {
|
|
81
|
+
const consent = useConsent();
|
|
82
|
+
const requiredConsent = requiredConsentFor(entry);
|
|
83
|
+
const id = scriptId(entry);
|
|
84
|
+
const consentKey = requiredConsent.join(",");
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
registerScript({ id, type: entry.type, requiredConsent });
|
|
87
|
+
return () => unregisterScript(id);
|
|
88
|
+
}, [id, entry.type, consentKey]);
|
|
89
|
+
if (!isConsentAllowed({ state: consent, requiredConsent })) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return /* @__PURE__ */ jsx(InjectedScript, { baseId: id, descriptors: descriptorsFor(entry) });
|
|
93
|
+
}
|
|
94
|
+
function ReploScripts({ scripts }) {
|
|
95
|
+
useEffect(() => installConsentWindowApi(), []);
|
|
96
|
+
return /* @__PURE__ */ jsx(Fragment, { children: scripts.map((entry) => /* @__PURE__ */ jsx(ManagedScript, { entry }, scriptId(entry))) });
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
InjectedScript,
|
|
100
|
+
ReploScripts
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=replo-scripts.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../consent/replo-scripts.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptEntry,\n ScriptTagDescriptor,\n} from \"schemas/generated/consent\";\n\nimport { useEffect } from \"react\";\n\nimport { isConsentAllowed, useConsent } from \"./consent-store\";\nimport { registerScript, unregisterScript } from \"./script-registration-store\";\nimport { buildScriptTags, defaultConsentFor } from \"./script-snippets\";\nimport { installConsentWindowApi } from \"./window-api\";\n\n/**\n * Stable identity for an entry, used as React key and DOM dedupe key. `snippet`\n * and `custom` entries key on their `id`; identifier providers on type+id.\n */\nfunction scriptId(entry: ReploScriptEntry): string {\n if (entry.type === \"custom\") {\n return `custom:${entry.id}`;\n }\n if (entry.type === \"snippet\") {\n return `snippet:${entry.id}`;\n }\n return `${entry.type}:${entry.identifier}`;\n}\n\nfunction requiredConsentFor(entry: ReploScriptEntry): ConsentCategory[] {\n if (entry.type === \"custom\" || entry.type === \"snippet\") {\n return entry.requiredConsent;\n }\n return entry.requiredConsent ?? defaultConsentFor(entry.type);\n}\n\n/**\n * Resolves an entry to the concrete tags to inject. An identifier provider may\n * resolve to multiple tags (GA4 = external loader + inline config); a `snippet`\n * entry injects its pasted body inline; a `custom` entry is a single external\n * `src` or inline `body`.\n */\nfunction descriptorsFor(entry: ReploScriptEntry): ScriptTagDescriptor[] {\n if (entry.type === \"custom\") {\n if (entry.src) {\n return [{ kind: \"external\", src: entry.src }];\n }\n if (entry.body) {\n return [{ kind: \"inline\", body: entry.body }];\n }\n return [];\n }\n if (entry.type === \"snippet\") {\n return [{ kind: \"inline\", body: entry.body }];\n }\n return buildScriptTags({ type: entry.type, identifier: entry.identifier });\n}\n\n/**\n * Injects the resolved tags into `document.head` once a caller has decided\n * consent allows it. Reused both for author-managed `ReploScripts` entries and\n * for the implicit Replo first-party pixel.\n *\n * DOM insertion (not JSX) is required because inline `<script>` bodies set via\n * React's `dangerouslySetInnerHTML` never execute. Each node is tagged with\n * `data-replo-script-id` (plus a per-tag index) so re-renders and React Strict\n * Mode double-invokes do not double-inject.\n */\nexport function InjectedScript({\n baseId,\n descriptors,\n}: {\n baseId: string;\n descriptors: ScriptTagDescriptor[];\n}) {\n // Re-inject only when the resolved tags actually change.\n const injectionKey = `${baseId}|${JSON.stringify(descriptors)}`;\n\n // eslint-disable-next-line replo/no-use-effect -- script injection is a DOM side effect that must run after mount and on consent changes\n useEffect(() => {\n if (typeof document === \"undefined\") {\n return;\n }\n const createdNodes: HTMLScriptElement[] = [];\n descriptors.forEach((descriptor, index) => {\n const nodeId = `${baseId}#${index}`;\n const existing = document.querySelector(\n `script[data-replo-script-id=\"${nodeId}\"]`,\n );\n if (existing) {\n return;\n }\n const scriptElement = document.createElement(\"script\");\n scriptElement.dataset.reploScriptId = nodeId;\n if (descriptor.kind === \"external\") {\n scriptElement.src = descriptor.src;\n scriptElement.async = true;\n for (const [attributeName, attributeValue] of Object.entries(\n descriptor.attributes ?? {},\n )) {\n scriptElement.setAttribute(attributeName, attributeValue);\n }\n } else {\n scriptElement.textContent = descriptor.body;\n }\n document.head.append(scriptElement);\n createdNodes.push(scriptElement);\n });\n\n return () => {\n // Removing the node does not unload an already-loaded vendor (see the plan's\n // revocation note); full teardown is a reload triggered by the banner. This\n // cleanup keeps the DOM tidy and prevents duplicates across remounts.\n for (const node of createdNodes) {\n node.remove();\n }\n };\n }, [injectionKey]);\n\n return null;\n}\n\nfunction ManagedScript({ entry }: { entry: ReploScriptEntry }) {\n const consent = useConsent();\n const requiredConsent = requiredConsentFor(entry);\n const id = scriptId(entry);\n const consentKey = requiredConsent.join(\",\");\n\n // eslint-disable-next-line replo/no-use-effect -- register/unregister with the singleton so the analytics provider can gate sinks by what's on the page\n useEffect(() => {\n registerScript({ id, type: entry.type, requiredConsent });\n return () => unregisterScript(id);\n }, [id, entry.type, consentKey]);\n\n if (!isConsentAllowed({ state: consent, requiredConsent })) {\n return null;\n }\n return <InjectedScript baseId={id} descriptors={descriptorsFor(entry)} />;\n}\n\n/**\n * The single registry component for all managed tracking scripts on the site.\n * Authored once in `app/layout.tsx`; the agent/miniapp only edit the `scripts`\n * array. Each entry registers itself and is gated + injected per consent.\n */\nexport function ReploScripts({ scripts }: { scripts: ReploScriptEntry[] }) {\n // eslint-disable-next-line replo/no-use-effect -- install the window.Replo.customerPrivacy API + change event once the consent runtime mounts\n useEffect(() => installConsentWindowApi(), []);\n\n return (\n <>\n {scripts.map((entry) => (\n <ManagedScript key={scriptId(entry)} entry={entry} />\n ))}\n </>\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ConsentCategory, ReploScriptType } from "../_vendor/schemas/generated/consent";
|
|
2
|
+
/**
|
|
3
|
+
* Tracks which managed scripts are currently mounted on the page. Each
|
|
4
|
+
* `ReploScripts` entry registers itself here on mount, so the analytics provider
|
|
5
|
+
* can create sinks only for providers that are actually present (and consented),
|
|
6
|
+
* without importing a static list. Like the consent store, this is a singleton
|
|
7
|
+
* rather than React context so it works regardless of tree position.
|
|
8
|
+
*/
|
|
9
|
+
export interface RegisteredScript {
|
|
10
|
+
id: string;
|
|
11
|
+
type: ReploScriptType;
|
|
12
|
+
requiredConsent: ConsentCategory[];
|
|
13
|
+
}
|
|
14
|
+
export declare const scriptRegistrationStore: {
|
|
15
|
+
register(script: RegisteredScript): void;
|
|
16
|
+
unregister(id: string): void;
|
|
17
|
+
getSnapshot(): RegisteredScript[];
|
|
18
|
+
getServerSnapshot(): RegisteredScript[];
|
|
19
|
+
subscribe(listener: () => void): () => void;
|
|
20
|
+
/** Test-only: clear the registry between cases. */
|
|
21
|
+
reset(): void;
|
|
22
|
+
};
|
|
23
|
+
export declare function registerScript(script: RegisteredScript): void;
|
|
24
|
+
export declare function unregisterScript(id: string): void;
|
|
25
|
+
export declare function useRegisteredScripts(): RegisteredScript[];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
const registry = /* @__PURE__ */ new Map();
|
|
4
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
5
|
+
let snapshot = [];
|
|
6
|
+
const SERVER_SNAPSHOT = [];
|
|
7
|
+
function emit() {
|
|
8
|
+
snapshot = [...registry.values()];
|
|
9
|
+
for (const listener of listeners) {
|
|
10
|
+
listener();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const scriptRegistrationStore = {
|
|
14
|
+
register(script) {
|
|
15
|
+
registry.set(script.id, script);
|
|
16
|
+
emit();
|
|
17
|
+
},
|
|
18
|
+
unregister(id) {
|
|
19
|
+
if (registry.delete(id)) {
|
|
20
|
+
emit();
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
getSnapshot() {
|
|
24
|
+
return snapshot;
|
|
25
|
+
},
|
|
26
|
+
getServerSnapshot() {
|
|
27
|
+
return SERVER_SNAPSHOT;
|
|
28
|
+
},
|
|
29
|
+
subscribe(listener) {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(listener);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
/** Test-only: clear the registry between cases. */
|
|
36
|
+
reset() {
|
|
37
|
+
registry.clear();
|
|
38
|
+
listeners.clear();
|
|
39
|
+
snapshot = [];
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function registerScript(script) {
|
|
43
|
+
scriptRegistrationStore.register(script);
|
|
44
|
+
}
|
|
45
|
+
function unregisterScript(id) {
|
|
46
|
+
scriptRegistrationStore.unregister(id);
|
|
47
|
+
}
|
|
48
|
+
function useRegisteredScripts() {
|
|
49
|
+
return useSyncExternalStore(
|
|
50
|
+
scriptRegistrationStore.subscribe,
|
|
51
|
+
scriptRegistrationStore.getSnapshot,
|
|
52
|
+
scriptRegistrationStore.getServerSnapshot
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
registerScript,
|
|
57
|
+
scriptRegistrationStore,
|
|
58
|
+
unregisterScript,
|
|
59
|
+
useRegisteredScripts
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=script-registration-store.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../consent/script-registration-store.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport type {\n ConsentCategory,\n ReploScriptType,\n} from \"schemas/generated/consent\";\n\nimport { useSyncExternalStore } from \"react\";\n\n/**\n * Tracks which managed scripts are currently mounted on the page. Each\n * `ReploScripts` entry registers itself here on mount, so the analytics provider\n * can create sinks only for providers that are actually present (and consented),\n * without importing a static list. Like the consent store, this is a singleton\n * rather than React context so it works regardless of tree position.\n */\n\nexport interface RegisteredScript {\n id: string;\n type: ReploScriptType;\n requiredConsent: ConsentCategory[];\n}\n\nconst registry = new Map<string, RegisteredScript>();\nconst listeners = new Set<() => void>();\n\n// useSyncExternalStore requires a referentially-stable snapshot between emits.\nlet snapshot: RegisteredScript[] = [];\n\nconst SERVER_SNAPSHOT: RegisteredScript[] = [];\n\nfunction emit() {\n snapshot = [...registry.values()];\n for (const listener of listeners) {\n listener();\n }\n}\n\nexport const scriptRegistrationStore = {\n register(script: RegisteredScript) {\n registry.set(script.id, script);\n emit();\n },\n unregister(id: string) {\n if (registry.delete(id)) {\n emit();\n }\n },\n getSnapshot(): RegisteredScript[] {\n return snapshot;\n },\n getServerSnapshot(): RegisteredScript[] {\n return SERVER_SNAPSHOT;\n },\n subscribe(listener: () => void): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n /** Test-only: clear the registry between cases. */\n reset() {\n registry.clear();\n listeners.clear();\n snapshot = [];\n },\n};\n\nexport function registerScript(script: RegisteredScript) {\n scriptRegistrationStore.register(script);\n}\n\nexport function unregisterScript(id: string) {\n scriptRegistrationStore.unregister(id);\n}\n\nexport function useRegisteredScripts(): RegisteredScript[] {\n return useSyncExternalStore(\n scriptRegistrationStore.subscribe,\n scriptRegistrationStore.getSnapshot,\n scriptRegistrationStore.getServerSnapshot,\n );\n}\n"],
|
|
5
|
+
"mappings": ";AAOA,SAAS,4BAA4B;AAgBrC,MAAM,WAAW,oBAAI,IAA8B;AACnD,MAAM,YAAY,oBAAI,IAAgB;AAGtC,IAAI,WAA+B,CAAC;AAEpC,MAAM,kBAAsC,CAAC;AAE7C,SAAS,OAAO;AACd,aAAW,CAAC,GAAG,SAAS,OAAO,CAAC;AAChC,aAAW,YAAY,WAAW;AAChC,aAAS;AAAA,EACX;AACF;AAEO,MAAM,0BAA0B;AAAA,EACrC,SAAS,QAA0B;AACjC,aAAS,IAAI,OAAO,IAAI,MAAM;AAC9B,SAAK;AAAA,EACP;AAAA,EACA,WAAW,IAAY;AACrB,QAAI,SAAS,OAAO,EAAE,GAAG;AACvB,WAAK;AAAA,IACP;AAAA,EACF;AAAA,EACA,cAAkC;AAChC,WAAO;AAAA,EACT;AAAA,EACA,oBAAwC;AACtC,WAAO;AAAA,EACT;AAAA,EACA,UAAU,UAAkC;AAC1C,cAAU,IAAI,QAAQ;AACtB,WAAO,MAAM;AACX,gBAAU,OAAO,QAAQ;AAAA,IAC3B;AAAA,EACF;AAAA;AAAA,EAEA,QAAQ;AACN,aAAS,MAAM;AACf,cAAU,MAAM;AAChB,eAAW,CAAC;AAAA,EACd;AACF;AAEO,SAAS,eAAe,QAA0B;AACvD,0BAAwB,SAAS,MAAM;AACzC;AAEO,SAAS,iBAAiB,IAAY;AAC3C,0BAAwB,WAAW,EAAE;AACvC;AAEO,SAAS,uBAA2C;AACzD,SAAO;AAAA,IACL,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,IACxB,wBAAwB;AAAA,EAC1B;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ConsentCategory, ReploScriptType, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
2
|
+
type ProviderType = Exclude<ReploScriptType, "custom" | "snippet">;
|
|
3
|
+
/**
|
|
4
|
+
* Snippet providers don't synthesize tags (the user pastes the provider-issued
|
|
5
|
+
* snippet as the entry's `body`), but they still get a first-class default
|
|
6
|
+
* consent categorization like identifier providers.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Returns the structured tag descriptors for a named provider. A single provider
|
|
10
|
+
* can produce multiple tags (e.g. GA4 = external loader + inline config), which
|
|
11
|
+
* the runtime injects in order.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildScriptTags({ type, identifier, }: {
|
|
14
|
+
type: ProviderType;
|
|
15
|
+
identifier: string;
|
|
16
|
+
}): ScriptTagDescriptor[];
|
|
17
|
+
/**
|
|
18
|
+
* The default consent categories that gate an identifier-provider script when an
|
|
19
|
+
* entry does not specify `requiredConsent`. `snippet` and `custom` entries carry
|
|
20
|
+
* `requiredConsent` explicitly (the runtime has no default for them), so this is
|
|
21
|
+
* only consulted for identifier providers. Overridable per entry by the customer.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defaultConsentFor(type: ProviderType): ConsentCategory[];
|
|
24
|
+
export {};
|