@replohq/sdk 2.19.0 → 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.
@@ -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
+ };
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sdk",
3
- "version": "2.19.0",
3
+ "version": "2.20.0",
4
4
  "branch": "HEAD",
5
- "commit": "377fd6358bd4e3d0d32ff03942701a9cede4e0e9",
6
- "builtAt": "2026-11-06T19:28:13.532Z"
5
+ "commit": "20365c00f7144702c4a7438ac5d907dc97b5b62a",
6
+ "builtAt": "2026-11-06T20:15:11.914Z"
7
7
  };
@@ -210,6 +210,7 @@ export declare const rebuyRecommendationsOutputSchema: z.ZodObject<{
210
210
  previewUrl: z.ZodNullable<z.ZodString>;
211
211
  }, z.core.$strip>], "kind">>>;
212
212
  }, z.core.$strip>>>;
213
+ externalCheckoutUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
213
214
  }, z.core.$strip>>;
214
215
  }, z.core.$strip>;
215
216
  export declare const smileEarningRuleSchema: z.ZodObject<{
@@ -389,6 +389,7 @@ export declare const productSchema: z.ZodObject<{
389
389
  previewUrl: z.ZodNullable<z.ZodString>;
390
390
  }, z.core.$strip>], "kind">>>;
391
391
  }, z.core.$strip>>>;
392
+ externalCheckoutUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
392
393
  }, z.core.$strip>;
393
394
  export type Product = z.infer<typeof productSchema>;
394
395
  export declare const okendoReviewMediaSchema: z.ZodObject<{
@@ -640,6 +641,7 @@ export declare const fullProductSchema: z.ZodObject<{
640
641
  previewUrl: z.ZodNullable<z.ZodString>;
641
642
  }, z.core.$strip>], "kind">>>;
642
643
  }, z.core.$strip>>;
644
+ externalCheckoutUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
643
645
  }, z.core.$strip>;
644
646
  export declare const fullCollectionSchema: z.ZodObject<{
645
647
  id: z.ZodString;
@@ -834,6 +836,7 @@ export declare const paginatedProductsSchema: z.ZodObject<{
834
836
  previewUrl: z.ZodNullable<z.ZodString>;
835
837
  }, z.core.$strip>], "kind">>>;
836
838
  }, z.core.$strip>>>;
839
+ externalCheckoutUrl: z.ZodOptional<z.ZodNullable<z.ZodString>>;
837
840
  }, z.core.$strip>>;
838
841
  pageInfo: z.ZodObject<{
839
842
  hasNextPage: z.ZodBoolean;
@@ -145,7 +145,8 @@ const productSchema = z.object({
145
145
  tags: z.array(z.string()).optional(),
146
146
  sellingPlanGroups: z.array(sellingPlanGroupSchema).optional(),
147
147
  requiresSellingPlan: z.boolean().optional(),
148
- metafields: z.array(metafieldSchema).optional()
148
+ metafields: z.array(metafieldSchema).optional(),
149
+ externalCheckoutUrl: z.string().nullish()
149
150
  });
150
151
  const okendoReviewMediaSchema = z.object({
151
152
  type: z.string().describe('Okendo media type, e.g. "image" or "video"'),
@@ -216,7 +217,8 @@ const fullProductSchema = z.object({
216
217
  tags: z.array(z.string()),
217
218
  sellingPlanGroups: z.array(sellingPlanGroupSchema),
218
219
  requiresSellingPlan: productSchema.shape.requiresSellingPlan,
219
- metafields: z.array(metafieldSchema)
220
+ metafields: z.array(metafieldSchema),
221
+ externalCheckoutUrl: productSchema.shape.externalCheckoutUrl
220
222
  });
221
223
  const fullCollectionSchema = z.object({
222
224
  id: z.string(),
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sdk",
3
- "version": "2.19.0",
3
+ "version": "2.20.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sdk",
6
- "version": "2.19.0",
6
+ "version": "2.20.0",
7
7
  "branch": "HEAD",
8
- "commit": "377fd6358bd4e3d0d32ff03942701a9cede4e0e9",
9
- "builtAt": "2026-11-06T19:28:13.532Z"
8
+ "commit": "20365c00f7144702c4a7438ac5d907dc97b5b62a",
9
+ "builtAt": "2026-11-06T20:15:11.914Z"
10
10
  },
11
11
  "description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -1,3 +1,4 @@
1
+ import type { CheckoutProvider } from "../lib/integration-utils";
1
2
  import React from "react";
2
3
  /**
3
4
  * What browser code needs to call canopy-api directly. Resolved from the worker
@@ -7,6 +8,7 @@ import React from "react";
7
8
  export type CanopyClientConfig = {
8
9
  canopyApiBaseUrl: string;
9
10
  projectId: string;
11
+ checkoutProvider: CheckoutProvider;
10
12
  };
11
13
  export declare function CanopyClientConfigProvider({ config, children, }: React.PropsWithChildren<{
12
14
  config: CanopyClientConfig | null;
@@ -19,7 +19,8 @@ async function ReploProvider({
19
19
  checkoutProvider = env.CHECKOUT_PROVIDER ?? "none";
20
20
  canopyClientConfig = {
21
21
  canopyApiBaseUrl: buildBaseUrl(env.CANOPY_API_HOST),
22
- projectId: env.PROJECT_ID
22
+ projectId: env.PROJECT_ID,
23
+ checkoutProvider
23
24
  };
24
25
  } catch {
25
26
  checkoutProvider = "none";