@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.cjs CHANGED
@@ -784,6 +784,7 @@ var ES = {
784
784
  pickDeliveryFirst: "Elige primero la entrega para ver el total y pagar con tarjeta.",
785
785
  updatingTotal: "Actualizando el total\u2026",
786
786
  syncFailed: "No pudimos actualizar el total con tu entrega. Puedes continuar al sitio del proveedor para pagar con tarjeta.",
787
+ quoteChanged: "Las tarifas de env\xEDo se actualizaron. Revisa tu env\xEDo para ver el total.",
787
788
  rebuildCart: "Volver a armar mi carrito",
788
789
  noPaymentTitle: "Por ahora no hay formas de pago disponibles",
789
790
  noPayment: "Intenta de nuevo en unos minutos o escr\xEDbenos para completar tu pedido.",
@@ -811,6 +812,7 @@ var EN = {
811
812
  pickDeliveryFirst: "Choose delivery first to see the total and pay by card.",
812
813
  updatingTotal: "Updating the total\u2026",
813
814
  syncFailed: "We couldn't update the total with your delivery. You can continue to the provider's site to pay by card.",
815
+ quoteChanged: "Shipping rates were updated. Check your delivery to see the total.",
814
816
  rebuildCart: "Rebuild my cart",
815
817
  noPaymentTitle: "No payment methods are available right now",
816
818
  noPayment: "Try again in a few minutes or contact us to complete your order.",
@@ -828,6 +830,178 @@ function paymentMethodMessages(locale, overrides) {
828
830
  const { options, ...rest } = overrides;
829
831
  return { ...base, ...rest, options: { ...base.options, ...options } };
830
832
  }
833
+
834
+ // src/checkout/brands.ts
835
+ var B = (key, name, kind, bg, fg, mark, shape) => ({
836
+ key,
837
+ name,
838
+ kind,
839
+ bg,
840
+ fg,
841
+ ...mark ? { mark } : {},
842
+ ...shape ? { shape } : {}
843
+ });
844
+ var LIST = [
845
+ // Parcel carriers (Mexico and global).
846
+ B("fedex", "FedEx", "carrier", "#4D148C", "#FFFFFF"),
847
+ B("dhl", "DHL", "carrier", "#FFCC00", "#B00410"),
848
+ B("estafeta", "Estafeta", "carrier", "#C8102E", "#FFFFFF"),
849
+ B("paquetexpress", "Paquetexpress", "carrier", "#0B4EA2", "#FFFFFF", "Paquet\nexpress"),
850
+ B("ups", "UPS", "carrier", "#351C15", "#FFB500"),
851
+ B("redpack", "Redpack", "carrier", "#C8102E", "#FFFFFF"),
852
+ B("99minutos", "99 Minutos", "carrier", "#00704A", "#FFFFFF", "99"),
853
+ B("jtexpress", "J&T Express", "carrier", "#C8102E", "#FFFFFF", "J&T"),
854
+ B("sendex", "Sendex", "carrier", "#003A70", "#FFFFFF"),
855
+ B("ampm", "AMPM", "carrier", "#1D1D1B", "#FFFFFF"),
856
+ B("tresguerras", "Tresguerras", "carrier", "#005BAC", "#FFFFFF", "3G"),
857
+ B("quiken", "Quiken", "carrier", "#1D1D1B", "#FFFFFF"),
858
+ B("imile", "iMile", "carrier", "#0057B8", "#FFFFFF"),
859
+ B("carssa", "Carssa", "carrier", "#00529B", "#FFFFFF"),
860
+ B("correosdemexico", "Correos de M\xE9xico", "carrier", "#7A1E3A", "#FFFFFF", "Correos\nM\xE9xico"),
861
+ B("usps", "USPS", "carrier", "#333366", "#FFFFFF"),
862
+ B("canadapost", "Canada Post", "carrier", "#DA291C", "#FFFFFF", "CPC"),
863
+ // Card networks.
864
+ B("visa", "Visa", "card", "#1A1F71", "#FFFFFF", "VISA"),
865
+ B("mastercard", "Mastercard", "card", "#FFFFFF", "#1D1D1B", "MC", "mastercard"),
866
+ B("amex", "American Express", "card", "#1F72CD", "#FFFFFF", "AMEX"),
867
+ B("carnet", "Carnet", "card", "#003B71", "#FFFFFF"),
868
+ // Wallets and payment providers.
869
+ B("mercadopago", "Mercado Pago", "wallet", "#00B1EA", "#0B1F4E", "mercado\npago"),
870
+ B("paypal", "PayPal", "wallet", "#003087", "#FFFFFF"),
871
+ B("clip", "Clip", "wallet", "#FC4C02", "#1D1D1B"),
872
+ B("applepay", "Apple Pay", "wallet", "#000000", "#FFFFFF", "Pay"),
873
+ B("googlepay", "Google Pay", "wallet", "#FFFFFF", "#3C4043", "G Pay"),
874
+ // Cash points and transfers (Mexico).
875
+ B("oxxo", "OXXO", "cash", "#E21E26", "#FFE600"),
876
+ B("spei", "SPEI", "bank", "#14365F", "#FFFFFF"),
877
+ B("stp", "STP", "bank", "#14365F", "#FFFFFF", "SPEI"),
878
+ B("7eleven", "7-Eleven", "cash", "#00704A", "#FFFFFF", "7-11"),
879
+ B("circlek", "Circle K", "cash", "#DA291C", "#FFFFFF", "K"),
880
+ B("soriana", "Soriana", "cash", "#C8102E", "#FFFFFF"),
881
+ B("extra", "Extra", "cash", "#E35205", "#FFFFFF"),
882
+ B("kiosko", "Kiosko", "cash", "#C8102E", "#FFFFFF"),
883
+ B("calimax", "Calimax", "cash", "#D52B1E", "#FFFFFF"),
884
+ B("farmaciasyza", "Farmacias Yza", "cash", "#0A5A9C", "#FFFFFF", "Yza"),
885
+ B("chedraui", "Chedraui", "cash", "#0047BB", "#FFFFFF"),
886
+ B("casaley", "Casa Ley", "cash", "#C8102E", "#FFFFFF", "Ley"),
887
+ B("telecomm", "Telecomm", "cash", "#6F1D46", "#FFFFFF"),
888
+ B("walmart", "Walmart", "cash", "#0071CE", "#FFFFFF"),
889
+ B("santander", "Santander", "bank", "#C00000", "#FFFFFF"),
890
+ B("bbva", "BBVA", "bank", "#004481", "#FFFFFF"),
891
+ B("citibanamex", "Citibanamex", "bank", "#004B8D", "#FFFFFF", "Citi"),
892
+ B("banorte", "Banorte", "bank", "#C8102E", "#FFFFFF")
893
+ ];
894
+ var ALIASES = {
895
+ fedexmx: "fedex",
896
+ fedexexpress: "fedex",
897
+ dhlexpress: "dhl",
898
+ dhlmx: "dhl",
899
+ paquetexpres: "paquetexpress",
900
+ paqueteexpress: "paquetexpress",
901
+ jt: "jtexpress",
902
+ jandtexpress: "jtexpress",
903
+ noventaynueveminutos: "99minutos",
904
+ correos: "correosdemexico",
905
+ sepomex: "correosdemexico",
906
+ master: "mastercard",
907
+ americanexpress: "amex",
908
+ mercadopagomx: "mercadopago",
909
+ mp: "mercadopago",
910
+ clabe: "spei",
911
+ banktransfer: "spei",
912
+ seveneleven: "7eleven",
913
+ "711": "7eleven",
914
+ circulok: "circlek",
915
+ yza: "farmaciasyza",
916
+ bancomer: "bbva",
917
+ banamex: "citibanamex",
918
+ ley: "casaley"
919
+ };
920
+ var BY_KEY = new Map(LIST.map((b) => [b.key, b]));
921
+ function brandKey(raw) {
922
+ const flat = (raw ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]/g, "");
923
+ const noCountry = flat.endsWith("mexico") && flat !== "correosdemexico" ? flat.slice(0, -6) : flat;
924
+ return ALIASES[noCountry] ?? (BY_KEY.has(noCountry) ? noCountry : ALIASES[flat] ?? flat);
925
+ }
926
+ function findBrand(raw) {
927
+ return BY_KEY.get(brandKey(raw)) ?? null;
928
+ }
929
+ function brandName(raw) {
930
+ const brand = findBrand(raw);
931
+ if (brand) return brand.name;
932
+ const text = (raw ?? "").trim();
933
+ if (!text) return null;
934
+ if (text !== text.toLowerCase() && text !== text.toUpperCase()) return text;
935
+ return text.toLowerCase().split(/[\s_-]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
936
+ }
937
+ function safeLogo(url) {
938
+ if (!url) return null;
939
+ try {
940
+ return new URL(url).protocol === "https:" ? url : null;
941
+ } catch {
942
+ return null;
943
+ }
944
+ }
945
+ function monogram(raw) {
946
+ const words = (brandName(raw) ?? raw).split(/\s+/).filter(Boolean);
947
+ const letters = words.length > 1 ? words.slice(0, 2).map((w) => w[0]) : [(words[0] ?? "?").slice(0, 2)];
948
+ return letters.join("").toUpperCase();
949
+ }
950
+ function BrandMark({ brand, logoUrl, size = 24, title, className }) {
951
+ const height = size;
952
+ const width = Math.round(size * 1.5);
953
+ const a11y = title ? { role: "img", "aria-label": title } : { "aria-hidden": true };
954
+ const logo = safeLogo(logoUrl);
955
+ const found = findBrand(brand);
956
+ const key = found?.key ?? "generic";
957
+ const style = { width, height, flex: "none" };
958
+ if (logo) {
959
+ return /* @__PURE__ */ jsxRuntime.jsx(
960
+ "img",
961
+ {
962
+ src: logo,
963
+ alt: title ?? "",
964
+ width,
965
+ height,
966
+ loading: "lazy",
967
+ decoding: "async",
968
+ className,
969
+ style: { ...style, objectFit: "contain" },
970
+ "data-sf-brand-mark": key,
971
+ ...title ? {} : { "aria-hidden": true }
972
+ }
973
+ );
974
+ }
975
+ const ring = "var(--sf-brand-mark-ring, rgb(127 127 127 / 0.35))";
976
+ if (found?.shape === "mastercard") {
977
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 48 32", width, height, className, style, "data-sf-brand-mark": key, focusable: "false", ...a11y, children: [
978
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "0.5", y: "0.5", width: "47", height: "31", rx: "5", fill: found.bg, stroke: ring }),
979
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "19", cy: "16", r: "9", fill: "#EB001B" }),
980
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "29", cy: "16", r: "9", fill: "#F79E1B" }),
981
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M24 8.52a9 9 0 0 1 0 14.96a9 9 0 0 1 0-14.96z", fill: "#FF5F00" })
982
+ ] });
983
+ }
984
+ const text = found ? found.mark ?? found.name : monogram(brand ?? "");
985
+ const bg = found?.bg ?? "var(--sf-brand-mark-bg, #E7E9EE)";
986
+ const fg = found?.fg ?? "var(--sf-brand-mark-fg, #2B2F38)";
987
+ const lines = text.split("\n");
988
+ const longest = Math.max(...lines.map((l) => l.length));
989
+ const fontSize = lines.length > 1 ? longest <= 5 ? 11 : 9.5 : longest <= 2 ? 15 : longest <= 4 ? 12 : 10;
990
+ const fit = (l) => l.length > 5 ? { textLength: 40, lengthAdjust: "spacingAndGlyphs" } : {};
991
+ const italic = found?.key === "visa" || found?.key === "fedex" ? "italic" : void 0;
992
+ const common = {
993
+ textAnchor: "middle",
994
+ fill: fg,
995
+ fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif",
996
+ fontWeight: 800,
997
+ fontSize,
998
+ fontStyle: italic
999
+ };
1000
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 48 32", width, height, className, style, "data-sf-brand-mark": key, focusable: "false", ...a11y, children: [
1001
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "0.5", y: "0.5", width: "47", height: "31", rx: "5", fill: bg, stroke: ring }),
1002
+ lines.length > 1 ? lines.slice(0, 2).map((l, i) => /* @__PURE__ */ jsxRuntime.jsx("text", { x: "24", y: i === 0 ? 15 : 25.5, ...common, ...fit(l), children: l }, i)) : /* @__PURE__ */ jsxRuntime.jsx("text", { x: "24", y: "16", dominantBaseline: "central", letterSpacing: longest <= 4 ? 0.3 : 0, ...common, ...fit(text), children: text })
1003
+ ] });
1004
+ }
831
1005
  var PAYMENT_CHOICE_FIELD = "payment_choice";
