@fayz-ai/storefront 0.8.0 → 0.8.2

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/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { setShopProvider, getShopProvider } from '@fayz-ai/shop/runtime';
1
+ import { getShopProvider, setShopProvider } from '@fayz-ai/shop/runtime';
2
2
  import { createMockShopProvider } from '@fayz-ai/shop/mock';
3
3
  import { createAuthRuntime, createMockAuthAdapter } from '@fayz-ai/plugin-auth';
4
+ import { setShopAccessTokenResolver } from '@fayz-ai/sdk/shop';
4
5
  import { create } from 'zustand';
5
6
  import { persist } from 'zustand/middleware';
6
- import React6, { createContext, useState, useRef, useEffect, useMemo, useContext, useCallback } from 'react';
7
- import { hashRouterAdapter, registerScaffold, registerBlock, renderBlocks, defineApp } from '@fayz-ai/core';
7
+ import React7, { createContext, useState, useRef, useEffect, useMemo, useContext, useCallback } from 'react';
8
+ import { hashRouterAdapter, normalizePostalCode, lookupPostalCode, registerScaffold, registerBlock, formatPostalCode, renderBlocks, defineApp } from '@fayz-ai/core';
8
9
  export { getSupabaseClientOptional, renderApp } from '@fayz-ai/core';
9
10
  import * as LucideIcons from 'lucide-react';
10
- import { ChevronLeft, ChevronRight, ShoppingBag, X, Trash2, Phone, Mail, Search, User, UserCircle, Package, CreditCard, LogOut, Truck, RefreshCcw, ShieldCheck, Lock, Minus, Plus, Info, AlertCircle, Check, Send, MessageCircle, MapPin, PackageCheck, QrCode, XCircle, RotateCcw, Clock } from 'lucide-react';
11
+ import { ChevronLeft, ChevronRight, ShoppingBag, X, Trash2, Phone, Mail, Search, User, UserCircle, Package, CreditCard, LogOut, Truck, RefreshCcw, ShieldCheck, Lock, Minus, Plus, MapPin, Info, AlertCircle, Check, Send, MessageCircle, PackageCheck, QrCode, XCircle, RotateCcw, Clock } from 'lucide-react';
11
12
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
12
13
  import { validateDiscount } from '@fayz-ai/shop';
13
14
 
@@ -53,10 +54,23 @@ function resolveAuthAdapter(configured, fallback) {
53
54
  }
54
55
  }).adapter;
55
56
  }
