@replohq/sdk 0.15.0 → 1.0.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/cart/buy-now-action.d.ts +7 -0
- package/cart/buy-now-action.js +20 -7
- package/cart/buy-now-action.js.map +2 -2
- package/cart/hooks/use-buy-now.d.ts +23 -4
- package/cart/hooks/use-buy-now.js +6 -2
- package/cart/hooks/use-buy-now.js.map +2 -2
- package/cart/return-urls.d.ts +10 -0
- package/cart/return-urls.js +44 -0
- package/cart/return-urls.js.map +7 -0
- package/cart/storefront-urls.d.ts +1 -10
- package/cart/storefront-urls.js +2 -41
- package/cart/storefront-urls.js.map +2 -2
- package/lib/buildMetadata.js +3 -3
- package/package.json +12 -4
package/cart/buy-now-action.d.ts
CHANGED
|
@@ -17,5 +17,12 @@ export declare function buyNowAction(input: {
|
|
|
17
17
|
discountCodes: string[];
|
|
18
18
|
pageUrl: string;
|
|
19
19
|
}): Promise<{
|
|
20
|
+
result: "created";
|
|
20
21
|
redirectUrl: string;
|
|
22
|
+
} | {
|
|
23
|
+
result: "outOfStock";
|
|
24
|
+
variants: {
|
|
25
|
+
variantId: string;
|
|
26
|
+
available: number;
|
|
27
|
+
}[];
|
|
21
28
|
}>;
|
package/cart/buy-now-action.js
CHANGED
|
@@ -8,7 +8,7 @@ import { getCheckoutProvider } from "../lib/integration-utils";
|
|
|
8
8
|
import { buildBaseUrl } from "../lib/url-utils";
|
|
9
9
|
import { createCartAction } from "./cart-actions";
|
|
10
10
|
import { REPLO_ATTRIBUTION_PROPERTY } from "./cart-types";
|
|
11
|
-
import {
|
|
11
|
+
import { getReturnUrls } from "./return-urls";
|
|
12
12
|
class BuyNowError extends CanopyError {
|
|
13
13
|
}
|
|
14
14
|
class BuyNowCartCreationError extends CanopyError {
|
|
@@ -20,7 +20,7 @@ async function buyNowAction(input) {
|
|
|
20
20
|
if (!host) {
|
|
21
21
|
throw new BuyNowError({ message: "Missing host header" });
|
|
22
22
|
}
|
|
23
|
-
const { origin, cancelUrl } =
|
|
23
|
+
const { origin, cancelUrl } = getReturnUrls({
|
|
24
24
|
host,
|
|
25
25
|
forwardedHost: headersList.get("x-forwarded-host"),
|
|
26
26
|
forwardedProto: headersList.get("x-forwarded-proto"),
|
|
@@ -71,14 +71,27 @@ async function buyNowAction(input) {
|
|
|
71
71
|
}
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
const payload = await response.json();
|
|
75
|
+
const outOfStock = z.object({
|
|
76
|
+
result: z.literal("outOfStock"),
|
|
77
|
+
variants: z.array(
|
|
78
|
+
z.object({
|
|
79
|
+
variantId: z.string(),
|
|
80
|
+
available: z.number().int().min(0)
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
}).safeParse(payload);
|
|
84
|
+
if (outOfStock.success) {
|
|
85
|
+
return { result: "outOfStock", variants: outOfStock.data.variants };
|
|
86
|
+
}
|
|
87
|
+
const created = z.object({ url: z.string() }).safeParse(payload);
|
|
88
|
+
if (!created.success) {
|
|
76
89
|
throw new BuyNowCartCreationError({
|
|
77
90
|
message: "Stripe checkout session response missing redirect URL",
|
|
78
|
-
cause:
|
|
91
|
+
cause: created.error
|
|
79
92
|
});
|
|
80
93
|
}
|
|
81
|
-
return { redirectUrl:
|
|
94
|
+
return { result: "created", redirectUrl: created.data.url };
|
|
82
95
|
}
|
|
83
96
|
const cart = await createCartAction({
|
|
84
97
|
lines: cartLines,
|
|
@@ -92,7 +105,7 @@ async function buyNowAction(input) {
|
|
|
92
105
|
});
|
|
93
106
|
}
|
|
94
107
|
const checkoutUrl = new URL(cart.checkoutUrl, origin);
|
|
95
|
-
return { redirectUrl: checkoutUrl.toString() };
|
|
108
|
+
return { result: "created", redirectUrl: checkoutUrl.toString() };
|
|
96
109
|
}
|
|
97
110
|
export {
|
|
98
111
|
buyNowAction
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../cart/buy-now-action.ts"],
|
|
4
|
-
"sourcesContent": ["\"use server\";\n\nimport type { CartLineAttribute } from \"./cart-types\";\n\nimport { headers } from \"next/headers\";\n\nimport { v4 as uuidv4 } from \"uuid\";\nimport { z } from \"zod\";\n\nimport { getEnv } from \"../env\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport { getCheckoutProvider } from \"../lib/integration-utils\";\nimport { buildBaseUrl } from \"../lib/url-utils\";\nimport { createCartAction } from \"./cart-actions\";\nimport { REPLO_ATTRIBUTION_PROPERTY } from \"./cart-types\";\nimport {
|
|
5
|
-
"mappings": ";AAIA,SAAS,eAAe;AAExB,SAAS,MAAM,cAAc;AAC7B,SAAS,SAAS;AAElB,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,kCAAkC;AAC3C,SAAS,
|
|
4
|
+
"sourcesContent": ["\"use server\";\n\nimport type { CartLineAttribute } from \"./cart-types\";\n\nimport { headers } from \"next/headers\";\n\nimport { v4 as uuidv4 } from \"uuid\";\nimport { z } from \"zod\";\n\nimport { getEnv } from \"../env\";\nimport { CanopyError } from \"../lib/canopy-error\";\nimport { getCheckoutProvider } from \"../lib/integration-utils\";\nimport { buildBaseUrl } from \"../lib/url-utils\";\nimport { createCartAction } from \"./cart-actions\";\nimport { REPLO_ATTRIBUTION_PROPERTY } from \"./cart-types\";\nimport { getReturnUrls } from \"./return-urls\";\n\nclass BuyNowError extends CanopyError {}\nclass BuyNowCartCreationError extends CanopyError {}\n\n/**\n * Server action that handles the full buy-now flow:\n * 1. For Shopify: creates a cart via the canopy-api-backed gateway, returns checkoutUrl.\n * 2. For Stripe: creates a Stripe checkout session via canopy-api, returns session URL.\n *\n * Variant IDs are expected to already be Storefront GIDs (from data loader output).\n * No internal\u2192external ID mapping is needed.\n */\nexport async function buyNowAction(input: {\n lineItems: {\n variantId: string;\n quantity: number;\n sellingPlanId: string | null;\n properties: CartLineAttribute[];\n }[];\n discountCodes: string[];\n pageUrl: string;\n}): Promise<\n | { result: \"created\"; redirectUrl: string }\n | {\n result: \"outOfStock\";\n // Each short variant with the purchasable remainder, so the cart can\n // clamp quantities to what's in stock instead of dropping whole lines.\n variants: { variantId: string; available: number }[];\n }\n> {\n const { lineItems, discountCodes, pageUrl } = input;\n\n const headersList = await headers();\n const host = headersList.get(\"host\");\n if (!host) {\n throw new BuyNowError({ message: \"Missing host header\" });\n }\n const { origin, cancelUrl } = getReturnUrls({\n host,\n forwardedHost: headersList.get(\"x-forwarded-host\"),\n forwardedProto: headersList.get(\"x-forwarded-proto\"),\n requestOrigin: headersList.get(\"origin\"),\n pageUrl,\n });\n\n const checkoutProvider = await getCheckoutProvider();\n\n const cartLines = lineItems.map((item) => {\n const hasReploAttribution = item.properties.some(\n (prop) => prop.key === REPLO_ATTRIBUTION_PROPERTY.key,\n );\n const properties = hasReploAttribution\n ? item.properties\n : [REPLO_ATTRIBUTION_PROPERTY, ...item.properties];\n\n return {\n id: item.variantId,\n quantity: item.quantity,\n sellingPlanId: item.sellingPlanId,\n properties,\n };\n });\n\n if (checkoutProvider === \"stripe\") {\n const env = await getEnv();\n const temporaryCartId = uuidv4();\n\n const body = {\n cartId: temporaryCartId,\n lines: cartLines.map((line) => ({\n variantId: line.id,\n quantity: line.quantity,\n properties: line.properties,\n })),\n projectId: env.PROJECT_ID,\n // canopy appends session_id={CHECKOUT_SESSION_ID} at session creation;\n // the success route resolves it into the order status page.\n successUrl: `${origin}/checkout/success`,\n cancelUrl,\n };\n\n const canopyApiUrl = `${buildBaseUrl(env.CANOPY_API_HOST)}/api/v1/stripe-checkout/create-session`;\n const response = await fetch(canopyApiUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n throw new BuyNowCartCreationError({\n message: \"Failed to create Stripe checkout session for buy now\",\n additionalData: {\n status: response.status,\n statusText: response.statusText,\n },\n });\n }\n\n const payload: unknown = await response.json();\n const outOfStock = z\n .object({\n result: z.literal(\"outOfStock\"),\n variants: z.array(\n z.object({\n variantId: z.string(),\n available: z.number().int().min(0),\n }),\n ),\n })\n .safeParse(payload);\n if (outOfStock.success) {\n return { result: \"outOfStock\", variants: outOfStock.data.variants };\n }\n const created = z.object({ url: z.string() }).safeParse(payload);\n if (!created.success) {\n throw new BuyNowCartCreationError({\n message: \"Stripe checkout session response missing redirect URL\",\n cause: created.error,\n });\n }\n return { result: \"created\", redirectUrl: created.data.url };\n }\n\n // Shopify and other integrations: create cart via canopy-api-backed gateway\n const cart = await createCartAction({\n lines: cartLines,\n skipStoringCart: true,\n discountCodes: discountCodes.length > 0 ? discountCodes : undefined,\n });\n\n if (!cart?.checkoutUrl) {\n throw new BuyNowCartCreationError({\n message: \"Failed to create cart or get checkout URL\",\n additionalData: { lineItems, discountCodes },\n });\n }\n\n const checkoutUrl = new URL(cart.checkoutUrl, origin);\n return { result: \"created\", redirectUrl: checkoutUrl.toString() };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a `buyNow` call did. Out of stock is an expected storefront outcome
|
|
3
|
+
* (a buyer can race the last unit), so it rides the return value rather than
|
|
4
|
+
* a thrown error; `variants` carries each short variant with the purchasable
|
|
5
|
+
* remainder so the button can render a sold-out state.
|
|
6
|
+
*/
|
|
7
|
+
export type BuyNowOutcome = {
|
|
8
|
+
result: "redirected";
|
|
9
|
+
} | {
|
|
10
|
+
result: "outOfStock";
|
|
11
|
+
variants: {
|
|
12
|
+
variantId: string;
|
|
13
|
+
available: number;
|
|
14
|
+
}[];
|
|
15
|
+
};
|
|
1
16
|
export interface VariantBuyNowInfo {
|
|
2
17
|
variantId: string;
|
|
3
18
|
quantity?: number;
|
|
@@ -15,13 +30,14 @@ export interface VariantBuyNowInfo {
|
|
|
15
30
|
*
|
|
16
31
|
* function MyComponent({ selectedVariant, selectedSellingPlan, quantity }) {
|
|
17
32
|
* const { buyNow } = useBuyNow();
|
|
33
|
+
* const [isSoldOut, setIsSoldOut] = React.useState(false);
|
|
18
34
|
*
|
|
19
35
|
* return (
|
|
20
36
|
* <Button
|
|
21
|
-
* disabled={!selectedVariant?.availableForSale}
|
|
37
|
+
* disabled={!selectedVariant?.availableForSale || isSoldOut}
|
|
22
38
|
* onClick={async () => {
|
|
23
39
|
* if (selectedVariant) {
|
|
24
|
-
* await buyNow(
|
|
40
|
+
* const outcome = await buyNow(
|
|
25
41
|
* [
|
|
26
42
|
* {
|
|
27
43
|
* variantId: selectedVariant.id,
|
|
@@ -31,10 +47,13 @@ export interface VariantBuyNowInfo {
|
|
|
31
47
|
* ],
|
|
32
48
|
* { discountCodes: ["SUMMER2024"] }
|
|
33
49
|
* );
|
|
50
|
+
* if (outcome.result === "outOfStock") {
|
|
51
|
+
* setIsSoldOut(true);
|
|
52
|
+
* }
|
|
34
53
|
* }
|
|
35
54
|
* }}
|
|
36
55
|
* >
|
|
37
|
-
* Shop Now
|
|
56
|
+
* {isSoldOut ? "Sold out" : "Shop Now"}
|
|
38
57
|
* </Button>
|
|
39
58
|
* );
|
|
40
59
|
* }
|
|
@@ -45,5 +64,5 @@ export interface VariantBuyNowInfo {
|
|
|
45
64
|
export declare function useBuyNow(): {
|
|
46
65
|
buyNow: (variants: VariantBuyNowInfo[], options?: {
|
|
47
66
|
discountCodes?: string[];
|
|
48
|
-
}) => Promise<
|
|
67
|
+
}) => Promise<BuyNowOutcome>;
|
|
49
68
|
};
|
|
@@ -21,7 +21,7 @@ function useBuyNow() {
|
|
|
21
21
|
message: "No variants provided"
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
|
-
const
|
|
24
|
+
const buyNowResult = await buyNowAction({
|
|
25
25
|
lineItems: variants.map((variant) => ({
|
|
26
26
|
variantId: variant.variantId,
|
|
27
27
|
quantity: variant.quantity ?? 1,
|
|
@@ -31,7 +31,10 @@ function useBuyNow() {
|
|
|
31
31
|
discountCodes: options?.discountCodes ?? [],
|
|
32
32
|
pageUrl: window.location.href
|
|
33
33
|
});
|
|
34
|
-
|
|
34
|
+
if (buyNowResult.result === "outOfStock") {
|
|
35
|
+
return { result: "outOfStock", variants: buyNowResult.variants };
|
|
36
|
+
}
|
|
37
|
+
const url = new URL(buyNowResult.redirectUrl);
|
|
35
38
|
const isStripeCheckout = url.hostname.endsWith("stripe.com");
|
|
36
39
|
if (!isStripeCheckout) {
|
|
37
40
|
const currentParams = new URLSearchParams(window.location.search);
|
|
@@ -52,6 +55,7 @@ function useBuyNow() {
|
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
navigateExternal(url.toString());
|
|
58
|
+
return { result: "redirected" };
|
|
55
59
|
},
|
|
56
60
|
[reploSessionString]
|
|
57
61
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../cart/hooks/use-buy-now.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Use useBuyNow() hook to create \"Buy Now\" buttons that skip the cart and go directly to checkout.\n * Calls a server action to create the cart/checkout session, then redirects the browser.\n * @module\n */\n\"use client\";\n\nimport React from \"react\";\n\nimport { useClientValue } from \"../../hooks/use-client-value\";\nimport { CanopyError } from \"../../lib/canopy-error\";\nimport { navigateExternal } from \"../../lib/external-navigation\";\nimport { buyNowAction } from \"../buy-now-action\";\n\n// Analytics constants - defined locally to avoid dependency on analytics-client package\nconst REPLO_LOCAL_STORAGE_SESSION_KEY = \"_replo_sid\";\nconst REPLO_SESSION_ID_KEY = \"rsid\";\nconst REPLO_CLIENT_ID_KEY = \"rclid\";\n\n// Analytics session data type - defined locally to avoid dependency\ninterface AnalyticsSessionData {\n sessionId: string;\n reploId: string;\n expiresAt: number;\n createdAt: number;\n conversionsCounter: number;\n isEntryPage: boolean;\n}\n\nclass ProductHasNoVariantsError extends CanopyError {}\n\nexport interface VariantBuyNowInfo {\n variantId: string;\n quantity?: number;\n sellingPlanId?: string | null;\n}\n\n/**\n * React hook that returns a `buyNow` function for express checkout flows.\n * Handles creating the cart/checkout session server-side and redirecting\n * the browser to checkout.\n *\n * @example\n *\n * ```tsx\n * import { useBuyNow } from \"@replohq/sdk/cart/hooks/use-buy-now\";\n *\n * function MyComponent({ selectedVariant, selectedSellingPlan, quantity }) {\n * const { buyNow } = useBuyNow();\n *\n * return (\n * <Button\n * disabled={!selectedVariant?.availableForSale}\n * onClick={async () => {\n * if (selectedVariant) {\n * await buyNow(\n * [\n * {\n * variantId: selectedVariant.id,\n * quantity,\n * sellingPlanId: selectedSellingPlan?.id,\n * },\n * ],\n * { discountCodes: [\"SUMMER2024\"] }\n * );\n * }\n * }}\n * >\n * Shop Now\n * </Button>\n * );\n * }\n * ```\n *\n * @returns Object containing the buyNow function\n */\nexport function useBuyNow() {\n const reploSessionString = useClientValue(\n () => localStorage.getItem(REPLO_LOCAL_STORAGE_SESSION_KEY),\n null,\n );\n\n const buyNow = React.useCallback(\n async (\n variants: VariantBuyNowInfo[],\n options?: { discountCodes?: string[] },\n ) => {\n if (!variants || variants.length === 0) {\n throw new ProductHasNoVariantsError({\n message: \"No variants provided\",\n });\n }\n\n const
|
|
5
|
-
"mappings": ";AAOA,OAAO,WAAW;AAElB,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAG7B,MAAM,kCAAkC;AACxC,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAY5B,MAAM,kCAAkC,YAAY;AAAC;
|
|
4
|
+
"sourcesContent": ["/**\n * Use useBuyNow() hook to create \"Buy Now\" buttons that skip the cart and go directly to checkout.\n * Calls a server action to create the cart/checkout session, then redirects the browser.\n * @module\n */\n\"use client\";\n\nimport React from \"react\";\n\nimport { useClientValue } from \"../../hooks/use-client-value\";\nimport { CanopyError } from \"../../lib/canopy-error\";\nimport { navigateExternal } from \"../../lib/external-navigation\";\nimport { buyNowAction } from \"../buy-now-action\";\n\n// Analytics constants - defined locally to avoid dependency on analytics-client package\nconst REPLO_LOCAL_STORAGE_SESSION_KEY = \"_replo_sid\";\nconst REPLO_SESSION_ID_KEY = \"rsid\";\nconst REPLO_CLIENT_ID_KEY = \"rclid\";\n\n// Analytics session data type - defined locally to avoid dependency\ninterface AnalyticsSessionData {\n sessionId: string;\n reploId: string;\n expiresAt: number;\n createdAt: number;\n conversionsCounter: number;\n isEntryPage: boolean;\n}\n\nclass ProductHasNoVariantsError extends CanopyError {}\n\n/**\n * What a `buyNow` call did. Out of stock is an expected storefront outcome\n * (a buyer can race the last unit), so it rides the return value rather than\n * a thrown error; `variants` carries each short variant with the purchasable\n * remainder so the button can render a sold-out state.\n */\nexport type BuyNowOutcome =\n | { result: \"redirected\" }\n | {\n result: \"outOfStock\";\n variants: { variantId: string; available: number }[];\n };\n\nexport interface VariantBuyNowInfo {\n variantId: string;\n quantity?: number;\n sellingPlanId?: string | null;\n}\n\n/**\n * React hook that returns a `buyNow` function for express checkout flows.\n * Handles creating the cart/checkout session server-side and redirecting\n * the browser to checkout.\n *\n * @example\n *\n * ```tsx\n * import { useBuyNow } from \"@replohq/sdk/cart/hooks/use-buy-now\";\n *\n * function MyComponent({ selectedVariant, selectedSellingPlan, quantity }) {\n * const { buyNow } = useBuyNow();\n * const [isSoldOut, setIsSoldOut] = React.useState(false);\n *\n * return (\n * <Button\n * disabled={!selectedVariant?.availableForSale || isSoldOut}\n * onClick={async () => {\n * if (selectedVariant) {\n * const outcome = await buyNow(\n * [\n * {\n * variantId: selectedVariant.id,\n * quantity,\n * sellingPlanId: selectedSellingPlan?.id,\n * },\n * ],\n * { discountCodes: [\"SUMMER2024\"] }\n * );\n * if (outcome.result === \"outOfStock\") {\n * setIsSoldOut(true);\n * }\n * }\n * }}\n * >\n * {isSoldOut ? \"Sold out\" : \"Shop Now\"}\n * </Button>\n * );\n * }\n * ```\n *\n * @returns Object containing the buyNow function\n */\nexport function useBuyNow() {\n const reploSessionString = useClientValue(\n () => localStorage.getItem(REPLO_LOCAL_STORAGE_SESSION_KEY),\n null,\n );\n\n const buyNow = React.useCallback(\n async (\n variants: VariantBuyNowInfo[],\n options?: { discountCodes?: string[] },\n ): Promise<BuyNowOutcome> => {\n if (!variants || variants.length === 0) {\n throw new ProductHasNoVariantsError({\n message: \"No variants provided\",\n });\n }\n\n const buyNowResult = await buyNowAction({\n lineItems: variants.map((variant) => ({\n variantId: variant.variantId,\n quantity: variant.quantity ?? 1,\n sellingPlanId: variant.sellingPlanId ?? null,\n properties: [],\n })),\n discountCodes: options?.discountCodes ?? [],\n pageUrl: window.location.href,\n });\n if (buyNowResult.result === \"outOfStock\") {\n return { result: \"outOfStock\", variants: buyNowResult.variants };\n }\n\n const url = new URL(buyNowResult.redirectUrl);\n\n // Only append tracking params for non-Stripe checkout URLs (e.g. Shopify).\n // Stripe checkout URLs should not be modified with extra query params.\n const isStripeCheckout = url.hostname.endsWith(\"stripe.com\");\n if (!isStripeCheckout) {\n const currentParams = new URLSearchParams(window.location.search);\n for (const [key, value] of currentParams.entries()) {\n if (!url.searchParams.has(key)) {\n url.searchParams.set(key, value);\n }\n }\n\n if (reploSessionString) {\n try {\n const sessionData: AnalyticsSessionData =\n JSON.parse(reploSessionString);\n if (sessionData.sessionId && sessionData.reploId) {\n url.searchParams.set(REPLO_SESSION_ID_KEY, sessionData.sessionId);\n url.searchParams.set(REPLO_CLIENT_ID_KEY, sessionData.reploId);\n }\n } catch {\n // Ignore invalid session data\n }\n }\n }\n\n navigateExternal(url.toString());\n return { result: \"redirected\" };\n },\n [reploSessionString],\n );\n\n return { buyNow };\n}\n"],
|
|
5
|
+
"mappings": ";AAOA,OAAO,WAAW;AAElB,SAAS,sBAAsB;AAC/B,SAAS,mBAAmB;AAC5B,SAAS,wBAAwB;AACjC,SAAS,oBAAoB;AAG7B,MAAM,kCAAkC;AACxC,MAAM,uBAAuB;AAC7B,MAAM,sBAAsB;AAY5B,MAAM,kCAAkC,YAAY;AAAC;AAgE9C,SAAS,YAAY;AAC1B,QAAM,qBAAqB;AAAA,IACzB,MAAM,aAAa,QAAQ,+BAA+B;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,SAAS,MAAM;AAAA,IACnB,OACE,UACA,YAC2B;AAC3B,UAAI,CAAC,YAAY,SAAS,WAAW,GAAG;AACtC,cAAM,IAAI,0BAA0B;AAAA,UAClC,SAAS;AAAA,QACX,CAAC;AAAA,MACH;AAEA,YAAM,eAAe,MAAM,aAAa;AAAA,QACtC,WAAW,SAAS,IAAI,CAAC,aAAa;AAAA,UACpC,WAAW,QAAQ;AAAA,UACnB,UAAU,QAAQ,YAAY;AAAA,UAC9B,eAAe,QAAQ,iBAAiB;AAAA,UACxC,YAAY,CAAC;AAAA,QACf,EAAE;AAAA,QACF,eAAe,SAAS,iBAAiB,CAAC;AAAA,QAC1C,SAAS,OAAO,SAAS;AAAA,MAC3B,CAAC;AACD,UAAI,aAAa,WAAW,cAAc;AACxC,eAAO,EAAE,QAAQ,cAAc,UAAU,aAAa,SAAS;AAAA,MACjE;AAEA,YAAM,MAAM,IAAI,IAAI,aAAa,WAAW;AAI5C,YAAM,mBAAmB,IAAI,SAAS,SAAS,YAAY;AAC3D,UAAI,CAAC,kBAAkB;AACrB,cAAM,gBAAgB,IAAI,gBAAgB,OAAO,SAAS,MAAM;AAChE,mBAAW,CAAC,KAAK,KAAK,KAAK,cAAc,QAAQ,GAAG;AAClD,cAAI,CAAC,IAAI,aAAa,IAAI,GAAG,GAAG;AAC9B,gBAAI,aAAa,IAAI,KAAK,KAAK;AAAA,UACjC;AAAA,QACF;AAEA,YAAI,oBAAoB;AACtB,cAAI;AACF,kBAAM,cACJ,KAAK,MAAM,kBAAkB;AAC/B,gBAAI,YAAY,aAAa,YAAY,SAAS;AAChD,kBAAI,aAAa,IAAI,sBAAsB,YAAY,SAAS;AAChE,kBAAI,aAAa,IAAI,qBAAqB,YAAY,OAAO;AAAA,YAC/D;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAEA,uBAAiB,IAAI,SAAS,CAAC;AAC/B,aAAO,EAAE,QAAQ,aAAa;AAAA,IAChC;AAAA,IACA,CAAC,kBAAkB;AAAA,EACrB;AAEA,SAAO,EAAE,OAAO;AAClB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function getReturnUrls({ host, forwardedHost, forwardedProto, requestOrigin, pageUrl, }: {
|
|
2
|
+
host: string;
|
|
3
|
+
forwardedHost: string | null;
|
|
4
|
+
forwardedProto: string | null;
|
|
5
|
+
requestOrigin: string | null;
|
|
6
|
+
pageUrl: string;
|
|
7
|
+
}): {
|
|
8
|
+
origin: string;
|
|
9
|
+
cancelUrl: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function firstHeaderValue(value) {
|
|
2
|
+
const firstValue = value?.split(",")[0]?.trim();
|
|
3
|
+
return firstValue ? firstValue : null;
|
|
4
|
+
}
|
|
5
|
+
function parseOrigin(value) {
|
|
6
|
+
if (!value) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
return new URL(value).origin;
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function getReturnUrls({
|
|
16
|
+
host,
|
|
17
|
+
forwardedHost,
|
|
18
|
+
forwardedProto,
|
|
19
|
+
requestOrigin,
|
|
20
|
+
pageUrl
|
|
21
|
+
}) {
|
|
22
|
+
const protocol = firstHeaderValue(forwardedProto) === "http" ? "http" : "https";
|
|
23
|
+
const proxyHost = firstHeaderValue(forwardedHost) ?? host;
|
|
24
|
+
const fallbackOrigin = `${protocol}://${proxyHost}`;
|
|
25
|
+
const trustedOrigins = /* @__PURE__ */ new Set([
|
|
26
|
+
fallbackOrigin,
|
|
27
|
+
parseOrigin(firstHeaderValue(requestOrigin))
|
|
28
|
+
]);
|
|
29
|
+
try {
|
|
30
|
+
const parsedPageUrl = new URL(pageUrl);
|
|
31
|
+
if (trustedOrigins.has(parsedPageUrl.origin)) {
|
|
32
|
+
return {
|
|
33
|
+
origin: parsedPageUrl.origin,
|
|
34
|
+
cancelUrl: parsedPageUrl.toString()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return { origin: fallbackOrigin, cancelUrl: fallbackOrigin };
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
getReturnUrls
|
|
43
|
+
};
|
|
44
|
+
//# sourceMappingURL=return-urls.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../cart/return-urls.ts"],
|
|
4
|
+
"sourcesContent": ["function firstHeaderValue(value: string | null) {\n const firstValue = value?.split(\",\")[0]?.trim();\n return firstValue ? firstValue : null;\n}\n\nfunction parseOrigin(value: string | null) {\n if (!value) {\n return null;\n }\n\n try {\n return new URL(value).origin;\n } catch {\n return null;\n }\n}\n\nexport function getReturnUrls({\n host,\n forwardedHost,\n forwardedProto,\n requestOrigin,\n pageUrl,\n}: {\n host: string;\n forwardedHost: string | null;\n forwardedProto: string | null;\n requestOrigin: string | null;\n pageUrl: string;\n}) {\n const protocol =\n firstHeaderValue(forwardedProto) === \"http\" ? \"http\" : \"https\";\n const proxyHost = firstHeaderValue(forwardedHost) ?? host;\n const fallbackOrigin = `${protocol}://${proxyHost}`;\n const trustedOrigins = new Set([\n fallbackOrigin,\n parseOrigin(firstHeaderValue(requestOrigin)),\n ]);\n\n try {\n const parsedPageUrl = new URL(pageUrl);\n if (trustedOrigins.has(parsedPageUrl.origin)) {\n return {\n origin: parsedPageUrl.origin,\n cancelUrl: parsedPageUrl.toString(),\n };\n }\n } catch {\n // The server-derived fallback below remains the only trusted destination.\n }\n\n return { origin: fallbackOrigin, cancelUrl: fallbackOrigin };\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,iBAAiB,OAAsB;AAC9C,QAAM,aAAa,OAAO,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAC9C,SAAO,aAAa,aAAa;AACnC;AAEA,SAAS,YAAY,OAAsB;AACzC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,EAAE;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,WACJ,iBAAiB,cAAc,MAAM,SAAS,SAAS;AACzD,QAAM,YAAY,iBAAiB,aAAa,KAAK;AACrD,QAAM,iBAAiB,GAAG,QAAQ,MAAM,SAAS;AACjD,QAAM,iBAAiB,oBAAI,IAAI;AAAA,IAC7B;AAAA,IACA,YAAY,iBAAiB,aAAa,CAAC;AAAA,EAC7C,CAAC;AAED,MAAI;AACF,UAAM,gBAAgB,IAAI,IAAI,OAAO;AACrC,QAAI,eAAe,IAAI,cAAc,MAAM,GAAG;AAC5C,aAAO;AAAA,QACL,QAAQ,cAAc;AAAA,QACtB,WAAW,cAAc,SAAS;AAAA,MACpC;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AAEA,SAAO,EAAE,QAAQ,gBAAgB,WAAW,eAAe;AAC7D;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
host: string;
|
|
3
|
-
forwardedHost: string | null;
|
|
4
|
-
forwardedProto: string | null;
|
|
5
|
-
requestOrigin: string | null;
|
|
6
|
-
pageUrl: string;
|
|
7
|
-
}): {
|
|
8
|
-
origin: string;
|
|
9
|
-
cancelUrl: string;
|
|
10
|
-
};
|
|
1
|
+
export { getReturnUrls as getStorefrontUrls } from "./return-urls";
|
package/cart/storefront-urls.js
CHANGED
|
@@ -1,44 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
const firstValue = value?.split(",")[0]?.trim();
|
|
3
|
-
return firstValue ? firstValue : null;
|
|
4
|
-
}
|
|
5
|
-
function parseOrigin(value) {
|
|
6
|
-
if (!value) {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
try {
|
|
10
|
-
return new URL(value).origin;
|
|
11
|
-
} catch {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
function getStorefrontUrls({
|
|
16
|
-
host,
|
|
17
|
-
forwardedHost,
|
|
18
|
-
forwardedProto,
|
|
19
|
-
requestOrigin,
|
|
20
|
-
pageUrl
|
|
21
|
-
}) {
|
|
22
|
-
const protocol = firstHeaderValue(forwardedProto) === "http" ? "http" : "https";
|
|
23
|
-
const proxyHost = firstHeaderValue(forwardedHost) ?? host;
|
|
24
|
-
const fallbackOrigin = `${protocol}://${proxyHost}`;
|
|
25
|
-
const trustedOrigins = /* @__PURE__ */ new Set([
|
|
26
|
-
fallbackOrigin,
|
|
27
|
-
parseOrigin(firstHeaderValue(requestOrigin))
|
|
28
|
-
]);
|
|
29
|
-
try {
|
|
30
|
-
const parsedPageUrl = new URL(pageUrl);
|
|
31
|
-
if (trustedOrigins.has(parsedPageUrl.origin)) {
|
|
32
|
-
return {
|
|
33
|
-
origin: parsedPageUrl.origin,
|
|
34
|
-
cancelUrl: parsedPageUrl.toString()
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
} catch {
|
|
38
|
-
}
|
|
39
|
-
return { origin: fallbackOrigin, cancelUrl: fallbackOrigin };
|
|
40
|
-
}
|
|
1
|
+
import { getReturnUrls } from "./return-urls";
|
|
41
2
|
export {
|
|
42
|
-
getStorefrontUrls
|
|
3
|
+
getReturnUrls as getStorefrontUrls
|
|
43
4
|
};
|
|
44
5
|
//# sourceMappingURL=storefront-urls.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../cart/storefront-urls.ts"],
|
|
4
|
-
"sourcesContent": ["
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["// Compat alias: sites scaffolded before the return-urls rename import\n// `@replohq/sdk/cart/storefront-urls` and their source never updates, so this\n// re-export must stay published for as long as those sites are live.\n// eslint-disable-next-line replo/no-export-from -- compat alias entrypoint: deployed site code imports this path and only a re-export can keep it resolving.\nexport { getReturnUrls as getStorefrontUrls } from \"./return-urls\";\n"],
|
|
5
|
+
"mappings": "AAIA,SAA0B,qBAAyB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-08-
|
|
5
|
+
"commit": "ac7b59160587bd6c8937d220fd31468a604b4a14",
|
|
6
|
+
"builtAt": "2026-08-30T20:30:37.919Z"
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "1.0.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-08-
|
|
8
|
+
"commit": "ac7b59160587bd6c8937d220fd31468a604b4a14",
|
|
9
|
+
"builtAt": "2026-08-30T20:30:37.919Z"
|
|
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",
|
|
@@ -65,6 +65,10 @@
|
|
|
65
65
|
"types": "./cart/gateways/cart-constants.d.ts",
|
|
66
66
|
"default": "./cart/gateways/cart-constants.js"
|
|
67
67
|
},
|
|
68
|
+
"./cart/return-urls": {
|
|
69
|
+
"types": "./cart/return-urls.d.ts",
|
|
70
|
+
"default": "./cart/return-urls.js"
|
|
71
|
+
},
|
|
68
72
|
"./money": {
|
|
69
73
|
"types": "./money.d.ts",
|
|
70
74
|
"default": "./money.js"
|
|
@@ -241,6 +245,10 @@
|
|
|
241
245
|
"types": "./_vendor/schemas/loaderKeys.d.ts",
|
|
242
246
|
"default": "./_vendor/schemas/loaderKeys.mjs"
|
|
243
247
|
},
|
|
248
|
+
"./cart/storefront-urls": {
|
|
249
|
+
"types": "./cart/storefront-urls.d.ts",
|
|
250
|
+
"default": "./cart/storefront-urls.js"
|
|
251
|
+
},
|
|
244
252
|
"./package.json": "./package.json"
|
|
245
253
|
},
|
|
246
254
|
"dependencies": {
|