@asteby/storefront-react 0.9.0 → 0.10.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-DSZVJ2PW.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';
@@ -654,6 +656,7 @@ var ES = {
654
656
  pickDeliveryFirst: "Elige primero la entrega para ver el total y pagar con tarjeta.",
655
657
  updatingTotal: "Actualizando el total\u2026",
656
658
  syncFailed: "No pudimos actualizar el total con tu entrega. Puedes continuar al sitio del proveedor para pagar con tarjeta.",
659
+ quoteChanged: "Las tarifas de env\xEDo se actualizaron. Revisa tu env\xEDo para ver el total.",
657
660
  rebuildCart: "Volver a armar mi carrito",
658
661
  noPaymentTitle: "Por ahora no hay formas de pago disponibles",
659
662
  noPayment: "Intenta de nuevo en unos minutos o escr\xEDbenos para completar tu pedido.",
@@ -681,6 +684,7 @@ var EN = {
681
684
  pickDeliveryFirst: "Choose delivery first to see the total and pay by card.",
682
685
  updatingTotal: "Updating the total\u2026",
683
686
  syncFailed: "We couldn't update the total with your delivery. You can continue to the provider's site to pay by card.",
687
+ quoteChanged: "Shipping rates were updated. Check your delivery to see the total.",
684
688
  rebuildCart: "Rebuild my cart",
685
689
  noPaymentTitle: "No payment methods are available right now",
686
690
  noPayment: "Try again in a few minutes or contact us to complete your order.",
@@ -731,6 +735,8 @@ function PaymentMethodSection(props) {
731
735
  nonce,
732
736
  methodLabel,
733
737
  icons,
738
+ brandMarks = true,
739
+ cardBrands = [],
734
740
  renderAmount,
735
741
  rebuildHref,
736
742
  renderLink,
@@ -756,7 +762,7 @@ function PaymentMethodSection(props) {
756
762
  const brick = useRef(null);
757
763
  const cardChosen = selected?.mode === "card" && !retryAddress && !addressFirst;
758
764
  const cardActive = hydrated && cardChosen && !fallback;
759
- const wantSync = cardActive && deliveryReady && Boolean(syncDelivery);
765
+ const wantSync = hydrated && selected !== null && !addressFirst && !retryAddress && deliveryReady && Boolean(syncDelivery);
760
766
  const [sync, setSync] = useState(null);
761
767
  const syncRef = useRef(syncDelivery);
762
768
  syncRef.current = syncDelivery;
@@ -765,7 +771,7 @@ function PaymentMethodSection(props) {
765
771
  if (sync?.key === delivery && sync.ok) return;
766
772
  let cancelled = false;
767
773
  syncRef.current(delivery).then(
768
- (ok) => !cancelled && setSync({ key: delivery, ok }),
774
+ (ok) => !cancelled && setSync(ok === "requote" ? { key: delivery, ok: false, requote: true } : { key: delivery, ok }),
769
775
  () => !cancelled && setSync({ key: delivery, ok: false })
770
776
  );
771
777
  return () => {
@@ -774,7 +780,8 @@ function PaymentMethodSection(props) {
774
780
  }, [wantSync, delivery]);
775
781
  const settled = sync?.key === delivery;
776
782
  const synced = !syncDelivery || settled && sync.ok;
777
- const syncFailed = wantSync && settled && !sync.ok;
783
+ const requote = wantSync && settled && sync.requote === true;
784
+ const syncFailed = wantSync && settled && !sync.ok && !requote;
778
785
  const brickReady = cardActive && deliveryReady && synced && !syncFailed;
779
786
  const transport = useMemo(
780
787
  () => props.transport ?? createFetchTransport(payUrl, {
@@ -795,15 +802,19 @@ function PaymentMethodSection(props) {
795
802
  }
796
803
  onEvent?.(e);
797
804
  };
798
- const total = renderAmount ? renderAmount(amount) : formatMoney(amount, locale);
805
+ const total = wantSync && (!settled || requote) ? null : renderAmount ? renderAmount(amount) : formatMoney(amount, locale);
799
806
  const label = retryAddress ? m.retryAddress : addressFirst ? m.continue : selected?.mode === "plain" || !selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
800
807
  m.pay,
801
- " ",
802
- total
808
+ total ? /* @__PURE__ */ jsxs(Fragment, { children: [
809
+ " ",
810
+ total
811
+ ] }) : null
803
812
  ] }) : selected.mode === "redirect" || selected.mode === "card" && (fallback || syncFailed) ? m.payRedirect : /* @__PURE__ */ jsxs(Fragment, { children: [
804
813
  m.placeOrder,
805
- " ",
806
- total
814
+ total ? /* @__PURE__ */ jsxs(Fragment, { children: [
815
+ " ",
816
+ total
817
+ ] }) : null
807
818
  ] });
808
819
  function onPayClick(event) {
809
820
  if (!cardActive || syncFailed) return;
@@ -819,7 +830,9 @@ function PaymentMethodSection(props) {
819
830
  choices.choices.map((choice) => {
820
831
  const { label: rowLabel, hint } = rowText(choice, m, methodLabel);
821
832
  const checked = choice.value === choiceValue2;
822
- const icon = icons?.[choice.option ?? "plain"];
833
+ const slot = choice.option ?? "plain";
834
+ const icon = icons?.[slot] ?? (brandMarks ? defaultIcon(choice) : null);
835
+ const marks = brandMarks ? rowMarks(choice, cardBrands) : [];
823
836
  return /* @__PURE__ */ jsxs("div", { className: cx.choice, "data-sf-pay-choice": true, children: [
824
837
  /* @__PURE__ */ jsxs("label", { className: cx.radioLabel, children: [
825
838
  /* @__PURE__ */ jsx(
@@ -844,9 +857,10 @@ function PaymentMethodSection(props) {
844
857
  " ",
845
858
  hint
846
859
  ] }) : null
847
- ] })
860
+ ] }),
861
+ 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
862
  ] }),
849
- /* @__PURE__ */ jsx(ChoicePanel, { choice, m, invalid, cx, tid, hidden: hydrated && !checked })
863
+ /* @__PURE__ */ jsx(ChoicePanel, { choice, m, invalid, cx, tid, hidden: hydrated && !checked, marks: brandMarks })
850
864
  ] }, choice.value);
851
865
  }),