57
+ var _accessToken = null;
58
+ var _hadAuthUser = false;
59
+ async function syncAccessToken(adapter2) {
60
+ try {
61
+ const existing = await adapter2.getSession();
62
+ _accessToken = existing?.session.accessToken ?? null;
63
+ } catch {
64
+ _accessToken = null;
65
+ }
66
+ }
56
67
  function initCustomerAuth(adapter2) {
57
68
  _adapter = adapter2;
69
+ setShopAccessTokenResolver(() => _accessToken);
58
70
  adapter2.getSession().then((existing) => {
71
+ _accessToken = existing?.session.accessToken ?? null;
59
72
  if (!existing) return;
73
+ _hadAuthUser = true;
60
74
  const store = useSessionStore.getState();
61
75
  if (store.email !== existing.user.email) {
62
76
  void linkCustomer(existing.user);
@@ -64,7 +78,16 @@ function initCustomerAuth(adapter2) {
64
78
  }).catch(() => {
65
79
  });
66
80
  adapter2.onAuthStateChange((user) => {
67
- if (!user) useSessionStore.getState().signOut();
81
+ if (!user) {
82
+ _accessToken = null;
83
+ if (_hadAuthUser) {
84
+ _hadAuthUser = false;
85
+ useSessionStore.getState().signOut();
86
+ }
87
+ return;
88
+ }
89
+ _hadAuthUser = true;
90
+ void syncAccessToken(adapter2);
68
91
  });
69
92
  }
70
93
  function getCustomerAuthAdapter() {
@@ -100,6 +123,8 @@ async function establishCustomerSession(email, opts) {
100
123
  if (opts?.password) {
101
124
  const adapter2 = getCustomerAuthAdapter();
102
125
  const session = await adapter2.signIn(normalized, opts.password);
126
+ _accessToken = session.accessToken ?? null;
127
+ _hadAuthUser = true;
103
128
  return linkCustomer(session.user.email ? session.user : { ...session.user, email: normalized }, opts.name);
104
129
  }
105
130
  return linkCustomer({ email: normalized }, opts?.name);
@@ -111,12 +136,16 @@ async function signUpCustomer(email, password, name) {
111
136
  const adapter2 = getCustomerAuthAdapter();
112
137
  const normalized = email.trim().toLowerCase();
113
138
  const session = await adapter2.signUp(normalized, password, name);
139
+ _accessToken = session.accessToken ?? null;
140
+ _hadAuthUser = true;
114
141
  return linkCustomer(session.user.email ? session.user : { ...session.user, email: normalized }, name);
115
142
  }
116
143
  async function signOutCustomer() {
117
144
  try {
118
145
  await getCustomerAuthAdapter().signOut();
119
146
  } finally {
147
+ _accessToken = null;
148
+ _hadAuthUser = false;
120
149
  useSessionStore.getState().signOut();
121
150
  }
122
151
  }
@@ -129,7 +158,10 @@ function resolveConfig(config) {
129
158
  currency: config.currency ?? "BRL",
130
159
  locale: config.locale ?? "pt-BR",
131
160
  shipping: { flatRate: config.shipping?.flatRate ?? 0, freeAbove: config.shipping?.freeAbove },
132
- payments: { mode: config.payments?.mode ?? "mock" },
161
+ payments: {
162
+ mode: config.payments?.mode ?? "mock",
163
+ methods: config.payments?.methods?.length ? config.payments.methods : ["pix", "credit_card", "cash"]
164
+ },
133
165
  commerceMode,
134
166
  enquiry: {
135
167
  label: config.enquiry?.label ?? "Contact me",
@@ -204,7 +236,7 @@ function matchPath(pattern, path) {
204
236
  return params;
205
237
  }
206
238
  function Link({ to, children, ...rest }) {
207
- return React6.createElement("a", { href: `#${to}`, ...rest }, children);
239
+ return React7.createElement("a", { href: `#${to}`, ...rest }, children);
208
240
  }
209
241
 
210
242
  // src/format.ts
@@ -214,6 +246,87 @@ function formatMoney(value, currency = "BRL", locale = "pt-BR") {
214
246
  function roundCents(value) {
215
247
  return Math.round(value * 100) / 100;
216
248
  }
249
+ var EMPTY = {
250
+ postalCode: "",
251
+ address: null,
252
+ options: [],
253
+ selectedZoneId: null,
254
+ status: "idle",
255
+ error: null
256
+ };
257
+ var useDeliveryStore = create()(
258
+ persist(
259
+ (set, get) => ({
260
+ ...EMPTY,
261
+ async resolve(postalCode, subtotal) {
262
+ const code = normalizePostalCode(postalCode);
263
+ if (code.length !== 8) {
264
+ set({ ...EMPTY, postalCode, status: "error", error: "Digite os 8 d\xEDgitos do CEP." });
265
+ return;
266
+ }
267
+ set({ postalCode: code, status: "loading", error: null });
268
+ let options = [];
269
+ try {
270
+ options = await getShopProvider().quoteShipping?.(code, subtotal) ?? [];
271
+ } catch {
272
+ set({
273
+ ...EMPTY,
274
+ postalCode: code,
275
+ status: "error",
276
+ error: "N\xE3o foi poss\xEDvel calcular o frete agora. Tente de novo."
277
+ });
278
+ return;
279
+ }
280
+ let address = null;
281
+ let notFound = false;
282
+ try {
283
+ address = await lookupPostalCode(code);
284
+ notFound = address === null;
285
+ } catch {
286
+ address = null;
287
+ }
288
+ set({
289
+ postalCode: code,
290
+ address,
291
+ options,
292
+ selectedZoneId: options[0]?.zoneId ?? null,
293
+ // A store with no zones configured quotes nothing, and that must NOT
294
+ // read as "we don't deliver here" — it falls back to the store-wide
295
+ // flat rate, which the cart already knows how to show.
296
+ status: options.length > 0 ? "served" : "unserved",
297
+ // Reported alongside the price, not instead of it: the CEP may be
298
+ // mistyped even though its range is covered.
299
+ error: notFound ? "CEP n\xE3o encontrado. Confira os n\xFAmeros." : null
300
+ });
301
+ },
302
+ selectZone(zoneId) {
303
+ if (get().options.some((option) => option.zoneId === zoneId)) set({ selectedZoneId: zoneId });
304
+ },
305
+ clear() {
306
+ set({ ...EMPTY });
307
+ }
308
+ }),
309
+ {
310
+ name: "fayz.storefront.delivery.v1",
311
+ // Deliberately NOT persisting `options`: a rehydrated
312
+ // price would be shown as current without anything having quoted it. The
313
+ // address survives, the money is asked for again.
314
+ partialize: (state) => ({ postalCode: state.postalCode, address: state.address }),
315
+ onRehydrateStorage: () => (state) => {
316
+ if (state?.address) state.status = "idle";
317
+ }
318
+ }
319
+ )
320
+ );
321
+ function resolveOptionRate(option, subtotal) {
322
+ if (option.freeAbove != null && subtotal >= option.freeAbove) return 0;
323
+ return option.baseRate;
324
+ }
325
+ var selectQuotedShipping = (state, subtotal) => {
326
+ if (state.status !== "served") return null;
327
+ const option = state.options.find((o) => o.zoneId === state.selectedZoneId) ?? state.options[0];
328
+ return option ? resolveOptionRate(option, subtotal) : null;
329
+ };
217
330
 
218
331
  // src/product-options.ts
219
332
  function cleanValues(value) {
@@ -337,6 +450,8 @@ var selectDiscountTotal = (s) => roundCents(selectSubtotal(s) * (s.discountPerce
337
450
  var selectShipping = (s, cfg) => {
338
451
  if (s.lines.length === 0) return 0;
339
452
  const subtotal = selectSubtotal(s);
453
+ const quoted = selectQuotedShipping(useDeliveryStore.getState(), subtotal);
454
+ if (quoted != null) return quoted;
340
455
  if (cfg.shipping.freeAbove != null && subtotal >= cfg.shipping.freeAbove) return 0;
341
456
  return cfg.shipping.flatRate;
342
457
  };
@@ -435,7 +550,7 @@ function StorefrontThemeStyle({ theme }) {
435
550
  }
436
551
  link.href = fontsHref;
437
552
  }, [fontsHref]);
438
- return React6.createElement("style", { "data-sf-theme": theme.name }, themeToCss(theme));
553
+ return React7.createElement("style", { "data-sf-theme": theme.name }, themeToCss(theme));
439
554
  }
440
555
  var initial = {
441
556
  search: "",
@@ -601,6 +716,9 @@ var TID = {
601
716
  pdpAddToCart: "pdp-add-to-cart",
602
717
  pdpRoot: "pdp-root",
603
718
  pdpGallery: "pdp-gallery",
719
+ pdpGalleryImage: "pdp-gallery-image",
720
+ pdpGalleryThumbs: "pdp-gallery-thumbs",
721
+ pdpGalleryThumb: "pdp-gallery-thumb",
604
722
  pdpActions: "pdp-actions",
605
723
  pdpEnquiryButton: "pdp-enquiry-button",
606
724
  enquiryForm: "enquiry-form",
@@ -629,6 +747,15 @@ var TID = {
629
747
  cartTotal: "cart-total",
630
748
  goCheckout: "go-checkout",
631
749
  cartEmpty: "cart-empty",
750
+ // delivery estimate (product page + cart)
751
+ deliveryEstimator: "delivery-estimator",
752
+ deliveryCepInput: "delivery-cep-input",
753
+ deliveryCepSubmit: "delivery-cep-submit",
754
+ deliveryOption: "delivery-option",
755
+ deliveryUnserved: "delivery-unserved",
756
+ deliveryError: "delivery-error",
757
+ deliveryAddress: "delivery-address",
758
+ deliveryClear: "delivery-clear",
632
759
  // checkout
633
760
  checkoutEmail: "checkout-email",
634
761
  checkoutName: "checkout-name",
@@ -1147,6 +1274,113 @@ function QuantityInput({ value, onChange, min = 1, max, testId, incTestId, decTe
1147
1274
  )
1148
1275
  ] });
1149
1276
  }
1277
+ function DeliveryEstimator({ compact = false }) {
1278
+ const config = useStorefrontConfig();
1279
+ const cart = useCartStore();
1280
+ const delivery = useDeliveryStore();
1281
+ const [draft, setDraft] = useState(() => formatPostalCode(delivery.postalCode));
1282
+ const subtotal = selectSubtotal(cart);
1283
+ const money = (value) => formatMoney(value, config.currency, config.locale);
1284
+ const loading = delivery.status === "loading";
1285
+ async function submit(event) {
1286
+ event.preventDefault();
1287
+ await delivery.resolve(draft, subtotal);
1288
+ }
1289
+ function eta(option) {
1290
+ const { etaMinDays: min, etaMaxDays: max } = option;
1291
+ if (min == null && max == null) return null;
1292
+ if (min === 0 && (max === 0 || max === 1)) return "hoje ou amanh\xE3";
1293
+ if (min != null && max != null && min !== max) return `${min} a ${max} dias \xFAteis`;
1294
+ return `${max ?? min} dia(s) \xFAtil(eis)`;
1295
+ }
1296
+ return /* @__PURE__ */ jsxs(
1297
+ "section",
1298
+ {
1299
+ "data-testid": TID.deliveryEstimator,
1300
+ className: compact ? "rounded-lg border p-3" : "mt-6 rounded-lg border p-4",
1301
+ children: [
1302
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm font-semibold", children: [
1303
+ /* @__PURE__ */ jsx(Truck, { className: "h-4 w-4 text-primary" }),
1304
+ "Calcular entrega"
1305
+ ] }),
1306
+ /* @__PURE__ */ jsxs("form", { onSubmit: submit, className: "mt-3 flex gap-2", children: [
1307
+ /* @__PURE__ */ jsx(
1308
+ "input",
1309
+ {
1310
+ "data-testid": TID.deliveryCepInput,
1311
+ "aria-label": "CEP",
1312
+ inputMode: "numeric",
1313
+ autoComplete: "postal-code",
1314
+ placeholder: "00000-000",
1315
+ value: draft,
1316
+ onChange: (event) => setDraft(formatPostalCode(event.target.value)),
1317
+ className: "min-w-0 flex-1 rounded-lg border bg-background px-3 py-2 text-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10"
1318
+ }
1319
+ ),
1320
+ /* @__PURE__ */ jsx(
1321
+ "button",
1322
+ {
1323
+ type: "submit",
1324
+ "data-testid": TID.deliveryCepSubmit,
1325
+ disabled: loading,
1326
+ className: "shrink-0 rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition hover:opacity-90 disabled:opacity-50",
1327
+ children: loading ? "..." : "Calcular"
1328
+ }
1329
+ )
1330
+ ] }),
1331
+ delivery.error && /* @__PURE__ */ jsx("p", { "data-testid": TID.deliveryError, className: "mt-3 text-sm text-destructive", children: delivery.error }),
1332
+ delivery.address && /* @__PURE__ */ jsxs("p", { "data-testid": TID.deliveryAddress, className: "mt-3 flex items-start gap-1.5 text-xs text-muted-foreground", children: [
1333
+ /* @__PURE__ */ jsx(MapPin, { className: "mt-0.5 h-3.5 w-3.5 shrink-0" }),
1334
+ /* @__PURE__ */ jsxs("span", { children: [
1335
+ [delivery.address.street, delivery.address.district].filter(Boolean).join(", "),
1336
+ delivery.address.street || delivery.address.district ? " \u2014 " : "",
1337
+ delivery.address.city,
1338
+ "/",
1339
+ delivery.address.state
1340
+ ] })
1341
+ ] }),
1342
+ delivery.status === "served" && /* @__PURE__ */ jsx("ul", { className: "mt-3 space-y-2", children: delivery.options.map((option) => {
1343
+ const selected = delivery.selectedZoneId === option.zoneId;
1344
+ const label = eta(option);
1345
+ const rate = resolveOptionRate(option, subtotal);
1346
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
1347
+ "button",
1348
+ {
1349
+ type: "button",
1350
+ "data-testid": TID.deliveryOption,
1351
+ "data-zone": option.zoneId,
1352
+ "data-rate": rate.toFixed(2),
1353
+ "aria-pressed": selected,
1354
+ onClick: () => delivery.selectZone(option.zoneId),
1355
+ className: `flex w-full items-center justify-between gap-3 rounded-lg border px-3 py-2 text-left text-sm transition ${selected ? "border-primary bg-primary/5" : "border-border hover:bg-muted/40"}`,
1356
+ children: [
1357
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
1358
+ /* @__PURE__ */ jsx("span", { className: "block truncate font-medium", children: option.carrier ? `${option.carrier} \xB7 ${option.name}` : option.name }),
1359
+ label && /* @__PURE__ */ jsx("span", { className: "block text-xs text-muted-foreground", children: label })
1360
+ ] }),
1361
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 font-semibold", children: rate === 0 ? "Gr\xE1tis" : money(rate) })
1362
+ ]
1363
+ }
1364
+ ) }, option.zoneId);
1365
+ }) }),
1366
+ delivery.status === "unserved" && /* @__PURE__ */ jsx("p", { "data-testid": TID.deliveryUnserved, className: "mt-3 text-sm text-destructive", children: "Ainda n\xE3o entregamos nesse CEP." }),
1367
+ delivery.postalCode && /* @__PURE__ */ jsx(
1368
+ "button",
1369
+ {
1370
+ type: "button",
1371
+ "data-testid": TID.deliveryClear,
1372
+ onClick: () => {
1373
+ delivery.clear();
1374
+ setDraft("");
1375
+ },
1376
+ className: "mt-3 text-xs text-muted-foreground underline-offset-2 hover:underline",
1377
+ children: "Trocar CEP"
1378
+ }
1379
+ )
1380
+ ]
1381
+ }
1382
+ );
1383
+ }
1150
1384
  function SmoothImage({
1151
1385
  imageLoading,
1152
1386
  loading = "lazy",
@@ -1164,9 +1398,9 @@ function SmoothImage({
1164
1398
  const durationMs = resolved.durationMs ?? 420;
1165
1399
  const easing = resolved.easing ?? "cubic-bezier(0.22, 1, 0.36, 1)";
1166
1400
  const blur = resolved.blur ?? true;
1167
- const ref = React6.useRef(null);
1168
- const [loaded, setLoaded] = React6.useState(mode === "none");
1169
- React6.useEffect(() => {
1401
+ const ref = React7.useRef(null);
1402
+ const [loaded, setLoaded] = React7.useState(mode === "none");
1403
+ React7.useEffect(() => {
1170
1404
  if (mode === "none") {
1171
1405
  setLoaded(true);
1172
1406
  return;
@@ -1209,6 +1443,7 @@ function CartDrawer() {
1209
1443
  document.addEventListener("keydown", onKey);
1210
1444
  return () => document.removeEventListener("keydown", onKey);
1211
1445
  }, [cart.isOpen, cart.closeDrawer]);
1446
+ const delivery = useDeliveryStore();
1212
1447
  const subtotal = selectSubtotal(cart);
1213
1448
  const discountTotal = selectDiscountTotal(cart);
1214
1449
  const shipping = selectShipping(cart, config);
@@ -1365,6 +1600,7 @@ function CartDrawer() {
1365
1600
  ] }),
1366
1601
  discountError && /* @__PURE__ */ jsx("p", { "data-testid": TID.discountError, className: "text-xs text-destructive", children: discountError })
1367
1602
  ] }),
1603
+ /* @__PURE__ */ jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsx(DeliveryEstimator, { compact: true }) }),
1368
1604
  /* @__PURE__ */ jsxs("dl", { className: "space-y-1.5 text-sm", children: [
1369
1605
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
1370
1606
  /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Subtotal" }),
@@ -1394,7 +1630,13 @@ function CartDrawer() {
1394
1630
  ] })
1395
1631
  ] }),