832
1006
  var CASH_POINT_FIELD = "cash_point";
833
1007
  function formFields(form) {
@@ -861,6 +1035,8 @@ function PaymentMethodSection(props) {
861
1035
  nonce,
862
1036
  methodLabel,
863
1037
  icons,
1038
+ brandMarks = true,
1039
+ cardBrands = [],
864
1040
  renderAmount,
865
1041
  rebuildHref,
866
1042
  renderLink,
@@ -886,7 +1062,7 @@ function PaymentMethodSection(props) {
886
1062
  const brick = react.useRef(null);
887
1063
  const cardChosen = selected?.mode === "card" && !retryAddress && !addressFirst;
888
1064
  const cardActive = hydrated && cardChosen && !fallback;
889
- const wantSync = cardActive && deliveryReady && Boolean(syncDelivery);
1065
+ const wantSync = hydrated && selected !== null && !addressFirst && !retryAddress && deliveryReady && Boolean(syncDelivery);
890
1066
  const [sync, setSync] = react.useState(null);
891
1067
  const syncRef = react.useRef(syncDelivery);
892
1068
  syncRef.current = syncDelivery;
@@ -895,7 +1071,7 @@ function PaymentMethodSection(props) {
895
1071
  if (sync?.key === delivery && sync.ok) return;
896
1072
  let cancelled = false;
897
1073
  syncRef.current(delivery).then(
898
- (ok) => !cancelled && setSync({ key: delivery, ok }),
1074
+ (ok) => !cancelled && setSync(ok === "requote" ? { key: delivery, ok: false, requote: true } : { key: delivery, ok }),
899
1075
  () => !cancelled && setSync({ key: delivery, ok: false })
900
1076
  );
901
1077
  return () => {
@@ -904,7 +1080,8 @@ function PaymentMethodSection(props) {
904
1080
  }, [wantSync, delivery]);
905
1081
  const settled = sync?.key === delivery;
906
1082
  const synced = !syncDelivery || settled && sync.ok;
907
- const syncFailed = wantSync && settled && !sync.ok;
1083
+ const requote = wantSync && settled && sync.requote === true;
1084
+ const syncFailed = wantSync && settled && !sync.ok && !requote;
908
1085
  const brickReady = cardActive && deliveryReady && synced && !syncFailed;
909
1086
  const transport = react.useMemo(
910
1087
  () => props.transport ?? createFetchTransport(payUrl, {
@@ -925,15 +1102,19 @@ function PaymentMethodSection(props) {
925
1102
  }
926
1103
  onEvent?.(e);
927
1104
  };
928
- const total = renderAmount ? renderAmount(amount) : storefront.formatMoney(amount, locale);
1105
+ const total = wantSync && (!settled || requote) ? null : renderAmount ? renderAmount(amount) : storefront.formatMoney(amount, locale);
929
1106
  const label = retryAddress ? m.retryAddress : addressFirst ? m.continue : selected?.mode === "plain" || !selected ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
930
1107
  m.pay,
931
- " ",
932
- total
1108
+ total ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1109
+ " ",
1110
+ total
1111
+ ] }) : null
933
1112
  ] }) : selected.mode === "redirect" || selected.mode === "card" && (fallback || syncFailed) ? m.payRedirect : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
