@asteby/storefront-react 0.25.0 → 0.26.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.
Files changed (48) hide show
  1. package/README.md +35 -2
  2. package/dist/account-server.cjs +128 -19
  3. package/dist/account-server.cjs.map +1 -1
  4. package/dist/account-server.d.cts +67 -4
  5. package/dist/account-server.d.ts +67 -4
  6. package/dist/account-server.js +125 -20
  7. package/dist/account-server.js.map +1 -1
  8. package/dist/account.cjs +252 -7
  9. package/dist/account.cjs.map +1 -1
  10. package/dist/account.css +91 -0
  11. package/dist/account.d.cts +130 -6
  12. package/dist/account.d.ts +130 -6
  13. package/dist/account.js +247 -8
  14. package/dist/account.js.map +1 -1
  15. package/dist/checkout.cjs +162 -8
  16. package/dist/checkout.cjs.map +1 -1
  17. package/dist/checkout.d.cts +104 -4
  18. package/dist/checkout.d.ts +104 -4
  19. package/dist/checkout.js +158 -10
  20. package/dist/checkout.js.map +1 -1
  21. package/dist/chunk-Z4C4LKXF.js +25 -0
  22. package/dist/chunk-Z4C4LKXF.js.map +1 -0
  23. package/dist/fit-key-B9sYQVwG.d.cts +49 -0
  24. package/dist/fit-key-B9sYQVwG.d.ts +49 -0
  25. package/dist/fitment.cjs +54 -9
  26. package/dist/fitment.cjs.map +1 -1
  27. package/dist/fitment.d.cts +20 -2
  28. package/dist/fitment.d.ts +20 -2
  29. package/dist/fitment.js +54 -10
  30. package/dist/fitment.js.map +1 -1
  31. package/dist/settings-types-jxFQFFqP.d.cts +77 -0
  32. package/dist/settings-types-jxFQFFqP.d.ts +77 -0
  33. package/dist/subjects-server.cjs +248 -0
  34. package/dist/subjects-server.cjs.map +1 -0
  35. package/dist/subjects-server.d.cts +130 -0
  36. package/dist/subjects-server.d.ts +130 -0
  37. package/dist/subjects-server.js +217 -0
  38. package/dist/subjects-server.js.map +1 -0
  39. package/dist/subjects.cjs +501 -0
  40. package/dist/subjects.cjs.map +1 -0
  41. package/dist/subjects.css +166 -0
  42. package/dist/subjects.d.cts +178 -0
  43. package/dist/subjects.d.ts +178 -0
  44. package/dist/subjects.js +465 -0
  45. package/dist/subjects.js.map +1 -0
  46. package/package.json +23 -2
  47. package/dist/types-D7pVK0GK.d.cts +0 -39
  48. package/dist/types-D7pVK0GK.d.ts +0 -39
