@replohq/sdk 2.18.3 → 2.20.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/_vendor/replo-utils/lib/crypto.d.ts +5 -0
- package/_vendor/schemas/generated/consent.d.ts +13 -1
- package/analytics/analytics-provider.js +3 -1
- package/analytics/get-analytics-sinks.js +7 -4
- package/analytics/replo-pixel-script.d.ts +5 -5
- package/analytics/replo-pixel-script.js +23 -22
- package/cart/hooks/use-add-to-cart.d.ts +17 -4
- package/cart/hooks/use-add-to-cart.js +6 -2
- package/cart/hooks/use-buy-now.d.ts +17 -5
- package/cart/hooks/use-buy-now.js +6 -2
- package/cart/hooks/use-checkout-availability.d.ts +21 -0
- package/cart/hooks/use-checkout-availability.js +71 -0
- package/consent/consent-platform.d.ts +20 -5
- package/consent/consent-platform.js +94 -11
- package/consent/consent-store.d.ts +7 -2
- package/consent/consent-store.js +16 -4
- package/consent/inject-script-descriptors.d.ts +8 -1
- package/consent/inject-script-descriptors.js +23 -0
- package/consent/replo-scripts.d.ts +6 -2
- package/consent/replo-scripts.js +35 -27
- package/consent/script-registration-store.d.ts +2 -0
- package/consent/script-snippets.d.ts +1 -1
- package/consent/types.d.ts +18 -4
- package/consent/usercentrics-consent.d.ts +15 -0
- package/consent/usercentrics-consent.js +96 -0
- package/consent/window-api.js +1 -1
- package/lib/buildMetadata.js +3 -3
- package/loaders/integration-loader-schemas.d.ts +1 -0
- package/loaders/loader-schemas.d.ts +3 -0
- package/loaders/loader-schemas.js +4 -2
- package/package.json +4 -4
- package/providers/canopy-client-config.d.ts +2 -0
- package/providers/replo-provider.js +2 -1
|
@@ -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
|
|
55
|
+
const isReploPixelAllowed = isConsentAllowed({
|
|
55
56
|
state: consent,
|
|
56
|
-
requiredConsent: ["analytics"]
|
|
57
|
+
requiredConsent: ["analytics"],
|
|
58
|
+
usercentricsService: findDelegatedPlatform()?.reploUsercentricsService ?? null
|
|
57
59
|
});
|
|
58
|
-
if (
|
|
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
|
-
* `
|
|
7
|
+
* `isReploPixelAllowed` gate that controls whether `ReploSink` is created at all.
|
|
8
8
|
*
|
|
9
|
-
* Under a consent platform the tag is
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
}):
|
|
19
|
+
}): null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import type { QueryClient } from "@tanstack/react-query";
|
|
2
2
|
import type { CartError } from "../cart-provider";
|
|
3
3
|
import type { CartLineAdjustment, CartLineAttribute, CartLineMerchandise } from "../cart-types";
|
|
4
|
+
import type { CheckoutCatalog } from "./use-checkout-availability";
|
|
4
5
|
/**
|
|
5
6
|
* Describes a single item to add to the cart. Uses the merchandiseId
|
|
6
7
|
* (e.g. a variant GID) directly from data loader output. Product
|
|
@@ -56,27 +57,39 @@ export declare function findMerchandiseInQueryCache({ queryClient, merchandiseId
|
|
|
56
57
|
* and opens the cart UI, which renders it, so a click handler needs no
|
|
57
58
|
* handling of its own.
|
|
58
59
|
*
|
|
60
|
+
* `checkoutAvailability` is `"unavailable"` while the store can't take
|
|
61
|
+
* payments yet, or when the `catalog` passed is one this site's checkout
|
|
62
|
+
* can't sell. Show the button disabled with "Coming soon" then: a shopper
|
|
63
|
+
* could fill a cart but never check out.
|
|
64
|
+
*
|
|
59
65
|
* @example
|
|
60
66
|
* ```tsx
|
|
61
67
|
* import { useAddToCart } from "@replohq/sdk/cart/hooks/use-add-to-cart";
|
|
62
68
|
*
|
|
63
69
|
* function AddToCartButton({ variant }) {
|
|
64
|
-
* const { addToCart, isAdding } = useAddToCart(
|
|
70
|
+
* const { addToCart, isAdding, checkoutAvailability } = useAddToCart({
|
|
71
|
+
* catalog: "shopify",
|
|
72
|
+
* });
|
|
73
|
+
* const isComingSoon = checkoutAvailability === "unavailable";
|
|
65
74
|
*
|
|
66
75
|
* return (
|
|
67
76
|
* <button
|
|
68
|
-
* disabled={!variant.availableForSale || isAdding}
|
|
77
|
+
* disabled={!variant.availableForSale || isAdding || isComingSoon}
|
|
69
78
|
* onClick={() => {
|
|
70
79
|
* void addToCart([{ merchandiseId: variant.id, quantity: 1 }]);
|
|
71
80
|
* }}
|
|
72
81
|
* >
|
|
73
|
-
* {isAdding ? "Adding..." : "Add to Cart"}
|
|
82
|
+
* {isComingSoon ? "Coming soon" : isAdding ? "Adding..." : "Add to Cart"}
|
|
74
83
|
* </button>
|
|
75
84
|
* );
|
|
76
85
|
* }
|
|
77
86
|
* ```
|
|
78
87
|
*/
|
|
79
|
-
export declare function useAddToCart(
|
|
88
|
+
export declare function useAddToCart({ catalog, }?: {
|
|
89
|
+
/** The catalog this control sells from, so `checkoutAvailability` can say whether this site's checkout sells it. */
|
|
90
|
+
catalog?: CheckoutCatalog | null;
|
|
91
|
+
}): {
|
|
80
92
|
addToCart: (items: AddToCartItem[], openCartAfterAdd?: boolean) => Promise<AddToCartResult>;
|
|
81
93
|
isAdding: boolean;
|
|
94
|
+
checkoutAvailability: import("./use-checkout-availability").CheckoutAvailability;
|
|
82
95
|
};
|
|
@@ -6,6 +6,7 @@ import { DATA_LOADER_KEYS } from "../../loaders/loader-keys";
|
|
|
6
6
|
import { isPlaceholderMerchandiseId } from "../../loaders/placeholder-product";
|
|
7
7
|
import { useCartInternal } from "../cart-provider";
|
|
8
8
|
import { toCartError } from "../utils/cart-error";
|
|
9
|
+
import { useCheckoutAvailability } from "./use-checkout-availability";
|
|
9
10
|
const PRODUCT_LOADER_KEYS = [
|
|
10
11
|
DATA_LOADER_KEYS.SHOPIFY_PRODUCT,
|
|
11
12
|
DATA_LOADER_KEYS.REPLO_PRODUCT
|
|
@@ -81,10 +82,13 @@ function findMerchandiseInQueryCache({
|
|
|
81
82
|
}
|
|
82
83
|
return void 0;
|
|
83
84
|
}
|
|
84
|
-
function useAddToCart(
|
|
85
|
+
function useAddToCart({
|
|
86
|
+
catalog = null
|
|
87
|
+
} = {}) {
|
|
85
88
|
const cart = useCartInternal();
|
|
86
89
|
const queryClient = useQueryClient();
|
|
87
90
|
const [isAdding, setIsAdding] = React.useState(false);
|
|
91
|
+
const checkoutAvailability = useCheckoutAvailability({ catalog });
|
|
88
92
|
const addToCart = React.useCallback(
|
|
89
93
|
async (items, openCartAfterAdd = true) => {
|
|
90
94
|
const buyableItems = items.filter((item) => {
|
|
@@ -121,7 +125,7 @@ function useAddToCart() {
|
|
|
121
125
|
},
|
|
122
126
|
[cart, queryClient]
|
|
123
127
|
);
|
|
124
|
-
return { addToCart, isAdding };
|
|
128
|
+
return { addToCart, isAdding, checkoutAvailability };
|
|
125
129
|
}
|
|
126
130
|
export {
|
|
127
131
|
findMerchandiseInQueryCache,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CartLineAttribute } from "../cart-types";
|
|
2
|
+
import type { CheckoutCatalog } from "./use-checkout-availability";
|
|
2
3
|
/**
|
|
3
4
|
* What a `buyNow` call did. Out of stock is an expected storefront outcome
|
|
4
5
|
* (a buyer can race the last unit), so it rides the return value rather than
|
|
@@ -34,18 +35,25 @@ export interface VariantBuyNowInfo {
|
|
|
34
35
|
* the browser to checkout. Stripe checkout ignores selling plans and discount
|
|
35
36
|
* codes, logging a server-side warning when either is supplied.
|
|
36
37
|
*
|
|
38
|
+
* `checkoutAvailability` is `"unavailable"` while the store can't take
|
|
39
|
+
* payments yet (Replo checkout not set up, or checkout turned off), or when
|
|
40
|
+
* the `catalog` passed is one this site's checkout can't sell. Show the button
|
|
41
|
+
* disabled with "Coming soon" then, instead of letting the shopper hit a
|
|
42
|
+
* failed checkout.
|
|
43
|
+
*
|
|
37
44
|
* @example
|
|
38
45
|
*
|
|
39
46
|
* ```tsx
|
|
40
47
|
* import { useBuyNow } from "@replohq/sdk/cart/hooks/use-buy-now";
|
|
41
48
|
*
|
|
42
49
|
* function MyComponent({ selectedVariant, selectedSellingPlan, quantity }) {
|
|
43
|
-
* const { buyNow } = useBuyNow();
|
|
50
|
+
* const { buyNow, checkoutAvailability } = useBuyNow();
|
|
44
51
|
* const [isSoldOut, setIsSoldOut] = React.useState(false);
|
|
52
|
+
* const isComingSoon = checkoutAvailability === "unavailable";
|
|
45
53
|
*
|
|
46
54
|
* return (
|
|
47
55
|
* <Button
|
|
48
|
-
* disabled={!selectedVariant?.availableForSale || isSoldOut}
|
|
56
|
+
* disabled={!selectedVariant?.availableForSale || isSoldOut || isComingSoon}
|
|
49
57
|
* onClick={async () => {
|
|
50
58
|
* if (selectedVariant) {
|
|
51
59
|
* const outcome = await buyNow(
|
|
@@ -70,15 +78,18 @@ export interface VariantBuyNowInfo {
|
|
|
70
78
|
* }
|
|
71
79
|
* }}
|
|
72
80
|
* >
|
|
73
|
-
* {isSoldOut ? "Sold out" : "Shop Now"}
|
|
81
|
+
* {isComingSoon ? "Coming soon" : isSoldOut ? "Sold out" : "Shop Now"}
|
|
74
82
|
* </Button>
|
|
75
83
|
* );
|
|
76
84
|
* }
|
|
77
85
|
* ```
|
|
78
86
|
*
|
|
79
|
-
* @returns Object containing the buyNow function
|
|
87
|
+
* @returns Object containing the buyNow function and whether checkout can take payments
|
|
80
88
|
*/
|
|
81
|
-
export declare function useBuyNow(
|
|
89
|
+
export declare function useBuyNow({ catalog, }?: {
|
|
90
|
+
/** The catalog this control sells from, so `checkoutAvailability` can say whether this site's checkout sells it. */
|
|
91
|
+
catalog?: CheckoutCatalog | null;
|
|
92
|
+
}): {
|
|
82
93
|
buyNow: (variants: VariantBuyNowInfo[], options?: {
|
|
83
94
|
discountCodes?: string[];
|
|
84
95
|
/** Order-level Shopify cart attributes, including experiment attribution. */
|
|
@@ -87,4 +98,5 @@ export declare function useBuyNow(): {
|
|
|
87
98
|
value: string;
|
|
88
99
|
}[];
|
|
89
100
|
}) => Promise<BuyNowOutcome>;
|
|
101
|
+
checkoutAvailability: import("./use-checkout-availability").CheckoutAvailability;
|
|
90
102
|
};
|
|
@@ -5,6 +5,7 @@ import { CanopyError } from "../../lib/canopy-error";
|
|
|
5
5
|
import { navigateExternal } from "../../lib/external-navigation";
|
|
6
6
|
import { buyNowWithRecoveryAction } from "../buy-now-action";
|
|
7
7
|
import { CartCountryContext } from "../cart-country-context";
|
|
8
|
+
import { useCheckoutAvailability } from "./use-checkout-availability";
|
|
8
9
|
const REPLO_LOCAL_STORAGE_SESSION_KEY = "_replo_sid";
|
|
9
10
|
const REPLO_SESSION_ID_KEY = "rsid";
|
|
10
11
|
const REPLO_CLIENT_ID_KEY = "rclid";
|
|
@@ -13,8 +14,11 @@ class ProductHasNoVariantsError extends CanopyError {
|
|
|
13
14
|
class CheckoutAttemptError extends CanopyError {
|
|
14
15
|
}
|
|
15
16
|
const checkoutAttempts = /* @__PURE__ */ new Map();
|
|
16
|
-
function useBuyNow(
|
|
17
|
+
function useBuyNow({
|
|
18
|
+
catalog = null
|
|
19
|
+
} = {}) {
|
|
17
20
|
const country = React.useContext(CartCountryContext);
|
|
21
|
+
const checkoutAvailability = useCheckoutAvailability({ catalog });
|
|
18
22
|
const reploSessionString = useClientValue(
|
|
19
23
|
() => localStorage.getItem(REPLO_LOCAL_STORAGE_SESSION_KEY),
|
|
20
24
|
null
|
|
@@ -88,7 +92,7 @@ function useBuyNow() {
|
|
|
88
92
|
},
|
|
89
93
|
[reploSessionString, country]
|
|
90
94
|
);
|
|
91
|
-
return { buyNow };
|
|
95
|
+
return { buyNow, checkoutAvailability };
|
|
92
96
|
}
|
|
93
97
|
export {
|
|
94
98
|
useBuyNow
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether shoppers can check out on this site right now. `"checking"` while
|
|
3
|
+
* the store's payments are being confirmed; buy controls stay usable then, so
|
|
4
|
+
* a store that can sell never flashes an unavailable state.
|
|
5
|
+
*/
|
|
6
|
+
export type CheckoutAvailability = "checking" | "available" | "unavailable";
|
|
7
|
+
/**
|
|
8
|
+
* Which catalog a buy control sells from: `"shopify"` for Shopify loader
|
|
9
|
+
* output, `"replo"` for Replo products. Shopify checkout can't sell Replo
|
|
10
|
+
* products and Replo checkout can't sell Shopify ones.
|
|
11
|
+
*/
|
|
12
|
+
export type CheckoutCatalog = "shopify" | "replo";
|
|
13
|
+
/**
|
|
14
|
+
* Replo checkout can only sell once the store's payments are set up, which
|
|
15
|
+
* can finish or lapse long after a publish, so it is read live. Shopify
|
|
16
|
+
* checkout is the store's own and is treated as available. Without a
|
|
17
|
+
* `catalog` only the site's checkout is judged, as before the option existed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useCheckoutAvailability({ catalog, }: {
|
|
20
|
+
catalog: CheckoutCatalog | null;
|
|
21
|
+
}): CheckoutAvailability;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { CanopyError } from "../../lib/canopy-error";
|
|
4
|
+
import { useCanopyClientConfig } from "../../providers/canopy-client-config";
|
|
5
|
+
class CheckoutAvailabilityError extends CanopyError {
|
|
6
|
+
}
|
|
7
|
+
const CHECKOUT_AVAILABILITY_STALE_MS = 6e4;
|
|
8
|
+
async function fetchCanAcceptPayments(config) {
|
|
9
|
+
const response = await fetch(
|
|
10
|
+
`${config.canopyApiBaseUrl}/api/v1/stripe-checkout/availability`,
|
|
11
|
+
{
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: { "Content-Type": "application/json" },
|
|
14
|
+
body: JSON.stringify({ projectId: config.projectId })
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
throw new CheckoutAvailabilityError({
|
|
19
|
+
message: `Checkout availability check failed with status ${response.status}`
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const body = await response.json();
|
|
23
|
+
if (typeof body !== "object" || body === null || !("canAcceptPayments" in body) || typeof body.canAcceptPayments !== "boolean") {
|
|
24
|
+
throw new CheckoutAvailabilityError({
|
|
25
|
+
message: "Checkout availability check returned an unexpected response"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return body.canAcceptPayments;
|
|
29
|
+
}
|
|
30
|
+
function useCheckoutAvailability({
|
|
31
|
+
catalog
|
|
32
|
+
}) {
|
|
33
|
+
const config = useCanopyClientConfig();
|
|
34
|
+
const checkoutProvider = config?.checkoutProvider ?? null;
|
|
35
|
+
const query = useQuery({
|
|
36
|
+
queryKey: ["replo", "checkout-availability", config?.projectId ?? null],
|
|
37
|
+
queryFn: () => {
|
|
38
|
+
if (!config) {
|
|
39
|
+
throw new CheckoutAvailabilityError({
|
|
40
|
+
message: "This site has no canopy-api config"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return fetchCanAcceptPayments(config);
|
|
44
|
+
},
|
|
45
|
+
enabled: checkoutProvider === "stripe" && catalog !== "shopify",
|
|
46
|
+
staleTime: CHECKOUT_AVAILABILITY_STALE_MS,
|
|
47
|
+
retry: 1
|
|
48
|
+
});
|
|
49
|
+
if (checkoutProvider === null) {
|
|
50
|
+
return "available";
|
|
51
|
+
}
|
|
52
|
+
if (checkoutProvider === "none") {
|
|
53
|
+
return "unavailable";
|
|
54
|
+
}
|
|
55
|
+
if (checkoutProvider === "shopify") {
|
|
56
|
+
return catalog === "replo" ? "unavailable" : "available";
|
|
57
|
+
}
|
|
58
|
+
if (catalog === "shopify") {
|
|
59
|
+
return "unavailable";
|
|
60
|
+
}
|
|
61
|
+
if (query.isPending) {
|
|
62
|
+
return "checking";
|
|
63
|
+
}
|
|
64
|
+
if (query.isError) {
|
|
65
|
+
return "available";
|
|
66
|
+
}
|
|
67
|
+
return query.data ? "available" : "unavailable";
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
useCheckoutAvailability
|
|
71
|
+
};
|
|
@@ -7,15 +7,21 @@ export interface ConsentPlatformConfig {
|
|
|
7
7
|
name: string;
|
|
8
8
|
values: Partial<Record<ConsentCategory, string>>;
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
|
|
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):
|
|
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
|
-
/**
|
|
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;
|