934
1113
  m.placeOrder,
935
- " ",
936
- total
1114
+ total ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1115
+ " ",
1116
+ total
1117
+ ] }) : null
937
1118
  ] });
938
1119
  function onPayClick(event) {
939
1120
  if (!cardActive || syncFailed) return;
@@ -949,7 +1130,9 @@ function PaymentMethodSection(props) {
949
1130
  choices.choices.map((choice) => {
950
1131
  const { label: rowLabel, hint } = rowText(choice, m, methodLabel);
951
1132
  const checked = choice.value === choiceValue2;
952
- const icon = icons?.[choice.option ?? "plain"];
1133
+ const slot = choice.option ?? "plain";
1134
+ const icon = icons?.[slot] ?? (brandMarks ? defaultIcon(choice) : null);
1135
+ const marks = brandMarks ? rowMarks(choice, cardBrands) : [];
953
1136
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.choice, "data-sf-pay-choice": true, children: [
954
1137
  /* @__PURE__ */ jsxRuntime.jsxs("label", { className: cx.radioLabel, children: [
955
1138
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -974,9 +1157,10 @@ function PaymentMethodSection(props) {
974
1157
  " ",
975
1158
  hint
976
1159
  ] }) : null
977
- ] })
1160
+ ] }),
1161
+ marks.length ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: cx.brands, "data-sf-pay-brands": choice.option ?? "plain", "aria-hidden": "true", children: marks.map((mk) => /* @__PURE__ */ jsxRuntime.jsx(BrandMark, { brand: mk.key, logoUrl: mk.logoUrl ?? null, size: 20 }, mk.key)) }) : null
978
1162
  ] }),
