@asteby/storefront-react 0.11.1 → 0.13.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/dist/checkout.js CHANGED
@@ -1,13 +1,355 @@
1
- import { brandName, BrandMark } from './chunk-ODIQR6TG.js';
2
- export { BrandMark, brandKey, brandLogo, brandName, findBrand, listBrands } from './chunk-ODIQR6TG.js';
1
+ import { brandName, BrandMark, brandKey } from './chunk-5WERSSU5.js';
2
+ export { BrandMark, brandKey, brandLogo, brandName, findBrand, listBrands } from './chunk-5WERSSU5.js';
3
3
  import { useResolvedLocale } from './chunk-T52H5HL3.js';
4
4
  import './chunk-DQBL2DFJ.js';
5
- import { useMemo, useSyncExternalStore } from 'react';
5
+ import { CheckoutField } from './chunk-DFGLCIEG.js';
6
+ export { CheckoutField, ErrorSummary, fieldMessages, useFieldErrors, validationMessage } from './chunk-DFGLCIEG.js';
7
+ import { useId, useState, useRef, useEffect, useMemo, useSyncExternalStore } from 'react';
8
+ import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
9
  import { formatMoney } from '@asteby/storefront';
7
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
8
10
 
9
- // src/checkout/delivery.ts
10
11
  var ES = {
12
+ email: "Correo",
13
+ phone: "Tel\xE9fono",
14
+ first_name: "Nombre",
15
+ last_name: "Apellidos",
16
+ zip: "C\xF3digo postal",
17
+ province: "Estado",
18
+ city: "Ciudad o municipio",
19
+ neighborhood: "Colonia",
20
+ address1: "Calle y n\xFAmero",
21
+ address2: "N\xFAmero interior o departamento",
22
+ reference: "Referencias para la entrega",
23
+ provincePlaceholder: "Elige tu estado",
24
+ address1Hint: "Por ejemplo: Av. Insurgentes Sur 1234",
25
+ referenceHint: "Entre qu\xE9 calles, color de la fachada\u2026",
26
+ emailHint: "Te enviamos aqu\xED la confirmaci\xF3n y el seguimiento.",
27
+ phoneHint: "Solo para la paqueter\xEDa, por si necesita contactarte.",
28
+ lookupFilled: "Completamos estado y ciudad con tu c\xF3digo postal."
29
+ };
30
+ var EN = {
31
+ email: "Email",
32
+ phone: "Phone",
33
+ first_name: "First name",
34
+ last_name: "Last name",
35
+ zip: "Postal code",
36
+ province: "State",
37
+ city: "City",
38
+ neighborhood: "Neighborhood",
39
+ address1: "Street and number",
40
+ address2: "Apartment, suite",
41
+ reference: "Delivery notes",
42
+ provincePlaceholder: "Choose your state",
43
+ address1Hint: "For example: 1234 Main St",
44
+ referenceHint: "Cross streets, building color\u2026",
45
+ emailHint: "We send the confirmation and tracking here.",
46
+ phoneHint: "Only for the carrier, in case they need to reach you.",
47
+ lookupFilled: "We filled in state and city from your postal code."
48
+ };
49
+ function addressFieldsMessages(locale, overrides) {
50
+ const base = (locale ?? "es-MX").toLowerCase().startsWith("en") ? EN : ES;
51
+ return overrides ? { ...base, ...overrides } : base;
52
+ }
53
+ var join = (...c) => c.filter(Boolean).join(" ") || void 0;
54
+ var field = (cx, span) => ({ ...cx, root: join(cx.root, cx[span]) });
55
+ function ContactFields({ defaults = {}, names = {}, errors = {}, phone, hints = true, locale, messages, fieldMessages: fm, classNames: cx = {} }) {
56
+ const m = addressFieldsMessages(locale, messages);
57
+ const n = (k) => names[k] ?? k;
58
+ const common = { ...locale ? { locale } : {}, ...fm ? { messages: fm } : {} };
59
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
60
+ /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("email"), label: m.email, type: "email", autoComplete: "email", inputMode: "email", autoCapitalize: "none", spellCheck: false, defaultValue: defaults.email ?? "", error: errors[n("email")] ?? null, hint: hints ? m.emailHint : void 0, classNames: field(cx, "wide") }),
61
+ /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("phone"), label: m.phone, type: "tel", autoComplete: "tel", inputMode: "tel", defaultValue: defaults.phone ?? "", error: errors[n("phone")] ?? null, hint: hints ? m.phoneHint : void 0, classNames: field(cx, "wide"), ...phone?.pattern ? { pattern: phone.pattern } : {}, ...phone?.title ? { title: phone.title } : {}, ...phone?.minLength ? { minLength: phone.minLength } : {} }),
62
+ /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("first_name"), label: m.first_name, autoComplete: "given-name", defaultValue: defaults.first_name ?? "", error: errors[n("first_name")] ?? null, classNames: field(cx, "half") }),
63
+ /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("last_name"), label: m.last_name, autoComplete: "family-name", defaultValue: defaults.last_name ?? "", error: errors[n("last_name")] ?? null, classNames: field(cx, "half") })
64
+ ] });
65
+ }
66
+ function AddressFields(props) {
67
+ const { defaults, names = {}, errors = {}, countryCode, provinces, postalCode, lookupPostalCode, locale, messages, fieldMessages: fm, classNames: cx = {}, children } = props;
68
+ const showNeighborhood = props.neighborhood ?? true;
69
+ const showReference = props.reference ?? true;
70
+ const m = addressFieldsMessages(locale, messages);
71
+ const n = (k) => names[k] ?? k;
72
+ const common = { ...locale ? { locale } : {}, ...fm ? { messages: fm } : {} };
73
+ const listId = useId();
74
+ const [colonias, setColonias] = useState([]);
75
+ const [filled, setFilled] = useState(false);
76
+ const lastZip = useRef(null);
77
+ const zipRef = useRef(null);
78
+ const lookupRef = useRef(lookupPostalCode);
79
+ lookupRef.current = lookupPostalCode;
80
+ const runLookup = (zip) => {
81
+ const lookup = lookupRef.current;
82
+ if (!lookup || !zip || zip === lastZip.current) return;
83
+ if (postalCode?.pattern && !new RegExp(`^(?:${postalCode.pattern})$`).test(zip)) return;
84
+ lastZip.current = zip;
85
+ const form = zipRef.current?.closest("form");
86
+ void lookup(zip).then(
87
+ (info) => {
88
+ if (!info || lastZip.current !== zip) return;
89
+ const fill2 = (name, value) => {
90
+ const el = form?.elements.namedItem(name);
91
+ if (!el || !value || "length" in el && !(el instanceof HTMLSelectElement)) return false;
92
+ if (el.value && el.dataset.sfAutofilled !== "1") return false;
93
+ if (el instanceof HTMLSelectElement && ![...el.options].some((o) => o.value === value)) return false;
94
+ el.value = value;
95
+ el.dataset.sfAutofilled = "1";
96
+ el.dispatchEvent(new Event("input", { bubbles: true }));
97
+ return true;
98
+ };
99
+ const a = fill2(n("province"), info.province);
100
+ const b = fill2(n("city"), info.city);
101
+ setColonias(info.neighborhoods ?? []);
102
+ setFilled(a || b);
103
+ },
104
+ () => {
105
+ }
106
+ );
107
+ };
108
+ useEffect(() => {
109
+ const zip = defaults?.zip;
110
+ if (zip) runLookup(zip);
111
+ }, []);
112
+ const numericZip = postalCode?.inputMode === "numeric";
113
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
114
+ /* @__PURE__ */ jsx("div", { ref: zipRef, className: join(cx.half), "data-sf-zip": true, children: /* @__PURE__ */ jsx(
115
+ CheckoutField,
116
+ {
117
+ ...common,
118
+ name: n("zip"),
119
+ label: m.zip,
120
+ autoComplete: "postal-code",
121
+ inputMode: postalCode?.inputMode ?? "text",
122
+ ...postalCode?.pattern ? { pattern: postalCode.pattern } : {},
123
+ ...postalCode?.maxLength ? { maxLength: postalCode.maxLength } : {},
124
+ ...postalCode?.title ? { title: postalCode.title } : {},
125
+ ...numericZip ? { autoCapitalize: "none" } : {},
126
+ defaultValue: defaults?.zip ?? "",
127
+ error: errors[n("zip")] ?? null,
128
+ classNames: { ...cx, root: cx.root },
129
+ onInput: (e) => {
130
+ const v = e.currentTarget.value.trim();
131
+ if (postalCode?.maxLength && v.length === postalCode.maxLength) runLookup(v);
132
+ },
133
+ onBlur: (e) => runLookup(e.currentTarget.value.trim())
134
+ }
135
+ ) }),
136
+ provinces && provinces.length ? /* @__PURE__ */ jsxs(CheckoutField, { ...common, as: "select", name: n("province"), label: m.province, autoComplete: "address-level1", defaultValue: defaults?.province ?? "", error: errors[n("province")] ?? null, classNames: field(cx, "half"), children: [
137
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: m.provincePlaceholder }),
138
+ provinces.map((p) => /* @__PURE__ */ jsx("option", { value: p.name, children: p.name }, p.code ?? p.name))
139
+ ] }) : /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("province"), label: m.province, autoComplete: "address-level1", defaultValue: defaults?.province ?? "", error: errors[n("province")] ?? null, classNames: field(cx, "half") }),
140
+ /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("city"), label: m.city, autoComplete: "address-level2", defaultValue: defaults?.city ?? "", error: errors[n("city")] ?? null, classNames: field(cx, "half") }),
141
+ showNeighborhood ? /* @__PURE__ */ jsx(
142
+ CheckoutField,
143
+ {
144
+ ...common,
145
+ name: n("neighborhood"),
146
+ label: m.neighborhood,
147
+ autoComplete: "address-level3",
148
+ optional: showNeighborhood !== "required",
149
+ defaultValue: defaults?.neighborhood ?? "",
150
+ error: errors[n("neighborhood")] ?? null,
151
+ classNames: field(cx, "half"),
152
+ ...colonias.length ? { list: listId } : {}
153
+ }
154
+ ) : null,
155
+ colonias.length ? /* @__PURE__ */ jsx("datalist", { id: listId, children: colonias.map((c) => /* @__PURE__ */ jsx("option", { value: c }, c)) }) : null,
156
+ /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", "data-sf-zip-status": true, style: filled ? void 0 : { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap" }, className: filled ? join(cx.hint, cx.wide) : void 0, children: filled ? m.lookupFilled : "" }),
157
+ /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("address1"), label: m.address1, autoComplete: "address-line1", defaultValue: defaults?.address1 ?? "", error: errors[n("address1")] ?? null, hint: m.address1Hint, classNames: field(cx, "wide") }),
158
+ /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("address2"), label: m.address2, autoComplete: "address-line2", optional: true, defaultValue: defaults?.address2 ?? "", error: errors[n("address2")] ?? null, classNames: field(cx, "half") }),
159
+ showReference ? /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("reference"), label: m.reference, optional: true, defaultValue: defaults?.reference ?? "", error: errors[n("reference")] ?? null, hint: m.referenceHint, classNames: field(cx, "wide"), maxLength: 200 }) : null,
160
+ /* @__PURE__ */ jsx("input", { type: "hidden", name: "country_code", value: countryCode }),
161
+ children
162
+ ] });
163
+ }
164
+ var ES2 = {
165
+ title: "Tu pedido",
166
+ subtotal: "Subtotal",
167
+ discount: "Descuento",
168
+ shipping: "Env\xEDo",
169
+ shippingPending: "Al elegir la entrega",
170
+ updating: "Actualizando\u2026",
171
+ free: "Gratis",
172
+ taxes: "Impuestos",
173
+ taxesIncluded: "Incluye {amount} de impuestos",
174
+ total: "Total",
175
+ quantity: "Cantidad: {n}",
176
+ show: "Ver resumen del pedido",
177
+ hide: "Ocultar resumen del pedido"
178
+ };
179
+ var EN2 = {
180
+ title: "Your order",
181
+ subtotal: "Subtotal",
182
+ discount: "Discount",
183
+ shipping: "Shipping",
184
+ shippingPending: "Calculated at delivery",
185
+ updating: "Updating\u2026",
186
+ free: "Free",
187
+ taxes: "Taxes",
188
+ taxesIncluded: "Includes {amount} in taxes",
189
+ total: "Total",
190
+ quantity: "Quantity: {n}",
191
+ show: "Show order summary",
192
+ hide: "Hide order summary"
193
+ };
194
+ function orderSummaryMessages(locale, overrides) {
195
+ const base = (locale ?? "es-MX").toLowerCase().startsWith("en") ? EN2 : ES2;
196
+ return overrides ? { ...base, ...overrides } : base;
197
+ }
198
+ var isZero = (m) => !!m && Number(m.amount) === 0;
199
+ var positive = (m) => !!m && Number(m.amount) > 0;
200
+ function OrderSummary(props) {
201
+ const { lines, cost, updating = false, renderLineMedia, renderAmount, title, headingLevel = 2, classNames: cx = {}, children } = props;
202
+ const locale = useResolvedLocale(props.locale);
203
+ const m = orderSummaryMessages(locale, props.messages);
204
+ const money = (v) => renderAmount ? renderAmount(v) : formatMoney(v, locale);
205
+ const H = headingLevel === 3 ? "h3" : "h2";
206
+ const discounts = cost.discounts?.filter((d) => positive(d.amount)) ?? [];
207
+ return /* @__PURE__ */ jsxs("div", { className: cx.root, "data-sf-summary": true, children: [
208
+ title === false ? null : /* @__PURE__ */ jsx(H, { className: cx.title, "data-sf-summary-title": true, children: title ?? m.title }),
209
+ /* @__PURE__ */ jsx("ul", { className: cx.lines, "data-sf-summary-lines": true, children: lines.map((l) => {
210
+ const variant = l.merchandise.title && !/^default( title)?$/i.test(l.merchandise.title) ? l.merchandise.title : null;
211
+ return /* @__PURE__ */ jsxs("li", { className: cx.line, "data-sf-summary-line": true, children: [
212
+ renderLineMedia ? /* @__PURE__ */ jsxs("span", { className: cx.media, "data-sf-summary-media": true, children: [
213
+ renderLineMedia(l),
214
+ l.quantity > 1 ? /* @__PURE__ */ jsx("span", { className: cx.qty, "data-sf-summary-qty": true, "aria-hidden": "true", children: l.quantity }) : null
215
+ ] }) : null,
216
+ /* @__PURE__ */ jsxs("span", { "data-sf-summary-line-body": true, children: [
217
+ /* @__PURE__ */ jsxs("span", { className: cx.lineTitle, "data-sf-summary-line-title": true, children: [
218
+ !renderLineMedia && l.quantity > 1 ? `${l.quantity} \xD7 ` : null,
219
+ l.merchandise.product.title
220
+ ] }),
221
+ variant ? /* @__PURE__ */ jsx("span", { className: cx.variant, "data-sf-summary-variant": true, children: variant }) : null,
222
+ renderLineMedia || l.quantity > 1 ? /* @__PURE__ */ jsx("span", { style: { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap" }, children: m.quantity.replace("{n}", String(l.quantity)) }) : null
223
+ ] }),
224
+ /* @__PURE__ */ jsx("span", { className: cx.linePrice, "data-sf-summary-line-price": true, children: money(l.cost.total) })
225
+ ] }, l.id);
226
+ }) }),
227
+ children,
228
+ /* @__PURE__ */ jsxs("dl", { className: cx.totals, "data-sf-summary-totals": true, "aria-busy": updating || void 0, children: [
229
+ /* @__PURE__ */ jsxs("div", { className: cx.row, "data-sf-summary-row": "subtotal", children: [
230
+ /* @__PURE__ */ jsx("dt", { children: m.subtotal }),
231
+ /* @__PURE__ */ jsx("dd", { children: money(cost.subtotal) })
232
+ ] }),
233
+ discounts.length ? discounts.map((d) => /* @__PURE__ */ jsxs("div", { className: cx.row ? `${cx.row} ${cx.discount ?? ""}`.trim() : cx.discount, "data-sf-summary-row": "discount", children: [
234
+ /* @__PURE__ */ jsx("dt", { children: d.code ? `${m.discount} (${d.code})` : d.title || m.discount }),
235
+ /* @__PURE__ */ jsxs("dd", { children: [
236
+ "\u2212",
237
+ money(d.amount)
238
+ ] })
239
+ ] }, d.id)) : positive(cost.discount) ? /* @__PURE__ */ jsxs("div", { className: cx.row ? `${cx.row} ${cx.discount ?? ""}`.trim() : cx.discount, "data-sf-summary-row": "discount", children: [
240
+ /* @__PURE__ */ jsx("dt", { children: m.discount }),
241
+ /* @__PURE__ */ jsxs("dd", { children: [
242
+ "\u2212",
243
+ money(cost.discount)
244
+ ] })
245
+ ] }) : null,
246
+ /* @__PURE__ */ jsxs("div", { className: cx.row, "data-sf-summary-row": "shipping", children: [
247
+ /* @__PURE__ */ jsx("dt", { children: m.shipping }),
248
+ /* @__PURE__ */ jsx("dd", { className: updating || !cost.shipping ? cx.muted : void 0, children: updating ? m.updating : !cost.shipping ? m.shippingPending : isZero(cost.shipping) ? /* @__PURE__ */ jsx("span", { className: cx.free, children: m.free }) : money(cost.shipping) })
249
+ ] }),
250
+ positive(cost.tax) && !cost.tax_included ? /* @__PURE__ */ jsxs("div", { className: cx.row, "data-sf-summary-row": "tax", children: [
251
+ /* @__PURE__ */ jsx("dt", { children: m.taxes }),
252
+ /* @__PURE__ */ jsx("dd", { children: money(cost.tax) })
253
+ ] }) : null,
254
+ /* @__PURE__ */ jsxs("div", { className: cx.total, "data-sf-summary-row": "total", children: [
255
+ /* @__PURE__ */ jsx("dt", { children: m.total }),
256
+ /* @__PURE__ */ jsx("dd", { "aria-live": "polite", "data-updating": updating || void 0, children: money(cost.total) })
257
+ ] })
258
+ ] }),
259
+ positive(cost.tax) && cost.tax_included ? /* @__PURE__ */ jsx("p", { className: cx.note, "data-sf-summary-tax-note": true, children: m.taxesIncluded.split("{amount}").flatMap((part, i) => i === 0 ? [part] : [/* @__PURE__ */ jsx("span", { children: money(cost.tax) }, i), part]) }) : null
260
+ ] });
261
+ }
262
+ function OrderSummaryDisclosure({ total, updating, renderAmount, locale: loc, messages, defaultOpen = false, className, classNames: cx = {}, children }) {
263
+ const locale = useResolvedLocale(loc);
264
+ const m = orderSummaryMessages(locale, messages);
265
+ return /* @__PURE__ */ jsxs("details", { className, "data-sf-summary-disclosure": true, ...defaultOpen ? { open: true } : {}, children: [
266
+ /* @__PURE__ */ jsxs("summary", { className: cx.summary, "data-sf-summary-toggle": true, children: [
267
+ /* @__PURE__ */ jsxs("span", { className: cx.label, "data-sf-summary-toggle-label": true, children: [
268
+ /* @__PURE__ */ jsx("span", { "data-sf-summary-show": true, children: m.show }),
269
+ /* @__PURE__ */ jsx("span", { "data-sf-summary-hide": true, children: m.hide })
270
+ ] }),
271
+ /* @__PURE__ */ jsx("span", { className: cx.total, "data-sf-summary-toggle-total": true, "data-updating": updating || void 0, children: renderAmount ? renderAmount(total) : formatMoney(total, locale) })
272
+ ] }),
273
+ /* @__PURE__ */ jsx("div", { className: cx.panel, "data-sf-summary-panel": true, children })
274
+ ] });
275
+ }
276
+
277
+ // src/checkout/recommend.ts
278
+ var costOf = (r) => {
279
+ const n = Number(r.cost.amount);
280
+ return Number.isFinite(n) ? n : null;
281
+ };
282
+ var daysOf = (r) => r.eta_days_max ?? r.eta_days_min ?? null;
283
+ var norm = (s) => (s ?? "").trim().toLowerCase();
284
+ var carrierKey = (r) => r.carrier ? brandKey(r.carrier) : r.provider ? brandKey(r.provider) : "";
285
+ function dedupeRates(rates) {
286
+ const best = /* @__PURE__ */ new Map();
287
+ const keyOf = (r) => `${carrierKey(r)}|${norm(r.service_code) || norm(r.title)}|${daysOf(r) ?? ""}|${r.cost.currency_code}`;
288
+ for (const r of rates) {
289
+ const k = keyOf(r);
290
+ const prev = best.get(k);
291
+ const c = costOf(r);
292
+ if (!prev || c !== null && (costOf(prev) === null || c < costOf(prev))) best.set(k, r);
293
+ }
294
+ const keep = new Set(best.values());
295
+ return rates.filter((r) => keep.has(r));
296
+ }
297
+ function recommendRates(input, options = {}) {
298
+ const count = Math.max(1, options.count ?? 3);
299
+ const rates = options.dedupe === false ? [...input] : dedupeRates(input);
300
+ const collapseAfter = options.collapseAfter ?? count + 1;
301
+ const sameCurrency = new Set(rates.map((r) => r.cost.currency_code)).size <= 1;
302
+ const byCost = (a, b) => (costOf(a) ?? Infinity) - (costOf(b) ?? Infinity);
303
+ const byDays = (a, b) => (daysOf(a) ?? Infinity) - (daysOf(b) ?? Infinity);
304
+ const tags = /* @__PURE__ */ new Map();
305
+ const tag = (r, t) => tags.set(r, [...tags.get(r) ?? [], t]);
306
+ const cheapest = sameCurrency && rates.length > 1 ? [...rates].sort((a, b) => byCost(a, b) || byDays(a, b))[0] : void 0;
307
+ if (cheapest && costOf(cheapest) !== null) tag(cheapest, "cheapest");
308
+ const fastest = rates.length > 1 ? [...rates].sort((a, b) => byDays(a, b) || byCost(a, b))[0] : void 0;
309
+ if (fastest && daysOf(fastest) !== null && (!cheapest || fastest === cheapest || (daysOf(cheapest) ?? Infinity) > daysOf(fastest))) tag(fastest, "fastest");
310
+ const chosen = [];
311
+ for (const r of [cheapest, fastest]) if (r && tags.has(r) && !chosen.includes(r)) chosen.push(r);
312
+ if (rates.length > collapseAfter) {
313
+ const rank = (sorted, val) => {
314
+ const out = /* @__PURE__ */ new Map();
315
+ let pos = 0;
316
+ let last;
317
+ sorted.forEach((r, i) => {
318
+ const v = val(r);
319
+ if (v !== last) pos = i;
320
+ last = v;
321
+ out.set(r, v === null ? sorted.length : pos);
322
+ });
323
+ return out;
324
+ };
325
+ const priceRank = rank([...rates].sort(byCost), costOf);
326
+ const timeRank = rank([...rates].sort(byDays), daysOf);
327
+ const rest = rates.filter((r) => !chosen.includes(r)).sort((a, b) => priceRank.get(a) + timeRank.get(a) - (priceRank.get(b) + timeRank.get(b)) || byCost(a, b));
328
+ let first = true;
329
+ for (const r of rest) {
330
+ if (chosen.length >= count) break;
331
+ chosen.push(r);
332
+ if (first) tag(r, "best_value");
333
+ first = false;
334
+ }
335
+ } else {
336
+ for (const r of rates) if (!chosen.includes(r)) chosen.push(r);
337
+ }
338
+ const recommended = chosen.sort(byCost).map((rate) => ({ rate, tags: tags.get(rate) ?? [] }));
339
+ const groups = /* @__PURE__ */ new Map();
340
+ for (const r of [...rates].sort(byCost)) {
341
+ if (chosen.includes(r)) continue;
342
+ const key = carrierKey(r);
343
+ const g = groups.get(key) ?? { key, carrier: r.carrier ?? r.provider ?? null, rates: [] };
344
+ g.rates.push(r);
345
+ groups.set(key, g);
346
+ }
347
+ const more = [...groups.values()];
348
+ return { recommended, more, moreCount: more.reduce((n, g) => n + g.rates.length, 0) };
349
+ }
350
+
351
+ // src/checkout/delivery.ts
352
+ var ES3 = {
11
353
  etaOne: "Llega en 1 d\xEDa h\xE1bil",
12
354
  etaMany: "Llega en {n} d\xEDas h\xE1biles",
13
355
  etaRange: "Llega en {min} a {max} d\xEDas h\xE1biles",
@@ -21,11 +363,16 @@ var ES = {
21
363
  quoteTomorrow: "Precios de env\xEDo vigentes hasta ma\xF1ana a las {time}.",
22
364
  quoteLater: "Precios de env\xEDo vigentes hasta el {date}.",
23
365
  quoteSoon: "Los precios de env\xEDo vencen pronto ({time}).",
24
- partial: "No pudimos consultar todas las paqueter\xEDas. Estas son las opciones disponibles ahora.",
366
+ partial: "Algunas paqueter\xEDas no respondieron a tiempo; estas son las opciones disponibles.",
25
367
  loading: "Buscando opciones de env\xEDo\u2026",
26
- requoted: "Las tarifas de env\xEDo cambiaron y ya est\xE1n actualizadas. Revisa tu env\xEDo antes de pagar."
368
+ requoted: "Las tarifas de env\xEDo cambiaron y ya est\xE1n actualizadas. Revisa tu env\xEDo antes de pagar.",
369
+ bestValue: "Recomendado",
370
+ moreOptions: "Ver m\xE1s opciones ({n})",
371
+ otherCarriers: "Otras opciones",
372
+ error: "No pudimos consultar las opciones de env\xEDo en este momento.",
373
+ retry: "Intentar de nuevo"
27
374
  };
28
- var EN = {
375
+ var EN3 = {
29
376
  etaOne: "Arrives in 1 business day",
30
377
  etaMany: "Arrives in {n} business days",
31
378
  etaRange: "Arrives in {min} to {max} business days",
@@ -41,10 +388,15 @@ var EN = {
41
388
  quoteSoon: "Shipping prices expire soon ({time}).",
42
389
  partial: "We couldn't reach every carrier. These are the options available now.",
43
390
  loading: "Looking for delivery options\u2026",
44
- requoted: "Shipping rates changed and are now up to date. Check your delivery before paying."
391
+ requoted: "Shipping rates changed and are now up to date. Check your delivery before paying.",
392
+ bestValue: "Recommended",
393
+ moreOptions: "Show more options ({n})",
394
+ otherCarriers: "Other options",
395
+ error: "We couldn't get delivery options right now.",
396
+ retry: "Try again"
45
397
  };
46
398
  function deliveryMessages(locale, overrides) {
47
- const base = (locale ?? "es-MX").toLowerCase().startsWith("en") ? EN : ES;
399
+ const base = (locale ?? "es-MX").toLowerCase().startsWith("en") ? EN3 : ES3;
48
400
  return overrides ? { ...base, ...overrides } : base;
49
401
  }
50
402
  var fill = (tpl, vars) => tpl.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? "")).replace(/\.\.$/, ".");
@@ -64,7 +416,7 @@ function rateService(rate) {
64
416
  }
65
417
  return title;
66
418
  }
67
- function rateEta(rate, messages = ES) {
419
+ function rateEta(rate, messages = ES3) {
68
420
  const { eta_days_min: min, eta_days_max: max } = rate;
69
421
  if (typeof min === "number" && typeof max === "number") {
70
422
  if (min === max) return min === 1 ? messages.etaOne : fill(messages.etaMany, { n: min });
@@ -74,11 +426,11 @@ function rateEta(rate, messages = ES) {
74
426
  if (typeof min === "number") return min === 1 ? messages.etaOne : fill(messages.etaFrom, { min });
75
427
  return rate.eta ?? null;
76
428
  }
77
- var costOf = (r) => {
429
+ var costOf2 = (r) => {
78
430
  const n = Number(r.cost.amount);
79
431
  return Number.isFinite(n) ? n : null;
80
432
  };
81
- var daysOf = (r) => r.eta_days_max ?? r.eta_days_min ?? null;
433
+ var daysOf2 = (r) => r.eta_days_max ?? r.eta_days_min ?? null;
82
434
  function rateBadges(rates) {
83
435
  const out = /* @__PURE__ */ new Map();
84
436
  if (rates.length < 2) return out;
@@ -92,10 +444,10 @@ function rateBadges(rates) {
92
444
  };
93
445
  const currencies = new Set(rates.map((r) => r.cost.currency_code));
94
446
  if (currencies.size === 1) {
95
- const id = unique(rates.map((r) => [r.id, costOf(r)]));
447
+ const id = unique(rates.map((r) => [r.id, costOf2(r)]));
96
448
  if (id) add(id, "cheapest");
97
449
  }
98
- const fast = unique(rates.map((r) => [r.id, daysOf(r)]));
450
+ const fast = unique(rates.map((r) => [r.id, daysOf2(r)]));
99
451
  if (fast) add(fast, "fastest");
100
452
  return out;
101
453
  }
@@ -114,7 +466,7 @@ function quoteNote(deadline, opts = {}) {
114
466
  const now = opts.now ?? /* @__PURE__ */ new Date();
115
467
  const left = deadline.getTime() - now.getTime();
116
468
  if (left <= 0) return null;
117
- const m = opts.messages ?? ES;
469
+ const m = opts.messages ?? ES3;
118
470
  const locale = opts.locale ?? "es-MX";
119
471
  const tz = opts.timeZone ? { timeZone: opts.timeZone } : {};
120
472
  const time = new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", ...tz }).format(deadline);
@@ -134,13 +486,13 @@ function isQuoteChange(err) {
134
486
  }
135
487
  function matchRate(previous, rates) {
136
488
  if (!previous) return null;
137
- const norm = (s) => (s ?? "").trim().toLowerCase();
138
- const sameCarrier = (r) => norm(r.carrier) === norm(previous.carrier);
489
+ const norm2 = (s) => (s ?? "").trim().toLowerCase();
490
+ const sameCarrier = (r) => norm2(r.carrier) === norm2(previous.carrier);
139
491
  if (previous.service_code) {
140
- const hit = rates.find((r) => sameCarrier(r) && norm(r.service_code) === norm(previous.service_code));
492
+ const hit = rates.find((r) => sameCarrier(r) && norm2(r.service_code) === norm2(previous.service_code));
141
493
  if (hit) return hit;
142
494
  }
143
- return rates.find((r) => sameCarrier(r) && norm(r.title) === norm(previous.title)) ?? null;
495
+ return rates.find((r) => sameCarrier(r) && norm2(r.title) === norm2(previous.title)) ?? null;
144
496
  }
145
497
  var SR_ONLY = { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap" };
146
498
  var useHydrated = () => useSyncExternalStore(
@@ -174,7 +526,10 @@ function DeliveryOptions(props) {
174
526
  renderAmount,
175
527
  classNames: cx = {},
176
528
  testId,
177
- children
529
+ children,
530
+ layout = "list",
531
+ error,
532
+ onRetry
178
533
  } = props;
179
534
  const locale = useResolvedLocale(props.locale);
180
535
  const m = useMemo(() => deliveryMessages(locale, props.messages), [locale, props.messages]);
@@ -187,7 +542,13 @@ function DeliveryOptions(props) {
187
542
  const deadline = mode === "never" ? null : quoteDeadline(rates);
188
543
  const note = hydrated && deadline ? quoteNote(deadline, { locale, messages: m, ...timeZone ? { timeZone } : {} }) : null;
189
544
  const shownNote = note && (mode === "always" || note.soon) ? note : null;
190
- const radio = (v) => /* @__PURE__ */ jsx(
545
+ const current = value ?? defaultValue ?? null;
546
+ const plan = useMemo(
547
+ () => layout === "recommended" ? recommendRates(rates, { count: props.recommendedCount ?? 3 }) : null,
548
+ [layout, rates, props.recommendedCount]
549
+ );
550
+ const inMore = plan ? plan.more.some((g) => g.rates.some((r) => rateValue(r) === current)) : false;
551
+ const radio = (v, describedBy) => /* @__PURE__ */ jsx(
191
552
  "input",
192
553
  {
193
554
  type: "radio",
@@ -195,36 +556,66 @@ function DeliveryOptions(props) {
195
556
  value: v,
196
557
  required,
197
558
  className: cx.radio,
559
+ ...{},
198
560
  ...value !== void 0 ? { checked: value === v, onChange: () => onValueChange?.(v) } : { defaultChecked: v === defaultValue, ...onValueChange ? { onChange: () => onValueChange(v) } : {} },
199
561
  ...testId ? { "data-testid": testId } : {}
200
562
  }
201
563
  );
202
- return /* @__PURE__ */ jsxs("div", { className: cx.root, "data-sf-delivery": true, "aria-busy": loading || void 0, children: [
203
- partial ? /* @__PURE__ */ jsx("p", { role: "status", className: cx.partial, "data-sf-delivery-partial": true, children: m.partial }) : null,
564
+ const tagText = (t) => t === "cheapest" ? m.cheapest : t === "fastest" ? m.fastest : m.bestValue;
565
+ const rateRow = (rate, opts) => {
566
+ const carrier = rateCarrier(rate);
567
+ const service = rateService(rate);
568
+ const eta = rateEta(rate, m);
569
+ return /* @__PURE__ */ jsxs("label", { className: cx.option, "data-sf-delivery-option": true, "data-loading": loading || void 0, "data-grouped": opts.grouped || void 0, children: [
570
+ radio(rateValue(rate)),
571
+ marks && !opts.grouped ? (
572
+ // Fixed slot: logos of different widths do not push the text column out of line.
573
+ /* @__PURE__ */ jsx("span", { "data-sf-delivery-mark": true, "aria-hidden": "true", children: /* @__PURE__ */ jsx(BrandMark, { brand: rate.carrier ?? rate.provider ?? rate.title, size: 28, ...cx.mark ? { className: cx.mark } : {} }) })
574
+ ) : null,
575
+ /* @__PURE__ */ jsxs("span", { className: cx.body, "data-sf-delivery-body": true, children: [
576
+ /* @__PURE__ */ jsx("span", { className: cx.title, "data-sf-delivery-title": true, children: carrier ? opts.grouped ? /* @__PURE__ */ jsxs(Fragment, { children: [
577
+ /* @__PURE__ */ jsxs("span", { style: SR_ONLY, children: [
578
+ carrier,
579
+ " "
580
+ ] }),
581
+ /* @__PURE__ */ jsx("span", { className: cx.carrier, "data-sf-delivery-carrier": true, "data-sf-delivery-service-only": true, children: service })
582
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
583
+ /* @__PURE__ */ jsx("span", { className: cx.carrier, "data-sf-delivery-carrier": true, children: carrier }),
584
+ " ",
585
+ /* @__PURE__ */ jsx("span", { className: cx.service, "data-sf-delivery-service": true, children: service })
586
+ ] }) : /* @__PURE__ */ jsx("span", { className: cx.carrier, "data-sf-delivery-carrier": true, children: service }) }),
587
+ eta ? /* @__PURE__ */ jsx("span", { className: cx.eta, "data-sf-delivery-eta": true, children: eta }) : null,
588
+ opts.tags.length ? /* @__PURE__ */ jsx("span", { className: cx.badges, "data-sf-delivery-badges": true, children: opts.tags.map((b) => /* @__PURE__ */ jsx("span", { className: cx.badge, "data-sf-delivery-badge": b, children: tagText(b) }, b)) }) : null
589
+ ] }),
590
+ /* @__PURE__ */ jsx("span", { className: cx.price, "data-sf-delivery-price": true, children: amount(rate.cost) })
591
+ ] }, rate.id);
592
+ };
593
+ const errorText = error ? typeof error === "string" ? error : m.error : null;
594
+ return /* @__PURE__ */ jsxs("div", { className: cx.root, "data-sf-delivery": true, "data-layout": layout, "aria-busy": loading || void 0, children: [
595
+ partial && !errorText ? /* @__PURE__ */ jsx("p", { role: "status", className: cx.partial, "data-sf-delivery-partial": true, children: m.partial }) : null,
204
596
  requoted ? /* @__PURE__ */ jsx("p", { role: "status", className: cx.partial, "data-sf-delivery-requoted": true, children: m.requoted }) : null,
205
597
  loading ? /* @__PURE__ */ jsx("span", { style: SR_ONLY, role: "status", "data-sf-delivery-loading": true, children: m.loading }) : null,
598
+ errorText && rates.length === 0 && !loading ? /* @__PURE__ */ jsxs("div", { role: "alert", className: cx.error, "data-sf-delivery-error": true, children: [
599
+ /* @__PURE__ */ jsx("p", { children: errorText }),
600
+ onRetry ? /* @__PURE__ */ jsx("button", { type: "button", className: cx.retry, onClick: onRetry, "data-sf-delivery-retry": true, children: m.retry }) : null
601
+ ] }) : null,
206
602
  children,
207
603
  loading && rates.length === 0 ? [0, 1, 2].map((i) => /* @__PURE__ */ jsx("div", { className: cx.skeleton, "data-sf-delivery-skeleton": true, "aria-hidden": "true" }, i)) : null,
208
- rates.map((rate) => {
209
- const carrier = rateCarrier(rate);
210
- const service = rateService(rate);
211
- const eta = rateEta(rate, m);
212
- const own = badges.get(rate.id) ?? [];
213
- return /* @__PURE__ */ jsxs("label", { className: cx.option, "data-sf-delivery-option": true, "data-loading": loading || void 0, children: [
214
- radio(rateValue(rate)),
215
- marks ? /* @__PURE__ */ jsx(BrandMark, { brand: rate.carrier ?? rate.provider ?? rate.title, size: 28, ...cx.mark ? { className: cx.mark } : {} }) : null,
216
- /* @__PURE__ */ jsxs("span", { className: cx.body, "data-sf-delivery-body": true, children: [
217
- /* @__PURE__ */ jsx("span", { className: cx.title, "data-sf-delivery-title": true, children: carrier ? /* @__PURE__ */ jsxs(Fragment, { children: [
218
- /* @__PURE__ */ jsx("span", { className: cx.carrier, "data-sf-delivery-carrier": true, children: carrier }),
219
- " ",
220
- /* @__PURE__ */ jsx("span", { className: cx.service, "data-sf-delivery-service": true, children: service })
221
- ] }) : /* @__PURE__ */ jsx("span", { className: cx.carrier, "data-sf-delivery-carrier": true, children: service }) }),
222
- eta ? /* @__PURE__ */ jsx("span", { className: cx.eta, "data-sf-delivery-eta": true, children: eta }) : null,
223
- own.length ? /* @__PURE__ */ jsx("span", { className: cx.badges, "data-sf-delivery-badges": true, children: own.map((b) => /* @__PURE__ */ jsx("span", { className: cx.badge, "data-sf-delivery-badge": b, children: b === "cheapest" ? m.cheapest : m.fastest }, b)) }) : null
224
- ] }),
225
- /* @__PURE__ */ jsx("span", { className: cx.price, "data-sf-delivery-price": true, children: amount(rate.cost) })
226
- ] }, rate.id);
227
- }),
604
+ plan ? plan.recommended.map(({ rate, tags }) => rateRow(rate, { tags: showBadges ? tags : [] })) : rates.map((rate) => rateRow(rate, { tags: badges.get(rate.id) ?? [] })),
605
+ plan && plan.moreCount > 0 ? /* @__PURE__ */ jsxs("details", { className: cx.more, "data-sf-delivery-more": true, ...inMore ? { open: true } : {}, children: [
606
+ /* @__PURE__ */ jsx("summary", { className: cx.moreSummary, "data-sf-delivery-more-summary": true, children: m.moreOptions.replace("{n}", String(plan.moreCount)) }),
607
+ /* @__PURE__ */ jsx("div", { "data-sf-delivery-groups": true, children: plan.more.map((g) => {
608
+ const carrierName = g.carrier ? rateCarrier({ carrier: g.carrier }) : null;
609
+ const id = `${name}-group-${g.key || "other"}`;
610
+ return /* @__PURE__ */ jsxs("div", { role: "group", "aria-labelledby": id, className: cx.group, "data-sf-delivery-group": g.key || "other", children: [
611
+ /* @__PURE__ */ jsxs("p", { id, className: cx.groupTitle, "data-sf-delivery-group-title": true, children: [
612
+ marks && g.carrier ? /* @__PURE__ */ jsx(BrandMark, { brand: g.carrier, size: 22 }) : null,
613
+ /* @__PURE__ */ jsx("span", { children: carrierName ?? m.otherCarriers })
614
+ ] }),
615
+ g.rates.map((rate) => rateRow(rate, { tags: [], grouped: Boolean(carrierName) }))
616
+ ] }, g.key || "other");
617
+ }) })
618
+ ] }) : null,
228
619
  pickup.map((b) => /* @__PURE__ */ jsxs("label", { className: cx.option, "data-sf-delivery-option": true, "data-sf-delivery-pickup": true, children: [
229
620
  radio(pickupValue(b)),
230
621
  marks ? /* @__PURE__ */ jsx(PickupGlyph, { size: 28 }) : null,
@@ -238,6 +629,6 @@ function DeliveryOptions(props) {
238
629
  ] });
239
630
  }
240
631
 
241
- export { DeliveryOptions, deliveryMessages, isQuoteChange, matchRate, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService };
632
+ export { AddressFields, ContactFields, DeliveryOptions, OrderSummary, OrderSummaryDisclosure, addressFieldsMessages, dedupeRates, deliveryMessages, isQuoteChange, matchRate, orderSummaryMessages, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService, recommendRates };
242
633
  //# sourceMappingURL=checkout.js.map
243
634
  //# sourceMappingURL=checkout.js.map