@ecomiq/storefront 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +358 -104
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,127 +1,363 @@
1
- # Login de clientes
1
+ # @ecomiq/storefront
2
+
3
+ SDK de temas de Ecomiq para React Router 8. Trae los loaders y actions de
4
+ catálogo, carrito, checkout y cuenta de cliente, las secciones editables y los
5
+ plugins de Vite que corren la tienda en un Worker de Cloudflare. Los datos
6
+ vienen de tu tienda en Ecomiq; el diseño es tuyo.
7
+
8
+ - [Empezar](#empezar)
9
+ - [Archivos de la plataforma](#archivos-de-la-plataforma)
10
+ - [Entradas del paquete](#entradas-del-paquete)
11
+ - [Uso básico](#uso-básico)
12
+ - [Login de clientes](#login-de-clientes)
13
+ - [Registro de clientes](#registro-de-clientes)
14
+ - [Enlaces de catálogo](#enlaces-de-catálogo)
15
+ - [Logos de medios de pago](#logos-de-medios-de-pago)
16
+ - [Medición y consentimiento](#medición-y-consentimiento)
17
+ - [Secciones de Ecomiq](#secciones-de-ecomiq)
18
+ - [Temas publicados](#temas-publicados)
19
+
20
+ ## Empezar
21
+
22
+ La forma más corta es bajar el código de tu tienda, que ya trae este paquete:
23
+
24
+ ```bash
25
+ npm create ecomiq@latest mi-tienda
26
+ cd mi-tienda && npm install
27
+ npx ecomiq dev
28
+ ```
2
29
 
3
- La tienda elige el modo de acceso en Admin, en **Configuración → Compra → Cuentas de clientes**. El SDK consulta esa configuración: `hosted` redirige a Identity y `storefront` muestra el formulario del tema. Ambos modos usan las mismas cuentas y sesión de cliente.
30
+ Para agregarlo a un proyecto React Router existente:
4
31
 
5
- La ruta `/account/login` integra dos helpers:
32
+ ```bash
33
+ npm install @ecomiq/storefront @ecomiq/core react react-dom react-router
34
+ ```
6
35
 
7
- ```tsx
8
- import { CustomerLoginForm, startCustomerLogin, submitCustomerLogin } from "@ecomiq/storefront";
9
- import type { Route } from "./+types/login";
36
+ | Requisito | Versión |
37
+ | ------------ | ---------------------------------------------- |
38
+ | Node.js | 22.22 o superior |
39
+ | React | 19 |
40
+ | React Router | 8, en modo framework con SSR |
41
+ | Runtime | Cloudflare Workers (`@cloudflare/vite-plugin`) |
10
42
 
11
- export function loader({ request, context }: Route.LoaderArgs) {
12
- return startCustomerLogin(request, context);
43
+ `react` y `react-router` son dependencias peer: el SDK usa las del tema.
44
+
45
+ Guía completa: <https://docs.ecomiq.pe/es/docs/guides/storefront/themes>
46
+
47
+ ## Archivos de la plataforma
48
+
49
+ `ecomiq init` y el editor de Ecomiq escriben estos archivos; no los edites en
50
+ un tema.
51
+
52
+ ```ts title="vite.config.ts"
53
+ import { publicAssets, catalogLinks, paymentLogos, sectionLoaders, themeUi } from "@ecomiq/storefront/vite";
54
+ import { fileURLToPath } from "node:url";
55
+ import { cloudflare } from "@cloudflare/vite-plugin";
56
+ import { reactRouter } from "@react-router/dev/vite";
57
+ import tailwindcss from "@tailwindcss/vite";
58
+ import { defineConfig } from "vite";
59
+
60
+ export default defineConfig({
61
+ plugins: [publicAssets(), catalogLinks(), sectionLoaders(), paymentLogos(), themeUi(), cloudflare({ viteEnvironment: { name: "ssr" } }), tailwindcss(), reactRouter()],
62
+ resolve: { alias: { "~": fileURLToPath(new URL("./app", import.meta.url)) } },
63
+ build: { rolldownOptions: { output: { codeSplitting: { groups: [{ name: "icons", test: /[\\/]node_modules[\\/](?:@phosphor-icons|@heroicons|@mdi|@remixicon|lucide-react|iconoir-react)[\\/]/ }] } } } },
64
+ });
65
+ ```
66
+
67
+ ```ts title="workers/app.ts"
68
+ import { createRequestHandler } from "@ecomiq/storefront";
69
+
70
+ export default createRequestHandler(() => import("virtual:react-router/server-build"), import.meta.env.MODE);
71
+ ```
72
+
73
+ `wrangler.jsonc` define la tienda en `vars`: `ECOMIQ_STORE` (el slug) y
74
+ `ECOMIQ_API_URL` (la API Storefront, `https://storefront.ecomiq.pe`). Los tipos
75
+ de los módulos virtuales del SDK se cargan con
76
+ `/// <reference types="@ecomiq/storefront/vite-client" />`.
77
+
78
+ | Plugin | Qué hace |
79
+ | ------------------ | ----------------------------------------------------------------------------------------------------------- |
80
+ | `publicAssets()` | Versiona las imágenes y fuentes de `public/` y, al compilar, genera sus anchos en WebP. |
81
+ | `catalogLinks()` | Genera `virtual:ecomiq-links` con los enlaces editables del tema. |
82
+ | `sectionLoaders()` | Suma al loader de cada ruta el `load` de las secciones que usa y aplica las plantillas de `app/templates/`. |
83
+ | `paymentLogos()` | Escribe los SVG de medios de pago en el build. |
84
+ | `themeUi()` | Lleva `@theme/ui/*` a `app/components/ui/*` del tema. |
85
+
86
+ ## Entradas del paquete
87
+
88
+ | Import | Para qué |
89
+ | ---------------------------------- | ---------------------------------------------------------------- |
90
+ | `@ecomiq/storefront` | Cliente de la tienda, loaders de páginas, sesión, enlaces y SEO. |
91
+ | `@ecomiq/storefront/vite` | Plugins de Vite. |
92
+ | `@ecomiq/storefront/cart` | Carrito: `CartForm`, `useCart`, drawer y carrito optimista. |
93
+ | `@ecomiq/storefront/checkout` | Checkout, medios de pago y comprobante. |
94
+ | `@ecomiq/storefront/account` | Cuenta: pedidos, direcciones y perfil. |
95
+ | `@ecomiq/storefront/order` | Estado y pago de un pedido. |
96
+ | `@ecomiq/storefront/analytics` | Medición, píxeles y aviso de consentimiento. |
97
+ | `@ecomiq/storefront/sections/<id>` | [Secciones de Ecomiq](#secciones-de-ecomiq) listas para usar. |
98
+ | `@ecomiq/storefront/vite-client` | Tipos de los módulos virtuales. |
99
+
100
+ ## Uso básico
101
+
102
+ ### Loader raíz
103
+
104
+ `store(context)` devuelve el cliente de la tienda configurada en el Worker.
105
+ `readStorefrontLayout` lee la configuración, las colecciones y los enlaces del
106
+ tema en una sola pasada.
107
+
108
+ ```tsx title="app/root.tsx"
109
+ import manifestLinks from "virtual:ecomiq-links";
110
+ import { readCart, readStorefrontLayout, store } from "@ecomiq/storefront";
111
+ import { readAnalytics } from "@ecomiq/storefront/analytics";
112
+ import type { Route } from "./+types/root";
113
+
114
+ export { shouldRevalidateCatalogRoot as shouldRevalidate } from "@ecomiq/storefront";
115
+
116
+ export async function loader({ request, context }: Route.LoaderArgs) {
117
+ const client = store(context);
118
+ const [{ settings, storefrontLinks }, cart] = await Promise.all([
119
+ readStorefrontLayout({ client, links: manifestLinks }),
120
+ readCart(request, client),
121
+ ]);
122
+ return {
123
+ storefrontLinks,
124
+ publishedPages: settings.cms.pages.map((page) => `/pages/${page.slug}`),
125
+ analytics: readAnalytics(context, settings, cart),
126
+ cart,
127
+ };
13
128
  }
129
+ ```
14
130
 
15
- export function action({ request, context }: Route.ActionArgs) {
16
- return submitCustomerLogin(request, context);
131
+ ### Una sección editable
132
+
133
+ Una sección exporta `settings` con lo que el comerciante cambia en el editor.
134
+ El loader de la ruta lee esos ajustes; nunca escribas a mano precios, stock ni
135
+ productos.
136
+
137
+ ```tsx title="app/components/sections/new-arrivals.tsx"
138
+ import type { ProductSummary } from "@ecomiq/core";
139
+ import { money, setting, StorefrontLink } from "@ecomiq/storefront";
140
+
141
+ export const settings = {
142
+ arrivals: {
143
+ title: setting.text("Recién llegados", "Título"),
144
+ collection: setting.collection("novedades", "Colección"),
145
+ limit: setting.number(8, "Cantidad"),
146
+ link: setting.link("/collections/novedades", "Enlace"),
147
+ },
148
+ };
149
+
150
+ type Props = { products: ProductSummary[]; currency: string; language: string };
151
+
152
+ export function NewArrivals({ products, currency, language }: Props) {
153
+ return (
154
+ <section>
155
+ <h2>{settings.arrivals.title}</h2>
156
+ <StorefrontLink to={settings.arrivals.link}>Ver todo</StorefrontLink>
157
+ <ul>
158
+ {products.map((product) => (
159
+ <li key={product.id}>
160
+ {product.name} · {money(product.price, currency, language)}
161
+ </li>
162
+ ))}
163
+ </ul>
164
+ </section>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ```tsx title="app/routes/home.tsx"
170
+ import { pickProducts, store } from "@ecomiq/storefront";
171
+ import { NewArrivals, settings } from "~/components/sections/new-arrivals";
172
+ import type { Route } from "./+types/home";
173
+
174
+ export async function loader({ context }: Route.LoaderArgs) {
175
+ const client = store(context);
176
+ const [shop, products] = await Promise.all([
177
+ client.getSettings(),
178
+ pickProducts(client, { collection: settings.arrivals.collection, limit: settings.arrivals.limit }),
179
+ ]);
180
+ return { products, currency: shop.store.currency, language: shop.store.language };
181
+ }
182
+
183
+ export default function Home({ loaderData }: Route.ComponentProps) {
184
+ return <NewArrivals {...loaderData} />;
17
185
  }
18
186
  ```
19
187
 
20
- El componente de la ruta puede usar `CustomerLoginForm` con `data={actionData ?? loaderData}`. Su prop `classNames` define `form`, `field`, `label`, `input`, `button`, `secondary`, `error` y `hint`; la página, tipografía, colores y controles conservan el diseño del tema. Los 13 temas incluidos tienen esta integración.
188
+ `pickProducts` acepta `collection`, `category`, `products`, `limit` y `sort`.
189
+ Los ajustes disponibles son `setting.text`, `image`, `link`, `collection`,
190
+ `category`, `product`, `products`, `number` y `toggle`. El primer argumento es
191
+ el valor inicial y debe ser literal; el segundo, la etiqueta que ve el
192
+ comerciante.
21
193
 
22
- También se puede crear una interfaz propia con `<Form method="post">` de React Router, manteniendo esos helpers y este contrato:
194
+ ### Carrito
23
195
 
24
- | `intent` | Campos del formulario |
25
- | --- | --- |
26
- | `send` | `email` y token CAPTCHA si está habilitado |
27
- | `verify` | `code` de seis dígitos; en el paso `secondFactor`, `secondFactor` de autenticación o recuperación |
28
- | `resend` | Token CAPTCHA si está habilitado; respeta `retryAt` |
29
- | `reset` | Ninguno; vuelve al ingreso del correo |
196
+ ```tsx title="app/routes/cart.tsx"
197
+ import { cartAction, loadCart } from "@ecomiq/storefront";
198
+ import type { Route } from "./+types/cart";
30
199
 
31
- Los datos públicos incluyen `step`, `email`, `captcha`, `retryAt` y `error`. La transacción OAuth, PKCE y las claves de verificación permanecen en una cookie firmada HttpOnly; el formulario propio no debe enviar esos valores.
200
+ export async function loader({ request, context }: Route.LoaderArgs) {
201
+ return loadCart(request, context);
202
+ }
32
203
 
33
- CAPTCHA es independiente del modo y opcional por tienda: `off`, Turnstile o Google reCAPTCHA v2. `CustomerLoginForm` carga y reinicia el proveedor configurado. Una UI propia debe enviar `cf-turnstile-response` o `g-recaptcha-response`; Turnstile usa la acción `customer_login_code`.
204
+ export async function action({ request, context }: Route.ActionArgs) {
205
+ return cartAction(request, context);
206
+ }
207
+ ```
34
208
 
35
- ## Datos del registro
209
+ Con un cliente con sesión, el Worker rechaza con 403 cualquier petición que no
210
+ sea de lectura si no trae el token de la sesión: los formularios llevan
211
+ `<CustomerCsrfField />` y los `fetch` propios, la cabecera `X-Customer-Csrf`.
212
+ `FORM_CONTRACTS` lista los `intent` y los campos que acepta cada acción.
36
213
 
37
- La tienda configura nombre, apellido, teléfono y fecha de nacimiento como `hidden`, `optional` o `required`. Después del código, ambos modos de login crean la sesión y consultan `GET /v1/customer/registration`. Si falta completar el alta, redirigen a `/account/register?next=...`; el destino se limita a una ruta interna segura.
214
+ ## Login de clientes
38
215
 
39
- La ruta de registro usa `loadCustomerRegistration` en su loader, `submitCustomerRegistration` en su action y `CustomerRegistrationForm` con las mismas clases personalizables del login. Los datos del formulario incluyen `registration`, `storeName`, `next`, `today` (fecha local de la tienda) y `error`. También puede implementarse una UI propia que envíe `firstName`, `lastName`, `phone` y `dateOfBirth` (ISO `YYYY-MM-DD`) con `<CustomerCsrfField />`.
216
+ La tienda elige el modo de acceso en Admin, en **Configuración → Compra →
217
+ Cuentas de clientes**. El SDK lee esa configuración: `hosted` redirige a
218
+ Identity y `storefront` muestra el formulario del tema. Los dos modos usan las
219
+ mismas cuentas y la misma sesión de cliente.
40
220
 
41
- `intent=complete` guarda los campos visibles; `intent=skip` permite omitir todos los opcionales cuando no hay obligatorios. Ambos llaman `POST /v1/customer/registration`. El backend valida los requisitos vigentes y recuerda que el cliente completó este paso. Los clientes existentes sólo vuelven si faltan datos que ahora son obligatorios.
221
+ La ruta `/account/login` usa dos helpers:
42
222
 
43
- Las rutas de cuenta usan `requireCustomer`; checkout permite invitados y llama `requireCustomerRegistration(request, context, current)` cuando hay sesión. La propia ruta de registro usa `customer` para no redirigirse a sí misma. La API también exige completar el registro antes de las operaciones protegidas.
223
+ ```tsx title="app/routes/login.tsx"
224
+ import { CustomerLoginForm, startCustomerLogin, submitCustomerLogin } from "@ecomiq/storefront";
225
+ import type { Route } from "./+types/login";
44
226
 
45
- ## Desarrollo y temas publicados
227
+ export function loader({ request, context }: Route.LoaderArgs) {
228
+ return startCustomerLogin(request, context);
229
+ }
46
230
 
47
- En local, `ECOMIQ_AUTH_URL=http://localhost:8000` configura el transporte del BFF hacia Identity sin cambiar la autoridad OAuth pública ni desactivar la validación TLS. Aspire ya proporciona esta dirección al deployer local, que la propaga a los sitios instalados. Los ejemplos `.dev.vars.example` de cada tema la incluyen.
231
+ export function action({ request, context }: Route.ActionArgs) {
232
+ return submitCustomerLogin(request, context);
233
+ }
234
+ ```
48
235
 
49
- Los temas publicados son artifacts compilados. Para habilitar el modo dentro de la tienda en un tema existente, hay que **recompilar y publicar una versión que incluya los loaders, actions y formularios de login y registro**. Cambiar solamente la configuración de la tienda no modifica artifacts anteriores.
236
+ El componente de la ruta pinta `CustomerLoginForm` con
237
+ `data={actionData ?? loaderData}`. Su prop `classNames` define `form`, `field`,
238
+ `label`, `input`, `button`, `secondary`, `error` y `hint`; la página, la
239
+ tipografía, los colores y los controles siguen siendo del tema. Los temas de
240
+ Ecomiq ya traen esta integración.
241
+
242
+ Para una interfaz propia, usa `<Form method="post">` de React Router con los
243
+ mismos helpers y este contrato:
244
+
245
+ | `intent` | Campos del formulario |
246
+ | -------- | --------------------------------------------------------------------------------------------------------------- |
247
+ | `send` | `email` y el token CAPTCHA si está habilitado. |
248
+ | `verify` | `code` de seis dígitos; en el paso `secondFactor`, el código de autenticación o recuperación en `secondFactor`. |
249
+ | `resend` | El token CAPTCHA si está habilitado; respeta `retryAt`. |
250
+ | `reset` | Ninguno; vuelve al ingreso del correo. |
251
+
252
+ Los datos públicos son `step`, `email`, `captcha`, `retryAt` y `error`. La
253
+ transacción OAuth, PKCE y las claves de verificación viajan en una cookie
254
+ firmada HttpOnly; el formulario propio no envía esos valores.
255
+
256
+ CAPTCHA es opcional por tienda e independiente del modo: `off`, Turnstile o
257
+ Google reCAPTCHA v2. `CustomerLoginForm` carga y reinicia el proveedor
258
+ configurado. Una interfaz propia envía `cf-turnstile-response` o
259
+ `g-recaptcha-response`; Turnstile usa la acción `customer_login_code`.
260
+
261
+ ## Registro de clientes
262
+
263
+ La tienda configura nombre, apellido, teléfono y fecha de nacimiento como
264
+ `hidden`, `optional` o `required`. Después del código, los dos modos de login
265
+ crean la sesión y consultan `GET /v1/customer/registration`. Si falta completar
266
+ el alta, redirigen a `/account/register?next=...`; el destino se limita a una
267
+ ruta interna segura.
268
+
269
+ La ruta de registro usa `loadCustomerRegistration` en el loader,
270
+ `submitCustomerRegistration` en la action y `CustomerRegistrationForm` con las
271
+ mismas clases del login. Los datos incluyen `registration`, `storeName`, `next`,
272
+ `today` (la fecha local de la tienda) y `error`. Una interfaz propia envía
273
+ `firstName`, `lastName`, `phone` y `dateOfBirth` (`YYYY-MM-DD`) con
274
+ `<CustomerCsrfField />`.
275
+
276
+ `intent=complete` guarda los campos visibles; `intent=skip` omite los opcionales
277
+ cuando no hay obligatorios. Los dos llaman `POST /v1/customer/registration`. El
278
+ backend valida los requisitos vigentes y recuerda que el cliente completó el
279
+ paso: un cliente existente solo vuelve si faltan datos que ahora son
280
+ obligatorios.
281
+
282
+ Las rutas de cuenta usan `requireCustomer`. El checkout admite invitados y llama
283
+ `requireCustomerRegistration(request, context, current)` cuando hay sesión. La
284
+ ruta de registro usa `customer` para no redirigirse a sí misma. La API también
285
+ exige el registro completo antes de las operaciones protegidas.
50
286
 
51
287
  ## Enlaces de catálogo
52
288
 
53
- Los temas incluidos mantienen los enlaces a productos y categorías al cambiar
54
- su patrón, slug o URL personalizada. Los menús se resuelven en la configuración
55
- cacheada del servidor. Para botones y otros enlaces editables, el plugin
56
- `catalogLinks()` de `@ecomiq/storefront/vite` genera `virtual:ecomiq-links`
57
- durante la compilación; no analiza código durante las visitas.
58
-
59
- El loader raíz pasa ese manifiesto a `readStorefrontLinks(client, manifestLinks)`
60
- y devuelve el resultado como `storefrontLinks`. `StorefrontLink` y
61
- `StorefrontAnchor` consumen ese mapa sin hacer peticiones. El SDK deduplica los
62
- destinos, agrupa hasta 100 por petición y comparte los resultados durante
63
- 30 segundos. Los cambios se reflejan al renovar esa caché.
64
-
65
- El loader también devuelve `publishedPages`, con las rutas
66
- ``settings.cms.pages.map((page) => `/pages/${page.slug}`)`` del snapshot público.
67
- Ambos componentes ocultan los enlaces locales `/pages/...` que aún no tienen
68
- publicación. Al publicar la página, el siguiente snapshot recupera su enlace;
69
- no se consulta la API por cada enlace ni se modifica la configuración guardada.
70
-
71
- El selector del editor conserva la identidad con
289
+ Los enlaces a productos y categorías siguen funcionando aunque cambien su
290
+ patrón, slug o URL personalizada. Los menús se resuelven en la configuración
291
+ cacheada del servidor. Para botones y otros enlaces editables, `catalogLinks()`
292
+ genera `virtual:ecomiq-links` al compilar; no analiza código durante las
293
+ visitas.
294
+
295
+ El loader raíz pasa ese manifiesto a `readStorefrontLayout` y devuelve
296
+ `storefrontLinks`. `StorefrontLink` y `StorefrontAnchor` usan ese mapa sin hacer
297
+ peticiones. El SDK deduplica los destinos, agrupa hasta 100 por petición y
298
+ comparte los resultados durante 30 segundos; los cambios se ven al renovar esa
299
+ caché.
300
+
301
+ El loader también devuelve `publishedPages` con las rutas `/pages/<slug>` del
302
+ snapshot público. Los dos componentes ocultan los enlaces a `/pages/...` que
303
+ todavía no están publicados; al publicar la página, el siguiente snapshot
304
+ recupera el enlace sin consultar la API por cada enlace.
305
+
306
+ El selector del editor guarda la identidad del destino con
72
307
  `setting.link("/products/cuaderno", "Comprar", { type: "product", id: "42" })`.
73
- Con referencia devuelve un `CatalogLink`; sin referencia conserva el texto de
74
- la URL. Ambos se pueden pasar a `StorefrontLink` y `StorefrontAnchor`, que
75
- mantienen separados los destinos aunque otro producto reutilice la URL antigua.
76
- También se reconocen los enlaces anteriores que resuelve el historial del
77
- catálogo. Los enlaces externos, las rutas ajenas al catálogo y los parámetros
78
- o anclas se conservan. En código personalizado, usar los componentes anteriores
79
- para los enlaces editables; las tarjetas que ya reciben `product.url` o
80
- `category.url` pueden utilizar directamente esos valores.
81
-
82
- La instalación inicial de este soporte requiere actualizar y publicar el tema.
83
- Los cambios posteriores de URLs de catálogo no requieren publicar otra vez.
308
+ Con referencia devuelve un `CatalogLink`; sin referencia conserva la URL como
309
+ texto. Los dos sirven para `StorefrontLink` y `StorefrontAnchor`, que mantienen
310
+ el destino aunque otro producto reutilice la URL antigua, y reconocen los
311
+ enlaces viejos que resuelve el historial del catálogo. Los enlaces externos,
312
+ las rutas fuera del catálogo, los parámetros y las anclas se conservan.
313
+
314
+ En código propio, usa esos componentes para los enlaces editables. Las tarjetas
315
+ que ya reciben `product.url` o `category.url` pueden usar esos valores tal cual.
84
316
 
85
317
  ## Logos de medios de pago
86
318
 
87
319
  `PaymentLogo` pinta un `<img>` de 38×24 que apunta a
88
320
  `/assets/payment-logos/<código>.svg`; el JavaScript de la tienda solo lleva los
89
- nombres. El plugin `paymentLogos()` de `@ecomiq/storefront/vite` escribe esos
90
- SVG en el build del navegador y los sirve en desarrollo.
321
+ nombres. `paymentLogos()` escribe esos SVG en el build del navegador y los sirve
322
+ en desarrollo. `paymentLogoCodes` convierte los medios de pago de la tienda en
323
+ esos códigos.
91
324
 
92
325
  ## Medición y consentimiento
93
326
 
94
327
  `@ecomiq/storefront/analytics` carga GA4, Google Tag Manager, Meta Pixel y el
95
- código propio que la tienda configura en Admin (Tienda online → Ajustes →
96
- Medición y píxeles), y solo después de que el cliente los acepta. Cada
328
+ código propio que la tienda configura en Admin (**Tienda online → Ajustes →
329
+ Medición y píxeles**), y solo después de que el cliente los acepta. Cada
97
330
  herramienta pide su finalidad: GA4, «Medición»; Meta Pixel, «Publicidad»;
98
- Google Tag Manager y el código propio, las dos (pueden llevar cualquier
99
- etiqueta).
331
+ Google Tag Manager y el código propio, las dos, porque pueden llevar cualquier
332
+ etiqueta.
100
333
 
101
- El loader raíz devuelve `analytics: readAnalytics(context, settings, cart)`;
102
- es `null` sin IDs ni código propio, y en el lienzo. El `Layout` pinta
334
+ El loader raíz devuelve `analytics: readAnalytics(context, settings, cart)`, que
335
+ es `null` sin IDs ni código propio y en el lienzo del editor. El `Layout` pinta
103
336
  `<Analytics />` y `<ConsentBanner />`. El aviso no trae estilos: el tema le pasa
104
337
  `className` y `classNames` (`text`, `link`, `configure`, `options`, `option`,
105
- `actions`, `accept`, `reject`; sin `configure` usa la clase de `link`).
106
- Nombra los proveedores que la tienda usa, avisa que los datos pueden tratarse
107
- fuera del país y enlaza `/pages/privacidad` si existe. «Rechazar» y «Aceptar»
108
- van en la primera capa; «Configurar» muestra una casilla sin marcar por
109
- finalidad y «Guardar». Se pinta solo en el navegador.
110
-
111
- La decisión se guarda 12 meses en `__Host-ecomiq-consent` como
112
- `<elección>|<versión>|<fecha ISO>` (`analytics.marketing|2.gm.4|2026-10-12T15:30:00.000Z`,
113
- `none|…`). La versión junta la del aviso, las herramientas activas y la versión
114
- publicada de la página de privacidad: si cambia, el aviso vuelve a preguntar y
115
- nada se carga hasta la nueva decisión. Una cookie sin versión no cuenta como
116
- decisión. Las cookies del SDK usan el prefijo `__Host-` y `Secure`, así que otra
117
- tienda del mismo dominio no puede escribirlas.
118
- `window.ecomiq.consent.get()` devuelve `{ analytics, marketing, version, date }`
119
- o `null`; `window.ecomiq.consent.set({ analytics, marketing })` y el evento
120
- `ecomiq:consent` sirven a otros scripts.
338
+ `actions`, `accept`, `reject`; sin `configure` usa la clase de `link`). Nombra
339
+ los proveedores que usa la tienda, avisa que los datos pueden tratarse fuera del
340
+ país y enlaza `/pages/privacidad` si existe. «Rechazar» y «Aceptar» van en la
341
+ primera capa; «Configurar» muestra una casilla sin marcar por finalidad y
342
+ «Guardar». Se pinta solo en el navegador.
343
+
344
+ La decisión se guarda 12 meses en la cookie `__Host-ecomiq-consent` como
345
+ `<elección>|<versión>|<fecha ISO>`, por ejemplo
346
+ `analytics.marketing|2.gm.4|2026-10-12T15:30:00.000Z` o `none|…`. La versión
347
+ junta la del aviso, las herramientas activas y la versión publicada de la página
348
+ de privacidad: si cambia, el aviso vuelve a preguntar y nada se carga hasta la
349
+ nueva decisión. Una cookie sin versión no cuenta como decisión. Las cookies del
350
+ SDK usan el prefijo `__Host-` y `Secure`, así que otra tienda del mismo dominio
351
+ no puede escribirlas.
352
+
353
+ Para otros scripts, `window.ecomiq.consent.get()` devuelve
354
+ `{ analytics, marketing, version, date }` o `null`,
355
+ `window.ecomiq.consent.set({ analytics, marketing })` guarda una decisión y el
356
+ evento `ecomiq:consent` avisa cada cambio.
121
357
 
122
358
  Para cambiar o retirar la decisión, el pie del tema pinta `<ConsentLink />`
123
- («Cookies», con las clases de los enlaces vecinos): vuelve a abrir el aviso y
124
- no pinta nada cuando la tienda no mide. Retirar una finalidad borra sus cookies
359
+ («Cookies», con las clases de los enlaces vecinos): vuelve a abrir el aviso y no
360
+ pinta nada cuando la tienda no mide. Retirar una finalidad borra sus cookies
125
361
  (`_ga*`, `_gid`, `_gat*`, `_fbp`, `_fbc`, `_gcl_*`), avisa a Google y Meta y
126
362
  recarga la página si alguna herramienta ya estaba cargada.
127
363
 
@@ -134,27 +370,45 @@ carrito de la raíz), `begin_checkout` una vez por carrito y `purchase`, que lee
134
370
  la cookie `__Host-ecomiq-purchase` de 10 minutos que deja el pago cuando hay
135
371
  consentimiento.
136
372
 
137
- ## Secciones por defecto
373
+ ## Secciones de Ecomiq
138
374
 
139
375
  `@ecomiq/storefront/sections/<id>` trae secciones de página listas:
140
- `order-tracking` («Seguimiento», `<OrderTracking loaderData={…} />` con el
141
- loader `loadTracking`), `draft-invoice` («Pedido por pagar»,
142
- `<DraftInvoice loaderData={…} actionData={…} />` con `loadOrderDraft` y
143
- `orderDraftAction`) y `complaint-book` («Libro de Reclamaciones»,
144
- `<ComplaintBook loaderData={…} actionData={…} />` con `loadComplaintBook` y
145
- `complaintBookAction`, en la ruta `libro-de-reclamaciones`). Cada una exporta
146
- `catalog` y `settings` como las secciones del tema; `complaint-book` no tiene
147
- ajustes. `complaintBookLinks(settings)` devuelve el enlace del pie solo para
148
- tiendas en soles (como la API, que responde 404 al resto), y el formulario usa `<Captcha>` con la acción Turnstile
149
- `complaint_book`. El formulario va en multipart con hasta 3 adjuntos (JPG, PNG,
150
- WebP o PDF de 5 MB); `complaintBookAttachmentError(files)` los valida en el
151
- navegador y en la action.
152
-
153
- Se pintan con el kit del tema: importan `@theme/ui/*` (`button`, `input`,
154
- `label`, `textarea`, `checkbox`, `radio-group`, `select`),
155
- que el plugin `themeUi()` de `@ecomiq/storefront/vite` lleva a
156
- `app/components/ui/*`, y solo usan clases con los tokens del tema. Si al tema le
157
- falta un componente, el build falla con el archivo que falta y la sección que lo
158
- usa. El código fuente viaja en `src/sections` del paquete: «Personalizar» en el
376
+
377
+ | Sección | Nombre en el editor | Componente y datos |
378
+ | ---------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
379
+ | `order-tracking` | Seguimiento | `<OrderTracking loaderData={…} />` con `loadTracking`. |
380
+ | `draft-invoice` | Pedido por pagar | `<DraftInvoice loaderData={…} actionData={…} />` con `loadOrderDraft` y `orderDraftAction`. |
381
+ | `complaint-book` | Libro de Reclamaciones | `<ComplaintBook loaderData={…} actionData={…} />` con `loadComplaintBook` y `complaintBookAction`, en la ruta `libro-de-reclamaciones`. |
382
+
383
+ Cada una exporta `catalog` y `settings` como las secciones del tema;
384
+ `complaint-book` no tiene ajustes. `complaintBookLinks(settings)` devuelve el
385
+ enlace del pie solo para tiendas en soles, igual que la API, que responde 404 al
386
+ resto. Su formulario usa `<Captcha>` con la acción Turnstile `complaint_book` y
387
+ va en multipart con hasta 3 adjuntos (JPG, PNG, WebP o PDF de hasta 5 MB);
388
+ `complaintBookAttachmentError(files)` los valida en el navegador y en la action.
389
+
390
+ Las secciones se pintan con el kit del tema: importan `@theme/ui/*` (`button`,
391
+ `input`, `label`, `textarea`, `checkbox`, `radio-group`, `select`), que
392
+ `themeUi()` lleva a `app/components/ui/*`, y solo usan clases con los tokens del
393
+ tema. Si al tema le falta un componente, el build falla con el archivo que falta
394
+ y la sección que lo usa.
395
+
396
+ El código fuente viaja en `src/sections` del paquete: «Personalizar» en el
159
397
  editor lo copia a `app/components/sections/<id>.tsx` y apunta la ruta a esa
160
398
  copia.
399
+
400
+ ## Temas publicados
401
+
402
+ Un tema publicado es un build compilado. Para que una tienda use una función
403
+ nueva del SDK, como el login dentro de la tienda o los enlaces de catálogo, hay
404
+ que actualizar el tema y volver a publicarlo. Cambiar solo la configuración de
405
+ la tienda no modifica builds anteriores. Los cambios posteriores de URLs del
406
+ catálogo no necesitan otra publicación.
407
+
408
+ En desarrollo contra un Identity local, `ECOMIQ_AUTH_URL` (por ejemplo
409
+ `http://localhost:8000`) cambia el transporte del BFF hacia Identity sin tocar
410
+ la autoridad OAuth pública ni la validación TLS.
411
+
412
+ ## Licencia
413
+
414
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Ecomiq para React Router: tienda, carrito, sesión de cliente y catálogo en un Worker",
5
5
  "type": "module",
6
6
  "license": "MIT",