979
- /* @__PURE__ */ jsxRuntime.jsx(ChoicePanel, { choice, m, invalid, cx, tid, hidden: hydrated && !checked })
1163
+ /* @__PURE__ */ jsxRuntime.jsx(ChoicePanel, { choice, m, invalid, cx, tid, hidden: hydrated && !checked, marks: brandMarks })
980
1164
  ] }, choice.value);
981
1165
  }),
982
1166
  noPayment ? /* @__PURE__ */ jsxRuntime.jsxs("div", { role: "alert", className: cx.alert, "data-sf-pay-notice": "no-payment", ...tid("noPayment"), children: [
@@ -984,7 +1168,7 @@ function PaymentMethodSection(props) {
984
1168
  /* @__PURE__ */ jsxRuntime.jsx("p", { children: m.noPayment })
985
1169
  ] }) : null,
986
1170
  cardChosen && hydrated && choices.cardClient ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.brickWrap, "data-sf-pay-card": true, ...tid("card"), children: [
987
- fallback ? null : !deliveryReady ? cardNotice("pickDelivery", m.pickDeliveryFirst) : syncFailed ? cardNotice("syncFailed", m.syncFailed) : !synced ? cardNotice("syncing", m.updatingTotal) : null,
1171
+ fallback ? null : !deliveryReady ? cardNotice("pickDelivery", m.pickDeliveryFirst) : syncFailed ? cardNotice("syncFailed", m.syncFailed) : requote ? cardNotice("requote", m.quoteChanged) : !synced ? cardNotice("syncing", m.updatingTotal) : null,
988
1172
  brickReady || fallback ? /* @__PURE__ */ jsxRuntime.jsx(
989
1173
  PaymentBrick,
990
1174
  {
@@ -1035,7 +1219,8 @@ function ChoicePanel({
1035
1219
  invalid,
1036
1220
  cx,
1037
1221
  tid,
1038
- hidden
1222
+ hidden,
1223
+ marks = true
1039
1224
  }) {
1040
1225
  if (choice.mode === "redirect") {
1041
1226
  return /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.panel, hidden, "data-sf-pay-panel": "redirect", ...tid("redirectNote"), children: m.redirectNote });
@@ -1045,6 +1230,7 @@ function ChoicePanel({
1045
1230
  const more = choice.points.filter((p) => p.group !== "main");
1046
1231
  const row = (p, i, first) => /* @__PURE__ */ jsxRuntime.jsxs("label", { className: cx.cashPoint, "data-sf-pay-point": true, children: [
1047
1232
  /* @__PURE__ */ jsxRuntime.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 }),
1233
+ marks ? /* @__PURE__ */ jsxRuntime.jsx(BrandMark, { brand: p.payment_point || p.id, logoUrl: p.logo_url ?? null, size: 22 }) : null,
1048
1234
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: p.label })
1049
1235
  ] }, p.id);
1050
1236
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cx.panel, hidden, "data-sf-pay-panel": "cash", ...tid("cash"), children: [
@@ -1059,6 +1245,30 @@ function ChoicePanel({
1059
1245
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: cx.note, children: m.cashNote })
1060
1246
  ] });
