@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.
- package/README.md +358 -104
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,127 +1,363 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
30
|
+
Para agregarlo a un proyecto React Router existente:
|
|
4
31
|
|
|
5
|
-
|
|
32
|
+
```bash
|
|
33
|
+
npm install @ecomiq/storefront @ecomiq/core react react-dom react-router
|
|
34
|
+
```
|
|
6
35
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
+
### Carrito
|
|
23
195
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
200
|
+
export async function loader({ request, context }: Route.LoaderArgs) {
|
|
201
|
+
return loadCart(request, context);
|
|
202
|
+
}
|
|
32
203
|
|
|
33
|
-
|
|
204
|
+
export async function action({ request, context }: Route.ActionArgs) {
|
|
205
|
+
return cartAction(request, context);
|
|
206
|
+
}
|
|
207
|
+
```
|
|
34
208
|
|
|
35
|
-
|
|
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
|
-
|
|
214
|
+
## Login de clientes
|
|
38
215
|
|
|
39
|
-
La
|
|
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
|
-
|
|
221
|
+
La ruta `/account/login` usa dos helpers:
|
|
42
222
|
|
|
43
|
-
|
|
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
|
-
|
|
227
|
+
export function loader({ request, context }: Route.LoaderArgs) {
|
|
228
|
+
return startCustomerLogin(request, context);
|
|
229
|
+
}
|
|
46
230
|
|
|
47
|
-
|
|
231
|
+
export function action({ request, context }: Route.ActionArgs) {
|
|
232
|
+
return submitCustomerLogin(request, context);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
48
235
|
|
|
49
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
cacheada del servidor. Para botones y otros enlaces editables,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
El loader raíz pasa ese manifiesto a `
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
El loader también devuelve `publishedPages
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
`category.url` pueden
|
|
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.
|
|
90
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
La decisión se guarda 12 meses en `__Host-ecomiq-consent` como
|
|
112
|
-
`<elección>|<versión>|<fecha ISO
|
|
113
|
-
`
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
decisión.
|
|
117
|
-
tienda del mismo dominio
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`ecomiq
|
|
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
|
-
|
|
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
|
|
373
|
+
## Secciones de Ecomiq
|
|
138
374
|
|
|
139
375
|
`@ecomiq/storefront/sections/<id>` trae secciones de página listas:
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
`<
|
|
145
|
-
`complaintBookAction`, en la ruta `libro-de-reclamaciones
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`app/components/ui/*`, y solo usan clases con los tokens del
|
|
157
|
-
falta un componente, el build falla con el archivo que falta
|
|
158
|
-
|
|
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
|