@asteby/storefront-react 0.15.0 → 0.16.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/README.md +78 -0
- package/dist/cart.cjs +266 -10
- package/dist/cart.cjs.map +1 -1
- package/dist/cart.css +14 -0
- package/dist/cart.d.cts +125 -2
- package/dist/cart.d.ts +125 -2
- package/dist/cart.js +260 -11
- package/dist/cart.js.map +1 -1
- package/dist/checkout.cjs +24 -3
- package/dist/checkout.cjs.map +1 -1
- package/dist/checkout.d.cts +7 -5
- package/dist/checkout.d.ts +7 -5
- package/dist/checkout.js +24 -3
- package/dist/checkout.js.map +1 -1
- package/dist/chunk-Q7UTJIKY.js +122 -0
- package/dist/chunk-Q7UTJIKY.js.map +1 -0
- package/dist/fiscal-server.cjs +288 -0
- package/dist/fiscal-server.cjs.map +1 -0
- package/dist/fiscal-server.d.cts +97 -0
- package/dist/fiscal-server.d.ts +97 -0
- package/dist/fiscal-server.js +241 -0
- package/dist/fiscal-server.js.map +1 -0
- package/dist/fiscal.cjs +1345 -0
- package/dist/fiscal.cjs.map +1 -0
- package/dist/fiscal.css +343 -0
- package/dist/fiscal.d.cts +313 -0
- package/dist/fiscal.d.ts +313 -0
- package/dist/fiscal.js +1106 -0
- package/dist/fiscal.js.map +1 -0
- package/dist/types-BnvSbV6t.d.cts +68 -0
- package/dist/types-BnvSbV6t.d.ts +68 -0
- package/package.json +23 -2
package/README.md
CHANGED
|
@@ -624,6 +624,42 @@ useOpenCartDrawerWhenDone(fetchers.some(isAdd), { skip: silent })
|
|
|
624
624
|
- Estilos: `cart.css` (CSS plano con los tokens de la tienda) y/o `classNames` por pieza (Tailwind). Cada pieza lleva `data-sf-cart-*`.
|
|
625
625
|
- `renderLine`, `renderImage`, `icons`, `notice` y `isDefaultVariantTitle` cubren las variaciones de cada tienda.
|
|
626
626
|
|
|
627
|
+
### Agregado optimista (`cartOptimistic`, `useOptimisticAdd`)
|
|
628
|
+
|
|
629
|
+
Al pulsar «Agregar» la fila aparece YA en el cajón, el contador sube y el cajón se abre (sin mover el foco al botón de cierre); cuando OPS responde, el carrito real la sustituye. Si la acción falla (409 sin existencias, `store_not_ready`, red, 5xx, tiempo agotado) la fila se quita, el contador regresa y aparece «No pudimos agregar X: motivo» (`role="alert"`, descartable); si el cajón queda vacío por el fallo, se cierra. **Solo es la fila**: precios, totales, envío e impuestos los dicta siempre OPS; el subtotal conserva el último valor de OPS con «calculando…» hasta confirmar. Sin JavaScript el formulario de la ruta sigue funcionando igual.
|
|
630
|
+
|
|
631
|
+
```tsx
|
|
632
|
+
import { useFetcher } from 'react-router'
|
|
633
|
+
import { useOptimisticAdd, useOptimisticCartCount } from '@asteby/storefront-react/cart'
|
|
634
|
+
|
|
635
|
+
function AddToCart({ product, variant }) {
|
|
636
|
+
const fetcher = useFetcher()
|
|
637
|
+
const { add } = useOptimisticAdd(fetcher) // confirma o revierte cuando el fetcher termina
|
|
638
|
+
return (
|
|
639
|
+
<fetcher.Form method="post" action="/carrito"
|
|
640
|
+
onSubmit={(e) => {
|
|
641
|
+
e.preventDefault()
|
|
642
|
+
const form = e.currentTarget
|
|
643
|
+
add(
|
|
644
|
+
{ variantId: variant.id, title: product.title, variantTitle: variant.title, image: product.featured_image, price: variant.price, quantity: 1 },
|
|
645
|
+
() => fetcher.submit(form),
|
|
646
|
+
)
|
|
647
|
+
}}>
|
|
648
|
+
<input type="hidden" name="intent" value="add" />
|
|
649
|
+
<input type="hidden" name="variant_id" value={variant.id} />
|
|
650
|
+
<button type="submit">Agregar</button>
|
|
651
|
+
</fetcher.Form>
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const badge = useOptimisticCartCount(cart?.total_quantity ?? 0) // contador de la cabecera
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
- La acción de ruta debe devolver el error en `fetcher.data` (`{ error: { code, status } }`, `{ ok: false }`, `{ success: false }` o `{ status >= 400 }`); para otro formato pasa `useOptimisticAdd(fetcher, { settle: (data) => ({ ok: true }) | ({ ok: false, reason }) })`. `describeAddFailure({ status, code })` da el motivo en es-MX.
|
|
659
|
+
- API sin React: `cartOptimistic.add(input, { open?, timeoutMs? })` devuelve el id de la acción; `confirm(id)`, `rollback(id, reason)`, `discard(id)` (sin aviso), `dismissNotice(id)`, `reset()`. Con `input.key` repetir la llamada no suma otra vez (doble clic); la misma variante comparte una fila y cada acción se confirma o revierte por separado, en cualquier orden.
|
|
660
|
+
- Un segundo `add` con el mismo fetcher antes de terminar da por entregado el anterior (el router lo reemplaza y manda el carrito real de la revalidación). Si el componente se desmonta, las filas en vuelo se descartan sin aviso.
|
|
661
|
+
- `optimisticLabels` en `CartDrawer` cambia los textos (`adding`, `added` «Agregado al carrito» en región `aria-live="polite"`, `calculating`, `failed(title, reason)`, `dismiss`). `cart.css` ya estiliza `data-sf-cart-line-pending`, `data-sf-cart-notice` y respeta `prefers-reduced-motion`.
|
|
662
|
+
|
|
627
663
|
## Compatibilidad de vehículos (`@asteby/storefront-react/fitment`)
|
|
628
664
|
|
|
629
665
|
Módulo opcional y sin interfaz para tiendas de refacciones: catálogo marca → modelo → año → motor, clave de vehículo, opciones en cascada del selector y estado de compatibilidad de cada pieza. No trae textos ni marcas: los prefijos de etiqueta, el atributo de la tabla de compatibilidad y las grafías del backend se inyectan con `fitmentConfig` / `parseCatalog`.
|
|
@@ -703,3 +739,45 @@ export default function Login() {
|
|
|
703
739
|
- `createAuthAction({ storefront, customerCookie, safeNext?, defaultNext?, clientIp?, capabilities? })` -> `({ request }) => Promise<Response | AuthActionData>`. Un solo `POST` con campo `intent`: `login`, `register`, `verify`, `otp-request`, `otp-login`, `forgot`, `reset`. Éxito: `303` a `next` con `Set-Cookie` de la sesión. Si no, `AuthActionData` (`{ ok, step?, email?, error?: { code, field? } }`, sin textos ni contraseñas). `forgot` siempre avanza a `reset` (no revela qué correos tienen cuenta).
|
|
704
740
|
- `<AccountAuth>`: `classNames` por pieza, atributos `data-sf-account-*`, `locale` (`es` por omisión, `en`), `messages` para cambiar cualquier texto, `form` para el `Form` del router. Con el mock (`createMockStorefront`) funciona entera: código `000000`.
|
|
705
741
|
- Sin JavaScript funciona el inicio de sesión con contraseña y los pasos que devuelve la action; el cambio entre pantallas (enlaces) necesita JavaScript.
|
|
742
|
+
|
|
743
|
+
## Facturación y constancia fiscal (`/fiscal`)
|
|
744
|
+
|
|
745
|
+
Subruta `@asteby/storefront-react/fiscal` (+ `/fiscal/server`, + `fiscal.css` opcional). Todo sale de `Region.fiscal_id` (etiquetas, formato, regímenes, usos, documentos); el núcleo no sabe de ningún país. Textos es-MX por omisión, `locale="en"` o `messages` para otros.
|
|
746
|
+
|
|
747
|
+
- `FiscalIdentityFields`: ID fiscal (mayúsculas, sin espacios, validado con el patrón de la región), razón social, CP fiscal, régimen (filtrado por tipo de persona si pasas `personTypeOf`, p. ej. `personTypeByLength({ individual: 13, company: 12 })`) y uso (filtrado por `allowed_regimes`). Controles `invoice.<campo>`.
|
|
748
|
+
- `IdentityDocumentReader`: «Sube tu constancia». Lee el PDF en el dispositivo (pdf.js se carga bajo demanda con `import()`, nunca en el bundle principal) y requiere `workerSrc`, un `pdf.worker.mjs` que sirve TU sitio (sin CDN). Muestra los campos prellenados con la marca «De tu constancia», avisos (QR, vigencia, régimen) y errores (no PDF, tamaño, escaneado, contraseña) con salida a captura manual. Nunca muestra ni conserva lo que la región lista en `discard_fields` (CURP, calle...). Entrega valores + evidencia (`buildDocumentEvidence`).
|
|
749
|
+
- `InvoiceToggle`: «Necesito factura» colapsado; perfil guardado (RFC enmascarado, Cambiar / Usar otros datos), lector o captura manual. `onChange` da `{ requested, invoice, ready, source, payLabel }` (`Pagar` / `Pagar y facturar`).
|
|
750
|
+
- `InvoiceStatusCard`: los estados de una `InvoiceSummary` (sin pedir, esperando pago, en proceso con `onRefresh` cada 10 s máx. 1 min, emitida, faltan datos, revisión, fuera de plazo, no disponible, cancelada).
|
|
751
|
+
- `/fiscal/server`: `loadFiscalContext(sf)` (región + `capabilities.invoicing`, caché corta, degrada a «sin facturación» sin lanzar) y `createInvoiceAction({ storefront, customerCookie })` con intents `set-invoice`, `save-profile`, `request-invoice`, `clear-profile` y `extract-document` (subida sin JS: se lee en memoria con el mismo motor, 5 MiB / 6 páginas / 3 s, sin disco ni logs; el archivo se descarta).
|
|
752
|
+
|
|
753
|
+
```tsx
|
|
754
|
+
// app/routes/checkout.tsx (React Router)
|
|
755
|
+
import { InvoiceToggle } from '@asteby/storefront-react/fiscal'
|
|
756
|
+
import { createInvoiceAction, loadFiscalContext } from '@asteby/storefront-react/fiscal/server'
|
|
757
|
+
import '@asteby/storefront-react/fiscal.css'
|
|
758
|
+
|
|
759
|
+
export const loader = async () => ({ fiscal: await loadFiscalContext(storefront) })
|
|
760
|
+
export const action = createInvoiceAction({ storefront, customerCookie })
|
|
761
|
+
|
|
762
|
+
export default function Checkout() {
|
|
763
|
+
const { fiscal } = useLoaderData<typeof loader>()
|
|
764
|
+
const data = useActionData<typeof action>()
|
|
765
|
+
const [pay, setPay] = useState('Pagar')
|
|
766
|
+
return (
|
|
767
|
+
<Form method="post" encType="multipart/form-data">
|
|
768
|
+
{fiscal.region && (
|
|
769
|
+
<InvoiceToggle region={fiscal.region} capabilities={fiscal.invoicing} profile={customer?.fiscal} data={data}
|
|
770
|
+
canSave={!!customer} workerSrc="/assets/pdf.worker.mjs" serverUpload noJsHref="?factura=1"
|
|
771
|
+
personTypeOf={personTypeByLength({ individual: 13, company: 12 })}
|
|
772
|
+
onChange={(s) => setPay(s.payLabel)} />
|
|
773
|
+
)}
|
|
774
|
+
<button type="submit" name="intent" value="set-invoice">{pay}</button>
|
|
775
|
+
</Form>
|
|
776
|
+
)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Cuenta: <IdentityDocumentReader region={region} workerSrc=... onConfirm={...} /> con intent save-profile (+ campo consent).
|
|
780
|
+
// Pedido: <InvoiceStatusCard invoice={order.invoice} available={fiscal.available} onRefresh={revalidate} />
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
CSP: el lector necesita `worker-src 'self'` (o el origen que sirva el worker). Sin `workerSrc` el navegador no puede leer el PDF y el comprador pasa a captura manual.
|
package/dist/cart.cjs
CHANGED
|
@@ -72,9 +72,17 @@ function useResolvedLocale(locale) {
|
|
|
72
72
|
}
|
|
73
73
|
var isOpen = false;
|
|
74
74
|
var listeners = /* @__PURE__ */ new Set();
|
|
75
|
+
var quietOpen = false;
|
|
76
|
+
function setQuietOpen(next) {
|
|
77
|
+
quietOpen = next;
|
|
78
|
+
}
|
|
79
|
+
function isQuietOpen() {
|
|
80
|
+
return quietOpen;
|
|
81
|
+
}
|
|
75
82
|
function set(next) {
|
|
76
83
|
if (isOpen === next) return;
|
|
77
84
|
isOpen = next;
|
|
85
|
+
if (!next) quietOpen = false;
|
|
78
86
|
for (const l of [...listeners]) l();
|
|
79
87
|
}
|
|
80
88
|
var subscribe = (l) => {
|
|
@@ -114,6 +122,191 @@ function handleCartLinkClick(e) {
|
|
|
114
122
|
e.preventDefault();
|
|
115
123
|
openCartDrawer();
|
|
116
124
|
}
|
|
125
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
126
|
+
var actions = [];
|
|
127
|
+
var notices = [];
|
|
128
|
+
var announcement = { kind: null, n: 0 };
|
|
129
|
+
var seq = 0;
|
|
130
|
+
var timers = /* @__PURE__ */ new Map();
|
|
131
|
+
var listeners2 = /* @__PURE__ */ new Set();
|
|
132
|
+
var snapshot = compute();
|
|
133
|
+
function compute() {
|
|
134
|
+
const byVariant = /* @__PURE__ */ new Map();
|
|
135
|
+
for (const a of actions) {
|
|
136
|
+
const row = byVariant.get(a.variantId);
|
|
137
|
+
if (row) {
|
|
138
|
+
row.quantity += a.quantity;
|
|
139
|
+
row.actionIds.push(a.id);
|
|
140
|
+
} else {
|
|
141
|
+
byVariant.set(a.variantId, {
|
|
142
|
+
variantId: a.variantId,
|
|
143
|
+
title: a.title,
|
|
144
|
+
variantTitle: a.variantTitle,
|
|
145
|
+
productHandle: a.productHandle,
|
|
146
|
+
image: a.image,
|
|
147
|
+
price: a.price,
|
|
148
|
+
quantity: a.quantity,
|
|
149
|
+
actionIds: [a.id]
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const rows = [...byVariant.values()];
|
|
154
|
+
return { rows, quantity: rows.reduce((n, r) => n + r.quantity, 0), notices, announcement };
|
|
155
|
+
}
|
|
156
|
+
function emit() {
|
|
157
|
+
snapshot = compute();
|
|
158
|
+
for (const l of [...listeners2]) l();
|
|
159
|
+
}
|
|
160
|
+
var subscribe2 = (l) => {
|
|
161
|
+
listeners2.add(l);
|
|
162
|
+
return () => {
|
|
163
|
+
listeners2.delete(l);
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
var EMPTY = { rows: [], quantity: 0, notices: [], announcement: { kind: null, n: 0 } };
|
|
167
|
+
function drop(id) {
|
|
168
|
+
const a = actions.find((x) => x.id === id);
|
|
169
|
+
if (!a) return void 0;
|
|
170
|
+
actions = actions.filter((x) => x.id !== id);
|
|
171
|
+
const t = timers.get(id);
|
|
172
|
+
if (t) clearTimeout(t);
|
|
173
|
+
timers.delete(id);
|
|
174
|
+
return a;
|
|
175
|
+
}
|
|
176
|
+
function announce() {
|
|
177
|
+
announcement = { kind: "added", n: announcement.n + 1 };
|
|
178
|
+
}
|
|
179
|
+
var cartOptimistic = {
|
|
180
|
+
/**
|
|
181
|
+
* Inserta YA la fila provisional, sube el contador y abre el cajón sin robar el foco. Devuelve el id de la acción
|
|
182
|
+
* para `confirm` / `rollback`. Una acción por clic: la misma variante suma cantidad en una sola fila.
|
|
183
|
+
*/
|
|
184
|
+
add(input, options = {}) {
|
|
185
|
+
if (input.key) {
|
|
186
|
+
const live = actions.find((a) => a.key === input.key);
|
|
187
|
+
if (live) return live.id;
|
|
188
|
+
}
|
|
189
|
+
const id = `opt-${++seq}`;
|
|
190
|
+
const quantity = Math.max(1, Math.floor(input.quantity ?? 1));
|
|
191
|
+
actions = [...actions, { ...input, id, quantity }];
|
|
192
|
+
const ms = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
193
|
+
if (ms > 0 && typeof setTimeout === "function") {
|
|
194
|
+
timers.set(id, setTimeout(() => cartOptimistic.rollback(id, describeAddFailure({ code: "timeout" })), ms));
|
|
195
|
+
}
|
|
196
|
+
announce();
|
|
197
|
+
if (options.open !== false) {
|
|
198
|
+
setQuietOpen(true);
|
|
199
|
+
openCartDrawer();
|
|
200
|
+
}
|
|
201
|
+
emit();
|
|
202
|
+
return id;
|
|
203
|
+
},
|
|
204
|
+
/** OPS confirmó: la fila provisional sale y el carrito real (ya en `cart`) ocupa su lugar. */
|
|
205
|
+
confirm(id) {
|
|
206
|
+
if (!drop(id)) return;
|
|
207
|
+
announce();
|
|
208
|
+
emit();
|
|
209
|
+
},
|
|
210
|
+
/** Falló: se quita la acción (y su cantidad) y se avisa «No pudimos agregar X: motivo». */
|
|
211
|
+
rollback(id, reason = "unknown") {
|
|
212
|
+
const a = drop(id);
|
|
213
|
+
if (!a) return;
|
|
214
|
+
notices = [...notices, { id: `n-${++seq}`, title: a.title, reason }];
|
|
215
|
+
emit();
|
|
216
|
+
},
|
|
217
|
+
/** Quita la acción sin avisar (el componente que la lanzó se desmontó). */
|
|
218
|
+
discard(id) {
|
|
219
|
+
if (drop(id)) emit();
|
|
220
|
+
},
|
|
221
|
+
dismissNotice(id) {
|
|
222
|
+
notices = notices.filter((n) => n.id !== id);
|
|
223
|
+
emit();
|
|
224
|
+
},
|
|
225
|
+
/** Limpia todo (pruebas, cierre de sesión). */
|
|
226
|
+
reset() {
|
|
227
|
+
for (const t of timers.values()) clearTimeout(t);
|
|
228
|
+
timers.clear();
|
|
229
|
+
actions = [];
|
|
230
|
+
notices = [];
|
|
231
|
+
announcement = { kind: null, n: 0 };
|
|
232
|
+
setQuietOpen(false);
|
|
233
|
+
emit();
|
|
234
|
+
},
|
|
235
|
+
getSnapshot: () => snapshot
|
|
236
|
+
};
|
|
237
|
+
function useOptimisticCart() {
|
|
238
|
+
return react.useSyncExternalStore(subscribe2, () => snapshot, () => EMPTY);
|
|
239
|
+
}
|
|
240
|
+
function useOptimisticCartCount(base) {
|
|
241
|
+
return base + useOptimisticCart().quantity;
|
|
242
|
+
}
|
|
243
|
+
function describeAddFailure(error) {
|
|
244
|
+
const code = error?.code;
|
|
245
|
+
const status = error?.status;
|
|
246
|
+
if (code === "store_not_ready") return "la tienda est\xE1 cerrada por ahora";
|
|
247
|
+
if (code === "timeout") return "tard\xF3 demasiado, intenta de nuevo";
|
|
248
|
+
if (status === 409 || code === "out_of_stock" || code === "insufficient_stock") return "sin existencias suficientes";
|
|
249
|
+
if (status === 0 || code === "network" || !status && !code) return "revisa tu conexi\xF3n";
|
|
250
|
+
if (status && status >= 500) return "hubo un problema, intenta de nuevo";
|
|
251
|
+
if (status === 404) return "el producto ya no est\xE1 disponible";
|
|
252
|
+
return error?.message?.trim() || "intenta de nuevo";
|
|
253
|
+
}
|
|
254
|
+
function defaultAddOutcome(data) {
|
|
255
|
+
if (!data || typeof data !== "object") return { ok: true };
|
|
256
|
+
const d = data;
|
|
257
|
+
const status = typeof d.status === "number" ? d.status : void 0;
|
|
258
|
+
const err = d.error && typeof d.error === "object" ? d.error : d;
|
|
259
|
+
const code = typeof err.code === "string" ? err.code : void 0;
|
|
260
|
+
const failed = d.error != null || d.ok === false || d.success === false || status !== void 0 && status >= 400;
|
|
261
|
+
if (!failed) return { ok: true };
|
|
262
|
+
return { ok: false, reason: describeAddFailure({ status: status ?? (typeof err.status === "number" ? err.status : void 0), code, message: typeof err.message === "string" ? err.message : void 0 }) };
|
|
263
|
+
}
|
|
264
|
+
function useOptimisticAdd(fetcher, options = {}) {
|
|
265
|
+
const current = react.useRef(null);
|
|
266
|
+
const was = react.useRef(false);
|
|
267
|
+
const settleRef = react.useRef(options.settle);
|
|
268
|
+
settleRef.current = options.settle;
|
|
269
|
+
react.useEffect(() => {
|
|
270
|
+
const busy = fetcher.state !== "idle";
|
|
271
|
+
const id = current.current;
|
|
272
|
+
if (was.current && !busy && id) {
|
|
273
|
+
current.current = null;
|
|
274
|
+
const out = (settleRef.current ?? defaultAddOutcome)(fetcher.data);
|
|
275
|
+
if (out.ok) cartOptimistic.confirm(id);
|
|
276
|
+
else cartOptimistic.rollback(id, out.reason);
|
|
277
|
+
}
|
|
278
|
+
was.current = busy;
|
|
279
|
+
}, [fetcher.state, fetcher.data]);
|
|
280
|
+
react.useEffect(
|
|
281
|
+
() => () => {
|
|
282
|
+
if (current.current) cartOptimistic.discard(current.current);
|
|
283
|
+
current.current = null;
|
|
284
|
+
},
|
|
285
|
+
[]
|
|
286
|
+
);
|
|
287
|
+
return {
|
|
288
|
+
add(input, submit) {
|
|
289
|
+
const prev = current.current;
|
|
290
|
+
const id = cartOptimistic.add(input);
|
|
291
|
+
if (prev && prev !== id) cartOptimistic.confirm(prev);
|
|
292
|
+
current.current = id;
|
|
293
|
+
try {
|
|
294
|
+
submit();
|
|
295
|
+
} catch (e) {
|
|
296
|
+
current.current = null;
|
|
297
|
+
cartOptimistic.rollback(id, describeAddFailure({ message: e instanceof Error ? e.message : void 0 }));
|
|
298
|
+
}
|
|
299
|
+
return id;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
var DEFAULT_OPTIMISTIC_LABELS = {
|
|
304
|
+
adding: "Agregando\u2026",
|
|
305
|
+
added: "Agregado al carrito",
|
|
306
|
+
calculating: "calculando\u2026",
|
|
307
|
+
failed: (title, reason) => `No pudimos agregar ${title}: ${reason}`,
|
|
308
|
+
dismiss: "Descartar aviso"
|
|
309
|
+
};
|
|
117
310
|
var DEFAULT_FORM = {
|
|
118
311
|
intentField: "intent",
|
|
119
312
|
lineField: "line_id",
|
|
@@ -138,6 +331,9 @@ function CartDrawer(props) {
|
|
|
138
331
|
const { cart, labels, paths, shipping, notice, upsell, upsellTitle, icons, classNames: cx = {}, onOpenChange } = props;
|
|
139
332
|
const locale = useResolvedLocale(props.locale);
|
|
140
333
|
const { open } = useCartDrawer();
|
|
334
|
+
const opt = useOptimisticCart();
|
|
335
|
+
const ol = { ...DEFAULT_OPTIMISTIC_LABELS, ...props.optimisticLabels };
|
|
336
|
+
const quiet = open && isQuietOpen();
|
|
141
337
|
const titleId = react.useId();
|
|
142
338
|
const upsellId = react.useId();
|
|
143
339
|
const { ref, onBackdropClick } = useModalDialog({ open, onClose: () => closeCartDrawer() });
|
|
@@ -145,12 +341,20 @@ function CartDrawer(props) {
|
|
|
145
341
|
react.useEffect(() => {
|
|
146
342
|
onOpenChange?.(open);
|
|
147
343
|
}, [open]);
|
|
148
|
-
|
|
344
|
+
const realLines = cart?.lines.length ?? 0;
|
|
345
|
+
const rollbackEmpty = open && quiet && realLines === 0 && opt.rows.length === 0 && opt.notices.length > 0;
|
|
346
|
+
react.useEffect(() => {
|
|
347
|
+
if (rollbackEmpty) closeCartDrawer();
|
|
348
|
+
}, [rollbackEmpty]);
|
|
349
|
+
if (!open) return opt.notices.length ? /* @__PURE__ */ jsxRuntime.jsx(OptimisticNotices, { notices: opt.notices, labels: ol, classNames: cx }) : null;
|
|
149
350
|
const money = props.renderMoney ?? ((m) => storefront.formatMoney(m, locale));
|
|
150
351
|
const link = props.renderLink ?? (({ href, className, children, onClick }) => /* @__PURE__ */ jsxRuntime.jsx("a", { href, className, onClick, children }));
|
|
151
|
-
const
|
|
352
|
+
const pendingByVariant = new Map(opt.rows.map((r) => [r.variantId, r]));
|
|
353
|
+
const count = (cart?.total_quantity ?? 0) + opt.quantity;
|
|
152
354
|
const lines = cart?.lines ?? [];
|
|
153
|
-
const
|
|
355
|
+
const extraRows = opt.rows.filter((r) => !lines.some((l) => l.merchandise.id === r.variantId));
|
|
356
|
+
const pending = opt.rows.length > 0;
|
|
357
|
+
const empty = lines.length === 0 && extraRows.length === 0;
|
|
154
358
|
const close = () => closeCartDrawer();
|
|
155
359
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
156
360
|
"dialog",
|
|
@@ -169,8 +373,10 @@ function CartDrawer(props) {
|
|
|
169
373
|
labels.title,
|
|
170
374
|
count > 0 ? ` (${count})` : ""
|
|
171
375
|
] }),
|
|
172
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: close, "aria-label": labels.close, className: cx.close, "data-sf-cart-close": "", "data-sf-autofocus": "", children: icons?.close ?? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
|
|
376
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: close, "aria-label": labels.close, className: cx.close, "data-sf-cart-close": "", "data-sf-autofocus": quiet ? void 0 : "", children: icons?.close ?? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
|
|
173
377
|
] }),
|
|
378
|
+
/* @__PURE__ */ jsxRuntime.jsx(OptimisticNotices, { notices: opt.notices, labels: ol, classNames: cx }),
|
|
379
|
+
/* @__PURE__ */ jsxRuntime.jsx(OptimisticStatus, { kind: opt.announcement.kind, n: opt.announcement.n, text: ol.added }),
|
|
174
380
|
empty ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.empty, "data-sf-cart-empty": "", children: [
|
|
175
381
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.emptyTitle, "data-sf-cart-empty-title": "", children: labels.empty }),
|
|
176
382
|
labels.emptyHint ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.emptyHint, "data-sf-cart-empty-hint": "", children: labels.emptyHint }) : null,
|
|
@@ -199,16 +405,25 @@ function CartDrawer(props) {
|
|
|
199
405
|
icons?.shippingReached,
|
|
200
406
|
notice
|
|
201
407
|
] }) : null,
|
|
202
|
-
/* @__PURE__ */ jsxRuntime.
|
|
408
|
+
/* @__PURE__ */ jsxRuntime.jsxs("ul", { className: cx.lines, "data-sf-cart-lines": "", children: [
|
|
409
|
+
lines.map((line) => /* @__PURE__ */ jsxRuntime.jsx(DrawerLine, { line, props, money, link, close, pending: pendingByVariant.get(line.merchandise.id), pendingLabel: ol.adding }, line.id)),
|
|
410
|
+
extraRows.map((row) => /* @__PURE__ */ jsxRuntime.jsx(PendingLine, { row, props, money, label: ol.adding }, `pending-${row.variantId}`))
|
|
411
|
+
] }),
|
|
203
412
|
upsell ? /* @__PURE__ */ jsxRuntime.jsxs("section", { className: cx.upsell, "data-sf-cart-upsell": "", "aria-labelledby": upsellTitle ? upsellId : void 0, children: [
|
|
204
413
|
upsellTitle ? /* @__PURE__ */ jsxRuntime.jsx("h3", { id: upsellId, className: cx.upsellTitle, children: upsellTitle }) : null,
|
|
205
414
|
upsell
|
|
206
415
|
] }) : null
|
|
207
416
|
] }),
|
|
208
417
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.footer, "data-sf-cart-footer": "", children: [
|
|
209
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.subtotal, "data-sf-cart-subtotal": "", children: [
|
|
418
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.subtotal, "data-sf-cart-subtotal": "", "aria-busy": pending || void 0, children: [
|
|
210
419
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.subtotalLabel, children: labels.subtotal }),
|
|
211
|
-
/* @__PURE__ */ jsxRuntime.
|
|
420
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: cx.subtotalValue, "data-sf-cart-subtotal-value": "", children: [
|
|
421
|
+
cart ? money(cart.cost.subtotal) : null,
|
|
422
|
+
pending ? /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-sf-cart-calculating": "", children: [
|
|
423
|
+
cart ? " " : "",
|
|
424
|
+
ol.calculating
|
|
425
|
+
] }) : null
|
|
426
|
+
] })
|
|
212
427
|
] }),
|
|
213
428
|
labels.note ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.note, "data-sf-cart-note": "", children: labels.note }) : null,
|
|
214
429
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.actions, "data-sf-cart-actions": "", children: [
|
|
@@ -226,12 +441,14 @@ function DrawerLine({
|
|
|
226
441
|
props,
|
|
227
442
|
money,
|
|
228
443
|
link,
|
|
229
|
-
close
|
|
444
|
+
close,
|
|
445
|
+
pending,
|
|
446
|
+
pendingLabel
|
|
230
447
|
}) {
|
|
231
448
|
const { labels, paths, icons, classNames: cx = {} } = props;
|
|
232
449
|
const f = { ...DEFAULT_FORM, ...props.form };
|
|
233
450
|
const { Form, formData } = (props.useLineForm ?? useNativeForm)();
|
|
234
|
-
const quantity = formData ? Number(formData.get(f.quantityField) ?? 0) : line.quantity;
|
|
451
|
+
const quantity = formData ? Number(formData.get(f.quantityField) ?? 0) : line.quantity + (pending?.quantity ?? 0);
|
|
235
452
|
if (formData && (formData.get(f.intentField) === f.removeIntent || quantity === 0)) return null;
|
|
236
453
|
const merch = line.merchandise;
|
|
237
454
|
const isDefault = props.isDefaultVariantTitle ?? defaultIsDefaultVariant;
|
|
@@ -266,18 +483,57 @@ function DrawerLine({
|
|
|
266
483
|
] }),
|
|
267
484
|
price
|
|
268
485
|
] }),
|
|
269
|
-
controls
|
|
486
|
+
controls,
|
|
487
|
+
pending ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.variant, "data-sf-cart-pending-label": "", children: pendingLabel }) : null
|
|
270
488
|
] })
|
|
271
489
|
] });
|
|
272
490
|
}
|
|
491
|
+
function PendingLine({ row, props, money, label }) {
|
|
492
|
+
const { classNames: cx = {} } = props;
|
|
493
|
+
const isDefault = props.isDefaultVariantTitle ?? defaultIsDefaultVariant;
|
|
494
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: cx.line, "data-sf-cart-line": "", "data-sf-cart-line-pending": "", "aria-busy": "true", children: [
|
|
495
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.image, "data-sf-cart-image": "", children: row.image ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: row.image.url, alt: row.image.alt ?? "", width: 80, height: 80, decoding: "async" }) : null }),
|
|
496
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineMain, "data-sf-cart-line-main": "", children: [
|
|
497
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineHead, "data-sf-cart-line-head": "", children: [
|
|
498
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.lineText, "data-sf-cart-line-text": "", children: [
|
|
499
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.lineTitle, "data-sf-cart-line-title": "", children: row.title }),
|
|
500
|
+
row.variantTitle && !isDefault(row.variantTitle) ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.variant, "data-sf-cart-variant": "", children: row.variantTitle }) : null
|
|
501
|
+
] }),
|
|
502
|
+
row.price ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.price, "data-sf-cart-price": "", children: money(row.price) }) : null
|
|
503
|
+
] }),
|
|
504
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: cx.variant, "data-sf-cart-pending-label": "", children: [
|
|
505
|
+
label,
|
|
506
|
+
" \xB7 \xD7",
|
|
507
|
+
row.quantity
|
|
508
|
+
] })
|
|
509
|
+
] })
|
|
510
|
+
] });
|
|
511
|
+
}
|
|
512
|
+
function OptimisticStatus({ kind, n, text }) {
|
|
513
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "data-sf-cart-status": "", style: VISUALLY_HIDDEN, children: kind === "added" ? `${text}${n % 2 ? "" : "\u200B"}` : "" });
|
|
514
|
+
}
|
|
515
|
+
var VISUALLY_HIDDEN = { position: "absolute", width: 1, height: 1, margin: -1, padding: 0, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap", border: 0 };
|
|
516
|
+
function OptimisticNotices({ notices: notices2, labels, classNames: cx }) {
|
|
517
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "alert", "aria-live": "assertive", "data-sf-cart-notices": "", className: cx.notices, children: notices2.map((n) => /* @__PURE__ */ jsxRuntime.jsxs("p", { "data-sf-cart-notice": "", children: [
|
|
518
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: labels.failed(n.title, n.reason) }),
|
|
519
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => cartOptimistic.dismissNotice(n.id), "aria-label": labels.dismiss, "data-sf-cart-notice-dismiss": "", children: /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\u2715" }) })
|
|
520
|
+
] }, n.id)) });
|
|
521
|
+
}
|
|
273
522
|
|
|
274
523
|
exports.CartDrawer = CartDrawer;
|
|
524
|
+
exports.DEFAULT_OPTIMISTIC_LABELS = DEFAULT_OPTIMISTIC_LABELS;
|
|
525
|
+
exports.cartOptimistic = cartOptimistic;
|
|
275
526
|
exports.closeCartDrawer = closeCartDrawer;
|
|
527
|
+
exports.defaultAddOutcome = defaultAddOutcome;
|
|
528
|
+
exports.describeAddFailure = describeAddFailure;
|
|
276
529
|
exports.handleCartLinkClick = handleCartLinkClick;
|
|
277
530
|
exports.isCartDrawerOpen = isCartDrawerOpen;
|
|
278
531
|
exports.openCartDrawer = openCartDrawer;
|
|
279
532
|
exports.toggleCartDrawer = toggleCartDrawer;
|
|
280
533
|
exports.useCartDrawer = useCartDrawer;
|
|
281
534
|
exports.useOpenCartDrawerWhenDone = useOpenCartDrawerWhenDone;
|
|
535
|
+
exports.useOptimisticAdd = useOptimisticAdd;
|
|
536
|
+
exports.useOptimisticCart = useOptimisticCart;
|
|
537
|
+
exports.useOptimisticCartCount = useOptimisticCartCount;
|
|
282
538
|
//# sourceMappingURL=cart.cjs.map
|
|
283
539
|
//# sourceMappingURL=cart.cjs.map
|