@asteby/storefront-react 0.9.0 → 0.11.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/dist/payments.css CHANGED
@@ -28,3 +28,30 @@
28
28
  [data-sf-pay-method] [data-sf-pay-method-submit] {
29
29
  min-height: 44px;
30
30
  }
31
+ /* Brand marks (`brandMarks`): trailing marks of a row (card networks, first cash points) and the mark of each point. */
32
+ [data-sf-pay-method] [data-sf-pay-brands] {
33
+ display: inline-flex;
34
+ flex: none;
35
+ flex-wrap: wrap;
36
+ justify-content: flex-end;
37
+ gap: 0.25rem;
38
+ margin-inline-start: auto;
39
+ }
40
+ [data-sf-pay-method] [data-sf-pay-glyph] {
41
+ display: block;
42
+ opacity: 0.8;
43
+ }
44
+ [data-sf-pay-method] [data-sf-brand-mark] {
45
+ display: inline-block;
46
+ vertical-align: middle;
47
+ }
48
+ /* The open cash row already lists every point with its mark: the preview would repeat them. */
49
+ [data-sf-pay-method] [data-sf-pay-choice]:has(input[name='payment_choice']:checked) [data-sf-pay-brands='cash'] {
50
+ display: none;
51
+ }
52
+ @media (max-width: 24rem) {
53
+ /* Narrow phones: the row text wins over the preview marks. */
54
+ [data-sf-pay-method] [data-sf-pay-choice] [data-sf-pay-brands] [data-sf-brand-mark]:nth-child(n + 3) {
55
+ display: none;
56
+ }
57
+ }
@@ -341,6 +341,8 @@ type PaymentMethodMessages = {
341
341
  pickDeliveryFirst: string;
342
342
  updatingTotal: string;
343
343
  syncFailed: string;
344
+ /** The shipping quote expired or changed: fresh rates are loading. */
345
+ quoteChanged: string;
344
346
  rebuildCart: string;
345
347
  noPaymentTitle: string;
346
348
  noPayment: string;
@@ -359,7 +361,7 @@ declare const PAYMENT_CHOICE_FIELD = "payment_choice";
359
361
  declare const CASH_POINT_FIELD = "cash_point";
360
362
  /** Fields of the page form (contact, delivery, address, method) for the JSON POST of the embedded payment. */
361
363
  declare function formFields(form: HTMLFormElement | null | undefined): Record<string, string>;
362
- type PaymentMethodClassNames = Partial<Record<'root' | 'legend' | 'choice' | 'radioLabel' | 'radio' | 'labelText' | 'hint' | 'icon' | 'panel' | 'cashPoint' | 'cashPointInput' | 'cashMore' | 'note' | 'alert' | 'status' | 'brickWrap' | 'brick' | 'submit' | 'rebuild', string>>;
364
+ type PaymentMethodClassNames = Partial<Record<'root' | 'legend' | 'choice' | 'radioLabel' | 'radio' | 'labelText' | 'hint' | 'icon' | 'brands' | 'panel' | 'cashPoint' | 'cashPointInput' | 'cashMore' | 'note' | 'alert' | 'status' | 'brickWrap' | 'brick' | 'submit' | 'rebuild', string>>;
363
365
  type PaymentMethodTestIds = Partial<Record<'method' | 'submit' | 'cash' | 'redirectNote' | 'card' | 'syncing' | 'syncFailed' | 'rebuild' | 'noPayment', string>>;
364
366
  type PaymentMethodSectionProps = {
365
367
  /** From `buildPayChoices(payment.methods)`. */
@@ -382,9 +384,14 @@ type PaymentMethodSectionProps = {
382
384
  deliveryReady?: boolean;
383
385
  /**
384
386
  * Saves the chosen delivery on the server cart so the server total includes it. Resolve `true` once the new total
385
- * has been read back (the `amount` prop is then up to date). Called once per delivery. Without it nothing waits.
387
+ * has been read back (the `amount` prop is then up to date). Called once per delivery, whatever the payment row (so
388
+ * the total on the button and in the shop's summary always holds the chosen delivery). Without it nothing waits.
389
+ *
390
+ * Resolve `'requote'` when the server refused the rate because the quote expired or changed and the shop is loading
391
+ * fresh rates: the section then says so (`quoteChanged`, a status, not an error), keeps the card in the page and
392
+ * tries again with the next delivery value. `false` is a real failure (the card degrades to the redirect).
386
393
  */
387
- syncDelivery?: (delivery: string) => Promise<boolean>;
394
+ syncDelivery?: (delivery: string) => Promise<boolean | 'requote'>;
388
395
  /** A two-step checkout still collecting the address: the button reads "continue" and no card form shows. */
389
396
  addressFirst?: boolean;
390
397
  /** The address must be saved again before paying. */
@@ -403,8 +410,16 @@ type PaymentMethodSectionProps = {
403
410
  label: string;
404
411
  hint?: string;
405
412
  };
406
- /** Per-row icon by option (`card`, `cash`, `wallet`, `credits`) or `plain`. */
413
+ /** Per-row icon by option (`card`, `cash`, `wallet`, `credits`) or `plain`. Replaces the default marks of that row. */
407
414
  icons?: Partial<Record<PaymentOptionKey | 'plain', ReactNode>>;
415
+ /**
416
+ * Brand marks (`BrandMark`): the provider's mark on its account / no-card rows and on a plain method, the cash point
417
+ * marks on the cash row and next to each point, and `cardBrands` on the card row. Default true; `false` keeps the
418
+ * rows text-only.
419
+ */
420
+ brandMarks?: boolean;
421
+ /** Card networks drawn on the card row (`visa`, `mastercard`, `amex`, `carnet`…), as the shop's provider accepts them. */
422
+ cardBrands?: readonly string[];
408
423
  /** How to print the total on the button. Default `formatMoney`. */
409
424
  renderAmount?: (amount: Money) => ReactNode;
410
425
  /** Anchor to the cart for the "rebuild" action when the cart expired. Default: plain `<a href>`. */
@@ -341,6 +341,8 @@ type PaymentMethodMessages = {
341
341
  pickDeliveryFirst: string;
342
342
  updatingTotal: string;
343
343
  syncFailed: string;
344
+ /** The shipping quote expired or changed: fresh rates are loading. */
345
+ quoteChanged: string;
344
346
  rebuildCart: string;
345
347
  noPaymentTitle: string;
346
348
  noPayment: string;
@@ -359,7 +361,7 @@ declare const PAYMENT_CHOICE_FIELD = "payment_choice";
359
361
  declare const CASH_POINT_FIELD = "cash_point";
360
362
  /** Fields of the page form (contact, delivery, address, method) for the JSON POST of the embedded payment. */
361
363
  declare function formFields(form: HTMLFormElement | null | undefined): Record<string, string>;
362
- type PaymentMethodClassNames = Partial<Record<'root' | 'legend' | 'choice' | 'radioLabel' | 'radio' | 'labelText' | 'hint' | 'icon' | 'panel' | 'cashPoint' | 'cashPointInput' | 'cashMore' | 'note' | 'alert' | 'status' | 'brickWrap' | 'brick' | 'submit' | 'rebuild', string>>;
364
+ type PaymentMethodClassNames = Partial<Record<'root' | 'legend' | 'choice' | 'radioLabel' | 'radio' | 'labelText' | 'hint' | 'icon' | 'brands' | 'panel' | 'cashPoint' | 'cashPointInput' | 'cashMore' | 'note' | 'alert' | 'status' | 'brickWrap' | 'brick' | 'submit' | 'rebuild', string>>;
363
365
  type PaymentMethodTestIds = Partial<Record<'method' | 'submit' | 'cash' | 'redirectNote' | 'card' | 'syncing' | 'syncFailed' | 'rebuild' | 'noPayment', string>>;
364
366
  type PaymentMethodSectionProps = {
365
367
  /** From `buildPayChoices(payment.methods)`. */
@@ -382,9 +384,14 @@ type PaymentMethodSectionProps = {
382
384
  deliveryReady?: boolean;
383
385
  /**
384
386
  * Saves the chosen delivery on the server cart so the server total includes it. Resolve `true` once the new total
385
- * has been read back (the `amount` prop is then up to date). Called once per delivery. Without it nothing waits.
387
+ * has been read back (the `amount` prop is then up to date). Called once per delivery, whatever the payment row (so
388
+ * the total on the button and in the shop's summary always holds the chosen delivery). Without it nothing waits.
389
+ *
390
+ * Resolve `'requote'` when the server refused the rate because the quote expired or changed and the shop is loading
391
+ * fresh rates: the section then says so (`quoteChanged`, a status, not an error), keeps the card in the page and
392
+ * tries again with the next delivery value. `false` is a real failure (the card degrades to the redirect).
386
393
  */
387
- syncDelivery?: (delivery: string) => Promise<boolean>;
394
+ syncDelivery?: (delivery: string) => Promise<boolean | 'requote'>;
388
395
  /** A two-step checkout still collecting the address: the button reads "continue" and no card form shows. */
389
396
  addressFirst?: boolean;
390
397
  /** The address must be saved again before paying. */
@@ -403,8 +410,16 @@ type PaymentMethodSectionProps = {
403
410
  label: string;
404
411
  hint?: string;
405
412
  };
406
- /** Per-row icon by option (`card`, `cash`, `wallet`, `credits`) or `plain`. */
413
+ /** Per-row icon by option (`card`, `cash`, `wallet`, `credits`) or `plain`. Replaces the default marks of that row. */
407
414
  icons?: Partial<Record<PaymentOptionKey | 'plain', ReactNode>>;
415
+ /**
416
+ * Brand marks (`BrandMark`): the provider's mark on its account / no-card rows and on a plain method, the cash point
417
+ * marks on the cash row and next to each point, and `cardBrands` on the card row. Default true; `false` keeps the
418
+ * rows text-only.
419
+ */
420
+ brandMarks?: boolean;
421
+ /** Card networks drawn on the card row (`visa`, `mastercard`, `amex`, `carnet`…), as the shop's provider accepts them. */
422
+ cardBrands?: readonly string[];
408
423
  /** How to print the total on the button. Default `formatMoney`. */
409
424
  renderAmount?: (amount: Money) => ReactNode;
410
425
  /** Anchor to the cart for the "rebuild" action when the cart expired. Default: plain `<a href>`. */
package/dist/payments.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { isSafeCssValue } from './chunk-672OTUM2.js';
2
+ import { BrandMark, findBrand } from './chunk-3ZJTDFHE.js';
2
3
  export { buildPayChoices, cashProviderData, checkoutToAttempt, choiceValue, parseChoice, payErrorResponse, paymentCsp, paymentCspHeaderName, paymentCspHeaders, paymentCspMode, paymentNonce, paymentNonceFromHeaders, voucherRows } from './chunk-ISEOCKIR.js';
3
- import { useResolvedLocale, useModalDialog } from './chunk-2WZIWIBW.js';
4
+ import { useModalDialog } from './chunk-ZBH7JYU4.js';
5
+ import { useResolvedLocale } from './chunk-T52H5HL3.js';
4
6
  import './chunk-DQBL2DFJ.js';
5
7
  import { forwardRef, useImperativeHandle, useRef, useEffect, useMemo, useId, useState, useCallback, useSyncExternalStore } from 'react';
6
8
  import { paymentMessages, formatMoney, generateIdempotencyKey, paymentOutcome, paymentErrorView, formatVoucherExpiry } from '@asteby/storefront';
@@ -101,6 +103,11 @@ function mercadoPagoBricksAdapter(options = {}) {
101
103
  paymentMethods: { minInstallments: 1, ...card?.max_installments ? { maxInstallments: card.max_installments } : {} },
102
104
  visual: {
103
105
  hidePaymentButton: true,
106
+ // El encabezado del Brick ("Tarjeta de crédito o débito" + banderas de tarjetas
107
+ // aceptadas) lo dibuja Mercado Pago, repite lo que la tienda ya muestra en su propia
108
+ // fila de método de pago y se veía cortado (p. ej. AMEX partido). hideFormTitle oculta
109
+ // la línea del título y las banderas.
110
+ hideFormTitle: true,
104
111
  style: { theme: "default", ...opts.theme && Object.keys(opts.theme).length ? { customVariables: opts.theme } : {} }
105
112
  }
106
113
  },
@@ -654,6 +661,7 @@ var ES = {
654
661
  pickDeliveryFirst: "Elige primero la entrega para ver el total y pagar con tarjeta.",
655
662
  updatingTotal: "Actualizando el total\u2026",
656
663
  syncFailed: "No pudimos actualizar el total con tu entrega. Puedes continuar al sitio del proveedor para pagar con tarjeta.",
664
+ quoteChanged: "Las tarifas de env\xEDo se actualizaron. Revisa tu env\xEDo para ver el total.",
657
665
  rebuildCart: "Volver a armar mi carrito",
658
666
  noPaymentTitle: "Por ahora no hay formas de pago disponibles",
659
667
  noPayment: "Intenta de nuevo en unos minutos o escr\xEDbenos para completar tu pedido.",
@@ -681,6 +689,7 @@ var EN = {
681
689
  pickDeliveryFirst: "Choose delivery first to see the total and pay by card.",
682
690
  updatingTotal: "Updating the total\u2026",
683
691
  syncFailed: "We couldn't update the total with your delivery. You can continue to the provider's site to pay by card.",
692
+ quoteChanged: "Shipping rates were updated. Check your delivery to see the total.",
684
693
  rebuildCart: "Rebuild my cart",
685
694
  noPaymentTitle: "No payment methods are available right now",
686
695
  noPayment: "Try again in a few minutes or contact us to complete your order.",
@@ -731,6 +740,8 @@ function PaymentMethodSection(props) {
731
740
  nonce,
732
741
  methodLabel,
733
742
  icons,
743
+ brandMarks = true,
744
+ cardBrands = [],
734
745
  renderAmount,
735
746
  rebuildHref,
736
747
  renderLink,
@@ -756,7 +767,7 @@ function PaymentMethodSection(props) {
756
767
  const brick = useRef(null);
757
768
  const cardChosen = selected?.mode === "card" && !retryAddress && !addressFirst;
758
769
  const cardActive = hydrated && cardChosen && !fallback;
759
- const wantSync = cardActive && deliveryReady && Boolean(syncDelivery);
770
+ const wantSync = hydrated && selected !== null && !addressFirst && !retryAddress && deliveryReady && Boolean(syncDelivery);
760
771
  const [sync, setSync] = useState(null);
761
772
  const syncRef = useRef(syncDelivery);
762
773
  syncRef.current = syncDelivery;
@@ -765,7 +776,7 @@ function PaymentMethodSection(props) {
765
776
  if (sync?.key === delivery && sync.ok) return;
766
777
  let cancelled = false;
767
778
  syncRef.current(delivery).then(
768
- (ok) => !cancelled && setSync({ key: delivery, ok }),
779
+ (ok) => !cancelled && setSync(ok === "requote" ? { key: delivery, ok: false, requote: true } : { key: delivery, ok }),
769
780
  () => !cancelled && setSync({ key: delivery, ok: false })
770
781
  );
771
782
  return () => {
@@ -774,7 +785,8 @@ function PaymentMethodSection(props) {
774
785
  }, [wantSync, delivery]);
775
786
  const settled = sync?.key === delivery;
776
787
  const synced = !syncDelivery || settled && sync.ok;
777
- const syncFailed = wantSync && settled && !sync.ok;
788
+ const requote = wantSync && settled && sync.requote === true;
789
+ const syncFailed = wantSync && settled && !sync.ok && !requote;
778
790
  const brickReady = cardActive && deliveryReady && synced && !syncFailed;
779
791
  const transport = useMemo(
780
792
  () => props.transport ?? createFetchTransport(payUrl, {
@@ -795,15 +807,19 @@ function PaymentMethodSection(props) {
795
807
  }
796
808
  onEvent?.(e);
797
809
  };
798
- const total = renderAmount ? renderAmount(amount) : formatMoney(amount, locale);
810
+ const total = wantSync && (!settled || requote) ? null : renderAmount ? renderAmount(amount) : formatMoney(amount, locale);
799
811
  const label = retryAddress ? m.retryAddress : addressFirst ? m.continue : selected?.mode === "plain" || !selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
800
812
  m.pay,
801
- " ",
802
- total
813
+ total ? /* @__PURE__ */ jsxs(Fragment, { children: [
814
+ " ",
815
+ total
816
+ ] }) : null
803
817
  ] }) : selected.mode === "redirect" || selected.mode === "card" && (fallback || syncFailed) ? m.payRedirect : /* @__PURE__ */ jsxs(Fragment, { children: [
804
818
  m.placeOrder,
805
- " ",
806
- total
819
+ total ? /* @__PURE__ */ jsxs(Fragment, { children: [
820
+ " ",
821
+ total
822
+ ] }) : null
807
823
  ] });
808
824
  function onPayClick(event) {
809
825
  if (!cardActive || syncFailed) return;
@@ -819,7 +835,9 @@ function PaymentMethodSection(props) {
819
835
  choices.choices.map((choice) => {
820
836
  const { label: rowLabel, hint } = rowText(choice, m, methodLabel);
821
837
  const checked = choice.value === choiceValue2;
822
- const icon = icons?.[choice.option ?? "plain"];
838
+ const slot = choice.option ?? "plain";
839
+ const icon = icons?.[slot] ?? (brandMarks ? defaultIcon(choice) : null);
840
+ const marks = brandMarks ? rowMarks(choice, cardBrands) : [];
823
841
  return /* @__PURE__ */ jsxs("div", { className: cx.choice, "data-sf-pay-choice": true, children: [
824
842
  /* @__PURE__ */ jsxs("label", { className: cx.radioLabel, children: [
825
843
  /* @__PURE__ */ jsx(
@@ -844,9 +862,10 @@ function PaymentMethodSection(props) {
844
862
  " ",
845
863
  hint
846
864
  ] }) : null
847
- ] })
865
+ ] }),
866
+ marks.length ? /* @__PURE__ */ jsx("span", { className: cx.brands, "data-sf-pay-brands": choice.option ?? "plain", "aria-hidden": "true", children: marks.map((mk) => /* @__PURE__ */ jsx(BrandMark, { brand: mk.key, logoUrl: mk.logoUrl ?? null, size: 20 }, mk.key)) }) : null
848
867
  ] }),
849
- /* @__PURE__ */ jsx(ChoicePanel, { choice, m, invalid, cx, tid, hidden: hydrated && !checked })
868
+ /* @__PURE__ */ jsx(ChoicePanel, { choice, m, invalid, cx, tid, hidden: hydrated && !checked, marks: brandMarks })
850
869
  ] }, choice.value);
851
870
  }),
852
871
  noPayment ? /* @__PURE__ */ jsxs("div", { role: "alert", className: cx.alert, "data-sf-pay-notice": "no-payment", ...tid("noPayment"), children: [
@@ -854,7 +873,7 @@ function PaymentMethodSection(props) {
854
873
  /* @__PURE__ */ jsx("p", { children: m.noPayment })
855
874
  ] }) : null,
856
875
  cardChosen && hydrated && choices.cardClient ? /* @__PURE__ */ jsxs("div", { className: cx.brickWrap, "data-sf-pay-card": true, ...tid("card"), children: [
857
- fallback ? null : !deliveryReady ? cardNotice("pickDelivery", m.pickDeliveryFirst) : syncFailed ? cardNotice("syncFailed", m.syncFailed) : !synced ? cardNotice("syncing", m.updatingTotal) : null,
876
+ fallback ? null : !deliveryReady ? cardNotice("pickDelivery", m.pickDeliveryFirst) : syncFailed ? cardNotice("syncFailed", m.syncFailed) : requote ? cardNotice("requote", m.quoteChanged) : !synced ? cardNotice("syncing", m.updatingTotal) : null,
858
877
  brickReady || fallback ? /* @__PURE__ */ jsx(
859
878
  PaymentBrick,
860
879
  {
@@ -905,7 +924,8 @@ function ChoicePanel({
905
924
  invalid,
906
925
  cx,
907
926
  tid,
908
- hidden
927
+ hidden,
928
+ marks = true
909
929
  }) {
910
930
  if (choice.mode === "redirect") {
911
931
  return /* @__PURE__ */ jsx("p", { className: cx.panel, hidden, "data-sf-pay-panel": "redirect", ...tid("redirectNote"), children: m.redirectNote });
@@ -915,6 +935,7 @@ function ChoicePanel({
915
935
  const more = choice.points.filter((p) => p.group !== "main");
916
936
  const row = (p, i, first) => /* @__PURE__ */ jsxs("label", { className: cx.cashPoint, "data-sf-pay-point": true, children: [
917
937
  /* @__PURE__ */ jsx("input", { type: "radio", name: CASH_POINT_FIELD, value: p.id, defaultChecked: first && i === 0, className: cx.cashPointInput, "aria-invalid": invalid?.has("cash_point") || void 0 }),
938
+ marks ? /* @__PURE__ */ jsx(BrandMark, { brand: p.payment_point || p.id, logoUrl: p.logo_url ?? null, size: 22 }) : null,
918
939
  /* @__PURE__ */ jsx("span", { children: p.label })
919
940
  ] }, p.id);
920
941
  return /* @__PURE__ */ jsxs("div", { className: cx.panel, hidden, "data-sf-pay-panel": "cash", ...tid("cash"), children: [
@@ -929,6 +950,30 @@ function ChoicePanel({
929
950
  /* @__PURE__ */ jsx("p", { className: cx.note, children: m.cashNote })
930
951
  ] });
931
952
  }
953
+ var glyph = (d) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", width: "24", height: "24", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", focusable: "false", "data-sf-pay-glyph": true, children: /* @__PURE__ */ jsx("path", { d }) });
954
+ var CARD_GLYPH = "M3 6.5h18v11H3zM3 10h18M6.5 14.5h4";
955
+ var CASH_GLYPH = "M3 7h18v10H3zM12 14.5a2.5 2.5 0 1 0 0-5a2.5 2.5 0 0 0 0 5zM6 10v4M18 10v4";
956
+ function defaultIcon(choice) {
957
+ if (choice.option === "card") return glyph(CARD_GLYPH);
958
+ if (choice.option === "cash") return glyph(CASH_GLYPH);
959
+ return findBrand(choice.methodKey) ? /* @__PURE__ */ jsx(BrandMark, { brand: choice.methodKey, size: 24 }) : null;
960
+ }
961
+ function rowMarks(choice, cardBrands) {
962
+ if (choice.option === "card") return cardBrands.map((key) => ({ key }));
963
+ if (choice.option === "cash") {
964
+ const seen = /* @__PURE__ */ new Set();
965
+ const out = [];
966
+ for (const p of choice.points) {
967
+ const key = p.payment_point || p.id;
968
+ if (p.group !== "main" || seen.has(key)) continue;
969
+ seen.add(key);
970
+ out.push({ key, logoUrl: p.logo_url ?? null });
971
+ if (out.length === 4) break;
972
+ }
973
+ return out;
974
+ }
975
+ return [];
976
+ }
932
977
 
933
978
  export { CASH_POINT_FIELD, MERCADOPAGO_BRICKS_KIND, PAYMENT_CHOICE_FIELD, PAYMENT_THEME_TOKENS, PaymentBrick, PaymentChallenge, PaymentHttpError, PaymentMethodSection, VoucherCard, createFetchTransport, formFields, loadPaymentSdk, mercadoPagoBricksAdapter, paymentMethodMessages, readPaymentTheme, resetPaymentSdkCache, sanitizeMercadoPagoFormData, useEmbeddedPayment };
934
979
  //# sourceMappingURL=payments.js.map