@replohq/sdk 2.18.2 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/_vendor/replo-utils/lib/crypto.d.ts +5 -0
  2. package/_vendor/schemas/generated/consent.d.ts +13 -1
  3. package/analytics/analytics-provider.js +3 -1
  4. package/analytics/get-analytics-sinks.js +7 -4
  5. package/analytics/replo-pixel-script.d.ts +5 -5
  6. package/analytics/replo-pixel-script.js +23 -22
  7. package/cart/cart-actions.js +42 -13
  8. package/cart/checkout-actions.js +1 -1
  9. package/cart/checkout-session-actions.js +2 -2
  10. package/cart/gateways/cart-gateway.d.ts +29 -8
  11. package/cart/gateways/cookie-cart-gateway.d.ts +4 -2
  12. package/cart/gateways/cookie-cart-gateway.js +4 -4
  13. package/cart/gateways/resolver-cart-gateway.js +28 -16
  14. package/cart/gateways/stripe-cart-gateway.js +1 -1
  15. package/cart/start-stripe-checkout.js +1 -1
  16. package/consent/consent-platform.d.ts +20 -5
  17. package/consent/consent-platform.js +94 -11
  18. package/consent/consent-store.d.ts +7 -2
  19. package/consent/consent-store.js +16 -4
  20. package/consent/inject-script-descriptors.d.ts +8 -1
  21. package/consent/inject-script-descriptors.js +23 -0
  22. package/consent/replo-scripts.d.ts +6 -2
  23. package/consent/replo-scripts.js +35 -27
  24. package/consent/script-registration-store.d.ts +2 -0
  25. package/consent/script-snippets.d.ts +1 -1
  26. package/consent/types.d.ts +18 -4
  27. package/consent/usercentrics-consent.d.ts +15 -0
  28. package/consent/usercentrics-consent.js +96 -0
  29. package/consent/window-api.js +1 -1
  30. package/gateways/canopy-api.d.ts +21 -15
  31. package/gateways/canopy-api.js +105 -54
  32. package/lib/buildMetadata.js +3 -3
  33. package/lib/buyer-ip.d.ts +6 -0
  34. package/lib/buyer-ip.js +7 -0
  35. package/loaders/invoke-loader-action.js +6 -1
  36. package/loaders/invoke-loader-server.js +5 -1
  37. package/loaders/invoke-loader.js +5 -1
  38. package/loaders/loader-output.d.ts +2 -1
  39. package/loaders/loader-output.js +7 -3
  40. package/package.json +4 -4
@@ -22,4 +22,9 @@ export declare function getHmacSha256Hex({ rawBody, signingKey, }: {
22
22
  rawBody: string;
23
23
  signingKey: string;
24
24
  }): Promise<string>;
25
+ export declare function isValidHmacSha256Hex({ rawBody, signingKey, signature, }: {
26
+ rawBody: string;
27
+ signingKey: string;
28
+ signature: string;
29
+ }): Promise<boolean>;
25
30
  export declare function deterministicUUID(value: string): string;
@@ -1,4 +1,4 @@
1
- export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "Mixpanel" | "HubSpot" | "Attentive" | "Alia" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
1
+ export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "Mixpanel" | "HubSpot" | "Attentive" | "Alia" | "Cookiebot" | "Usercentrics" | "snippet" | "custom" | "consentPlatform";
2
2
  export type ConsentCategory = "necessary" | "analytics" | "marketing" | "preferences" | "sale_of_data";
3
3
  export type ConsentMode = "off" | "simple" | "per-category";