1396
1632
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
1397
- /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Frete" }),
1633
+ /* @__PURE__ */ jsxs("dt", { className: "text-muted-foreground", children: [
1634
+ "Frete",
1635
+ delivery.status === "served" && delivery.postalCode && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-xs", children: [
1636
+ "\xB7 ",
1637
+ formatPostalCode(delivery.postalCode)
1638
+ ] })
1639
+ ] }),
1398
1640
  /* @__PURE__ */ jsx("dd", { "data-testid": TID.cartShipping, "data-price": shipping.toFixed(2), children: shipping === 0 ? "Gr\xE1tis" : money(shipping) })
1399
1641
  ] }),
1400
1642
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between border-t pt-2 text-base font-semibold", children: [
@@ -2737,6 +2979,128 @@ function ProductEnquiryForm({ product, onSuccess }) {
2737
2979
  }
2738
2980
  );
2739
2981
  }
2982
+ function ProductGallery({ product, images, primaryImage }) {
2983
+ const ordered = orderImages(images, primaryImage);
2984
+ const [index, setIndex] = useState(0);
2985
+ const [zoom, setZoom] = useState(null);
2986
+ const frameRef = useRef(null);
2987
+ const canHover = useCanHover();
2988
+ useEffect(() => {
2989
+ setIndex(0);
2990
+ setZoom(null);
2991
+ }, [product.id]);
2992
+ const current = ordered[Math.min(index, ordered.length - 1)];
2993
+ function trackPointer(event) {
2994
+ if (!canHover) return;
2995
+ const frame = frameRef.current;
2996
+ if (!frame) return;
2997
+ const rect = frame.getBoundingClientRect();
2998
+ setZoom({
2999
+ x: (event.clientX - rect.left) / rect.width * 100,
3000
+ y: (event.clientY - rect.top) / rect.height * 100
3001
+ });
3002
+ }
3003
+ function step(delta) {
3004
+ if (ordered.length < 2) return;
3005
+ setIndex((current2) => (current2 + delta + ordered.length) % ordered.length);
3006
+ setZoom(null);
3007
+ }
3008
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 self-start", children: [
3009
+ /* @__PURE__ */ jsxs(
3010
+ "div",
3011
+ {
3012
+ ...storefrontComponentContracts.productDetail.gallery,
3013
+ ref: frameRef,
3014
+ className: "group relative aspect-square w-full overflow-hidden border bg-muted",
3015
+ style: { borderRadius: "var(--sf-radius-card)" },
3016
+ onMouseMove: trackPointer,
3017
+ onMouseLeave: () => setZoom(null),
3018
+ onKeyDown: (event) => {
3019
+ if (event.key === "ArrowRight") step(1);
3020
+ if (event.key === "ArrowLeft") step(-1);
3021
+ },
3022
+ tabIndex: ordered.length > 1 ? 0 : -1,
3023
+ role: ordered.length > 1 ? "group" : void 0,
3024
+ "aria-label": ordered.length > 1 ? `Imagens de ${product.name}` : void 0,
3025
+ children: [
3026
+ current && /* @__PURE__ */ jsx(
3027
+ "div",
3028
+ {
3029
+ className: "h-full w-full transition-transform duration-150 ease-out",
3030
+ style: zoom ? { transform: "scale(2)", transformOrigin: `${zoom.x}% ${zoom.y}%` } : { transform: "scale(1)" },
3031
+ children: /* @__PURE__ */ jsx(
3032
+ SmoothImage,
3033
+ {
3034
+ src: current.url,
3035
+ alt: current.altText ?? product.name,
3036
+ "data-testid": TID.pdpGalleryImage,
3037
+ className: "h-full w-full object-cover"
3038
+ },
3039
+ current.id
3040
+ )
3041
+ }
3042
+ ),
3043
+ ordered.length > 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
3044
+ /* @__PURE__ */ jsx(GalleryArrow, { side: "left", onClick: () => step(-1) }),
3045
+ /* @__PURE__ */ jsx(GalleryArrow, { side: "right", onClick: () => step(1) }),
3046
+ /* @__PURE__ */ jsxs("span", { className: "pointer-events-none absolute bottom-3 right-3 rounded-full bg-black/60 px-2 py-0.5 text-[11px] font-medium text-white", children: [
3047
+ index + 1,
3048
+ "/",
3049
+ ordered.length
3050
+ ] })
3051
+ ] })
3052
+ ]
3053
+ }
3054
+ ),
3055
+ ordered.length > 1 && /* @__PURE__ */ jsx("ul", { "data-testid": TID.pdpGalleryThumbs, className: "flex gap-2 overflow-x-auto pb-1", children: ordered.map((image, position) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
3056
+ "button",
3057
+ {
3058
+ type: "button",
3059
+ "data-testid": TID.pdpGalleryThumb,
3060
+ "data-index": position,
3061
+ "aria-label": `Imagem ${position + 1} de ${ordered.length}`,
3062
+ "aria-current": position === index,
3063
+ onClick: () => {
3064
+ setIndex(position);
3065
+ setZoom(null);
3066
+ },
3067
+ className: `h-16 w-16 shrink-0 overflow-hidden rounded-lg border transition ${position === index ? "border-primary ring-2 ring-primary/20" : "border-border opacity-70 hover:opacity-100"}`,
3068
+ children: /* @__PURE__ */ jsx("img", { src: image.url, alt: "", className: "h-full w-full object-cover", loading: "lazy" })
3069
+ }
3070
+ ) }, image.id)) })
3071
+ ] });
3072
+ }
3073
+ function orderImages(images, primary) {
3074
+ const all = images.length > 0 ? images : primary ? [primary] : [];
3075
+ return [...all].sort((a, b) => {
3076
+ if (a.isPrimary !== b.isPrimary) return a.isPrimary ? -1 : 1;
3077
+ return (a.sortOrder ?? 0) - (b.sortOrder ?? 0);
3078
+ });
3079
+ }
3080
+ function useCanHover() {
3081
+ const [canHover, setCanHover] = useState(false);
3082
+ useEffect(() => {
3083
+ const query = window.matchMedia?.("(hover: hover) and (pointer: fine)");
3084
+ if (!query) return;
3085
+ setCanHover(query.matches);
3086
+ const onChange = (event) => setCanHover(event.matches);
3087
+ query.addEventListener?.("change", onChange);
3088
+ return () => query.removeEventListener?.("change", onChange);
3089
+ }, []);
3090
+ return canHover;
3091
+ }
3092
+ function GalleryArrow({ side, onClick }) {
3093
+ return /* @__PURE__ */ jsx(
3094
+ "button",
3095
+ {
3096
+ type: "button",
3097
+ onClick,
3098
+ "aria-label": side === "left" ? "Imagem anterior" : "Pr\xF3xima imagem",
3099
+ className: `absolute top-1/2 z-10 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full bg-background/80 opacity-0 shadow-md backdrop-blur transition hover:bg-background focus:opacity-100 group-hover:opacity-100 ${side === "left" ? "left-3" : "right-3"}`,
3100
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "text-lg leading-none", children: side === "left" ? "\u2039" : "\u203A" })
3101
+ }
3102
+ );
3103
+ }
2740
3104
  function ProductDetailPage({ slug: slug2 }) {
2741
3105
  const config = useStorefrontConfig();
2742
3106
  const actions = useStorefrontActions();
@@ -2777,6 +3141,7 @@ function ProductDetailPage({ slug: slug2 }) {
2777
3141
  const image = product.images.find((i) => i.isPrimary) ?? product.images[0];
2778
3142
  const components = getStorefrontComponents(config);
2779
3143
  const ProductDetailComponent = components.ProductDetail;
3144
+ const GalleryComponent = components.ProductGallery ?? ProductGallery;
2780
3145
  const addToCart = () => addItem(product, qty, selectedOptions);
2781
3146
  const openEnquiry = () => {
2782
3147
  document.getElementById("storefront-product-enquiry")?.scrollIntoView({ behavior: "smooth", block: "start" });
@@ -2810,19 +3175,13 @@ function ProductDetailPage({ slug: slug2 }) {
2810
3175
  ] }),
2811
3176
  /* @__PURE__ */ jsxs("div", { className: "grid animate-fade-up gap-10 md:grid-cols-2", children: [
2812
3177
  /* @__PURE__ */ jsx(
2813
- "div",
3178
+ GalleryComponent,
2814
3179
  {
2815
- ...storefrontComponentContracts.productDetail.gallery,
2816
- className: "group overflow-hidden border bg-muted",
2817
- style: { borderRadius: "var(--sf-radius-card)" },
2818
- children: image && /* @__PURE__ */ jsx(
2819
- SmoothImage,
2820
- {
2821
- src: image.url,
2822
- alt: image.altText ?? product.name,
2823
- className: "aspect-square w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
2824
- }
2825
- )
3180
+ product,
3181
+ images: product.images,
3182
+ primaryImage: image,
3183
+ config,
3184
+ commerceMode: config.commerceMode
2826
3185
  }
2827
3186
  ),
2828
3187
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col py-2 lg:sticky lg:top-24 lg:self-start", children: [
@@ -2920,6 +3279,7 @@ function ProductDetailPage({ slug: slug2 }) {
2920
3279
  children: "Voltar ao cat\xE1logo"
2921
3280
  }
2922
3281
  ) }),
3282
+ config.commerceMode === "checkout" && /* @__PURE__ */ jsx(DeliveryEstimator, {}),
2923
3283
  config.commerceMode === "checkout" && /* @__PURE__ */ jsxs("div", { className: "mt-6 grid grid-cols-3 gap-3 border-t pt-6 text-center", children: [
2924
3284
  /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [
2925
3285
  /* @__PURE__ */ jsx(Truck, { className: "h-5 w-5 text-primary" }),
@@ -2945,9 +3305,10 @@ function ProductDetailPage({ slug: slug2 }) {
2945
3305
  /* @__PURE__ */ jsx(RelatedProducts, { product })
2946
3306
  ] });
2947
3307
  }
2948
- function formatDeliveryNotes(address) {
3308
+ function formatDeliveryNotes(address, shippingAddress) {
3309
+ if (address?.notes?.trim()) return address.notes.trim();
3310
+ if (shippingAddress) return void 0;
2949
3311
  if (!address) return void 0;
2950
- if (address.notes?.trim()) return address.notes.trim();
2951
3312
  const parts = [address.street, address.city, address.zip].map((part) => part?.trim()).filter(Boolean);
2952
3313
  return parts.length > 0 ? `Entrega: ${parts.join(", ")}` : void 0;
2953
3314
  }
@@ -2957,6 +3318,8 @@ async function placeStorefrontOrder({
2957
3318
  session,
2958
3319
  customer,
2959
3320
  address,
3321
+ shippingAddress,
3322
+ paymentMethod,
2960
3323
  markPaid = false
2961
3324
  }) {
2962
3325
  const email = customer.email.trim().toLowerCase();
@@ -2971,9 +3334,14 @@ async function placeStorefrontOrder({
2971
3334
  customerId: customerId ?? void 0,
2972
3335
  customer: { name, email },
2973
3336
  currency: config.currency,
2974
- notes: formatDeliveryNotes(address),
3337
+ notes: formatDeliveryNotes(address, shippingAddress),
2975
3338
  discountCode: cart.discountCode ?? void 0,
2976
3339
  shippingTotal: selectShipping(cart, config),
3340
+ // Structured address + method are what populate public.addresses and the
3341
+ // transactions ledger. `notes` stays for backwards compatibility with
3342
+ // checkouts that have not been updated yet.
3343
+ shippingAddress,
3344
+ paymentMethod,
2977
3345
  items: cart.lines.map((line) => ({
2978
3346
  productId: line.productId,
2979
3347
  quantity: line.quantity,
@@ -3096,19 +3464,18 @@ function SignInModal({ defaultEmail = "", onClose, onSignedIn }) {
3096
3464
  }
3097
3465
  );
3098
3466
  }
3099
- var SAVED_ADDRESSES = [
3100
- { id: "home", label: "Casa", street: "Rua das Flores, 123", city: "S\xE3o Paulo", zip: "01310-100" }
3101
- ];
3102
- var SAVED_PAYMENT_METHODS = [
3103
- { id: "visa", label: "Visa final 4242", card: "4242 4242 4242 4242", expiry: "12/29", cvc: "123" }
3104
- ];
3467
+ var PAYMENT_LABELS = {
3468
+ pix: { label: "Pix", hint: "Voc\xEA recebe a chave para pagar ap\xF3s confirmar o pedido" },
3469
+ credit_card: { label: "Cart\xE3o de cr\xE9dito", hint: "Maquininha na entrega" },
3470
+ debit_card: { label: "Cart\xE3o de d\xE9bito", hint: "Maquininha na entrega" },
3471
+ boleto: { label: "Boleto", hint: "Enviado por e-mail ap\xF3s a confirma\xE7\xE3o" },
3472
+ cash: { label: "Dinheiro", hint: "Pagamento na entrega" },
3473
+ other: { label: "Combinar com a loja", hint: "A loja entra em contato para acertar o pagamento" }
3474
+ };
3105
3475
  var PROCESSING_STEPS = [
3106
3476
  { icon: Lock, label: "Validando seus dados..." },
3107
- { icon: CreditCard, label: "Processando pagamento..." },
3108
- { icon: PackageCheck, label: "Confirmando seu pedido..." }
3477
+ { icon: PackageCheck, label: "Registrando seu pedido..." }
3109
3478
  ];
3110
- var DEFAULT_ADDRESS = SAVED_ADDRESSES[0];
3111
- var DEFAULT_PAYMENT = SAVED_PAYMENT_METHODS[0];
3112
3479
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3113
3480
  function ProcessingOverlay({ step }) {
3114
3481
  const current = PROCESSING_STEPS[Math.min(step, PROCESSING_STEPS.length - 1)];
@@ -3151,23 +3518,24 @@ function CheckoutPage() {
3151
3518
  const config = useStorefrontConfig();
3152
3519
  const cart = useCartStore();
3153
3520
  const session = useSessionStore();
3521
+ const delivery = useDeliveryStore();
3154
3522
  const validateDiscount2 = useDiscountValidator();
3155
3523
  useStorefrontHead({ title: `Checkout \u2014 ${config.name}` });
3156
- const [selectedAddressId, setSelectedAddressId] = useState(DEFAULT_ADDRESS.id);
3157
- const [selectedPaymentId, setSelectedPaymentId] = useState(DEFAULT_PAYMENT.id);
3158
- const [savedAddresses, setSavedAddresses] = useState(SAVED_ADDRESSES);
3159
- const [savedPaymentMethods, setSavedPaymentMethods] = useState(SAVED_PAYMENT_METHODS);
3160
- const [addressMode, setAddressMode] = useState("saved");
3161
- const [paymentMode, setPaymentMode] = useState("saved");
3524
+ const paymentMethods = config.payments.methods;
3525
+ const [selectedAddressId, setSelectedAddressId] = useState("new");
3526
+ const [savedAddresses, setSavedAddresses] = useState([]);
3527
+ const [addressMode, setAddressMode] = useState("new");
3528
+ const [paymentMethod, setPaymentMethod] = useState(paymentMethods[0] ?? "pix");
3162
3529
  const [form, setForm] = useState({
3163
3530
  email: session.email ?? "",
3164
3531
  name: session.name ?? "",
3165
- street: DEFAULT_ADDRESS.street,
3166
- city: DEFAULT_ADDRESS.city,
3167
- zip: DEFAULT_ADDRESS.zip,
3168
- card: DEFAULT_PAYMENT.card,
3169
- expiry: DEFAULT_PAYMENT.expiry,
3170
- cvc: DEFAULT_PAYMENT.cvc
3532
+ street: "",
3533
+ city: "",
3534
+ zip: "",
3535
+ number: "",
3536
+ complement: "",
3537
+ district: "",
3538
+ state: ""
3171
3539
  });
3172
3540
  const [error, setError] = useState(null);
3173
3541
  const [discountCode, setDiscountCode] = useState("");
@@ -3177,6 +3545,7 @@ function CheckoutPage() {
3177
3545
  const [placing, setPlacing] = useState(false);
3178
3546
  const [processingStep, setProcessingStep] = useState(0);
3179
3547
  const [showSignin, setShowSignin] = useState(false);
3548
+ const [zipLookup, setZipLookup] = useState("idle");
3180
3549
  const subtotal = selectSubtotal(cart);
3181
3550
  const discountTotal = selectDiscountTotal(cart);
3182
3551
  const shipping = selectShipping(cart, config);
@@ -3186,6 +3555,32 @@ function CheckoutPage() {
3186
3555
  if (cart.lines.length === 0 && !placing) navigateTo(config.catalogPath);
3187
3556
  }, [cart.lines.length, config.catalogPath, placing]);
3188
3557
  const set = (key) => (value) => setForm((current) => ({ ...current, [key]: value }));
3558
+ async function setZip(value) {
3559
+ const masked = formatPostalCode(value);
3560
+ setForm((current) => ({ ...current, zip: masked }));
3561
+ if (normalizePostalCode(masked).length !== 8) {
3562
+ setZipLookup("idle");
3563
+ return;
3564
+ }
3565
+ setZipLookup("loading");
3566
+ try {
3567
+ const found = await lookupPostalCode(masked);
3568
+ if (!found) {
3569
+ setZipLookup("not-found");
3570
+ return;
3571
+ }
3572
+ setZipLookup("found");
3573
+ setForm((current) => ({
3574
+ ...current,
3575
+ street: found.street || current.street,
3576
+ district: found.district || current.district,
3577
+ city: found.city,
3578
+ state: found.state
3579
+ }));
3580
+ } catch {
3581
+ setZipLookup("idle");
3582
+ }
3583
+ }
3189
3584
  async function applyDiscountCode() {
3190
3585
  setApplyingDiscount(true);
3191
3586
  setDiscountError(null);
@@ -3212,10 +3607,54 @@ function CheckoutPage() {
3212
3607
  setDiscountSuccess(null);
3213
3608
  setDiscountError(null);
3214
3609
  }
3610
+ useEffect(() => {
3611
+ const customerId = session.customerId;
3612
+ if (!customerId) {
3613
+ setSavedAddresses([]);
3614
+ setAddressMode("new");
3615
+ return;
3616
+ }
3617
+ let cancelled = false;
3618
+ const provider = getShopProvider();
3619
+ void provider.listCustomerAddresses?.(customerId).then((addresses) => {
3620
+ if (cancelled || addresses.length === 0) return;
3621
+ setSavedAddresses(addresses);
3622
+ applySavedAddress(addresses[0]);
3623
+ }).catch(() => {
3624
+ });
3625
+ return () => {
3626
+ cancelled = true;
3627
+ };
3628
+ }, [session.customerId]);
3629
+ useEffect(() => {
3630
+ if (addressMode !== "new") return;
3631
+ const looked = delivery.address;
3632
+ if (!looked) return;
3633
+ setForm((current) => {
3634
+ if (current.zip.trim() || current.street.trim()) return current;
3635
+ return {
3636
+ ...current,
3637
+ zip: formatPostalCode(looked.postalCode),
3638
+ street: looked.street,
3639
+ district: looked.district,
3640
+ city: looked.city,
3641
+ state: looked.state
3642
+ };
3643
+ });
3644
+ }, [delivery.address, addressMode]);
3215
3645
  function applySavedAddress(address) {
3216
3646
  setAddressMode("saved");
3217
3647
  setSelectedAddressId(address.id);
3218
- setForm((current) => ({ ...current, street: address.street, city: address.city, zip: address.zip }));
3648
+ setForm((current) => ({
3649
+ ...current,
3650
+ street: address.street,
3651
+ city: address.city,
3652
+ zip: address.postalCode,
3653
+ number: address.number ?? "",
3654
+ complement: address.complement ?? "",
3655
+ district: address.district ?? "",
3656
+ state: address.state
3657
+ }));
3219
3658
  }
3220
3659
  function selectAddress(addressId) {
3221
3660
  const address = savedAddresses.find((item) => item.id === addressId);
@@ -3224,37 +3663,7 @@ function CheckoutPage() {
3224
3663
  function addNewAddress() {
3225
3664
  setAddressMode("new");
3226
3665
  setSelectedAddressId("new");
3227
- setForm((current) => ({ ...current, street: "", city: "", zip: "" }));
3228
- }
3229
- function removeAddress(addressId) {
3230
- const nextAddresses = savedAddresses.filter((item) => item.id !== addressId);
3231
- setSavedAddresses(nextAddresses);
3232
- if (selectedAddressId !== addressId) return;
3233
- const fallback = nextAddresses[0];
3234
- if (fallback) applySavedAddress(fallback);
3235
- else addNewAddress();
3236
- }
3237
- function applySavedPayment(method) {
3238
- setPaymentMode("saved");
3239
- setSelectedPaymentId(method.id);
3240
- setForm((current) => ({ ...current, card: method.card, expiry: method.expiry, cvc: method.cvc }));
3241
- }
3242
- function selectPayment(paymentId) {
3243
- const method = savedPaymentMethods.find((item) => item.id === paymentId);
3244
- if (method) applySavedPayment(method);
3245
- }
3246
- function addNewPayment() {
3247
- setPaymentMode("new");
3248
- setSelectedPaymentId("new");
3249
- setForm((current) => ({ ...current, card: "", expiry: "", cvc: "" }));
3250
- }
3251
- function removePayment(paymentId) {
3252
- const nextMethods = savedPaymentMethods.filter((item) => item.id !== paymentId);
3253
- setSavedPaymentMethods(nextMethods);
3254
- if (selectedPaymentId !== paymentId) return;
3255
- const fallback = nextMethods[0];
3256
- if (fallback) applySavedPayment(fallback);
3257
- else addNewPayment();
3666
+ setForm((current) => ({ ...current, street: "", city: "", zip: "", number: "", complement: "", district: "", state: "" }));
3258
3667
  }
3259
3668
  function validate() {
3260
3669
  setError(null);
@@ -3266,12 +3675,20 @@ function CheckoutPage() {
3266
3675
  setError("Informe seu nome completo.");
3267
3676
  return false;
3268
3677
  }
3269
- if (!form.street.trim() || !form.city.trim() || !form.zip.trim()) {
3678
+ if (!form.street.trim() || !form.city.trim() || !form.zip.trim() || !form.district.trim()) {
3270
3679
  setError("Preencha ou selecione o endere\xE7o de entrega.");
3271
3680
  return false;
3272
3681
  }
3273
- if (!/^\d{16}$/.test(form.card.replace(/\s+/g, "")) || !form.expiry.trim() || !form.cvc.trim()) {
3274
- setError("Selecione ou preencha um m\xE9todo de pagamento v\xE1lido.");
3682
+ if (!form.number.trim()) {
3683
+ setError("Falta o n\xFAmero do endere\xE7o.");
3684
+ return false;
3685
+ }
3686
+ if (form.state.trim().length !== 2) {
3687
+ setError("Informe a UF com duas letras (ex.: RJ).");
3688
+ return false;
3689
+ }
3690
+ if (delivery.status === "unserved" && delivery.postalCode === normalizePostalCode(form.zip)) {
3691
+ setError("Ainda n\xE3o entregamos nesse CEP. Tente outro endere\xE7o.");
3275
3692
  return false;
3276
3693
  }
3277
3694
  return true;
@@ -3293,9 +3710,22 @@ function CheckoutPage() {
3293
3710
  session,
3294
3711
  customer: { email: form.email, name: form.name },
3295
3712
  address: { street: form.street, city: form.city, zip: form.zip },
3296
- // Demo only: mock mode instantly approves. Real Pix/Mercado Pago (M4)
3297
- // leaves the order pending until the payment webhook confirms.
3298
- markPaid: config.payments.mode === "mock"
3713
+ shippingAddress: {
3714
+ postalCode: form.zip.trim(),
3715
+ street: form.street.trim(),
3716
+ number: form.number.trim() || void 0,
3717
+ complement: form.complement.trim() || void 0,
3718
+ district: form.district.trim() || void 0,
3719
+ city: form.city.trim(),
3720
+ state: form.state.trim().toUpperCase()
3721
+ },
3722
+ // The method the buyer actually chose, which opens the ledger row in
3723
+ // public.transactions with the right kind.
3724
+ paymentMethod
3725
+ // Never marked paid from the browser. It used to be, through an RPC that
3726
+ // was granted to anon — so any buyer holding their own order id could
3727
+ // declare it settled. The order is now born `pending` and only the
3728
+ // merchant (or a PSP webhook) can confirm the money arrived.
3299
3729
  });
3300
3730
  await minDuration;
3301
3731
  cart.clear();
@@ -3368,35 +3798,25 @@ function CheckoutPage() {
3368
3798
  /* @__PURE__ */ jsxs("section", { children: [
3369
3799
  /* @__PURE__ */ jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Endere\xE7o de entrega" }),
3370
3800
  /* @__PURE__ */ jsxs("div", { className: "mb-3 grid gap-3", children: [
3371
- savedAddresses.map((address) => /* @__PURE__ */ jsxs(
3801
+ savedAddresses.map((address) => /* @__PURE__ */ jsx(
3372
3802
  "div",
3373
3803
  {
3374
3804
  className: `group relative rounded-lg border p-3 pr-11 text-left text-sm transition hover:bg-muted/40 ${selectedAddressId === address.id ? "border-primary bg-primary/5" : "border-border bg-background"}`,
3375
- children: [
3376
- /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => selectAddress(address.id), className: "block w-full text-left", children: [
3377
- /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3378
- address.label,
3379
- addressMode === "saved" && selectedAddressId === address.id && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3380
- ] }),
3381
- /* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: address.street }),
3382
- /* @__PURE__ */ jsxs("span", { className: "block text-muted-foreground", children: [
3383
- address.city,
3384
- " - ",
3385
- address.zip
3386
- ] })
3805
+ children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => selectAddress(address.id), className: "block w-full text-left", children: [
3806
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3807
+ address.label,
3808
+ addressMode === "saved" && selectedAddressId === address.id && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3387
3809
  ] }),
3388
- /* @__PURE__ */ jsx(
3389
- "button",
3390
- {
3391
- type: "button",
3392
- onClick: () => removeAddress(address.id),
3393
- className: "absolute right-3 top-3 inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground opacity-0 transition hover:bg-destructive/10 hover:text-destructive focus:opacity-100 group-hover:opacity-100",
3394
- "aria-label": `Remover endere\xE7o ${address.label}`,
3395
- title: "Remover endere\xE7o",
3396
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
3397
- }
3398
- )
3399
- ]
3810
+ /* @__PURE__ */ jsxs("span", { className: "mt-1 block text-muted-foreground", children: [
3811
+ [address.street, address.number].filter(Boolean).join(", "),
3812
+ address.complement ? ` \u2014 ${address.complement}` : ""
3813
+ ] }),
3814
+ /* @__PURE__ */ jsxs("span", { className: "block text-muted-foreground", children: [
3815
+ [address.district, address.city, address.state].filter(Boolean).join(" \xB7 "),
3816
+ " \u2014 ",
3817
+ address.postalCode
3818
+ ] })
3819
+ ] })
3400
3820
  },
3401
3821
  address.id
3402
3822
  )),
@@ -3407,83 +3827,62 @@ function CheckoutPage() {
3407
3827
  onClick: addNewAddress,
3408
3828
  className: `rounded-lg border border-dashed p-3 text-left text-sm transition hover:bg-muted/40 ${addressMode === "new" ? "border-primary bg-primary/5" : "border-border bg-muted/20"}`,
3409
3829
  children: [
3410
- /* @__PURE__ */ jsx("span", { className: "block font-semibold text-primary", children: "+ Adicionar novo endere\xE7o" }),
3830
+ /* @__PURE__ */ jsx("span", { className: "block font-semibold text-primary", children: savedAddresses.length > 0 ? "+ Adicionar novo endere\xE7o" : "Informe o endere\xE7o de entrega" }),
3411
3831
  /* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: "Preencher outro endere\xE7o para esta compra" })
3412
3832
  ]
3413
3833
  }
3414
3834
  )
3415
3835
  ] }),
3416
3836
  addressMode === "new" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
3417
- field("Endere\xE7o", TID.checkoutStreet, form.street, set("street"), { placeholder: "Endere\xE7o" }),
3418
- /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-[1fr_160px]", children: [
3419
- field("Cidade", TID.checkoutCity, form.city, set("city"), { placeholder: "Cidade" }),
3420
- field("CEP", TID.checkoutZip, form.zip, set("zip"), { placeholder: "CEP" })
3421
- ] })
3837
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-1 sm:grid-cols-[200px_1fr] sm:items-center", children: [
3838
+ field("CEP", TID.checkoutZip, form.zip, setZip, {
3839
+ placeholder: "CEP",
3840
+ inputMode: "numeric",
3841
+ autoComplete: "postal-code"
3842
+ }),
3843
+ /* @__PURE__ */ jsx("p", { className: "px-1 text-xs text-muted-foreground", children: zipLookup === "loading" ? "Buscando endere\xE7o\u2026" : zipLookup === "not-found" ? "CEP n\xE3o encontrado \u2014 preencha o endere\xE7o \xE0 m\xE3o." : "Preenchemos o endere\xE7o para voc\xEA." })
3844
+ ] }),
3845
+ field("Endere\xE7o", TID.checkoutStreet, form.street, set("street"), { placeholder: "Rua, avenida\u2026" }),
3846
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
3847
+ field("N\xFAmero", "checkout-number", form.number, set("number"), { placeholder: "N\xFAmero" }),
3848
+ field("Complemento", "checkout-complement", form.complement, set("complement"), { placeholder: "Apto, bloco (opcional)" })
3849
+ ] }),
3850
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
3851
+ field("Bairro", "checkout-district", form.district, set("district"), { placeholder: "Bairro" }),
3852
+ field("UF", "checkout-state", form.state, set("state"), { placeholder: "UF" })
3853
+ ] }),
3854
+ field("Cidade", TID.checkoutCity, form.city, set("city"), { placeholder: "Cidade" })
3422
3855
  ] })
3423
3856
  ] }),
3424
3857
  /* @__PURE__ */ jsxs("section", { children: [
3425
3858
  /* @__PURE__ */ jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Pagamento" }),
3426
- /* @__PURE__ */ jsxs("div", { className: "mb-3 grid gap-3", children: [
3427
- savedPaymentMethods.map((method) => /* @__PURE__ */ jsxs(
3428
- "div",
3429
- {
3430
- className: `group relative rounded-lg border p-3 pr-11 text-left text-sm transition hover:bg-muted/40 ${selectedPaymentId === method.id ? "border-primary bg-primary/5" : "border-border bg-background"}`,
3431
- children: [
3432
- /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => selectPayment(method.id), className: "block w-full text-left", children: [
3433
- /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3434
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
3435
- /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4" }),
3436
- method.label
3437
- ] }),
3438
- paymentMode === "saved" && selectedPaymentId === method.id && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3439
- ] }),
3440
- /* @__PURE__ */ jsxs("span", { className: "mt-1 block text-muted-foreground", children: [
3441
- "Expira em ",
3442
- method.expiry
3443
- ] })
3444
- ] }),
3445
- /* @__PURE__ */ jsx(
3446
- "button",
3447
- {
3448
- type: "button",
3449
- onClick: () => removePayment(method.id),
3450
- className: "absolute right-3 top-3 inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground opacity-0 transition hover:bg-destructive/10 hover:text-destructive focus:opacity-100 group-hover:opacity-100",
3451
- "aria-label": `Remover cart\xE3o ${method.label}`,
3452
- title: "Remover cart\xE3o",
3453
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-4 w-4" })
3454
- }
3455
- )
3456
- ]
3457
- },
3458
- method.id
3459
- )),
3460
- /* @__PURE__ */ jsxs(
3859
+ /* @__PURE__ */ jsx("div", { className: "mb-3 grid gap-3", role: "radiogroup", "aria-label": "Forma de pagamento", children: paymentMethods.map((method) => {
3860
+ const copy = PAYMENT_LABELS[method];
3861
+ const selected = paymentMethod === method;
3862
+ return /* @__PURE__ */ jsxs(
3461
3863
  "button",
3462
3864
  {
3463
3865
  type: "button",
3464
- onClick: addNewPayment,
3465
- className: `rounded-lg border border-dashed p-3 text-left text-sm transition hover:bg-muted/40 ${paymentMode === "new" ? "border-primary bg-primary/5" : "border-border bg-muted/20"}`,
3866
+ role: "radio",
3867
+ "aria-checked": selected,
3868
+ "data-testid": `checkout-payment-${method}`,
3869
+ onClick: () => setPaymentMethod(method),
3870
+ className: `rounded-lg border p-3 text-left text-sm transition hover:bg-muted/40 ${selected ? "border-primary bg-primary/5" : "border-border bg-background"}`,
3466
3871
  children: [
3467
- /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2 font-semibold text-primary", children: [
3468
- /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4" }),
3469
- "+ Adicionar novo cart\xE3o"
3872
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
3873
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
3874
+ /* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4" }),
3875
+ copy.label
3876
+ ] }),
3877
+ selected && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: "Selecionado" })
3470
3878
  ] }),
3471
- /* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: "Usar outro cart\xE3o nesta compra" })
3879
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: copy.hint })
3472
3880
  ]
3473
- }
3474
- )
3475
- ] }),
3476
- paymentMode === "new" && /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
3477
- field("N\xFAmero do cart\xE3o", TID.checkoutCard, form.card, set("card"), {
3478
- placeholder: "N\xFAmero do cart\xE3o",
3479
- inputMode: "numeric"
3480
- }),
3481
- /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
3482
- field("Validade", TID.checkoutExpiry, form.expiry, set("expiry"), { placeholder: "MM/AA" }),
3483
- field("CVC", TID.checkoutCvc, form.cvc, set("cvc"), { placeholder: "CVC", inputMode: "numeric" })
3484
- ] })
3485
- ] }),
3486
- /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "Pagamento de demonstra\xE7\xE3o - nenhuma cobran\xE7a \xE9 feita." })
3881
+ },
3882
+ method
3883
+ );
3884
+ }) }),
3885
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "Nenhuma cobran\xE7a \xE9 feita agora. Seu pedido \xE9 registrado e a loja confirma o pagamento." })
3487
3886
  ] }),
3488
3887
  error && /* @__PURE__ */ jsx("p", { "data-testid": TID.checkoutError, className: "rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2 text-sm text-destructive", children: error })
3489
3888
  ] }),
@@ -4307,7 +4706,7 @@ function PolicyPage({ kind }) {
4307
4706
  ] }) })
4308
4707
  ] });
4309
4708
  }
4310
- var StorefrontErrorBoundary = class extends React6.Component {
4709
+ var StorefrontErrorBoundary = class extends React7.Component {
4311
4710
  constructor() {
4312
4711
  super(...arguments);
4313
4712
  this.state = { error: null };
@@ -4602,9 +5001,9 @@ function manifestToStorefrontConfig(manifest, surfaceName) {
4602
5001
  };
4603
5002
  }
4604
5003
  function StorefrontScaffold({ manifest, surface }) {
4605
- const config = React6.useMemo(() => manifestToStorefrontConfig(manifest, surface), [manifest, surface]);
4606
- const resolved = React6.useMemo(() => resolveConfig(config), [config]);
4607
- const inited = React6.useRef(false);
5004
+ const config = React7.useMemo(() => manifestToStorefrontConfig(manifest, surface), [manifest, surface]);
5005
+ const resolved = React7.useMemo(() => resolveConfig(config), [config]);
5006
+ const inited = React7.useRef(false);
4608
5007
  if (!inited.current) {
4609
5008
  initStorefrontRuntime(config);
4610
5009
  inited.current = true;