@asteby/storefront-react 0.18.0 → 0.20.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 +60 -0
- package/dist/account-server.cjs +405 -7
- package/dist/account-server.cjs.map +1 -1
- package/dist/account-server.d.cts +41 -1
- package/dist/account-server.d.ts +41 -1
- package/dist/account-server.js +73 -3
- package/dist/account-server.js.map +1 -1
- package/dist/account.cjs +1507 -4
- package/dist/account.cjs.map +1 -1
- package/dist/account.css +203 -0
- package/dist/account.d.cts +526 -6
- package/dist/account.d.ts +526 -6
- package/dist/account.js +1145 -2
- package/dist/account.js.map +1 -1
- package/dist/checkout.js +2 -2
- package/dist/chunk-PPQEUTXJ.js +260 -0
- package/dist/chunk-PPQEUTXJ.js.map +1 -0
- package/dist/chunk-TXEYBDFG.js +182 -0
- package/dist/chunk-TXEYBDFG.js.map +1 -0
- package/dist/fiscal.d.cts +34 -34
- package/dist/fiscal.d.ts +34 -34
- package/dist/fiscal.js +4 -258
- package/dist/fiscal.js.map +1 -1
- package/dist/payments.cjs +22 -3
- package/dist/payments.cjs.map +1 -1
- package/dist/payments.css +15 -14
- package/dist/payments.d.cts +4 -1
- package/dist/payments.d.ts +4 -1
- package/dist/payments.js +24 -5
- package/dist/payments.js.map +1 -1
- package/dist/voucher-document-SFWJCLTN.js +124 -0
- package/dist/voucher-document-SFWJCLTN.js.map +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -797,6 +797,66 @@ export default function Login() {
|
|
|
797
797
|
- `<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`.
|
|
798
798
|
- 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.
|
|
799
799
|
|
|
800
|
+
### Invitación posterior a la compra (`/pagar/gracias`)
|
|
801
|
+
|
|
802
|
+
`<PostPurchaseAccountPrompt>` (`@asteby/storefront-react/account`) invita al invitado a crear cuenta justo después de pagar. Sin estilos propios: `data-sf-ppa-*`, `classNames`, `locale`, `messages`, `form`, `headingLevel`. La tienda lo compone con su diseño y apunta su `action` a la ruta que monta `createAuthAction`.
|
|
803
|
+
|
|
804
|
+
```tsx
|
|
805
|
+
<PostPurchaseAccountPrompt
|
|
806
|
+
email={receipt.email} // del comprobante de la sesión, leído en el servidor
|
|
807
|
+
capabilities={capabilities} // loadAuthCapabilities
|
|
808
|
+
data={actionData} // resultado de createAuthAction
|
|
809
|
+
next="/cuenta/pedidos"
|
|
810
|
+
action="/cuenta/entrar"
|
|
811
|
+
form={Form}
|
|
812
|
+
/>
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
- El correo viaja como campo oculto y no se edita; solo se pide contraseña (o el código por correo si la tienda no tiene contraseñas). No pide dirección ni teléfono.
|
|
816
|
+
- Sin correo válido o sin forma de registrarse no pinta nada. Con verificación de correo la respuesta es la misma exista o no la cuenta (no enumera).
|
|
817
|
+
- El pedido de invitado hecho con ese correo (y su pago pendiente por ficha) aparece en «Mis pedidos» cuando el buzón queda verificado: `storefront_customers` filtra por `customer_id` o por correo VERIFICADO. En una tienda sin verificación de correo la cuenta nace sin verificar y no ve pedidos de invitado; el texto entonces no lo promete.
|
|
818
|
+
|
|
819
|
+
### Pantallas de pedidos (contrato 1.8)
|
|
820
|
+
|
|
821
|
+
Componentes **headless** de `@asteby/storefront-react/account` (+ `account.css` opcional y neutro, + `/account/server`). Sin estilos de marca: `data-sf-*`, `classNames`, variables CSS (`--sf-account-*`, `--sf-tone-*`), textos es/en con `locale` y `messages`, fechas con `timeZone` (pasa la zona de la tienda para que servidor y navegador impriman lo mismo). Todos los enums del servidor son abiertos: un valor desconocido se escribe con palabras genéricas, nunca como código.
|
|
822
|
+
|
|
823
|
+
| Componente | Qué pinta | Hooks de estilo |
|
|
824
|
+
|---|---|---|
|
|
825
|
+
| `OrderList` / `OrderCard` | Lista con estados `loading` (esqueletos de alto fijo), `error` (reintento), vacío útil y `ready`; cada tarjeta: número, fecha, estado, mini-progreso, envío, total y UNA acción | `data-sf-order`, `data-sf-order-status` |
|
|
826
|
+
| `OrderStatusBadge` | Estado en palabras + tono (`done/current/problem/muted`) | `data-sf-tone` |
|
|
827
|
+
| `OrderProgress` | Línea de tiempo pagado → preparación → enviado → en camino → entregado (variante recoger, incidencia escrita, `compact`) como `<ol>` con `aria-current="step"` | `data-sf-step`, `data-sf-step-state` |
|
|
828
|
+
| `NextActionBar` | «Rastrear envío», «Descargar factura», «Pagar ahora»… `href` solo `https` (rastreo) o el que dé la tienda (`hrefFor`) | `data-sf-action` |
|
|
829
|
+
| `ShipmentCard` | Paquetería (marca por `renderCarrierMark`), número con copiar, enlace de rastreo `https`, fecha prometida, eventos (últimos 3 + «Ver todo»). `test_mode` → `testMode="hide"` (omisión) o `"mark"` («De prueba», sin número ni enlace). La guía solo en devoluciones. Hueco del mapa: `renderMap({ shipment, map })` | `data-sf-shipment-*` |
|
|
830
|
+
| `InvoiceList` | Facturas y notas de crédito (bajo su factura); estados `issued/pending/cancelled/error`; descarga solo por `fileHref` (tu ruta autenticada) o `onDownload`; sandbox marcado «de prueba, sin validez» o `testMode="hide"`; sin documentos, `renderSummary` (p. ej. `InvoiceStatusCard`) | `data-sf-invoice-*` |
|
|
831
|
+
| `PendingPaymentCard` / `PendingPaymentList` | Ficha de efectivo: referencia/CLABE, copiar, código de barras Code 128 (SVG), vencimiento, imprimir y descargar (por `voucherHref` de tu servidor o armada en el navegador, con su código en un chunk aparte) | `data-sf-voucher-*` |
|
|
832
|
+
| `RefundList`, `PaymentSummary` | Reembolsos y pagos | `data-sf-refund`, `data-sf-payment` |
|
|
833
|
+
| `useCustomerOrders`, `useCustomerOrder(id, { poll })`, `usePendingPayments` | Carga con `status` (`loading/ready/error/not_found`), `reload`, `loadMore`; `initial` desde el loader (SSR). Los datos son del cliente (sesión) y del id que los pidieron: al cambiar de comprador, cerrar sesión o cambiar de pedido no se muestra nada del anterior; pasa un cliente estable (`useMemo`), uno nuevo por render recarga cada vez | — |
|
|
834
|
+
|
|
835
|
+
`degraded` (secciones que el servidor no pudo leer) se pasa a la sección: error recuperable con «Reintentar» y el resto del pedido se ve.
|
|
836
|
+
|
|
837
|
+
```tsx
|
|
838
|
+
// Detalle de pedido (loader: sf.customer.order(id))
|
|
839
|
+
<h1>Pedido {order.number}</h1>
|
|
840
|
+
<OrderStatusBadge order={order} />
|
|
841
|
+
<NextActionBar order={order} hrefFor={(a) => (a.kind === 'download_invoice' ? `/cuenta/pedidos/${order.id}/facturas/${a.ref}.pdf` : null)} />
|
|
842
|
+
<OrderProgress order={order} />
|
|
843
|
+
{order.shipments?.map((s) => <ShipmentCard key={s.id} shipment={s} headingLevel={2} renderCarrierMark={(x) => <BrandMark brand={x.carrier_logo_key} />} />)}
|
|
844
|
+
<InvoiceList invoices={order.invoices} summary={order.invoice} degraded={order.degraded?.includes('invoices')} onRetry={revalidate}
|
|
845
|
+
fileHref={(d, kind) => `/cuenta/pedidos/${order.id}/facturas/${d.id}.${kind}`} renderSummary={(s) => <InvoiceStatusCard invoice={s} requestHref="/cuenta/factura" />} />
|
|
846
|
+
<PaymentSummary payments={order.payments} /> <RefundList refunds={order.refunds} />
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
Rutas recurso de la tienda (`@asteby/storefront-react/account/server`), cada una en una línea; el navegador nunca ve el token del comprador ni una URL del PAC:
|
|
850
|
+
|
|
851
|
+
```ts
|
|
852
|
+
// account.order.$id.invoice.$doc.$kind.ts
|
|
853
|
+
export const loader = async ({ request, params }) => invoiceFileResponse({ storefront: await sfFor(request), orderId: params.id, invoiceId: params.doc, kind: params.kind })
|
|
854
|
+
// account.pending.$id.ficha.ts
|
|
855
|
+
export const loader = async ({ request, params }) => pendingVoucherResponse({ storefront: await sfFor(request), id: params.id, download: new URL(request.url).searchParams.get('descargar') === '1', shopName: 'Mi tienda' })
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
`invoiceFileResponse` sirve `attachment` con `Cache-Control: private, no-store`, `nosniff` y CSP `sandbox` (también en cada respuesta de error); el tipo sale de `kind`, no de lo que diga la API, y un archivo de más de `maxBytes` (10 MB por defecto) es un 502 genérico sin decodificarlo. `pendingVoucherResponse` solo entrega una ficha que está en la lista del comprador (si no, 404) con CSP cerrada y nonce por respuesta. Accesibilidad: WCAG 2.2 AA verificada con axe en las pruebas (contraste lo decide cada tienda con sus tokens), objetivos ≥ 44 px en el CSS opcional, `prefers-reduced-motion`. Presupuesto: todas las pantallas ≈ 13 KB gzip (prueba `size-budget`).
|
|
859
|
+
|
|
800
860
|
## Facturación y constancia fiscal (`/fiscal`)
|
|
801
861
|
|
|
802
862
|
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.
|
package/dist/account-server.cjs
CHANGED
|
@@ -2,7 +2,332 @@
|
|
|
2
2
|
|
|
3
3
|
var storefront = require('@asteby/storefront');
|
|
4
4
|
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __esm = (fn, res) => function __init() {
|
|
8
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// src/account/voucher.ts
|
|
16
|
+
function text(v) {
|
|
17
|
+
if (typeof v === "number" && Number.isFinite(v)) return String(v);
|
|
18
|
+
if (typeof v !== "string") return null;
|
|
19
|
+
const s = v.trim().slice(0, MAX);
|
|
20
|
+
return s || null;
|
|
21
|
+
}
|
|
22
|
+
function moneyOf(v) {
|
|
23
|
+
if (!v || typeof v !== "object") return null;
|
|
24
|
+
const m = v;
|
|
25
|
+
const amount = text(m.amount);
|
|
26
|
+
const cur = text(m.currency_code);
|
|
27
|
+
return amount && cur && Number.isFinite(Number(amount)) ? { amount, currency_code: cur.toUpperCase() } : null;
|
|
28
|
+
}
|
|
29
|
+
function voucherOf(p) {
|
|
30
|
+
const pay = p?.payment;
|
|
31
|
+
if (!pay || typeof pay !== "object") return null;
|
|
32
|
+
const v = {
|
|
33
|
+
method: text(pay.method ?? pay.provider),
|
|
34
|
+
reference: text(pay.reference),
|
|
35
|
+
barcode: text(pay.barcode),
|
|
36
|
+
clabe: text(pay.clabe),
|
|
37
|
+
expires_at: text(pay.expires_at) ?? text(p?.expires_at),
|
|
38
|
+
amount: moneyOf(pay.amount) ?? moneyOf(p?.total),
|
|
39
|
+
instructions: text(pay.instructions)
|
|
40
|
+
};
|
|
41
|
+
return v.reference || v.barcode || v.clabe ? v : null;
|
|
42
|
+
}
|
|
43
|
+
function barcodeValue(v) {
|
|
44
|
+
return v.barcode ?? (v.method?.toLowerCase() === "oxxo" ? v.reference : null);
|
|
45
|
+
}
|
|
46
|
+
function code128Modules(value) {
|
|
47
|
+
if (!value || value.length > 80) return null;
|
|
48
|
+
let codes;
|
|
49
|
+
if (/^\d+$/.test(value) && value.length % 2 === 0) {
|
|
50
|
+
codes = [105];
|
|
51
|
+
for (let i = 0; i < value.length; i += 2) codes.push(Number(value.slice(i, i + 2)));
|
|
52
|
+
} else {
|
|
53
|
+
codes = [104];
|
|
54
|
+
for (const ch of value) {
|
|
55
|
+
const c = ch.charCodeAt(0);
|
|
56
|
+
if (c < 32 || c > 126) return null;
|
|
57
|
+
codes.push(c - 32);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let sum = codes[0];
|
|
61
|
+
for (let i = 1; i < codes.length; i++) sum += codes[i] * i;
|
|
62
|
+
codes.push(sum % 103);
|
|
63
|
+
return codes.map((c) => BARS[c]).join("") + STOP;
|
|
64
|
+
}
|
|
65
|
+
function barcodeGeometry(value, opts = {}) {
|
|
66
|
+
const bits = code128Modules(value);
|
|
67
|
+
if (!bits) return null;
|
|
68
|
+
const mod = opts.module ?? 2;
|
|
69
|
+
const quiet = 10;
|
|
70
|
+
const rects = [];
|
|
71
|
+
for (let i = 0; i < bits.length; ) {
|
|
72
|
+
if (bits[i] === "0") {
|
|
73
|
+
i++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let j = i;
|
|
77
|
+
while (j < bits.length && bits[j] === "1") j++;
|
|
78
|
+
rects.push({ x: (quiet + i) * mod, w: (j - i) * mod });
|
|
79
|
+
i = j;
|
|
80
|
+
}
|
|
81
|
+
return { width: (bits.length + quiet * 2) * mod, height: opts.height ?? 70, rects };
|
|
82
|
+
}
|
|
83
|
+
var MAX, BARS, STOP;
|
|
84
|
+
var init_voucher = __esm({
|
|
85
|
+
"src/account/voucher.ts"() {
|
|
86
|
+
MAX = 256;
|
|
87
|
+
BARS = [
|
|
88
|
+
"11011001100",
|
|
89
|
+
"11001101100",
|
|
90
|
+
"11001100110",
|
|
91
|
+
"10010011000",
|
|
92
|
+
"10010001100",
|
|
93
|
+
"10001001100",
|
|
94
|
+
"10011001000",
|
|
95
|
+
"10011000100",
|
|
96
|
+
"10001100100",
|
|
97
|
+
"11001001000",
|
|
98
|
+
"11001000100",
|
|
99
|
+
"11000100100",
|
|
100
|
+
"10110011100",
|
|
101
|
+
"10011011100",
|
|
102
|
+
"10011001110",
|
|
103
|
+
"10111001100",
|
|
104
|
+
"10011101100",
|
|
105
|
+
"10011100110",
|
|
106
|
+
"11001110010",
|
|
107
|
+
"11001011100",
|
|
108
|
+
"11001001110",
|
|
109
|
+
"11011100100",
|
|
110
|
+
"11001110100",
|
|
111
|
+
"11101101110",
|
|
112
|
+
"11101001100",
|
|
113
|
+
"11100101100",
|
|
114
|
+
"11100100110",
|
|
115
|
+
"11101100100",
|
|
116
|
+
"11100110100",
|
|
117
|
+
"11100110010",
|
|
118
|
+
"11011011000",
|
|
119
|
+
"11011000110",
|
|
120
|
+
"11000110110",
|
|
121
|
+
"10100011000",
|
|
122
|
+
"10001011000",
|
|
123
|
+
"10001000110",
|
|
124
|
+
"10110001000",
|
|
125
|
+
"10001101000",
|
|
126
|
+
"10001100010",
|
|
127
|
+
"11010001000",
|
|
128
|
+
"11000101000",
|
|
129
|
+
"11000100010",
|
|
130
|
+
"10110111000",
|
|
131
|
+
"10110001110",
|
|
132
|
+
"10001101110",
|
|
133
|
+
"10111011000",
|
|
134
|
+
"10111000110",
|
|
135
|
+
"10001110110",
|
|
136
|
+
"11101110110",
|
|
137
|
+
"11010001110",
|
|
138
|
+
"11000101110",
|
|
139
|
+
"11011101000",
|
|
140
|
+
"11011100010",
|
|
141
|
+
"11011101110",
|
|
142
|
+
"11101011000",
|
|
143
|
+
"11101000110",
|
|
144
|
+
"11100010110",
|
|
145
|
+
"11101101000",
|
|
146
|
+
"11101100010",
|
|
147
|
+
"11100011010",
|
|
148
|
+
"11101111010",
|
|
149
|
+
"11001000010",
|
|
150
|
+
"11110001010",
|
|
151
|
+
"10100110000",
|
|
152
|
+
"10100001100",
|
|
153
|
+
"10010110000",
|
|
154
|
+
"10010000110",
|
|
155
|
+
"10000101100",
|
|
156
|
+
"10000100110",
|
|
157
|
+
"10110010000",
|
|
158
|
+
"10110000100",
|
|
159
|
+
"10011010000",
|
|
160
|
+
"10011000010",
|
|
161
|
+
"10000110100",
|
|
162
|
+
"10000110010",
|
|
163
|
+
"11000010010",
|
|
164
|
+
"11001010000",
|
|
165
|
+
"11110111010",
|
|
166
|
+
"11000010100",
|
|
167
|
+
"10001111010",
|
|
168
|
+
"10100111100",
|
|
169
|
+
"10010111100",
|
|
170
|
+
"10010011110",
|
|
171
|
+
"10111100100",
|
|
172
|
+
"10011110100",
|
|
173
|
+
"10011110010",
|
|
174
|
+
"11110100100",
|
|
175
|
+
"11110010100",
|
|
176
|
+
"11110010010",
|
|
177
|
+
"11011011110",
|
|
178
|
+
"11011110110",
|
|
179
|
+
"11110110110",
|
|
180
|
+
"10101111000",
|
|
181
|
+
"10100011110",
|
|
182
|
+
"10001011110",
|
|
183
|
+
"10111101000",
|
|
184
|
+
"10111100010",
|
|
185
|
+
"11110101000",
|
|
186
|
+
"11110100010",
|
|
187
|
+
"10111011110",
|
|
188
|
+
"10111101110",
|
|
189
|
+
"11101011110",
|
|
190
|
+
"11110101110",
|
|
191
|
+
"11010000100",
|
|
192
|
+
"11010010000",
|
|
193
|
+
"11010011100"
|
|
194
|
+
];
|
|
195
|
+
STOP = "1100011101011";
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/account/voucher-document.ts
|
|
200
|
+
var voucher_document_exports = {};
|
|
201
|
+
__export(voucher_document_exports, {
|
|
202
|
+
esc: () => esc,
|
|
203
|
+
renderVoucherDocument: () => renderVoucherDocument
|
|
204
|
+
});
|
|
205
|
+
function esc(s) {
|
|
206
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
207
|
+
}
|
|
208
|
+
function formatMoney(m, locale) {
|
|
209
|
+
if (!m) return "";
|
|
210
|
+
try {
|
|
211
|
+
return new Intl.NumberFormat(locale, { style: "currency", currency: m.currency_code }).format(Number(m.amount));
|
|
212
|
+
} catch {
|
|
213
|
+
return `${m.amount} ${m.currency_code}`;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function formatDate(iso, locale, timeZone) {
|
|
217
|
+
if (!iso) return "";
|
|
218
|
+
const d = new Date(iso);
|
|
219
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
220
|
+
try {
|
|
221
|
+
return d.toLocaleString(locale, { dateStyle: "long", timeStyle: "short", ...timeZone ? { timeZone } : {} });
|
|
222
|
+
} catch {
|
|
223
|
+
return d.toISOString();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function barcodeSvg(value, label) {
|
|
227
|
+
const g = barcodeGeometry(value);
|
|
228
|
+
if (!g) return null;
|
|
229
|
+
const rects = g.rects.map((r) => `<rect x="${r.x}" y="0" width="${r.w}" height="${g.height}"/>`).join("");
|
|
230
|
+
return `<svg viewBox="0 0 ${g.width} ${g.height}" width="${g.width}" height="${g.height}" role="img" aria-label="${esc(label)} ${esc(value)}" shape-rendering="crispEdges" fill="#000">${rects}</svg>`;
|
|
231
|
+
}
|
|
232
|
+
function renderVoucherDocument(v, opts = {}) {
|
|
233
|
+
const locale = opts.locale ?? "es-MX";
|
|
234
|
+
const t = locale.toLowerCase().startsWith("en") ? EN : ES;
|
|
235
|
+
const shop = opts.shopName || "";
|
|
236
|
+
const method = v.method ? Object.prototype.hasOwnProperty.call(t.methods, v.method.toLowerCase()) ? t.methods[v.method.toLowerCase()] : v.method : t.cash;
|
|
237
|
+
const isSpei = !!v.clabe && !v.reference;
|
|
238
|
+
const code = barcodeValue(v);
|
|
239
|
+
const svg = code ? barcodeSvg(code, t.barcode) : null;
|
|
240
|
+
const expires = formatDate(v.expires_at, locale, opts.timeZone);
|
|
241
|
+
const field = (label, value) => value ? `<div class="box"><div class="label">${esc(label)}</div><div class="value">${esc(value)}</div></div>` : "";
|
|
242
|
+
const steps = isSpei ? t.stepsSpei : t.stepsCash;
|
|
243
|
+
const subtitle = [shop, method, opts.number ? `${t.order} ${opts.number}` : ""].filter(Boolean).map(esc).join(" \xB7 ");
|
|
244
|
+
return `<!doctype html>
|
|
245
|
+
<html lang="${esc(locale)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
246
|
+
<meta name="robots" content="noindex"><title>${esc(t.title)}${shop ? ` - ${esc(shop)}` : ""}</title><style>${CSS}</style></head>
|
|
247
|
+
<body><main>
|
|
248
|
+
<div class="head"><div><h1>${esc(t.title)}</h1><div class="muted">${subtitle}</div></div>
|
|
249
|
+
${v.amount ? `<div><div class="label" style="text-align:right">${esc(t.total)}</div><div class="amount">${esc(formatMoney(v.amount, locale))}</div></div>` : ""}</div>
|
|
250
|
+
${field(t.reference, v.reference)}${field(t.clabe, v.clabe)}
|
|
251
|
+
${svg ? `<div class="bar">${svg}<div class="muted" style="font-family:ui-monospace,monospace">${esc(code)}</div></div>` : ""}
|
|
252
|
+
${expires ? `<p><strong>${esc(t.expires)}:</strong> ${esc(expires)}. ${esc(t.expiresNote)}</p>` : ""}
|
|
253
|
+
<ol class="steps">${steps.map((s) => `<li>${esc(s)}</li>`).join("")}</ol>
|
|
254
|
+
${v.instructions ? `<p class="note pre muted">${esc(v.instructions)}</p>` : ""}
|
|
255
|
+
<p class="note muted">${esc(t.footer)}</p>
|
|
256
|
+
${opts.printButton ? `<div class="actions"><button type="button" id="print">${esc(t.print)}</button></div>` : ""}
|
|
257
|
+
</main>
|
|
258
|
+
${opts.printButton || opts.autoPrint ? `<script${opts.nonce ? ` nonce="${esc(opts.nonce)}"` : ""}>(function(){var b=document.getElementById("print");if(b)b.addEventListener("click",function(){window.print()});${opts.autoPrint ? 'window.addEventListener("load",function(){setTimeout(function(){window.print()},300)});' : ""}})();</script>` : ""}
|
|
259
|
+
</body></html>`;
|
|
260
|
+
}
|
|
261
|
+
var ES, EN, CSS;
|
|
262
|
+
var init_voucher_document = __esm({
|
|
263
|
+
"src/account/voucher-document.ts"() {
|
|
264
|
+
init_voucher();
|
|
265
|
+
ES = {
|
|
266
|
+
title: "Ficha de pago",
|
|
267
|
+
total: "Total a pagar",
|
|
268
|
+
reference: "Referencia",
|
|
269
|
+
clabe: "CLABE",
|
|
270
|
+
expires: "Vence",
|
|
271
|
+
expiresNote: "Pasada esa fecha la ficha deja de ser v\xE1lida.",
|
|
272
|
+
order: "Pedido",
|
|
273
|
+
barcode: "C\xF3digo de barras",
|
|
274
|
+
print: "Imprimir ficha",
|
|
275
|
+
footer: "Tu pedido se genera cuando se confirma el pago. Esta ficha es de un solo uso; no la compartas.",
|
|
276
|
+
stepsCash: [
|
|
277
|
+
"Acude a la tienda OXXO (o el comercio indicado) y di que vas a hacer un pago de servicio en efectivo.",
|
|
278
|
+
"Muestra el c\xF3digo de barras o dicta la referencia y paga el monto exacto.",
|
|
279
|
+
"Conserva tu comprobante: confirmaremos tu pago autom\xE1ticamente."
|
|
280
|
+
],
|
|
281
|
+
stepsSpei: ["Entra a la banca en l\xEDnea o app de tu banco.", "Haz una transferencia SPEI a la CLABE indicada por el monto exacto.", "Conserva tu comprobante: confirmaremos tu pago autom\xE1ticamente."],
|
|
282
|
+
methods: { oxxo: "OXXO", spei: "Transferencia SPEI", bank_transfer: "Transferencia bancaria", bank_deposit: "Dep\xF3sito bancario", cash: "Pago en efectivo" },
|
|
283
|
+
cash: "Pago en efectivo"
|
|
284
|
+
};
|
|
285
|
+
EN = {
|
|
286
|
+
title: "Payment voucher",
|
|
287
|
+
total: "Amount to pay",
|
|
288
|
+
reference: "Reference",
|
|
289
|
+
clabe: "CLABE",
|
|
290
|
+
expires: "Expires",
|
|
291
|
+
expiresNote: "After that date the voucher is no longer valid.",
|
|
292
|
+
order: "Order",
|
|
293
|
+
barcode: "Barcode",
|
|
294
|
+
print: "Print voucher",
|
|
295
|
+
footer: "Your order is created when the payment is confirmed. This voucher is single-use; do not share it.",
|
|
296
|
+
stepsCash: [
|
|
297
|
+
"Go to an OXXO store (or the store indicated) and say you want to make a cash service payment.",
|
|
298
|
+
"Show the barcode or read out the reference and pay the exact amount.",
|
|
299
|
+
"Keep your receipt: we will confirm your payment automatically."
|
|
300
|
+
],
|
|
301
|
+
stepsSpei: ["Open your bank online or app.", "Make a SPEI transfer to the CLABE below for the exact amount.", "Keep your receipt: we will confirm your payment automatically."],
|
|
302
|
+
methods: { oxxo: "OXXO", spei: "SPEI transfer", bank_transfer: "Bank transfer", bank_deposit: "Bank deposit", cash: "Cash payment" },
|
|
303
|
+
cash: "Cash payment"
|
|
304
|
+
};
|
|
305
|
+
CSS = `
|
|
306
|
+
@page { size: A4; margin: 18mm; }
|
|
307
|
+
* { box-sizing: border-box; }
|
|
308
|
+
body { margin: 0; font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color: #111; background: #fff; }
|
|
309
|
+
main { max-width: 174mm; margin: 0 auto; padding: 24px 16px; }
|
|
310
|
+
h1 { font-size: 22px; margin: 0; }
|
|
311
|
+
.muted { color: #555; }
|
|
312
|
+
.head { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; border-bottom: 3px solid #111; padding-bottom: 12px; }
|
|
313
|
+
.amount { font-size: 28px; font-weight: 700; text-align: right; }
|
|
314
|
+
.box { border: 1px solid #111; padding: 14px 16px; margin-top: 18px; }
|
|
315
|
+
.label { font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: #555; }
|
|
316
|
+
.value { font: 700 22px/1.3 ui-monospace, "SF Mono", Menlo, Consolas, monospace; word-break: break-all; letter-spacing: .04em; }
|
|
317
|
+
.bar { margin-top: 18px; text-align: center; }
|
|
318
|
+
.bar svg { max-width: 100%; height: auto; }
|
|
319
|
+
.steps { margin: 18px 0 0; padding-left: 20px; }
|
|
320
|
+
.note { margin-top: 18px; font-size: 12px; }
|
|
321
|
+
.pre { white-space: pre-line; }
|
|
322
|
+
.actions { margin-top: 24px; display: flex; gap: 12px; }
|
|
323
|
+
.actions button { font: inherit; padding: 10px 18px; border: 1px solid #111; background: #111; color: #fff; cursor: pointer; }
|
|
324
|
+
@media print { .actions { display: none; } main { padding: 0; } }
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
5
329
|
// src/account/server.ts
|
|
330
|
+
init_voucher();
|
|
6
331
|
var SAFE_AUTH_CAPABILITIES = Object.freeze({
|
|
7
332
|
password: true,
|
|
8
333
|
email_verification: false,
|
|
@@ -102,15 +427,15 @@ function createAuthAction(options) {
|
|
|
102
427
|
return async function authAction({ request }) {
|
|
103
428
|
const form = await request.formData().catch(() => null);
|
|
104
429
|
if (!form) return fail("validation");
|
|
105
|
-
const
|
|
430
|
+
const text2 = (name) => {
|
|
106
431
|
const v = form.get(name);
|
|
107
432
|
return typeof v === "string" ? v : "";
|
|
108
433
|
};
|
|
109
|
-
const intent =
|
|
110
|
-
const email =
|
|
111
|
-
const password =
|
|
112
|
-
const code =
|
|
113
|
-
const next = safeNext(
|
|
434
|
+
const intent = text2("intent");
|
|
435
|
+
const email = text2("email").trim();
|
|
436
|
+
const password = text2("password");
|
|
437
|
+
const code = text2("code").replace(/\s+/g, "");
|
|
438
|
+
const next = safeNext(text2("next") || null);
|
|
114
439
|
const ip = options.clientIp?.(request);
|
|
115
440
|
const w = ip ? { clientIp: ip } : void 0;
|
|
116
441
|
const carry = { email };
|
|
@@ -131,7 +456,7 @@ function createAuthAction(options) {
|
|
|
131
456
|
if (!passwordOn) return off();
|
|
132
457
|
if (!password) return fail("validation", carry, "password");
|
|
133
458
|
const res = await sf.customer.register(
|
|
134
|
-
{ email, password, ...
|
|
459
|
+
{ email, password, ...text2("first_name").trim() ? { first_name: text2("first_name").trim() } : {}, ...text2("last_name").trim() ? { last_name: text2("last_name").trim() } : {} },
|
|
135
460
|
w
|
|
136
461
|
);
|
|
137
462
|
return res.status === "created" ? await signedIn(res.session, next) : { ok: true, step: "verify", email };
|
|
@@ -176,10 +501,83 @@ function createAuthAction(options) {
|
|
|
176
501
|
}
|
|
177
502
|
};
|
|
178
503
|
}
|
|
504
|
+
var LOCKED_HEADERS = {
|
|
505
|
+
"Cache-Control": "private, no-store",
|
|
506
|
+
"X-Content-Type-Options": "nosniff",
|
|
507
|
+
"Referrer-Policy": "no-referrer",
|
|
508
|
+
"X-Robots-Tag": "noindex"
|
|
509
|
+
};
|
|
510
|
+
var TEXT_HEADERS = { ...LOCKED_HEADERS, "Content-Type": "text/plain; charset=utf-8", "Content-Security-Policy": "default-src 'none'; sandbox" };
|
|
511
|
+
var textResponse = (status, body, extra = {}) => new Response(body, { status, headers: { ...TEXT_HEADERS, ...extra } });
|
|
512
|
+
var MAX_INVOICE_FILE_BYTES = 10 * 1024 * 1024;
|
|
513
|
+
function failureResponse(err) {
|
|
514
|
+
if (storefront.isStorefrontError(err)) {
|
|
515
|
+
if (err.status === 401) return textResponse(401, "Sesion vencida");
|
|
516
|
+
if (err.status === 404) return textResponse(404, "No encontrado");
|
|
517
|
+
if (err.status === 429) {
|
|
518
|
+
const wait = typeof err.retryAfter === "number" && Number.isFinite(err.retryAfter) && err.retryAfter > 0 ? Math.min(3600, Math.ceil(err.retryAfter)) : null;
|
|
519
|
+
return textResponse(429, "Demasiados intentos", wait ? { "Retry-After": String(wait) } : {});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return textResponse(502, "No pudimos obtener el archivo");
|
|
523
|
+
}
|
|
524
|
+
var FILE_TYPES = { pdf: "application/pdf", xml: "application/xml" };
|
|
525
|
+
async function invoiceFileResponse({ storefront: storefront$1, orderId, invoiceId, kind, signal, maxBytes = MAX_INVOICE_FILE_BYTES }) {
|
|
526
|
+
if (kind !== "pdf" && kind !== "xml") return textResponse(404, "No encontrado");
|
|
527
|
+
if (!orderId || !invoiceId) return textResponse(404, "No encontrado");
|
|
528
|
+
try {
|
|
529
|
+
const file = await storefront$1.customer.invoiceFile(orderId, invoiceId, kind, signal ? { signal } : void 0);
|
|
530
|
+
if (!file) return textResponse(404, "No encontrado");
|
|
531
|
+
if (file.content_base64.length > Math.ceil(maxBytes / 3) * 4 * 1.1 + 64) return textResponse(502, "No pudimos obtener el archivo");
|
|
532
|
+
const bytes = storefront.invoiceFileBytes(file);
|
|
533
|
+
if (bytes.byteLength > maxBytes) return textResponse(502, "No pudimos obtener el archivo");
|
|
534
|
+
const name = storefront.safeInvoiceFilename(file.filename, kind === "pdf" ? "factura.pdf" : "factura.xml");
|
|
535
|
+
const filename = name.toLowerCase().endsWith(`.${kind}`) ? name : `${name}.${kind}`;
|
|
536
|
+
return new Response(bytes, {
|
|
537
|
+
status: 200,
|
|
538
|
+
headers: {
|
|
539
|
+
"Content-Type": FILE_TYPES[kind],
|
|
540
|
+
"Content-Length": String(bytes.byteLength),
|
|
541
|
+
"Content-Disposition": `attachment; filename="${filename}"`,
|
|
542
|
+
...LOCKED_HEADERS,
|
|
543
|
+
"Content-Security-Policy": "default-src 'none'; sandbox"
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
} catch (err) {
|
|
547
|
+
return failureResponse(err);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function pendingVoucherResponse({ storefront, id, download = false, shopName, locale, timeZone, signal }) {
|
|
551
|
+
try {
|
|
552
|
+
const page = await storefront.customer.pendingPayments({ first: 100 }, signal ? { signal } : void 0);
|
|
553
|
+
const item = page.nodes.find((p) => p.id === id);
|
|
554
|
+
const voucher = item ? voucherOf(item) : null;
|
|
555
|
+
if (!item || !voucher) return textResponse(404, "Ficha no encontrada");
|
|
556
|
+
const nonce = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16))));
|
|
557
|
+
const { renderVoucherDocument: renderVoucherDocument2 } = await Promise.resolve().then(() => (init_voucher_document(), voucher_document_exports));
|
|
558
|
+
const html = renderVoucherDocument2(voucher, { ...shopName ? { shopName } : {}, ...locale ? { locale } : {}, ...timeZone ? { timeZone } : {}, printButton: !download, nonce });
|
|
559
|
+
const safeId = id.replace(/[^A-Za-z0-9-]/g, "").slice(0, 8) || "ficha";
|
|
560
|
+
return new Response(html, {
|
|
561
|
+
status: 200,
|
|
562
|
+
headers: {
|
|
563
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
564
|
+
...LOCKED_HEADERS,
|
|
565
|
+
// The download carries no script, so its policy allows none.
|
|
566
|
+
"Content-Security-Policy": `default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src ${download ? "'none'" : `'nonce-${nonce}'`}; base-uri 'none'; form-action 'none'; frame-ancestors 'self'`,
|
|
567
|
+
...download ? { "Content-Disposition": `attachment; filename="ficha-de-pago-${safeId}.html"` } : {}
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
} catch (err) {
|
|
571
|
+
return failureResponse(err);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
179
574
|
|
|
575
|
+
exports.MAX_INVOICE_FILE_BYTES = MAX_INVOICE_FILE_BYTES;
|
|
180
576
|
exports.SAFE_AUTH_CAPABILITIES = SAFE_AUTH_CAPABILITIES;
|
|
181
577
|
exports.createAuthAction = createAuthAction;
|
|
578
|
+
exports.invoiceFileResponse = invoiceFileResponse;
|
|
182
579
|
exports.loadAuthCapabilities = loadAuthCapabilities;
|
|
580
|
+
exports.pendingVoucherResponse = pendingVoucherResponse;
|
|
183
581
|
exports.safeNextPath = safeNextPath;
|
|
184
582
|
//# sourceMappingURL=account-server.cjs.map
|
|
185
583
|
//# sourceMappingURL=account-server.cjs.map
|