package/README.md CHANGED
@@ -727,11 +727,44 @@ import { parseCatalog, resolveVehicle, fitmentConfig, fitFilterTags, withFit, sa
727
727
  const config = fitmentConfig({ compatAttribute: 'compatibilidad' }) // por defecto: tags `fit:*`, atributo `compatibility`
728
728
  const catalog = parseCatalog(section.props, { priorityAliases: { alta: 'high' } })
729
729
  const vehicle = resolveVehicle(catalog, cookieKey) // null si la clave ya no existe
730
- const tags = vehicle ? fitFilterTags(vehicle, config) : [] // filter.tag = alguno de
730
+ const tags = vehicle ? fitFilterTags(vehicle, config) : [] // products.list({ tags_any: tags }), no `q` ni `filter.tag`
731
731
  const product = withFit(rawProduct, vehicle, config) // cientos de tags fit:* → UNA etiqueta de estado
732
732
  ```
733
733
 
734
- La cookie, el formulario y el redirect son de la tienda; `safeRedirect` valida el destino. Un componente de selector sobre estas funciones llega en una versión posterior.
734
+ El catálogo acepta motores con tramos de años reales (`["2.4", 2007, 2017, 2020, 2022]` o `{ id, ranges }`): un hueco es un hueco, un modelo cubre un año solo si algún motor lo cubre y el selector no ofrece años fantasma. `vehicleCatalogToTree(catalog)` lo convierte al árbol que la organización publica para «Mis artículos» (abajo). La cookie, el formulario y el redirect son de la tienda; `safeRedirect` valida el destino. Para guardar varios vehículos en la cuenta, con selector y filtro listos, usa `/subjects`.
735
+
736
+ ## Mis artículos / Mi garage (`@asteby/storefront-react/subjects`, contrato 1.9)
737
+
738
+ Algo del comprador para lo que compra (un vehículo, una impresora, un celular, una mascota, una talla), de un **tipo que la organización declara como dato**: el SDK no nombra ninguno y los textos salen de los datos (`kind.label`, `kind.label_plural`). Una tienda que no declara tipos (God's Nutrition, Local) **no tiene sección**: `loadSubjectsContext` devuelve `kinds: []` y no se pinta nada.
739
+
740
+ | Pieza | Para qué |
741
+ | --- | --- |
742
+ | `SubjectGarage` | «Mi garage» de un tipo: tarjetas con etiqueta, apodo, insignia «Activo», valores privados enmascarados, «Ver piezas compatibles», hacer activo, editar, eliminar (confirmación en línea) y agregar. Estados vacío, límite y «No pudimos cargar esto · Reintentar» |
743
+ | `SubjectPicker` | El formulario de un artículo, guiado por los campos del tipo: cascada que se angosta (marca → modelo → año → motor), años por rangos, campos libres y el campo privado. Sin JavaScript es un `<form method="post">` que avanza el nivel al enviar (`subject-refine`) |
744
+ | `SubjectFilter` / `SubjectFitBadge` | La barra «Para: Nissan NP300 2012 · Cambiar · Ver todo» de un catálogo y la insignia «Compatible con tu vehículo» de un producto. Enlaces, no botones |
745
+ | `useSubjects(client?)` | Hook para tiendas que pintan en el cliente (`ready` / `error` / `unavailable`) |
746
+ | `/subjects/server` | `loadSubjectsContext` (nunca lanza), `createSubjectAction`, cookie del invitado `sf_subject_<tipo>`, `subjectCacheHeaders` |
747
+ | `/subjects.css` | Hoja neutra opcional (`--sf-subject-*`) |
748
+
749
+ ```tsx
750
+ // loader (/garage y los catálogos)
751
+ const subjects = await loadSubjectsContext(sf, { customer: customerClient, cookieHeader: request.headers.get('cookie') })
752
+ // subjects.kinds → [] si la tienda no declara tipos: no renderices la sección
753
+ // subjects.active.vehicle → { label, fit_key, source: 'account' | 'cookie' }
754
+
755
+ // catálogo: solo lo compatible, con el filtro del servidor
756
+ const products = await sf.products.list({ tags_any: fitTagsAny(subjects.active.vehicle?.fit_key) })
757
+ headers = subjectCacheHeaders(cookieHeader, 'public, max-age=60', { signedIn }) // con artículo activo la página es privada; ambas respuestas llevan Vary: Cookie
758
+
759
+ // acción (POST /garage)
760
+ export const action = createSubjectAction({ storefront: sf, readCustomerToken })
761
+ // página
762
+ <SubjectGarage kind={kind} subjects={subjects.subjects} data={actionData} partsHref={(s) => `/piezas?para=${s.fit_key}`} status={subjects.degraded.subjects ? 'error' : 'ready'} />
763
+ ```
764
+
765
+ **Privacidad.** Un campo `private` (placas) nunca entra a la etiqueta, la clave de compatibilidad, el filtro, la caché, la cookie ni el pedido. El servidor lo devuelve solo enmascarado (`••23`): el formulario no lo rellena, un campo vacío al editar conserva el guardado y `.clear` lo quita. La cookie del invitado guarda **solo** la `fit_key`, y `serializeSubjectCookie` se niega (`TypeError`) a escribir cualquier valor que no resuelva contra el catálogo del tipo. La acción no registra nada, no devuelve valores privados en `values` y los errores nombran el campo, nunca el valor.
766
+
767
+ **Adoptar en ProtekAuto** (la tienda no se toca en este paquete): (1) subir el SDK y usar `fitment` en lugar de `app/lib/vehicle.ts` (`fitmentConfig({ tagPrefix: 'fit:', stateTagPrefix: 'ajuste:', compatAttribute: 'compatibilidad' })`, `parseCatalog(props, { priorityAliases: { alta: 'high', media: 'medium', baja: 'low' } })`); (2) publicar los datos: registrar las secciones `subject_kinds` y `subject_catalog` (`PUT /storefront/content/section-types`, ver `shared/subjectspec/README.md` de addons), la página `subject-kinds` con el tipo `vehicle` (`label_plural: «Mi garage»`, plantilla `{make}/{model}/{year}/{engine}`, campo `plate` con `private: true`) y el catálogo con `splitSubjectTree(vehicleCatalogToTree(parseCatalog(...)))`: el real (50 marcas, 417 modelos, 1 165 opciones, ~70 KB como texto) no cabe en una página de OPS (una escritura de 64 KB con borrador y copia pública), así que va en 3 páginas `vehiculos-1..3` y el tipo las nombra todas en `catalog_ref`; (3) en el loader, `loadSubjectsContext` y `products.list({ tags_any })` en vez de buscar la etiqueta con `q` (`vehicle.server.ts` deja de filtrar a mano); (4) cookie `sf_subject_vehicle` en lugar de `pa_vehiculo` (leer la vieja una vez y reescribirla); (5) `/garage` con `SubjectGarage` + `createSubjectAction`; (6) `subjectCacheHeaders` en lugar de `vehicleCacheHeaders`. Pendiente fuera del SDK: ligar el artículo al pedido (`CheckoutRequest.subject`, `storefront_cart` + `customers`).
735
768
 
736
769
  ## API
737
770
 
@@ -328,6 +328,111 @@ h1 { font-size: 22px; margin: 0; }
328
328
 
329
329
  // src/account/server.ts
330
330
  init_voucher();
331
+ async function loadAccountSessions(customer, options = {}) {
332
+ try {
333
+ return await customer.customer.sessions.list(options.signal ? { signal: options.signal } : void 0);
334
+ } catch {
335
+ return null;
336
+ }
337
+ }
338
+ async function loadAccountSettings(customer, options = {}) {
339
+ const o = options.signal ? { signal: options.signal } : void 0;
340
+ const [profile, capabilities, sessions] = await Promise.all([
341
+ customer.customer.get(o).catch(() => null),
342
+ customer.customer.capabilities(o).catch(() => null),
343
+ loadAccountSessions(customer, options)
344
+ ]);
345
+ return { customer: profile, capabilities, sessions };
346
+ }
347
+ var fail = (intent, code, extra = {}) => ({
348
+ ok: false,
349
+ ...intent ? { intent } : {},
350
+ error: { code, ...extra.field ? { field: extra.field } : {}, ...extra.retryAfter ? { retryAfter: extra.retryAfter } : {} }
351
+ });
352
+ function toSettingsFailure(err, intent, ctx = "other") {
353
+ if (!storefront.isStorefrontError(err)) return fail(intent, "unavailable");
354
+ const e = err;
355
+ if (e.code === "insufficient_scope") return fail(intent, "unsupported");
356
+ switch (e.status) {
357
+ case 401:
358
+ return fail(intent, "session_expired");
359
+ case 403:
360
+ return fail(intent, e.code === "reauth_required" ? "reauth_required" : "unsupported");
361
+ case 404:
362
+ return fail(intent, "unsupported");
363
+ case 409:
364
+ return fail(intent, e.field === "orders" ? "blocked_orders" : e.field === "pending_payments" ? "blocked_pending_payments" : "validation", e.field ? { field: e.field } : {});
365
+ case 429: {
366
+ const wait = typeof e.retryAfter === "number" && Number.isFinite(e.retryAfter) && e.retryAfter > 0 ? Math.min(3600, Math.ceil(e.retryAfter)) : void 0;
367
+ return fail(intent, "rate_limited", wait ? { retryAfter: wait } : {});
368
+ }
369
+ case 400:
370
+ case 422:
371
+ return ctx === "credential" ? fail(intent, "invalid_credential") : fail(intent, "validation", e.field ? { field: e.field } : {});
372
+ default:
373
+ return fail(intent, "unavailable");
374
+ }
375
+ }
376
+ function createAccountSettingsAction(options) {
377
+ const { storefront: sf, customerCookie } = options;
378
+ return async function settingsAction({ request }) {
379
+ if (request.headers.get("Sec-Fetch-Site")?.toLowerCase() === "cross-site") return fail(void 0, "validation");
380
+ const form = await request.formData().catch(() => null);
381
+ if (!form) return fail(void 0, "validation");
382
+ const text2 = (name) => {
383
+ const v = form.get(name);
384
+ return typeof v === "string" ? v : "";
385
+ };
386
+ const intent = text2("intent");
387
+ const raw = await customerCookie.parse(request.headers.get("Cookie"));
388
+ const token = typeof raw === "string" && raw ? raw : null;
389
+ if (!token) return fail(intent, "session_expired");
390
+ const ip = options.clientIp?.(request);
391
+ const w = ip ? { clientIp: ip } : void 0;
392
+ const customer = sf.withCustomerToken(token);
393
+ try {
394
+ switch (intent) {
395
+ case "session-revoke": {
396
+ const id = text2("session_id").trim();
397
+ if (!id || id.length > 64) return fail(intent, "validation", { field: "session_id" });
398
+ if (id.toLowerCase() === "others") return fail(intent, "validation", { field: "session_id" });
399
+ await customer.customer.sessions.revoke(id, w);
400
+ return { ok: true, intent };
401
+ }
402
+ case "session-revoke-others": {
403
+ await customer.customer.sessions.revokeOthers(w);
404
+ return { ok: true, intent };
405
+ }
406
+ case "locale": {
407
+ const locale = text2("locale").trim();
408
+ if (locale.length > 35) return fail(intent, "validation", { field: "locale" });
409
+ await customer.customer.update({ locale: locale || null }, w);
410
+ return { ok: true, intent, saved: true };
411
+ }
412
+ case "notifications": {
413
+ const shown = form.getAll("pref_shown").filter((v) => typeof v === "string");
414
+ const checked = new Set(form.getAll("pref").filter((v) => typeof v === "string"));
415
+ if (shown.length === 0 || shown.length > 30) return fail(intent, "validation", { field: "preferences" });
416
+ const notifications = {};
417
+ for (const pair of shown) {
418
+ const m = /^([a-z][a-z0-9_]{0,31}):([a-z][a-z0-9_]{0,31})$/.exec(pair);
419
+ if (!m) return fail(intent, "validation", { field: "preferences" });
420
+ (notifications[m[1]] ??= {})[m[2]] = checked.has(pair);
421
+ }
422
+ await customer.customer.update({ preferences: { notifications } }, w);
423
+ return { ok: true, intent, saved: true };
424
+ }
425
+ default:
426
+ return fail(void 0, "validation");
427
+ }
428
+ } catch (err) {
429
+ if (intent === "session-revoke" && storefront.isStorefrontError(err) && err.status === 404) return { ok: true, intent };
430
+ return toSettingsFailure(err, intent);
431
+ }
432
+ };
433
+ }
434
+
435
+ // src/account/server.ts
331
436
  var SAFE_AUTH_CAPABILITIES = Object.freeze({
332
437
  password: true,
333
438
  email_verification: false,
@@ -387,32 +492,32 @@ function safeNextPath(raw, fallback = "/account") {
387
492
  if (!v.startsWith("/") || v.startsWith("//") || v.startsWith("/\\") || /[\u0000-\u001f\\]/.test(v)) return fallback;
388
493
  return v;
389
494
  }
390
- var fail = (code, extra = {}, field) => ({
495
+ var fail2 = (code, extra = {}, field) => ({
391
496
  ok: false,
392
497
  ...extra,
393
498
  error: { code, ...field ? { field } : {} }
394
499
  });
395
500
  function toFailure(err, ctx, extra) {
396
- if (!storefront.isStorefrontError(err)) return fail("unavailable", extra);
501
+ if (!storefront.isStorefrontError(err)) return fail2("unavailable", extra);
397
502
  if (err.code === "insufficient_scope") {
398
503
  logMisconfigured(err.requiredScopes);
399
- return fail("store_misconfigured", extra);
504
+ return fail2("store_misconfigured", extra);
400
505
  }
401
506
  switch (err.status) {
402
507
  case 401:
403
- return fail(ctx === "credentials" ? "invalid_credentials" : ctx === "code" ? "invalid_code" : "unavailable", extra, ctx === "code" ? "code" : void 0);
508
+ return fail2(ctx === "credentials" ? "invalid_credentials" : ctx === "code" ? "invalid_code" : "unavailable", extra, ctx === "code" ? "code" : void 0);
404
509
  case 409:
405
- return fail("email_taken", extra, "email");
510
+ return fail2("email_taken", extra, "email");
406
511
  case 429:
407
- return fail("rate_limited", extra);
512
+ return fail2("rate_limited", extra);
408
513
  case 400:
409
514
  case 422:
410
- return fail("validation", extra, err.field);
515
+ return fail2("validation", extra, err.field);
411
516
  case 403:
412
517
  case 404:
413
- return fail("unsupported", extra);
518
+ return fail2("unsupported", extra);
414
519
  default:
415
- return fail("unavailable", extra);
520
+ return fail2("unavailable", extra);
416
521
  }
417
522
  }
418
523
  function createAuthAction(options) {
@@ -426,7 +531,7 @@ function createAuthAction(options) {
426
531
  }
427
532
  return async function authAction({ request }) {
428
533
  const form = await request.formData().catch(() => null);
429
- if (!form) return fail("validation");
534
+ if (!form) return fail2("validation");
430
535
  const text2 = (name) => {
431
536
  const v = form.get(name);
432
537
  return typeof v === "string" ? v : "";
@@ -439,22 +544,22 @@ function createAuthAction(options) {
439
544
  const ip = options.clientIp?.(request);
440
545
  const w = ip ? { clientIp: ip } : void 0;
441
546
  const carry = { email };
442
- if (!email) return fail("validation", carry, "email");
547
+ if (!email) return fail2("validation", carry, "email");
443
548
  const caps = await loadCaps(sf);
444
549
  const passwordOn = caps.password;
445
550
  const misconfigured = caps.unavailable_reason === "missing_scope";
446
- const off = () => fail(misconfigured ? "store_misconfigured" : "unsupported", carry);
551
+ const off = () => fail2(misconfigured ? "store_misconfigured" : "unsupported", carry);
447
552
  const otpOn = caps.otp_channels.includes("email");
448
553
  try {
449
554
  switch (intent) {
450
555
  case "login": {
451
556
  if (!passwordOn) return off();
452
- if (!password) return fail("validation", carry, "password");
557
+ if (!password) return fail2("validation", carry, "password");
453
558
  return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next);
454
559
  }
455
560
  case "register": {
456
561
  if (!passwordOn) return off();
457
- if (!password) return fail("validation", carry, "password");
562
+ if (!password) return fail2("validation", carry, "password");
458
563
  const res = await sf.customer.register(
459
564
  { email, password, ...text2("first_name").trim() ? { first_name: text2("first_name").trim() } : {}, ...text2("last_name").trim() ? { last_name: text2("last_name").trim() } : {} },
460
565
  w
@@ -462,7 +567,7 @@ function createAuthAction(options) {
462
567
  return res.status === "created" ? await signedIn(res.session, next) : { ok: true, step: "verify", email };
463
568
  }
464
569
  case "verify": {
465
- if (!code) return fail("validation", carry, "code");
570
+ if (!code) return fail2("validation", carry, "code");
466
571
  return await signedIn(await sf.customer.verifyRegistration({ email, code }, w), next);
467
572
  }
468
573
  case "otp-request": {
@@ -472,7 +577,7 @@ function createAuthAction(options) {
472
577
  }
473
578
  case "otp-login": {
474
579
  if (!otpOn) return off();
475
- if (!code) return fail("validation", carry, "code");
580
+ if (!code) return fail2("validation", carry, "code");
476
581
  return await signedIn(await sf.customer.createSession({ email, code }, w), next);
477
582
  }
478
583
  case "forgot": {
@@ -486,13 +591,13 @@ function createAuthAction(options) {
486
591
  }
487
592
  case "reset": {
488
593
  if (!passwordOn) return off();
489
- if (!code) return fail("validation", carry, "code");
490
- if (!password) return fail("validation", carry, "password");
594
+ if (!code) return fail2("validation", carry, "code");
595
+ if (!password) return fail2("validation", carry, "password");
491
596
  await sf.customer.resetPassword({ email, code, password }, w);
492
597
  return await signedIn(await sf.customer.loginWithPassword({ email, password }, w), next);
493
598
  }
494
599
  default:
495
- return fail("validation", carry);
600
+ return fail2("validation", carry);
496
601
  }
497
602
  } catch (err) {
498
603
  const step = intent === "verify" ? "verify" : intent === "otp-login" ? "otp" : intent === "reset" || intent === "forgot" ? "reset" : void 0;
@@ -574,10 +679,14 @@ async function pendingVoucherResponse({ storefront, id, download = false, shopNa
574
679
 
575
680
  exports.MAX_INVOICE_FILE_BYTES = MAX_INVOICE_FILE_BYTES;
576
681
  exports.SAFE_AUTH_CAPABILITIES = SAFE_AUTH_CAPABILITIES;
682
+ exports.createAccountSettingsAction = createAccountSettingsAction;
577
683
  exports.createAuthAction = createAuthAction;
578
684
  exports.invoiceFileResponse = invoiceFileResponse;
685
+ exports.loadAccountSessions = loadAccountSessions;
686
+ exports.loadAccountSettings = loadAccountSettings;
579
687
  exports.loadAuthCapabilities = loadAuthCapabilities;
580
688
  exports.pendingVoucherResponse = pendingVoucherResponse;
581
689
  exports.safeNextPath = safeNextPath;
690
+ exports.toSettingsFailure = toSettingsFailure;
582
691
  //# sourceMappingURL=account-server.cjs.map
583
692
  //# sourceMappingURL=account-server.cjs.map