@replohq/sdk 2.5.0 → 2.7.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/json.mjs +4 -4
- package/_vendor/replo-utils/lib/misc.mjs +726 -4
- package/_vendor/replo-utils/lib/object.mjs +3 -3
- package/_vendor/schemas/contentCollection.d.ts +2 -2
- package/_vendor/schemas/contentEntry.mjs +3 -3
- package/_vendor/schemas/contentManifest.mjs +6 -6
- package/_vendor/schemas/routing/locale.d.ts +1 -1
- package/_vendor/schemas/routing/locale.mjs +0 -5
- package/_vendor/schemas/routing/rules.d.ts +1 -1
- package/_vendor/schemas/routing/rules.mjs +0 -5
- package/analytics/pixel-meta.d.ts +14 -0
- package/analytics/pixel-meta.js +28 -0
- package/analytics/replo-first-party-pixel.d.ts +5 -4
- package/analytics/replo-first-party-pixel.js +7 -3
- package/analytics/replo-pixel-from-meta.d.ts +7 -0
- package/analytics/replo-pixel-from-meta.js +27 -0
- package/analytics/replo-pixel-script.d.ts +3 -1
- package/analytics/replo-pixel-script.js +4 -2
- package/cart/buy-now-action.d.ts +2 -13
- package/cart/buy-now-action.js +11 -75
- package/cart/checkout-actions.d.ts +34 -0
- package/cart/checkout-actions.js +27 -0
- package/cart/checkout-appearance.d.ts +35 -0
- package/cart/checkout-appearance.js +131 -0
- package/cart/checkout-session-actions.d.ts +20 -0
- package/cart/checkout-session-actions.js +12 -0
- package/cart/checkout.d.ts +110 -0
- package/cart/checkout.js +420 -0
- package/cart/start-stripe-checkout.d.ts +40 -0
- package/cart/start-stripe-checkout.js +75 -0
- package/{chunk-TTEBDCWT.mjs → chunk-4YTXURVN.mjs} +2 -2
- package/{chunk-O4QBR4QC.mjs → chunk-DMJ5VUD6.mjs} +2 -2
- package/{chunk-OPKMOW43.mjs → chunk-GNIXARC7.mjs} +1 -1
- package/{chunk-XWWFRIXV.mjs → chunk-N4WC2Z43.mjs} +1 -1
- package/{chunk-PSYP2DYM.mjs → chunk-QYGRVXWQ.mjs} +1 -1
- package/env.d.ts +1 -0
- package/env.js +1 -0
- package/gateways/canopy-api.d.ts +97 -0
- package/gateways/canopy-api.js +120 -0
- package/lib/buildMetadata.js +3 -3
- package/package.json +14 -4
- package/_vendor/replo-utils/lib/errors.d.ts +0 -84
- package/_vendor/schemas/errors.d.ts +0 -450
- package/_vendor/schemas/generated/errors.d.ts +0 -64
- package/chunk-XGXJOHUY.mjs +0 -724
- package/chunk-XSMYSAKY.mjs +0 -78
- /package/{chunk-ZPDXVFCR.mjs → chunk-JBCOJHZN.mjs} +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AddCrossSellOutcome, CheckoutAddress, CheckoutUpdateOutcome } from "../gateways/canopy-api";
|
|
2
|
+
/**
|
|
3
|
+
* Re-quotes an embedded Stripe checkout's shipping for the buyer's address.
|
|
4
|
+
* Called by `<StripePaymentForm />`; the project comes from the site's own
|
|
5
|
+
* config, never the browser.
|
|
6
|
+
*/
|
|
7
|
+
export declare function updateCheckoutShippingAction(input: {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
address: CheckoutAddress;
|
|
10
|
+
}): Promise<CheckoutUpdateOutcome>;
|
|
11
|
+
/**
|
|
12
|
+
* Adds one unit of an offered product to an embedded Stripe checkout and
|
|
13
|
+
* re-quotes its shipping for the new cart, for the address shipping was last
|
|
14
|
+
* quoted for. Returns what to offer next. Called by
|
|
15
|
+
* `useCheckoutSession().addCrossSell`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function addCrossSellAction(input: {
|
|
18
|
+
sessionId: string;
|
|
19
|
+
variantId: string;
|
|
20
|
+
}): Promise<AddCrossSellOutcome>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
import { getCanopyApiGateway } from "../gateways/canopy-api";
|
|
3
|
+
async function updateCheckoutShippingAction(input) {
|
|
4
|
+
return await (await getCanopyApiGateway()).updateCheckoutShipping(input);
|
|
5
|
+
}
|
|
6
|
+
async function addCrossSellAction(input) {
|
|
7
|
+
return await (await getCanopyApiGateway()).addCrossSell(input);
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
addCrossSellAction,
|
|
11
|
+
updateCheckoutShippingAction
|
|
12
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/** One line of the order, with Stripe's preformatted amounts. */
|
|
3
|
+
export interface CheckoutLineItem {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
imageUrl: string | null;
|
|
8
|
+
/** Line total, e.g. "$58.00". */
|
|
9
|
+
total: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The live order behind the form. Amounts are formatted by Stripe in the
|
|
13
|
+
* session's currency and follow every shipping re-quote and promo code.
|
|
14
|
+
*/
|
|
15
|
+
export interface CheckoutSummary {
|
|
16
|
+
/** ISO code, e.g. "USD". */
|
|
17
|
+
currencyCode: string;
|
|
18
|
+
lineItems: CheckoutLineItem[];
|
|
19
|
+
subtotal: string;
|
|
20
|
+
discount: {
|
|
21
|
+
amount: string;
|
|
22
|
+
name: string;
|
|
23
|
+
code: string | null;
|
|
24
|
+
} | null;
|
|
25
|
+
/** Null when nothing in the order ships. */
|
|
26
|
+
shipping: {
|
|
27
|
+
name: string | null;
|
|
28
|
+
amount: string;
|
|
29
|
+
/** e.g. "3–5 business days". */
|
|
30
|
+
deliveryEstimate: string | null;
|
|
31
|
+
/**
|
|
32
|
+
* False until the buyer completes an address: Stripe prices the first
|
|
33
|
+
* option up front, and the cost can change once the address is quoted.
|
|
34
|
+
*/
|
|
35
|
+
isQuoted: boolean;
|
|
36
|
+
} | null;
|
|
37
|
+
tax: {
|
|
38
|
+
status: "ready";
|
|
39
|
+
amount: string;
|
|
40
|
+
} | {
|
|
41
|
+
status: "pending";
|
|
42
|
+
};
|
|
43
|
+
total: string;
|
|
44
|
+
}
|
|
45
|
+
/** A product offered in the summary, with its price formatted. */
|
|
46
|
+
export interface CheckoutCrossSell {
|
|
47
|
+
variantId: string;
|
|
48
|
+
title: string;
|
|
49
|
+
imageUrl: string | null;
|
|
50
|
+
/** e.g. "$38.00". */
|
|
51
|
+
price: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What `useCheckoutSession()` returns. `retry` opens a fresh checkout from
|
|
55
|
+
* the cart, which also recovers an expired or already-paid session.
|
|
56
|
+
*/
|
|
57
|
+
export type CheckoutState = {
|
|
58
|
+
status: "loading";
|
|
59
|
+
} | {
|
|
60
|
+
status: "empty";
|
|
61
|
+
} | {
|
|
62
|
+
status: "error";
|
|
63
|
+
message: string;
|
|
64
|
+
retry: () => void;
|
|
65
|
+
} | {
|
|
66
|
+
status: "ready";
|
|
67
|
+
summary: CheckoutSummary;
|
|
68
|
+
/** True while the shipping cost is being re-quoted for a new address. */
|
|
69
|
+
isQuotingShipping: boolean;
|
|
70
|
+
/** Why the last address, add or payment didn't go through, for the buyer. */
|
|
71
|
+
message: string | null;
|
|
72
|
+
/** Up to three products to add, none already in the order. */
|
|
73
|
+
crossSells: CheckoutCrossSell[];
|
|
74
|
+
/**
|
|
75
|
+
* Adds one of `crossSells` to the order and re-quotes shipping for it;
|
|
76
|
+
* the summary and Stripe's form update when it lands. Does nothing
|
|
77
|
+
* while shipping is being re-quoted or another item is being added.
|
|
78
|
+
*/
|
|
79
|
+
addCrossSell: (variantId: string) => Promise<void>;
|
|
80
|
+
/** The cross-sell being added, while it is. */
|
|
81
|
+
addingCrossSellVariantId: string | null;
|
|
82
|
+
};
|
|
83
|
+
/** The live checkout session, inside `<CheckoutSessionProvider>`. */
|
|
84
|
+
export declare function useCheckoutSession(): CheckoutState;
|
|
85
|
+
/**
|
|
86
|
+
* Loads the checkout session `createCheckoutSessionAction` opened and shares
|
|
87
|
+
* it with `<StripePaymentForm />` and `useCheckoutSession()` below it. The
|
|
88
|
+
* form takes its colors, corner radius and font from the site's design tokens
|
|
89
|
+
* (`--primary`, `--background`, `--foreground`, `--border`, `--radius`, ...).
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
*
|
|
93
|
+
* ```tsx
|
|
94
|
+
* <CheckoutSessionProvider startCheckoutPath="/checkout">
|
|
95
|
+
* <OrderSummary />
|
|
96
|
+
* <StripePaymentForm />
|
|
97
|
+
* </CheckoutSessionProvider>
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function CheckoutSessionProvider({ startCheckoutPath, children, }: React.PropsWithChildren<{
|
|
101
|
+
/** The route that starts checkout from the cart (usually "/checkout"); Retry opens it. */
|
|
102
|
+
startCheckoutPath: string;
|
|
103
|
+
}>): import("react/jsx-runtime").JSX.Element;
|
|
104
|
+
/**
|
|
105
|
+
* Stripe's contact, shipping-address and payment form, inside
|
|
106
|
+
* `<CheckoutSessionProvider>`. Each completed address is re-quoted on the
|
|
107
|
+
* server, so print-on-demand items show their real shipping cost before the
|
|
108
|
+
* buyer pays. Renders nothing until the session has loaded.
|
|
109
|
+
*/
|
|
110
|
+
export declare function StripePaymentForm(): import("react/jsx-runtime").JSX.Element | null;
|
package/cart/checkout.js
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
CheckoutForm,
|
|
6
|
+
CheckoutFormProvider,
|
|
7
|
+
useCheckoutForm
|
|
8
|
+
} from "@stripe/react-stripe-js/checkout";
|
|
9
|
+
import { loadStripe } from "@stripe/stripe-js";
|
|
10
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
|
+
import { parseJsonOrNull } from "../_vendor/replo-utils/lib/json.mjs";
|
|
12
|
+
import * as v from "valibot";
|
|
13
|
+
import { CanopyError } from "../lib/canopy-error";
|
|
14
|
+
import { formatAmount } from "../money";
|
|
15
|
+
import { getCrossSellsAction } from "./checkout-actions";
|
|
16
|
+
import { readSiteCheckoutAppearance } from "./checkout-appearance";
|
|
17
|
+
import {
|
|
18
|
+
addCrossSellAction,
|
|
19
|
+
updateCheckoutShippingAction
|
|
20
|
+
} from "./checkout-session-actions";
|
|
21
|
+
class CheckoutUpdateRejectedError extends CanopyError {
|
|
22
|
+
}
|
|
23
|
+
const checkoutConfigSchema = v.object({
|
|
24
|
+
clientSecret: v.pipe(v.string(), v.nonEmpty()),
|
|
25
|
+
publishableKey: v.pipe(v.string(), v.nonEmpty()),
|
|
26
|
+
stripeAccountId: v.pipe(v.string(), v.nonEmpty()),
|
|
27
|
+
hasLiveShipping: v.pipe(
|
|
28
|
+
v.nullable(v.string()),
|
|
29
|
+
v.transform((value) => value === "1")
|
|
30
|
+
)
|
|
31
|
+
});
|
|
32
|
+
const quotedAddressSchema = v.object({
|
|
33
|
+
sessionId: v.string(),
|
|
34
|
+
address: v.object({
|
|
35
|
+
country: v.string(),
|
|
36
|
+
line1: v.string(),
|
|
37
|
+
line2: v.nullable(v.string()),
|
|
38
|
+
city: v.nullable(v.string()),
|
|
39
|
+
state: v.nullable(v.string()),
|
|
40
|
+
postalCode: v.nullable(v.string())
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
const CONFIG_STORAGE_KEY = "replo-checkout";
|
|
44
|
+
const QUOTED_ADDRESS_STORAGE_KEY = "replo-checkout-quoted-address";
|
|
45
|
+
function readStorage(key) {
|
|
46
|
+
try {
|
|
47
|
+
return sessionStorage.getItem(key);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn("[Replo] Session storage is unavailable", error);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function writeStorage(key, value) {
|
|
54
|
+
try {
|
|
55
|
+
if (value === null) {
|
|
56
|
+
sessionStorage.removeItem(key);
|
|
57
|
+
} else {
|
|
58
|
+
sessionStorage.setItem(key, value);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn("[Replo] Session storage is unavailable", error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function readQuotedAddress(sessionId) {
|
|
65
|
+
const stored = v.safeParse(
|
|
66
|
+
quotedAddressSchema,
|
|
67
|
+
parseJsonOrNull(readStorage(QUOTED_ADDRESS_STORAGE_KEY) ?? "")
|
|
68
|
+
);
|
|
69
|
+
return stored.success && stored.output.sessionId === sessionId ? stored.output.address : null;
|
|
70
|
+
}
|
|
71
|
+
function readCheckoutConfig() {
|
|
72
|
+
const fragment = window.location.hash.slice(1);
|
|
73
|
+
const isEmpty = new URLSearchParams(fragment).get("empty") === "1";
|
|
74
|
+
if (isEmpty) {
|
|
75
|
+
writeStorage(CONFIG_STORAGE_KEY, null);
|
|
76
|
+
} else if (fragment) {
|
|
77
|
+
writeStorage(CONFIG_STORAGE_KEY, fragment);
|
|
78
|
+
}
|
|
79
|
+
if (fragment) {
|
|
80
|
+
window.history.replaceState(
|
|
81
|
+
window.history.state,
|
|
82
|
+
"",
|
|
83
|
+
window.location.pathname + window.location.search
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (isEmpty) {
|
|
87
|
+
return "empty";
|
|
88
|
+
}
|
|
89
|
+
const params = new URLSearchParams(
|
|
90
|
+
fragment || (readStorage(CONFIG_STORAGE_KEY) ?? "")
|
|
91
|
+
);
|
|
92
|
+
const parsed = v.safeParse(checkoutConfigSchema, {
|
|
93
|
+
clientSecret: params.get("clientSecret"),
|
|
94
|
+
publishableKey: params.get("publishableKey"),
|
|
95
|
+
stripeAccountId: params.get("stripeAccountId"),
|
|
96
|
+
hasLiveShipping: params.get("hasLiveShipping")
|
|
97
|
+
});
|
|
98
|
+
return parsed.success ? parsed.output : "missing";
|
|
99
|
+
}
|
|
100
|
+
let loadedCheckout = null;
|
|
101
|
+
function getLoadedCheckout() {
|
|
102
|
+
const fragment = window.location.hash;
|
|
103
|
+
if (!loadedCheckout || fragment && fragment !== loadedCheckout.fragment) {
|
|
104
|
+
const config = readCheckoutConfig();
|
|
105
|
+
loadedCheckout = {
|
|
106
|
+
fragment,
|
|
107
|
+
value: {
|
|
108
|
+
config,
|
|
109
|
+
stripe: typeof config === "string" ? null : loadStripe(config.publishableKey, {
|
|
110
|
+
stripeAccount: config.stripeAccountId
|
|
111
|
+
}),
|
|
112
|
+
...readSiteCheckoutAppearance()
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return loadedCheckout.value;
|
|
117
|
+
}
|
|
118
|
+
function subscribeToNothing() {
|
|
119
|
+
return () => void 0;
|
|
120
|
+
}
|
|
121
|
+
const CARD_ONLY_FORM_OPTIONS = {
|
|
122
|
+
expressCheckout: {
|
|
123
|
+
paymentMethods: {
|
|
124
|
+
applePay: "never",
|
|
125
|
+
googlePay: "never",
|
|
126
|
+
link: "never",
|
|
127
|
+
amazonPay: "never",
|
|
128
|
+
paypal: "never",
|
|
129
|
+
klarna: "never"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const CheckoutSessionContext = React.createContext({ state: { status: "loading" }, form: null });
|
|
134
|
+
function useCheckoutSession() {
|
|
135
|
+
return React.useContext(CheckoutSessionContext).state;
|
|
136
|
+
}
|
|
137
|
+
function formatEstimate(estimate) {
|
|
138
|
+
const minimum = estimate?.minimum ?? null;
|
|
139
|
+
const maximum = estimate?.maximum ?? null;
|
|
140
|
+
const unit = maximum?.unit ?? minimum?.unit;
|
|
141
|
+
if (!unit) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const label = unit === "business_day" ? "business day" : unit;
|
|
145
|
+
const range = minimum && maximum && minimum.value !== maximum.value ? `${minimum.value}\u2013${maximum.value}` : String(maximum?.value ?? minimum?.value);
|
|
146
|
+
const plural = (maximum?.value ?? minimum?.value) === 1 ? "" : "s";
|
|
147
|
+
return `${range} ${label}${plural}`;
|
|
148
|
+
}
|
|
149
|
+
function toSummary(session, isQuoted) {
|
|
150
|
+
const discount = session.discountAmounts?.[0] ?? null;
|
|
151
|
+
const shippingOption = session.shipping?.shippingOption ?? null;
|
|
152
|
+
return {
|
|
153
|
+
currencyCode: session.currency.toUpperCase(),
|
|
154
|
+
lineItems: session.lineItems.map((lineItem) => ({
|
|
155
|
+
id: lineItem.id,
|
|
156
|
+
name: lineItem.name,
|
|
157
|
+
quantity: lineItem.quantity,
|
|
158
|
+
imageUrl: lineItem.images[0] ?? null,
|
|
159
|
+
total: lineItem.subtotal.amount
|
|
160
|
+
})),
|
|
161
|
+
subtotal: session.total.subtotal.amount,
|
|
162
|
+
discount: session.total.discount.minorUnitsAmount > 0 ? {
|
|
163
|
+
amount: `-${session.total.discount.amount}`,
|
|
164
|
+
name: discount?.displayName ?? "Discount",
|
|
165
|
+
code: discount?.promotionCode ?? null
|
|
166
|
+
} : null,
|
|
167
|
+
shipping: shippingOption ? {
|
|
168
|
+
name: shippingOption.displayName,
|
|
169
|
+
amount: session.total.shippingRate.amount,
|
|
170
|
+
deliveryEstimate: formatEstimate(shippingOption.deliveryEstimate),
|
|
171
|
+
isQuoted
|
|
172
|
+
} : null,
|
|
173
|
+
tax: session.tax.status === "ready" ? { status: "ready", amount: session.total.taxExclusive.amount } : { status: "pending" },
|
|
174
|
+
total: session.total.total.amount
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function CheckoutSessionBridge({
|
|
178
|
+
sessionId,
|
|
179
|
+
hasLiveShipping,
|
|
180
|
+
retry,
|
|
181
|
+
children
|
|
182
|
+
}) {
|
|
183
|
+
const checkoutState = useCheckoutForm();
|
|
184
|
+
const queryClient = useQueryClient();
|
|
185
|
+
const checkout = checkoutState.type === "success" ? checkoutState.checkout : null;
|
|
186
|
+
const [quotedAddress, setQuotedAddress] = React.useState(() => {
|
|
187
|
+
return readQuotedAddress(sessionId);
|
|
188
|
+
});
|
|
189
|
+
const [formAddress, setFormAddress] = React.useState(quotedAddress);
|
|
190
|
+
const [message, setMessage] = React.useState(null);
|
|
191
|
+
const crossSellsQueryKey = ["replo-checkout-cross-sells", sessionId];
|
|
192
|
+
const { data: crossSellProducts } = useQuery({
|
|
193
|
+
queryKey: crossSellsQueryKey,
|
|
194
|
+
queryFn: () => {
|
|
195
|
+
return getCrossSellsAction({ sessionId, excludeVariantIds: [] });
|
|
196
|
+
},
|
|
197
|
+
enabled: checkout !== null,
|
|
198
|
+
staleTime: Number.POSITIVE_INFINITY
|
|
199
|
+
});
|
|
200
|
+
const runServerUpdate = async (update) => {
|
|
201
|
+
if (!checkout) {
|
|
202
|
+
throw new CheckoutUpdateRejectedError({
|
|
203
|
+
message: "Checkout isn't ready"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const response = {
|
|
207
|
+
accepted: null,
|
|
208
|
+
rejection: null
|
|
209
|
+
};
|
|
210
|
+
const result = await checkout.runServerUpdate(async () => {
|
|
211
|
+
const outcome = await update();
|
|
212
|
+
if (outcome.type === "reject") {
|
|
213
|
+
response.rejection = outcome.message;
|
|
214
|
+
throw new CheckoutUpdateRejectedError({ message: outcome.message });
|
|
215
|
+
}
|
|
216
|
+
response.accepted = outcome;
|
|
217
|
+
return outcome;
|
|
218
|
+
});
|
|
219
|
+
if (result.type === "error") {
|
|
220
|
+
throw new CheckoutUpdateRejectedError({
|
|
221
|
+
message: response.rejection ?? result.error.message
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
if (!response.accepted) {
|
|
225
|
+
throw new CheckoutUpdateRejectedError({
|
|
226
|
+
message: "Checkout didn't update"
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return response.accepted;
|
|
230
|
+
};
|
|
231
|
+
const shippingQuote = useMutation({
|
|
232
|
+
mutationFn: async (address) => {
|
|
233
|
+
return await runServerUpdate(() => {
|
|
234
|
+
return updateCheckoutShippingAction({ sessionId, address });
|
|
235
|
+
});
|
|
236
|
+
},
|
|
237
|
+
onSuccess: (_outcome, address) => {
|
|
238
|
+
setQuotedAddress(address);
|
|
239
|
+
writeStorage(
|
|
240
|
+
QUOTED_ADDRESS_STORAGE_KEY,
|
|
241
|
+
JSON.stringify({ sessionId, address })
|
|
242
|
+
);
|
|
243
|
+
setMessage(null);
|
|
244
|
+
},
|
|
245
|
+
onError: (error) => setMessage(error.message)
|
|
246
|
+
});
|
|
247
|
+
const crossSellAdd = useMutation({
|
|
248
|
+
mutationFn: async (variantId) => {
|
|
249
|
+
return await runServerUpdate(() => {
|
|
250
|
+
return addCrossSellAction({ sessionId, variantId });
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
onSuccess: (outcome) => {
|
|
254
|
+
queryClient.setQueryData(crossSellsQueryKey, outcome.products);
|
|
255
|
+
setMessage(null);
|
|
256
|
+
},
|
|
257
|
+
onError: (error) => setMessage(error.message)
|
|
258
|
+
});
|
|
259
|
+
const handleChange = async (event) => {
|
|
260
|
+
const address = event.value.shippingAddress?.address;
|
|
261
|
+
const country = address?.country;
|
|
262
|
+
const line1 = address?.line1;
|
|
263
|
+
const shippingAddress = country && line1 ? {
|
|
264
|
+
country: country.toUpperCase(),
|
|
265
|
+
line1,
|
|
266
|
+
line2: address.line2 || null,
|
|
267
|
+
city: address.city || null,
|
|
268
|
+
state: address.state || null,
|
|
269
|
+
postalCode: address.postal_code || null
|
|
270
|
+
} : null;
|
|
271
|
+
setFormAddress(shippingAddress);
|
|
272
|
+
if (!checkout || !shippingAddress || !event.status.shippingAddress?.complete) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const key = JSON.stringify(shippingAddress);
|
|
276
|
+
if (key === JSON.stringify(quotedAddress) || shippingQuote.isPending && key === JSON.stringify(shippingQuote.variables)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
await shippingQuote.mutateAsync(shippingAddress).catch(() => void 0);
|
|
280
|
+
};
|
|
281
|
+
const handleConfirm = async (event) => {
|
|
282
|
+
if (!checkout) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const result = await checkout.confirm({ formConfirmEvent: event });
|
|
286
|
+
if (result.type === "error") {
|
|
287
|
+
setMessage(result.error.message);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
const state = (() => {
|
|
291
|
+
if (checkout) {
|
|
292
|
+
return {
|
|
293
|
+
status: "ready",
|
|
294
|
+
summary: toSummary(
|
|
295
|
+
checkout,
|
|
296
|
+
formAddress !== null && JSON.stringify(formAddress) === JSON.stringify(quotedAddress)
|
|
297
|
+
),
|
|
298
|
+
isQuotingShipping: shippingQuote.isPending,
|
|
299
|
+
message,
|
|
300
|
+
crossSells: (crossSellProducts ?? []).map((product) => ({
|
|
301
|
+
variantId: product.variantId,
|
|
302
|
+
title: product.title,
|
|
303
|
+
imageUrl: product.imageUrl,
|
|
304
|
+
price: formatAmount({
|
|
305
|
+
amount: product.unitPriceMinor,
|
|
306
|
+
currencyCode: product.currencyCode
|
|
307
|
+
})
|
|
308
|
+
})),
|
|
309
|
+
addCrossSell: async (variantId) => {
|
|
310
|
+
if (shippingQuote.isPending || crossSellAdd.isPending) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
await crossSellAdd.mutateAsync(variantId).catch(() => void 0);
|
|
314
|
+
},
|
|
315
|
+
addingCrossSellVariantId: crossSellAdd.isPending ? crossSellAdd.variables : null
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (checkoutState.type === "error") {
|
|
319
|
+
return { status: "error", message: checkoutState.error.message, retry };
|
|
320
|
+
}
|
|
321
|
+
return { status: "loading" };
|
|
322
|
+
})();
|
|
323
|
+
return /* @__PURE__ */ jsx(
|
|
324
|
+
CheckoutSessionContext.Provider,
|
|
325
|
+
{
|
|
326
|
+
value: {
|
|
327
|
+
state,
|
|
328
|
+
form: {
|
|
329
|
+
hasLiveShipping,
|
|
330
|
+
onChange: handleChange,
|
|
331
|
+
onConfirm: handleConfirm
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
children
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
function CheckoutSessionProvider({
|
|
339
|
+
startCheckoutPath,
|
|
340
|
+
children
|
|
341
|
+
}) {
|
|
342
|
+
const loaded = React.useSyncExternalStore(
|
|
343
|
+
subscribeToNothing,
|
|
344
|
+
getLoadedCheckout,
|
|
345
|
+
() => null
|
|
346
|
+
);
|
|
347
|
+
const retry = () => {
|
|
348
|
+
window.location.assign(startCheckoutPath);
|
|
349
|
+
};
|
|
350
|
+
if (!loaded || typeof loaded.config === "string" || !loaded.stripe) {
|
|
351
|
+
const state = loaded?.config === "empty" ? { status: "empty" } : {
|
|
352
|
+
status: "error",
|
|
353
|
+
message: "This checkout link is incomplete. Return to your cart and check out again.",
|
|
354
|
+
retry
|
|
355
|
+
};
|
|
356
|
+
return /* @__PURE__ */ jsx(
|
|
357
|
+
CheckoutSessionContext.Provider,
|
|
358
|
+
{
|
|
359
|
+
value: { state: loaded ? state : { status: "loading" }, form: null },
|
|
360
|
+
children
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const config = loaded.config;
|
|
365
|
+
const sessionId = config.clientSecret.split("_secret_")[0] ?? "";
|
|
366
|
+
const quotedAddress = readQuotedAddress(sessionId);
|
|
367
|
+
return /* @__PURE__ */ jsx(
|
|
368
|
+
CheckoutFormProvider,
|
|
369
|
+
{
|
|
370
|
+
stripe: loaded.stripe,
|
|
371
|
+
options: {
|
|
372
|
+
clientSecret: config.clientSecret,
|
|
373
|
+
appearance: loaded.appearance,
|
|
374
|
+
fonts: loaded.fonts,
|
|
375
|
+
...quotedAddress ? {
|
|
376
|
+
defaultValues: {
|
|
377
|
+
shippingAddress: {
|
|
378
|
+
address: {
|
|
379
|
+
country: quotedAddress.country,
|
|
380
|
+
line1: quotedAddress.line1,
|
|
381
|
+
line2: quotedAddress.line2,
|
|
382
|
+
city: quotedAddress.city,
|
|
383
|
+
state: quotedAddress.state,
|
|
384
|
+
postal_code: quotedAddress.postalCode
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
} : {}
|
|
389
|
+
},
|
|
390
|
+
children: /* @__PURE__ */ jsx(
|
|
391
|
+
CheckoutSessionBridge,
|
|
392
|
+
{
|
|
393
|
+
sessionId,
|
|
394
|
+
hasLiveShipping: config.hasLiveShipping,
|
|
395
|
+
retry,
|
|
396
|
+
children
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
function StripePaymentForm() {
|
|
403
|
+
const { state, form } = React.useContext(CheckoutSessionContext);
|
|
404
|
+
if (!form || state.status !== "ready") {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
return /* @__PURE__ */ jsx(
|
|
408
|
+
CheckoutForm,
|
|
409
|
+
{
|
|
410
|
+
options: form.hasLiveShipping ? CARD_ONLY_FORM_OPTIONS : void 0,
|
|
411
|
+
onChange: form.onChange,
|
|
412
|
+
onConfirm: form.onConfirm
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
export {
|
|
417
|
+
CheckoutSessionProvider,
|
|
418
|
+
StripePaymentForm,
|
|
419
|
+
useCheckoutSession
|
|
420
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CartLineAttribute } from "./cart-types";
|
|
2
|
+
import { CanopyError } from "../lib/canopy-error";
|
|
3
|
+
export declare class StripeCheckoutError extends CanopyError {
|
|
4
|
+
}
|
|
5
|
+
export type StripeCheckoutOutcome = {
|
|
6
|
+
result: "created";
|
|
7
|
+
redirectUrl: string;
|
|
8
|
+
} | {
|
|
9
|
+
result: "restartCheckout";
|
|
10
|
+
message: string;
|
|
11
|
+
} | {
|
|
12
|
+
result: "outOfStock";
|
|
13
|
+
variants: {
|
|
14
|
+
variantId: string;
|
|
15
|
+
available: number;
|
|
16
|
+
}[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Opens a Stripe Checkout session for a cart: Stripe's hosted page, or the
|
|
20
|
+
* site's own `paymentPagePath` page (built on `<CheckoutSessionProvider>`) with
|
|
21
|
+
* the session in the URL fragment. Canopy answers an embedded request with a
|
|
22
|
+
* hosted session where it can't serve embedded checkout.
|
|
23
|
+
*/
|
|
24
|
+
export declare function startStripeCheckout({ checkoutAttemptId, lineItems, discountCodes, pageUrl, ui, }: {
|
|
25
|
+
checkoutAttemptId: string | null;
|
|
26
|
+
lineItems: {
|
|
27
|
+
variantId: string;
|
|
28
|
+
quantity: number;
|
|
29
|
+
sellingPlanId: string | null;
|
|
30
|
+
properties: CartLineAttribute[];
|
|
31
|
+
}[];
|
|
32
|
+
discountCodes: string[];
|
|
33
|
+
pageUrl: string;
|
|
34
|
+
ui: {
|
|
35
|
+
mode: "hosted";
|
|
36
|
+
} | {
|
|
37
|
+
mode: "form";
|
|
38
|
+
paymentPagePath: string;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<StripeCheckoutOutcome>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { headers } from "next/headers";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { getCanopyApiGateway } from "../gateways/canopy-api";
|
|
4
|
+
import { CanopyError } from "../lib/canopy-error";
|
|
5
|
+
import { REPLO_ATTRIBUTION_PROPERTY } from "./cart-types";
|
|
6
|
+
import { getReturnUrls } from "./return-urls";
|
|
7
|
+
class StripeCheckoutError extends CanopyError {
|
|
8
|
+
}
|
|
9
|
+
async function startStripeCheckout({
|
|
10
|
+
checkoutAttemptId,
|
|
11
|
+
lineItems,
|
|
12
|
+
discountCodes,
|
|
13
|
+
pageUrl,
|
|
14
|
+
ui
|
|
15
|
+
}) {
|
|
16
|
+
const headersList = await headers();
|
|
17
|
+
const host = headersList.get("host");
|
|
18
|
+
if (!host) {
|
|
19
|
+
throw new StripeCheckoutError({ message: "Missing host header" });
|
|
20
|
+
}
|
|
21
|
+
const { origin, cancelUrl } = getReturnUrls({
|
|
22
|
+
host,
|
|
23
|
+
forwardedHost: headersList.get("x-forwarded-host"),
|
|
24
|
+
forwardedProto: headersList.get("x-forwarded-proto"),
|
|
25
|
+
requestOrigin: headersList.get("origin"),
|
|
26
|
+
pageUrl
|
|
27
|
+
});
|
|
28
|
+
const checkoutPage = ui.mode === "form" ? new URL(ui.paymentPagePath, origin) : null;
|
|
29
|
+
if (checkoutPage && checkoutPage.origin !== origin) {
|
|
30
|
+
throw new StripeCheckoutError({
|
|
31
|
+
message: "paymentPagePath must be a path on this site",
|
|
32
|
+
additionalData: { paymentPagePath: checkoutPage.toString() }
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (discountCodes.length > 0 || lineItems.some((lineItem) => Boolean(lineItem.sellingPlanId))) {
|
|
36
|
+
console.warn(
|
|
37
|
+
"[Replo] Stripe buy now does not support selling plans or discount codes; those options were ignored."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const outcome = await (await getCanopyApiGateway()).createStripeCheckoutSession({
|
|
41
|
+
checkoutAttemptId: checkoutAttemptId ?? uuidv4(),
|
|
42
|
+
lines: lineItems.map((item) => ({
|
|
43
|
+
variantId: item.variantId,
|
|
44
|
+
quantity: item.quantity,
|
|
45
|
+
properties: item.properties.some((property) => {
|
|
46
|
+
return property.key === REPLO_ATTRIBUTION_PROPERTY.key;
|
|
47
|
+
}) ? item.properties : [REPLO_ATTRIBUTION_PROPERTY, ...item.properties]
|
|
48
|
+
})),
|
|
49
|
+
successUrl: `${origin}/checkout/success`,
|
|
50
|
+
cancelUrl,
|
|
51
|
+
uiMode: ui.mode
|
|
52
|
+
});
|
|
53
|
+
if (outcome.result !== "created") {
|
|
54
|
+
return outcome;
|
|
55
|
+
}
|
|
56
|
+
const session = outcome.session;
|
|
57
|
+
if (checkoutPage && session.form) {
|
|
58
|
+
const { hasLiveShipping, ...config } = session.form;
|
|
59
|
+
checkoutPage.hash = new URLSearchParams({
|
|
60
|
+
...config,
|
|
61
|
+
...hasLiveShipping ? { hasLiveShipping: "1" } : {}
|
|
62
|
+
}).toString();
|
|
63
|
+
return { result: "created", redirectUrl: checkoutPage.toString() };
|
|
64
|
+
}
|
|
65
|
+
if (!session.url) {
|
|
66
|
+
throw new StripeCheckoutError({
|
|
67
|
+
message: "Stripe checkout session response has no page to open"
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return { result: "created", redirectUrl: session.url };
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
StripeCheckoutError,
|
|
74
|
+
startStripeCheckout
|
|
75
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CONTENT_SEO_VALUE_KEYS, CONTENT_RELATED_VALUE_STORAGE_KEY, CONTENT_PRIVATE_VALUE_KEYS, LEGACY_CONTENT_ENTRY_STATUSES, isContentPrivateValueKey, getContentFieldValueSchema } from './chunk-RQ76VK3L.mjs';
|
|
2
|
-
import { MapCache_default, getAllKeysIn_default, identity_default, isIndex_default, assignValue_default, keys_default, Stack_default, isLength_default, isArguments_default, getTag_default, isBuffer_default, isTypedArray_default, eq_default, Uint8Array_default, getAllKeys_default } from './chunk-
|
|
3
|
-
import { Symbol_default, isArray_default, isObject_default, isObjectLike_default, baseGetTag_default } from './chunk-
|
|
2
|
+
import { MapCache_default, getAllKeysIn_default, identity_default, isIndex_default, assignValue_default, keys_default, Stack_default, isLength_default, isArguments_default, getTag_default, isBuffer_default, isTypedArray_default, eq_default, Uint8Array_default, getAllKeys_default } from './chunk-N4WC2Z43.mjs';
|
|
3
|
+
import { Symbol_default, isArray_default, isObject_default, isObjectLike_default, baseGetTag_default } from './chunk-JBCOJHZN.mjs';
|
|
4
4
|
import { stringify, parseDocument } from 'yaml';
|
|
5
5
|
import * as z2 from 'zod';
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isString_default, isNumber_default } from './chunk-
|
|
2
|
-
import { isObject_default } from './chunk-
|
|
1
|
+
import { isString_default, isNumber_default } from './chunk-GNIXARC7.mjs';
|
|
2
|
+
import { isObject_default } from './chunk-JBCOJHZN.mjs';
|
|
3
3
|
|
|
4
4
|
var has = Object.prototype.hasOwnProperty;
|
|
5
5
|
function dequal(foo, bar) {
|