852
866
  noPayment ? /* @__PURE__ */ jsxs("div", { role: "alert", className: cx.alert, "data-sf-pay-notice": "no-payment", ...tid("noPayment"), children: [
@@ -854,7 +868,7 @@ function PaymentMethodSection(props) {
854
868
  /* @__PURE__ */ jsx("p", { children: m.noPayment })
855
869
  ] }) : null,
856
870
  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,
871
+ fallback ? null : !deliveryReady ? cardNotice("pickDelivery", m.pickDeliveryFirst) : syncFailed ? cardNotice("syncFailed", m.syncFailed) : requote ? cardNotice("requote", m.quoteChanged) : !synced ? cardNotice("syncing", m.updatingTotal) : null,
858
872
  brickReady || fallback ? /* @__PURE__ */ jsx(
859
873
  PaymentBrick,
860
874
  {
@@ -905,7 +919,8 @@ function ChoicePanel({
905
919
  invalid,
906
920
  cx,
907
921
  tid,
908
- hidden
922
+ hidden,
923
+ marks = true
909
924
  }) {
910
925
  if (choice.mode === "redirect") {
911
926
  return /* @__PURE__ */ jsx("p", { className: cx.panel, hidden, "data-sf-pay-panel": "redirect", ...tid("redirectNote"), children: m.redirectNote });
@@ -915,6 +930,7 @@ function ChoicePanel({
915
930
  const more = choice.points.filter((p) => p.group !== "main");
916
931
  const row = (p, i, first) => /* @__PURE__ */ jsxs("label", { className: cx.cashPoint, "data-sf-pay-point": true, children: [
917
932
  /* @__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 }),
933
+ marks ? /* @__PURE__ */ jsx(BrandMark, { brand: p.payment_point || p.id, logoUrl: p.logo_url ?? null, size: 22 }) : null,
918
934
  /* @__PURE__ */ jsx("span", { children: p.label })
919
935
  ] }, p.id);
920
936
  return /* @__PURE__ */ jsxs("div", { className: cx.panel, hidden, "data-sf-pay-panel": "cash", ...tid("cash"), children: [
@@ -929,6 +945,30 @@ function ChoicePanel({
929
945
  /* @__PURE__ */ jsx("p", { className: cx.note, children: m.cashNote })
930
946
  ] });
931
947
  }
948
+ 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 }) });
949
+ var CARD_GLYPH = "M3 6.5h18v11H3zM3 10h18M6.5 14.5h4";
950
+ 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";
951
+ function defaultIcon(choice) {
952
+ if (choice.option === "card") return glyph(CARD_GLYPH);
953
+ if (choice.option === "cash") return glyph(CASH_GLYPH);
954
+ return findBrand(choice.methodKey) ? /* @__PURE__ */ jsx(BrandMark, { brand: choice.methodKey, size: 24 }) : null;
955
+ }
956
+ function rowMarks(choice, cardBrands) {
957
+ if (choice.option === "card") return cardBrands.map((key) => ({ key }));
958
+ if (choice.option === "cash") {
959
+ const seen = /* @__PURE__ */ new Set();
960
+ const out = [];
961
+ for (const p of choice.points) {
962
+ const key = p.payment_point || p.id;
963
+ if (p.group !== "main" || seen.has(key)) continue;
964
+ seen.add(key);
965
+ out.push({ key, logoUrl: p.logo_url ?? null });
966
+ if (out.length === 4) break;
967
+ }
968
+ return out;
969
+ }
970
+ return [];
971
+ }
932
972
 
933
973
  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
974
  //# sourceMappingURL=payments.js.map