@replohq/sdk 2.5.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/_vendor/replo-utils/lib/json.mjs +4 -4
  2. package/_vendor/replo-utils/lib/misc.mjs +726 -4
  3. package/_vendor/replo-utils/lib/object.mjs +3 -3
  4. package/_vendor/schemas/contentCollection.d.ts +2 -2
  5. package/_vendor/schemas/contentEntry.mjs +3 -3
  6. package/_vendor/schemas/contentManifest.mjs +6 -6
  7. package/_vendor/schemas/loaderKeys.mjs +2 -0
  8. package/_vendor/schemas/routing/locale.d.ts +1 -1
  9. package/_vendor/schemas/routing/locale.mjs +0 -5
  10. package/_vendor/schemas/routing/rules.d.ts +1 -1
  11. package/_vendor/schemas/routing/rules.mjs +0 -5
  12. package/analytics/pixel-meta.d.ts +14 -0
  13. package/analytics/pixel-meta.js +28 -0
  14. package/analytics/replo-first-party-pixel.d.ts +5 -4
  15. package/analytics/replo-first-party-pixel.js +7 -3
  16. package/analytics/replo-pixel-from-meta.d.ts +7 -0
  17. package/analytics/replo-pixel-from-meta.js +27 -0
  18. package/analytics/replo-pixel-script.d.ts +3 -1
  19. package/analytics/replo-pixel-script.js +4 -2
  20. package/cart/buy-now-action.d.ts +2 -13
  21. package/cart/buy-now-action.js +11 -75
  22. package/cart/checkout-actions.d.ts +34 -0
  23. package/cart/checkout-actions.js +27 -0
  24. package/cart/checkout-appearance.d.ts +35 -0
  25. package/cart/checkout-appearance.js +131 -0
  26. package/cart/checkout-session-actions.d.ts +20 -0
  27. package/cart/checkout-session-actions.js +12 -0
  28. package/cart/checkout.d.ts +110 -0
  29. package/cart/checkout.js +420 -0
  30. package/cart/start-stripe-checkout.d.ts +40 -0
  31. package/cart/start-stripe-checkout.js +75 -0
  32. package/{chunk-TTEBDCWT.mjs → chunk-4YTXURVN.mjs} +2 -2
  33. package/{chunk-O4QBR4QC.mjs → chunk-DMJ5VUD6.mjs} +2 -2
  34. package/{chunk-OPKMOW43.mjs → chunk-GNIXARC7.mjs} +1 -1
  35. package/{chunk-XWWFRIXV.mjs → chunk-N4WC2Z43.mjs} +1 -1
  36. package/{chunk-PSYP2DYM.mjs → chunk-QYGRVXWQ.mjs} +1 -1
  37. package/env.d.ts +1 -0
  38. package/env.js +1 -0
  39. package/gateways/canopy-api.d.ts +97 -0
  40. package/gateways/canopy-api.js +120 -0
  41. package/lib/buildMetadata.js +3 -3
  42. package/loaders/junip-review-aggregate-loader.d.ts +12 -0
  43. package/loaders/junip-review-aggregate-loader.js +53 -0
  44. package/loaders/junip-reviews-loader.d.ts +13 -0
  45. package/loaders/junip-reviews-loader.js +58 -0
  46. package/loaders/loader-keys.d.ts +2 -0
  47. package/loaders/loader-keys.js +2 -0
  48. package/loaders/loader-schemas.d.ts +27 -0
  49. package/loaders/loader-schemas.js +21 -0
  50. package/package.json +22 -4
  51. package/_vendor/replo-utils/lib/errors.d.ts +0 -84
  52. package/_vendor/schemas/errors.d.ts +0 -450
  53. package/_vendor/schemas/generated/errors.d.ts +0 -64
  54. package/chunk-XGXJOHUY.mjs +0 -724
  55. package/chunk-XSMYSAKY.mjs +0 -78
  56. /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;
@@ -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-XWWFRIXV.mjs';
3
- import { Symbol_default, isArray_default, isObject_default, isObjectLike_default, baseGetTag_default } from './chunk-ZPDXVFCR.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-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-OPKMOW43.mjs';
2
- import { isObject_default } from './chunk-ZPDXVFCR.mjs';
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) {