4
4
  export type ScriptTagDescriptor = {
@@ -30,37 +30,49 @@ export type ReploScriptEntry = {
30
30
  type: "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Attentive" | "Alia";
31
31
  identifier: string;
32
32
  requiredConsent?: ConsentCategory[];
33
+ usercentricsService?: string;
33
34
  } | {
34
35
  type: "GA4";
35
36
  identifier: string;
36
37
  requiredConsent?: ConsentCategory[];
37
38
  runtimeOwner?: ScriptRuntimeOwner;
39
+ usercentricsService?: string;
38
40
  } | {
39
41
  type: "X";
40
42
  identifier: string;
41
43
  events?: Partial<XPixelEvents>;
42
44
  requiredConsent?: ConsentCategory[];
45
+ usercentricsService?: string;
43
46
  } | {
44
47
  type: "Mixpanel";
45
48
  identifier: string;
46
49
  region?: MixpanelRegion;
47
50
  requiredConsent?: ConsentCategory[];
51
+ usercentricsService?: string;
48
52
  } | {
49
53
  type: "Cookiebot";
50
54
  identifier: string;
51
55
  requiredConsent?: ConsentCategory[];
56
+ } | {
57
+ type: "Usercentrics";
58
+ identifier: string;
59
+ loaderVersion?: "v2" | "v3";
60
+ runtimeOwner?: ScriptRuntimeOwner;
61
+ reploUsercentricsService?: string;
52
62
  } | {
53
63
  type: "snippet";
54
64
  id: string;
55
65
  body: string;
56
66
  module?: boolean;
57
67
  requiredConsent: ConsentCategory[];
68
+ usercentricsService?: string;
58
69
  } | {
59
70
  type: "custom";
60
71
  id: string;
61
72
  requiredConsent: ConsentCategory[];
62
73
  src?: string;
63
74
  body?: string;
75
+ usercentricsService?: string;
64
76
  } | {
65
77
  type: "consentPlatform";
66
78
  id: string;
@@ -37,7 +37,8 @@ function AnalyticsProvider({
37
37
  cmp: consent.cmp,
38
38
  mode: consent.mode,
39
39
  categories: consent.categories,
40
- decidedAt: consent.decidedAt
40
+ decidedAt: consent.decidedAt,
41
+ grantedUsercentricsServices: consent.grantedUsercentricsServices
41
42
  },
42
43
  registeredScripts,
43
44
  checkoutProvider
@@ -49,6 +50,7 @@ function AnalyticsProvider({
49
50
  consent.mode,
50
51
  consent.categories,
51
52
  consent.decidedAt,
53
+ consent.grantedUsercentricsServices,
52
54
  registeredScripts,
53
55
  checkoutProvider,
54
56
  additionalSinks
@@ -1,3 +1,4 @@
1
+ import { findDelegatedPlatform } from "../consent/consent-platform";
1
2
  import { isConsentAllowed } from "../consent/consent-store";
2
3
  import { snippetScriptId } from "../consent/script-registration-store";
3
4
  import { ConvergeSink } from "./sinks/converge-sink";
@@ -51,11 +52,12 @@ function getAnalyticsSinks({
51
52
  checkoutProvider
52
53
  }) {
53
54
  const sinks = [new WindowSink()];
54
- const firstPartyAllowed = isConsentAllowed({
55
+ const isReploPixelAllowed = isConsentAllowed({
55
56
  state: consent,
56
- requiredConsent: ["analytics"]
57
+ requiredConsent: ["analytics"],
58
+ usercentricsService: findDelegatedPlatform()?.reploUsercentricsService ?? null
57
59
  });
58
- if (firstPartyAllowed) {
60
+ if (isReploPixelAllowed) {
59
61
  sinks.push(new ReploSink());
60
62
  }
61
63
  const shopifyOwnsCheckout = checkoutProvider === "shopify";
@@ -68,7 +70,8 @@ function getAnalyticsSinks({
68
70
  }
69
71
  if (isConsentAllowed({
70
72
  state: consent,
71
- requiredConsent: script.requiredConsent
73
+ requiredConsent: script.requiredConsent,
74
+ usercentricsService: script.usercentricsService
72
75
  })) {
73
76
  const sink = factory();
74
77
  const gated = shopifyOwnsCheckout && !SHOPIFY_FUNNEL_GATE_EXEMPT_PROVIDERS.has(script.type);
@@ -4,11 +4,11 @@
4
4
  * events into, so gating it on `analytics` consent keeps the first-party sink
5
5
  * consent-correct without the pixel loading unconditionally in `<head>` the way
6
6
  * the legacy scaffold does. `analytics` consent here matches the
7
- * `firstPartyAllowed` gate that controls whether `ReploSink` is created at all.
7
+ * `isReploPixelAllowed` gate that controls whether `ReploSink` is created at all.
8
8
  *
9
- * Under a consent platform the tag is rendered regardless and `InjectedScript`
10
- * stamps the blocking attributes at mount time, since delegation is not yet
11
- * set here.
9
+ * Under a consent platform the tag is injected regardless, with its blocking
10
+ * attributes read in the effect because this renders before `ReploScripts`
11
+ * publishes the platform.
12
12
  */
13
13
  export declare function ReploPixelScript({ pixelUrl, apiUrl, projectId, siteVersion, }: {
14
14
  pixelUrl: string;
@@ -16,4 +16,4 @@ export declare function ReploPixelScript({ pixelUrl, apiUrl, projectId, siteVers
16
16
  projectId: string;
17
17
  /** Publish id of the running site; absent on unpublished or older deploys. */
18
18
  siteVersion?: string;
19
- }): import("react/jsx-runtime").JSX.Element | null;
19
+ }): null;
@@ -1,6 +1,7 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
3
- import { InjectedScript } from "../consent/replo-scripts";
2
+ import { useEffect } from "react";
3
+ import { findDelegatedPlatform } from "../consent/consent-platform";
4
+ import { injectGatedScript } from "../consent/inject-script-descriptors";
4
5
  import { useIsBlockedByNativeConsent } from "../consent/use-is-blocked-by-native-consent";
5
6
  const PIXEL_SCRIPT_ID = "replo:first-party-pixel";
6
7
  const PIXEL_REQUIRED_CONSENT = ["analytics"];
@@ -13,28 +14,28 @@ function ReploPixelScript({
13
14
  const isBlockedByNativeConsent = useIsBlockedByNativeConsent(
14
15
  PIXEL_REQUIRED_CONSENT
15
16
  );
16
- if (isBlockedByNativeConsent) {
17
- return null;
18
- }
19
- const descriptors = [
20
- {
21
- kind: "external",
22
- src: `${pixelUrl}/latest.js`,
23
- attributes: {
24
- "data-api-url": apiUrl,
25
- "data-project-id": projectId,
26
- ...siteVersion ? { "data-site-version": siteVersion } : {}
27
- }
17
+ useEffect(() => {
18
+ if (isBlockedByNativeConsent) {
19
+ return;
28
20
  }
29
- ];
30
- return /* @__PURE__ */ jsx(
31
- InjectedScript,
32
- {
21
+ injectGatedScript({
33
22
  baseId: PIXEL_SCRIPT_ID,
34
- descriptors,
35
- requiredConsent: PIXEL_REQUIRED_CONSENT
36
- }
37
- );
23
+ descriptors: [
24
+ {
25
+ kind: "external",
26
+ src: `${pixelUrl}/latest.js`,
27
+ attributes: {
28
+ "data-api-url": apiUrl,
29
+ "data-project-id": projectId,
30
+ ...siteVersion ? { "data-site-version": siteVersion } : {}
31
+ }
32
+ }
33
+ ],
34
+ requiredConsent: PIXEL_REQUIRED_CONSENT,
35
+ usercentricsService: findDelegatedPlatform()?.reploUsercentricsService ?? null
36
+ });
37
+ }, [isBlockedByNativeConsent, pixelUrl, apiUrl, projectId, siteVersion]);
38
+ return null;
38
39
  }
39
40
  export {
40
41
  ReploPixelScript
@@ -1,5 +1,6 @@
1
1
  "use server";
2
2
  import { cookies } from "next/headers";
3
+ import { readBuyerIp } from "../lib/buyer-ip";
3
4
  import { getCheckoutProvider } from "../lib/integration-utils";
4
5
  import { REPLO_ATTRIBUTION_PROPERTY } from "./cart-types";
5
6
  import {
@@ -23,7 +24,8 @@ async function reconcileExistingCart(cart, country) {
23
24
  if (country && reconciledCart.buyerIdentity?.country !== country) {
24
25
  return await cartGateway.updateCartBuyerIdentity({
25
26
  cartId: reconciledCart.id,
26
- buyerIdentity: { country }
27
+ buyerIdentity: { country },
28
+ buyerIp: await readBuyerIp()
27
29
  });
28
30
  }
29
31
  return reconciledCart;
@@ -51,9 +53,10 @@ async function getCartAction() {
51
53
  const cartGateway = await getCartGateway();
52
54
  const sharedToken = checkoutProvider === "shopify" ? cookieStore.get(SHARED_SHOPIFY_CART_COOKIE_NAME)?.value : void 0;
53
55
  if (sharedToken) {
54
- const sharedCart = await cartGateway.getCart(
55
- `${SHOPIFY_CART_ID_PREFIX}${sharedToken}`
56
- );
56
+ const sharedCart = await cartGateway.getCart({
57
+ cartId: `${SHOPIFY_CART_ID_PREFIX}${sharedToken}`,
58
+ buyerIp: await readBuyerIp()
59
+ });
57
60
  if (sharedCart) {
58
61
  return sharedCart;
59
62
  }
@@ -62,7 +65,10 @@ async function getCartAction() {
62
65
  if (!existingCartId) {
63
66
  return null;
64
67
  }
65
- const cart = await cartGateway.getCart(existingCartId);
68
+ const cart = await cartGateway.getCart({
69
+ cartId: existingCartId,
70
+ buyerIp: await readBuyerIp()
71
+ });
66
72
  if (!cart) {
67
73
  try {
68
74
  cookieStore.set(SHOPIFY_CART_COOKIE_NAME, "", {
@@ -92,7 +98,8 @@ async function createCartAction(options) {
92
98
  lines,
93
99
  discountCodes,
94
100
  attributes,
95
- buyerIdentity: country ? { country } : void 0
101
+ buyerIdentity: country ? { country } : void 0,
102
+ buyerIp: await readBuyerIp()
96
103
  });
97
104
  if (cart && !skipStoringCart) {
98
105
  try {
@@ -117,7 +124,8 @@ async function createCartAction(options) {
117
124
  }
118
125
  async function addToCartAction(lines, cartId) {
119
126
  const cartGateway = await getCartGateway();
120
- const cart = await cartGateway.addToCart({ lines, cartId });
127
+ const buyerIp = await readBuyerIp();
128
+ const cart = await cartGateway.addToCart({ lines, cartId, buyerIp });
121
129
  if (!cart || await getCheckoutProvider() !== "shopify" || cart.attributes?.some(
122
130
  (attribute) => attribute.key === REPLO_ATTRIBUTION_PROPERTY.key
123
131
  )) {
@@ -125,29 +133,50 @@ async function addToCartAction(lines, cartId) {
125
133
  }
126
134
  const attributedCart = await cartGateway.replaceCartAttributes({
127
135
  cartId: cart.id,
128
- attributes: [...cart.attributes ?? [], REPLO_ATTRIBUTION_PROPERTY]
136
+ attributes: [...cart.attributes ?? [], REPLO_ATTRIBUTION_PROPERTY],
137
+ buyerIp
129
138
  });
130
139
  return attributedCart ?? cart;
131
140
  }
132
141
  async function updateCartLineItemAction(line, cartId) {
133
142
  const cartGateway = await getCartGateway();
134
- return await cartGateway.updateCartLineItem({ line, cartId });
143
+ return await cartGateway.updateCartLineItem({
144
+ line,
145
+ cartId,
146
+ buyerIp: await readBuyerIp()
147
+ });
135
148
  }
136
149
  async function removeCartLineItemsAction(lineIds, cartId) {
137
150
  const cartGateway = await getCartGateway();
138
- return await cartGateway.removeCartLineItems({ lineIds, cartId });
151
+ return await cartGateway.removeCartLineItems({
152
+ lineIds,
153
+ cartId,
154
+ buyerIp: await readBuyerIp()
155
+ });
139
156
  }
140
157
  async function updateCartDiscountCodesAction(discountCodes, cartId) {
141
158
  const cartGateway = await getCartGateway();
142
- return await cartGateway.updateCartDiscountCodes({ discountCodes, cartId });
159
+ return await cartGateway.updateCartDiscountCodes({
160
+ discountCodes,
161
+ cartId,
162
+ buyerIp: await readBuyerIp()
163
+ });
143
164
  }
144
165
  async function updateCartNoteAction(note, cartId) {
145
166
  const cartGateway = await getCartGateway();
146
- return await cartGateway.updateCartNote({ note, cartId });
167
+ return await cartGateway.updateCartNote({
168
+ note,
169
+ cartId,
170
+ buyerIp: await readBuyerIp()
171
+ });
147
172
  }
148
173
  async function replaceCartAttributesAction(attributes, cartId) {
149
174
  const cartGateway = await getCartGateway();
150
- return await cartGateway.replaceCartAttributes({ attributes, cartId });
175
+ return await cartGateway.replaceCartAttributes({
176
+ attributes,
177
+ cartId,
178
+ buyerIp: await readBuyerIp()
179
+ });
151
180
  }
152
181
  export {
153
182
  addToCartAction,
@@ -19,7 +19,7 @@ async function getCrossSellsAction(input) {
19
19
  if (await getCheckoutProvider() !== "stripe") {
20
20
  return [];
21
21
  }
22
- return await (await getCanopyApiGateway()).findCrossSells(input);
22
+ return await getCanopyApiGateway().findCrossSells(input);
23
23
  }
24
24
  export {
25
25
  createCheckoutSessionAction,
@@ -1,10 +1,10 @@
1
1
  "use server";
2
2
  import { getCanopyApiGateway } from "../gateways/canopy-api";
3
3
  async function updateCheckoutShippingAction(input) {
4
- return await (await getCanopyApiGateway()).updateCheckoutShipping(input);
4
+ return await getCanopyApiGateway().updateCheckoutShipping(input);
5
5
  }
6
6
  async function addCrossSellAction(input) {
7
- return await (await getCanopyApiGateway()).addCrossSell(input);
7
+ return await getCanopyApiGateway().addCrossSell(input);
8
8
  }
9
9
  export {
10
10
  addCrossSellAction,
@@ -1,6 +1,11 @@
1
1
  import type { AddToCartPayload, Cart, CartBuyerIdentity, CartLinePayload, RemoveFromCartPayload, ReplaceCartAttributesPayload, UpdateCartDiscountCodesPayload, UpdateCartLineItemPayload, UpdateCartNotePayload } from "../cart-types";
2
+ /** `buyerIp` is the shopper's IP; Shopify carts forward it, others ignore it. */
2
3
  export interface CartGateway {
3
- getCart(cartId: string): Promise<Cart | null>;
4
+ getCart(request: {
5
+ cartId: string;
6
+ } & {
7
+ buyerIp: string | null;
8
+ }): Promise<Cart | null>;
4
9
  /**
5
10
  * Bring a stored cart back in line with its source of truth. Cookie carts
6
11
  * reprice against the catalog and drop unavailable lines (persisting the
@@ -8,7 +13,7 @@ export interface CartGateway {
8
13
  * cart unchanged.
9
14
  */
10
15
  reconcileStoredCart(cart: Cart): Promise<Cart>;
11
- createCart(options?: {
16
+ createCart(options: {
12
17
  lines?: CartLinePayload[];
13
18
  discountCodes?: string[];
14
19
  attributes?: {
@@ -16,16 +21,32 @@ export interface CartGateway {
16
21
  value: string;
17
22
  }[];
18
23
  buyerIdentity?: CartBuyerIdentity;
24
+ } & {
25
+ buyerIp: string | null;
26
+ }): Promise<Cart | null>;
27
+ addToCart(cartPayload: AddToCartPayload & {
28
+ buyerIp: string | null;
19
29
  }): Promise<Cart | null>;
20
- addToCart(cartPayload: AddToCartPayload): Promise<Cart | null>;
21
30
  updateCartBuyerIdentity(payload: {
22
31
  cartId: string;
23
32
  buyerIdentity: CartBuyerIdentity;
33
+ } & {
34
+ buyerIp: string | null;
35
+ }): Promise<Cart | null>;
36
+ updateCartLineItem(cartPayload: UpdateCartLineItemPayload & {
37
+ buyerIp: string | null;
38
+ }): Promise<Cart | null>;
39
+ removeCartLineItems(cartPayload: RemoveFromCartPayload & {
40
+ buyerIp: string | null;
41
+ }): Promise<Cart | null>;
42
+ updateCartDiscountCodes(cartPayload: UpdateCartDiscountCodesPayload & {
43
+ buyerIp: string | null;
44
+ }): Promise<Cart | null>;
45
+ updateCartNote(cartPayload: UpdateCartNotePayload & {
46
+ buyerIp: string | null;
47
+ }): Promise<Cart | null>;
48
+ replaceCartAttributes(cartPayload: ReplaceCartAttributesPayload & {
49
+ buyerIp: string | null;
24
50
  }): Promise<Cart | null>;
25
- updateCartLineItem(cartPayload: UpdateCartLineItemPayload): Promise<Cart | null>;
26
- removeCartLineItems(cartPayload: RemoveFromCartPayload): Promise<Cart | null>;
27
- updateCartDiscountCodes(cartPayload: UpdateCartDiscountCodesPayload): Promise<Cart | null>;
28
- updateCartNote(cartPayload: UpdateCartNotePayload): Promise<Cart | null>;
29
- replaceCartAttributes(cartPayload: ReplaceCartAttributesPayload): Promise<Cart | null>;
30
51
  }
31
52
  export declare function getCartGateway(): Promise<CartGateway>;
@@ -27,8 +27,10 @@ export declare abstract class CookieBasedCartGateway implements CartGateway {
27
27
  private reconcile;
28
28
  private reconcileForWrite;
29
29
  reconcileStoredCart(cart: Cart): Promise<Cart>;
30
- getCart(cartId: string): Promise<Cart | null>;
31
- createCart({ lines, }?: {
30
+ getCart({ cartId }: {
31
+ cartId: string;
32
+ }): Promise<Cart | null>;
33
+ createCart({ lines, }: {
32
34
  lines?: CartLinePayload[];
33
35
  discountCodes?: string[];
34
36
  buyerIdentity?: CartBuyerIdentity;
@@ -51,7 +51,7 @@ class CookieBasedCartGateway {
51
51
  }
52
52
  return reconciledCart;
53
53
  }
54
- async getCart(cartId) {
54
+ async getCart({ cartId }) {
55
55
  try {
56
56
  const cart = await loadCartFromCookie(this.config.cookieName);
57
57
  return cart?.id === cartId ? cart : null;
@@ -65,7 +65,7 @@ class CookieBasedCartGateway {
65
65
  }
66
66
  async createCart({
67
67
  lines
68
- } = {}) {
68
+ }) {
69
69
  try {
70
70
  const cart = await createCartWithPersistence({
71
71
  cookieName: this.config.cookieName,
@@ -146,7 +146,7 @@ class CookieBasedCartGateway {
146
146
  `${this.config.gatewayName} cart gateway does not yet support discount codes`
147
147
  );
148
148
  try {
149
- return await this.getCart(cartPayload.cartId);
149
+ return await this.getCart({ cartId: cartPayload.cartId });
150
150
  } catch (error) {
151
151
  console.warn("Failed to fetch cart for discount code update:", error);
152
152
  return null;
@@ -155,7 +155,7 @@ class CookieBasedCartGateway {
155
155
  async updateCartBuyerIdentity({
156
156
  cartId
157
157
  }) {
158
- return this.getCart(cartId);
158
+ return this.getCart({ cartId });
159
159
  }
160
160
  async updateCartNote({
161
161
  cartId,
@@ -4,18 +4,21 @@ class ResolverCartGateway {
4
4
  async reconcileStoredCart(cart) {
5
5
  return cart;
6
6
  }
7
- async getCart(cartId) {
8
- const gateway = await getCanopyApiGateway();
9
- return await gateway.cartGet({ cartId });
7
+ async getCart({
8
+ cartId,
9
+ buyerIp
10
+ }) {
11
+ return await getCanopyApiGateway().cartGet({ cartId, buyerIp });
10
12
  }
11
13
  async createCart({
12
14
  lines,
13
15
  discountCodes,
14
16
  attributes,
15
- buyerIdentity
16
- } = {}) {
17
+ buyerIdentity,
18
+ buyerIp
19
+ }) {
17
20
  try {
18
- const gateway = await getCanopyApiGateway();
21
+ const gateway = getCanopyApiGateway();
19
22
  return await gateway.cartCreate({
20
23
  lines: lines?.map((line) => ({
21
24
  merchandiseId: line.id,
@@ -28,7 +31,8 @@ class ResolverCartGateway {
28
31
  })),
29
32
  discountCodes,
30
33
  attributes,
31
- country: buyerIdentity?.country
34
+ country: buyerIdentity?.country,
35
+ buyerIp
32
36
  });
33
37
  } catch (error) {
34
38
  console.warn("Failed to create cart via canopy-api:", error);
@@ -37,9 +41,10 @@ class ResolverCartGateway {
37
41
  }
38
42
  async addToCart(cartPayload) {
39
43
  try {
40
- const gateway = await getCanopyApiGateway();
44
+ const gateway = getCanopyApiGateway();
41
45
  return await gateway.cartAddLines({
42
46
  cartId: cartPayload.cartId,
47
+ buyerIp: cartPayload.buyerIp,
43
48
  lines: cartPayload.lines.map((line) => ({
44
49
  merchandiseId: line.id,
45
50
  quantity: line.quantity,
@@ -57,9 +62,10 @@ class ResolverCartGateway {
57
62
  }
58
63
  async updateCartLineItem(cartPayload) {
59
64
  try {
60
- const gateway = await getCanopyApiGateway();
65
+ const gateway = getCanopyApiGateway();
61
66
  return await gateway.cartUpdateLines({
62
67
  cartId: cartPayload.cartId,
68
+ buyerIp: cartPayload.buyerIp,
63
69
  lines: [
64
70
  {
65
71
  lineId: cartPayload.line.id,
@@ -79,9 +85,10 @@ class ResolverCartGateway {
79
85
  }
80
86
  async removeCartLineItems(cartPayload) {
81
87
  try {
82
- const gateway = await getCanopyApiGateway();
88
+ const gateway = getCanopyApiGateway();
83
89
  return await gateway.cartRemoveLines({
84
90
  cartId: cartPayload.cartId,
91
+ buyerIp: cartPayload.buyerIp,
85
92
  lineIds: cartPayload.lineIds
86
93
  });
87
94
  } catch (error) {
@@ -91,9 +98,10 @@ class ResolverCartGateway {
91
98
  }
92
99
  async updateCartDiscountCodes(cartPayload) {
93
100
  try {
94
- const gateway = await getCanopyApiGateway();
101
+ const gateway = getCanopyApiGateway();
95
102
  return await gateway.cartUpdateDiscountCodes({
96
103
  cartId: cartPayload.cartId,
104
+ buyerIp: cartPayload.buyerIp,
97
105
  discountCodes: cartPayload.discountCodes
98
106
  });
99
107
  } catch (error) {
@@ -103,13 +111,15 @@ class ResolverCartGateway {
103
111
  }
104
112
  async updateCartBuyerIdentity({
105
113
  cartId,
106
- buyerIdentity
114
+ buyerIdentity,
115
+ buyerIp
107
116
  }) {
108
117
  try {
109
- const gateway = await getCanopyApiGateway();
118
+ const gateway = getCanopyApiGateway();
110
119
  return await gateway.cartUpdateBuyerIdentity({
111
120
  cartId,
112
- country: buyerIdentity.country
121
+ country: buyerIdentity.country,
122
+ buyerIp
113
123
  });
114
124
  } catch (error) {
115
125
  console.warn(
@@ -121,9 +131,10 @@ class ResolverCartGateway {
121
131
  }
122
132
  async updateCartNote(cartPayload) {
123
133
  try {
124
- const gateway = await getCanopyApiGateway();
134
+ const gateway = getCanopyApiGateway();
125
135
  return await gateway.cartUpdateNote({
126
136
  cartId: cartPayload.cartId,
137
+ buyerIp: cartPayload.buyerIp,
127
138
  note: cartPayload.note
128
139
  });
129
140
  } catch (error) {
@@ -133,9 +144,10 @@ class ResolverCartGateway {
133
144
  }
134
145
  async replaceCartAttributes(cartPayload) {
135
146
  try {
136
- const gateway = await getCanopyApiGateway();
147
+ const gateway = getCanopyApiGateway();
137
148
  return await gateway.cartReplaceAttributes({
138
149
  cartId: cartPayload.cartId,
150
+ buyerIp: cartPayload.buyerIp,
139
151
  attributes: cartPayload.attributes.map((attribute) => ({
140
152
  key: attribute.key,
141
153
  value: String(attribute.value)
@@ -19,7 +19,7 @@ class StripeCartGateway extends CookieBasedCartGateway {
19
19
  cartIdGenerator: stripeCartIdGenerator,
20
20
  gatewayName: "Stripe",
21
21
  reconcileLines: async (lines) => {
22
- const gateway = await getCanopyApiGateway();
22
+ const gateway = getCanopyApiGateway();
23
23
  return await gateway.reconcileCartLines({ lines });
24
24
  }
25
25
  };
@@ -37,7 +37,7 @@ async function startStripeCheckout({
37
37
  "[Replo] Stripe buy now does not support selling plans or discount codes; those options were ignored."
38
38
  );
39
39
  }
40
- const outcome = await (await getCanopyApiGateway()).createStripeCheckoutSession({
40
+ const outcome = await getCanopyApiGateway().createStripeCheckoutSession({
41
41
  checkoutAttemptId: checkoutAttemptId ?? uuidv4(),
42
42
  lines: lineItems.map((item) => ({
43
43
  variantId: item.variantId,
@@ -7,15 +7,21 @@ export interface ConsentPlatformConfig {
7
7
  name: string;
8
8
  values: Partial<Record<ConsentCategory, string>>;
9
9
  };
10
- /** Cookiebot preset only: enables consent read-back and runScripts re-scan. */
11
- isCookiebot: boolean;
10
+ /** Presets read the visitor's answer back; a generic platform only reports grants. */
11
+ kind: "cookiebot" | "usercentrics" | "generic";
12
+ /** Usercentrics only: the service that releases Replo's own pixel, or null until one is chosen. */
13
+ reploUsercentricsService: string | null;
14
+ /** The GTM container that loads this platform, which therefore can't wait for it. */
15
+ loaderContainerId: string | null;
12
16
  }
13
17
  /** Whether this entry connects a consent platform (vs a tracker it would gate). */
14
- export declare function isConsentPlatformEntry(entry: ReploScriptEntry): boolean;
18
+ export declare function isConsentPlatformEntry(entry: ReploScriptEntry): entry is Extract<ReploScriptEntry, {
19
+ type: "Cookiebot" | "Usercentrics" | "consentPlatform";
20
+ }>;
15
21
  /** The connected platform's config, or null when Replo's banner owns consent. */
16
22
  export declare function findConsentPlatform(scripts: ReploScriptEntry[]): ConsentPlatformConfig | null;
17
23
  /** Blocks a gated script; `text/plain` is unconditional so an unmapped category fails closed. */
18
- export declare function getBlockingAttributes(config: ConsentPlatformConfig, requiredConsent: ConsentCategory[]): Record<string, string>;
24
+ export declare function getBlockingAttributes(config: ConsentPlatformConfig, requiredConsent: ConsentCategory[], usercentricsService: string | null): Record<string, string>;
19
25
  /** The bits of Cookiebot's `window.Cookiebot` global we read. */
20
26
  interface CookiebotGlobal {
21
27
  consent?: Partial<Record<CookiebotCategory, boolean>>;
@@ -26,6 +32,14 @@ type CookiebotCategory = "necessary" | "preferences" | "statistics" | "marketing
26
32
  declare global {
27
33
  interface Window {
28
34
  Cookiebot?: CookiebotGlobal;
35
+ __ucCmp?: {
36
+ getConsentDetails?: () => Promise<unknown>;
37
+ refreshScripts?: () => Promise<unknown>;
38
+ };
39
+ UC_UI?: {
40
+ getServicesBaseInfo?: () => unknown;
41
+ enableScriptsForServicesWithConsent?: () => unknown;
42
+ };
29
43
  }
30
44
  }
31
45
  type OptionalConsentCategory = Exclude<ConsentCategory, "necessary">;
@@ -36,12 +50,13 @@ export declare function findDelegatedPlatform(): ConsentPlatformConfig | null;
36
50
  export declare function getDelegatedConsent(): {
37
51
  categories: ConsentCategories;
38
52
  decidedAt: number | null;
53
+ grantedUsercentricsServices?: string[];
39
54
  };
40
55
  /** Inline marker a generic CMP executes when it releases one consent category. */
41
56
  export declare function buildGrantMarker(category: OptionalConsentCategory): ScriptTagDescriptor;
42
57
  /** Latches the platform as the consent backend; un-delegating would need a reload, as native revocation already does. */
43
58
  export declare function enableConsentDelegation(config: ConsentPlatformConfig): void;
44
- /** Cookiebot-only re-scan; other platforms rely on the loader injecting after every tag. */
59
+ /** Re-scan for late tags; a generic platform relies on the loader injecting after every tag. */
45
60
  export declare function activateBlockedScripts(): void;
46
61
  /** Subscribe to delegated-consent changes. Returns an unsubscribe function. */
47
62
  export declare function subscribeToConsentDelegation(listener: () => void): () => void;