1061
1247
  }
1248
+ var glyph = (d) => /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx("path", { d }) });
1249
+ var CARD_GLYPH = "M3 6.5h18v11H3zM3 10h18M6.5 14.5h4";
1250
+ 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";
1251
+ function defaultIcon(choice) {
1252
+ if (choice.option === "card") return glyph(CARD_GLYPH);
1253
+ if (choice.option === "cash") return glyph(CASH_GLYPH);
1254
+ return findBrand(choice.methodKey) ? /* @__PURE__ */ jsxRuntime.jsx(BrandMark, { brand: choice.methodKey, size: 24 }) : null;
1255
+ }
1256
+ function rowMarks(choice, cardBrands) {
1257
+ if (choice.option === "card") return cardBrands.map((key) => ({ key }));
1258
+ if (choice.option === "cash") {
1259
+ const seen = /* @__PURE__ */ new Set();
1260
+ const out = [];
1261
+ for (const p of choice.points) {
1262
+ const key = p.payment_point || p.id;
1263
+ if (p.group !== "main" || seen.has(key)) continue;
1264
+ seen.add(key);
1265
+ out.push({ key, logoUrl: p.logo_url ?? null });
1266
+ if (out.length === 4) break;
1267
+ }
1268
+ return out;
1269
+ }
1270
+ return [];
1271
+ }
1062
1272
 
1063
1273
  // src/payments/choices.ts
1064
1274
  var SEP = "|";