@fayz-ai/storefront 0.14.3 → 0.16.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.
@@ -6,7 +6,7 @@ import { setShopCarrierQuotesResolver, setShopAccessTokenResolver, setShopShippi
6
6
  import { normalizePostalCode, lookupPostalCode, defaultRouterAdapter, defineLoader, staticRouterAdapter, isLoaderParamRef, isLoaderPropRef, resolveLoader, dataNeedKey, getBlockMeta, defaultPropsFromSettings, getBlockErrorVisibility, isBlockEnabledOn, checkBlockConstraints, checkBlockContext, repairBlockTree, renderBlocks, clearExtensionPoints, defineExtensionPoint, runExtensionPoint, runEscapeValve, breadcrumbJsonLd, hasEntityDeclaration, resolveEntityDeclaration, defineBlock, formatPostalCode, defineEntity, listEntityDeclarations, resolveHandle, seoOrigin, RESERVED_ENTITY_FIELDS, ENTITY_FIELD_TYPES, handleNameFor, registerHandle, hasHandle, listHandles, blockRegistry, loaderRegistry, listLoaders } from '@fayz-ai/core';
7
7
  import React4, { createContext, useContext, useEffect, useRef, useState, useSyncExternalStore, useMemo, useCallback } from 'react';
8
8
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
- import { isStorageImageUrl, imageVariantUrl, imageSrcSet, IMAGE_SRCSET_WIDTHS, validateDiscount, documentDigits, isChargeableProvider, formatDocument, isValidDocument } from '@fayz-ai/shop';
9
+ import { isStorageImageUrl, imageVariantUrl, imageSrcSet, IMAGE_SRCSET_WIDTHS, validateDiscount, documentDigits, isChargeableProvider, supportsCard, formatDocument, isValidDocument, gatewayLabel } from '@fayz-ai/shop';
10
10
  import { ChevronLeft, ChevronRight, ShoppingBag, X, Minus, Plus, Send, MessageCircle, Mail, Truck, RefreshCcw, ShieldCheck, ChevronDown, Check, Search, Trash2, Lock, CreditCard, Star, MapPin, Phone, User, UserCircle, Package, LogOut, Menu, QrCode, Copy, XCircle, RotateCcw, Clock, PackageCheck, Info, AlertCircle } from 'lucide-react';
11
11
  import { getLucideIcon } from '@fayz-ai/ui/icons';
12
12
  import { storefrontDiscountToPromotion, previewCouponPercent, discountToPromotion, PROMOTION_TYPES, PROMOTION_STATUSES, APPLICATION_METHOD_TYPES, APPLICATION_METHOD_TARGET_TYPES, APPLICATION_METHOD_ALLOCATIONS, hasRuleAttribute, listRuleAttributeKeys, RULE_OPERATORS, resolveRuleAttribute } from '@fayz-ai/shop/rules';
@@ -329,6 +329,7 @@ var useCartStore = create()(
329
329
  lines: [],
330
330
  discountCode: null,
331
331
  discountPercent: 0,
332
+ discountFreeShipping: false,
332
333
  isOpen: false,
333
334
  justAddedLineId: null,
334
335
  addItem: (product, qty = 1, options) => set((state) => {
@@ -384,9 +385,13 @@ var useCartStore = create()(
384
385
  };
385
386
  }),
386
387
  removeItem: (lineId) => set((state) => ({ lines: state.lines.filter((l) => (l.lineId ?? l.productId) !== lineId) })),
387
- applyDiscount: (code, percent) => set({ discountCode: code, discountPercent: percent }),
388
- clearDiscount: () => set({ discountCode: null, discountPercent: 0 }),
389
- clear: () => set({ lines: [], discountCode: null, discountPercent: 0 }),
388
+ applyDiscount: (code, percent, options) => set({
389
+ discountCode: code,
390
+ discountPercent: percent,
391
+ discountFreeShipping: options?.freeShipping === true
392
+ }),
393
+ clearDiscount: () => set({ discountCode: null, discountPercent: 0, discountFreeShipping: false }),
394
+ clear: () => set({ lines: [], discountCode: null, discountPercent: 0, discountFreeShipping: false }),
390
395
  openDrawer: () => set({ isOpen: true }),
391
396
  closeDrawer: () => set({ isOpen: false, justAddedLineId: null }),
392
397
  reconcile: async (resolve) => {
@@ -404,7 +409,8 @@ var useCartStore = create()(
404
409
  partialize: (state) => ({
405
410
  lines: state.lines,
406
411
  discountCode: state.discountCode,
407
- discountPercent: state.discountPercent
412
+ discountPercent: state.discountPercent,
413
+ discountFreeShipping: state.discountFreeShipping
408
414
  })
409
415
  }
410
416
  )
@@ -416,6 +422,7 @@ var selectRequiresShipping = (s2) => s2.lines.some((l) => l.requiresShipping !==
416
422
  var selectShipping = (s2, cfg) => {
417
423
  if (s2.lines.length === 0) return 0;
418
424
  if (!selectRequiresShipping(s2)) return 0;
425
+ if (s2.discountFreeShipping) return 0;
419
426
  const subtotal = selectSubtotal(s2);
420
427
  const quoted = selectQuotedShipping(useDeliveryStore.getState(), subtotal);
421
428
  if (quoted != null) return quoted;
@@ -1136,10 +1143,12 @@ var TID = {
1136
1143
  authTabSignup: "auth-tab-signup",
1137
1144
  signinEmail: "signin-email",
1138
1145
  signinPassword: "signin-password",
1146
+ signinPasswordConfirm: "signin-password-confirm",
1139
1147
  signinName: "signin-name",
1140
1148
  signinSubmit: "signin-submit",
1141
1149
  authError: "auth-error",
1142
1150
  authNotice: "auth-notice",
1151
+ checkoutExpired: "checkout-expired",
1143
1152
  purchasesList: "purchases-list",
1144
1153
  purchaseItem: "purchase-item",
1145
1154
  purchasesEmpty: "purchases-empty",
@@ -6907,6 +6916,9 @@ var REASON_MESSAGE = {
6907
6916
  rules_not_matched: "Este cupom n\xE3o se aplica ao seu carrinho.",
6908
6917
  unsupported: "Este cupom n\xE3o \xE9 suportado na loja."
6909
6918
  };
6919
+ function cartSubtotal() {
6920
+ return selectSubtotal({ lines: useCartStore.getState().lines });
6921
+ }
6910
6922
  function cartSnapshot() {
6911
6923
  const lines = useCartStore.getState().lines;
6912
6924
  return {
@@ -6928,15 +6940,21 @@ function useDiscountValidator() {
6928
6940
  ...(config.discounts ?? []).map(storefrontDiscountToPromotion)
6929
6941
  ];
6930
6942
  const localPreview = previewCouponPercent(local, normalized, cartSnapshot());
6931
- if (localPreview.valid) return { valid: true, percent: localPreview.percent };
6943
+ if (localPreview.valid) {
6944
+ return { valid: true, percent: localPreview.percent, freeShipping: localPreview.freeShipping };
6945
+ }
6932
6946
  if (localPreview.reason !== "not_found") {
6933
6947
  return { valid: false, percent: 0, message: REASON_MESSAGE[localPreview.reason ?? "not_found"] ?? "Cupom inv\xE1lido." };
6934
6948
  }
6935
- const validation = await validateDiscount({ code: normalized });
6949
+ const validation = await validateDiscount({ code: normalized, subtotal: cartSubtotal() });
6936
6950
  if (!validation.valid) {
6951
+ if (validation.reason === "min_subtotal" && validation.minSubtotal != null) {
6952
+ const missing = formatMoney(validation.minSubtotal, config.currency, config.locale);
6953
+ return { valid: false, percent: 0, message: `Este cupom vale em pedidos a partir de ${missing}.` };
6954
+ }
6937
6955
  return { valid: false, percent: 0, message: REASON_MESSAGE[validation.reason ?? "not_found"] ?? "Cupom inv\xE1lido ou expirado." };
6938
6956
  }
6939
- if (validation.type !== "percentage") {
6957
+ if (validation.type !== "percentage" && validation.type !== "free_shipping") {
6940
6958
  return { valid: false, percent: 0, message: REASON_MESSAGE.unsupported };
6941
6959
  }
6942
6960
  const promotion = discountToPromotion({
@@ -6944,7 +6962,7 @@ function useDiscountValidator() {
6944
6962
  tenantId: "storefront",
6945
6963
  title: normalized,
6946
6964
  code: validation.code ?? normalized,
6947
- type: "percentage",
6965
+ type: validation.type,
6948
6966
  method: "code",
6949
6967
  value: validation.value,
6950
6968
  usageLimit: null,
@@ -6964,7 +6982,7 @@ function useDiscountValidator() {
6964
6982
  if (!preview?.valid) {
6965
6983
  return { valid: false, percent: 0, message: REASON_MESSAGE[preview?.reason ?? "not_found"] ?? "Cupom inv\xE1lido." };
6966
6984
  }
6967
- return { valid: true, percent: preview.percent };
6985
+ return { valid: true, percent: preview.percent, freeShipping: preview.freeShipping };
6968
6986
  }, [config.discounts, config.promotions]);
6969
6987
  }
6970
6988
  var seq = 0;
@@ -7035,9 +7053,10 @@ function CartDrawer() {
7035
7053
  const result = await validate(code);
7036
7054
  if (result.valid) {
7037
7055
  const applied = code.trim().toUpperCase();
7038
- cart.applyDiscount(applied, result.percent);
7056
+ cart.applyDiscount(applied, result.percent, { freeShipping: result.freeShipping });
7039
7057
  setCode("");
7040
- toast.success("Cupom aplicado!", `${applied} \u2022 ${result.percent}% de desconto`);
7058
+ const effect = result.freeShipping ? "frete gr\xE1tis" : `${result.percent}% de desconto`;
7059
+ toast.success("Cupom aplicado!", `${applied} \u2022 ${effect}`);
7041
7060
  } else {
7042
7061
  setDiscountError(result.message ?? "Cupom inv\xE1lido.");
7043
7062
  toast.error("Cupom inv\xE1lido", result.message ?? "Verifique o c\xF3digo e tente novamente.");
@@ -7231,12 +7250,25 @@ function CartDrawer() {
7231
7250
  ] })
7232
7251
  ] }),
7233
7252
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
7234
- /* @__PURE__ */ jsxs("dt", { className: "text-muted-foreground", children: [
7253
+ /* @__PURE__ */ jsxs("dt", { className: cart.discountFreeShipping ? "text-emerald-700" : "text-muted-foreground", children: [
7235
7254
  "Frete",
7236
7255
  delivery.status === "served" && delivery.postalCode && /* @__PURE__ */ jsxs("span", { className: "ml-1 text-xs", children: [
7237
7256
  "\xB7 ",
7238
7257
  formatPostalCode(delivery.postalCode)
7239
- ] })
7258
+ ] }),
7259
+ cart.discountFreeShipping && /* @__PURE__ */ jsxs(
7260
+ "button",
7261
+ {
7262
+ type: "button",
7263
+ onClick: cart.clearDiscount,
7264
+ className: "ml-1 py-1 text-xs text-muted-foreground underline",
7265
+ children: [
7266
+ "(",
7267
+ cart.discountCode,
7268
+ " \u2715)"
7269
+ ]
7270
+ }
7271
+ )
7240
7272
  ] }),
7241
7273
  /* @__PURE__ */ jsx("dd", { "data-testid": TID.cartShipping, "data-price": shipping.toFixed(2), children: shipping === 0 ? "Gr\xE1tis" : money(shipping) })
7242
7274
  ] }),
@@ -7466,6 +7498,41 @@ async function placeStorefrontOrder({
7466
7498
  rememberLocalOrder(order.id);
7467
7499
  return { order, customerId: customerId ?? order.customerId ?? "" };
7468
7500
  }
7501
+ function useMyOrders() {
7502
+ const customerId = useSessionStore((s2) => s2.customerId);
7503
+ const email = useSessionStore((s2) => s2.email);
7504
+ const [orders, setOrders] = useState([]);
7505
+ const [loading, setLoading] = useState(true);
7506
+ const [tick, setTick] = useState(0);
7507
+ const refresh = useCallback(() => setTick((t) => t + 1), []);
7508
+ useEffect(() => {
7509
+ let cancelled = false;
7510
+ const local = listLocalOrderIds();
7511
+ if (!customerId && !email) {
7512
+ if (local.length === 0) {
7513
+ setOrders([]);
7514
+ setLoading(false);
7515
+ return;
7516
+ }
7517
+ }
7518
+ setLoading(true);
7519
+ const query = customerId ? { customerId, limit: 50 } : { customerEmail: email, limit: 50 };
7520
+ getShopProvider().listOrders(query).then((data) => {
7521
+ if (!cancelled) setOrders(data);
7522
+ }).catch(async () => {
7523
+ const mine = await Promise.all(
7524
+ listLocalOrderIds().map((id) => getShopProvider().getOrder(id).catch(() => null))
7525
+ );
7526
+ if (!cancelled) setOrders(mine.filter((o) => !!o));
7527
+ }).finally(() => {
7528
+ if (!cancelled) setLoading(false);
7529
+ });
7530
+ return () => {
7531
+ cancelled = true;
7532
+ };
7533
+ }, [customerId, email, tick]);
7534
+ return { orders, loading, refresh };
7535
+ }
7469
7536
  function SignInModal({ defaultEmail = "", onClose, onSignedIn }) {
7470
7537
  const [email, setEmail] = useState(defaultEmail);
7471
7538
  const [password, setPassword] = useState("");
@@ -7652,13 +7719,199 @@ function PulseDot({ className = "" }) {
7652
7719
  /* @__PURE__ */ jsx("span", { className: "relative inline-flex h-2.5 w-2.5 rounded-full bg-current" })
7653
7720
  ] });
7654
7721
  }
7722
+ var SDK_URL = "https://sdk.mercadopago.com/js/v2";
7723
+ var CONTAINER_ID = "fayz-mp-card-brick";
7724
+ var DEVICE_SCRIPT_URL = "https://www.mercadopago.com/v2/security.js";
7725
+ var DEVICE_VIEW = "checkout";
7726
+ function loadDeviceScript() {
7727
+ if (typeof document === "undefined") return;
7728
+ if (document.querySelector(`script[src="${DEVICE_SCRIPT_URL}"]`)) return;
7729
+ const script = document.createElement("script");
7730
+ script.src = DEVICE_SCRIPT_URL;
7731
+ script.setAttribute("view", DEVICE_VIEW);
7732
+ script.async = true;
7733
+ document.body.appendChild(script);
7734
+ }
7735
+ function deviceSessionId() {
7736
+ if (typeof window === "undefined") return null;
7737
+ const id = window.MP_DEVICE_SESSION_ID;
7738
+ return typeof id === "string" && id.trim() ? id.trim() : null;
7739
+ }
7740
+ var sdkPromise = null;
7741
+ function loadSdk() {
7742
+ if (typeof window === "undefined") return Promise.reject(new Error("no window"));
7743
+ if (window.MercadoPago) return Promise.resolve();
7744
+ if (sdkPromise) return sdkPromise;
7745
+ sdkPromise = new Promise((resolve, reject) => {
7746
+ const existing = document.querySelector(`script[src="${SDK_URL}"]`);
7747
+ const script = existing ?? document.createElement("script");
7748
+ script.src = SDK_URL;
7749
+ script.async = true;
7750
+ script.addEventListener("load", () => resolve());
7751
+ script.addEventListener("error", () => {
7752
+ sdkPromise = null;
7753
+ reject(new Error("N\xE3o conseguimos carregar o pagamento com cart\xE3o."));
7754
+ });
7755
+ if (!existing) document.body.appendChild(script);
7756
+ });
7757
+ return sdkPromise;
7758
+ }
7759
+ function CardBrick({ publicKey, amount, email, onSubmit, onError }) {
7760
+ const [ready, setReady] = useState(false);
7761
+ const [failed, setFailed] = useState(null);
7762
+ const mounted = useRef(false);
7763
+ const controller = useRef(null);
7764
+ const submitRef = useRef(onSubmit);
7765
+ submitRef.current = onSubmit;
7766
+ useEffect(() => {
7767
+ if (mounted.current) return;
7768
+ mounted.current = true;
7769
+ let cancelled = false;
7770
+ loadDeviceScript();
7771
+ void (async () => {
7772
+ try {
7773
+ await loadSdk();
7774
+ if (cancelled || !window.MercadoPago) return;
7775
+ const mp = new window.MercadoPago(publicKey, { locale: "pt-BR" });
7776
+ const builder = mp.bricks();
7777
+ controller.current = await builder.create("cardPayment", CONTAINER_ID, {
7778
+ initialization: {
7779
+ amount,
7780
+ ...email ? { payer: { email } } : {}
7781
+ },
7782
+ customization: {
7783
+ visual: { style: { theme: "default" } },
7784
+ paymentMethods: { maxInstallments: 12 }
7785
+ },
7786
+ callbacks: {
7787
+ onReady: () => {
7788
+ if (!cancelled) setReady(true);
7789
+ },
7790
+ onSubmit: (formData) => (
7791
+ // Their contract: resolve when the payment is handled, reject to
7792
+ // keep the form up. Rejecting is what lets a declined card be
7793
+ // corrected in place instead of re-typed from scratch.
7794
+ submitRef.current({
7795
+ token: String(formData?.token ?? ""),
7796
+ paymentMethodId: formData?.payment_method_id ?? null,
7797
+ issuerId: formData?.issuer_id == null ? null : String(formData.issuer_id),
7798
+ installments: Number(formData?.installments) || 1,
7799
+ email: formData?.payer?.email ?? null,
7800
+ document: formData?.payer?.identification?.number ? {
7801
+ type: String(formData.payer.identification.type ?? "CPF"),
7802
+ number: String(formData.payer.identification.number)
7803
+ } : null
7804
+ })
7805
+ ),
7806
+ onError: (brickError) => {
7807
+ const message = brickError?.message ?? "N\xE3o conseguimos carregar o formul\xE1rio do cart\xE3o.";
7808
+ if (!cancelled) onError?.(message);
7809
+ }
7810
+ }
7811
+ });
7812
+ } catch (err) {
7813
+ if (cancelled) return;
7814
+ const message = err instanceof Error ? err.message : "N\xE3o conseguimos carregar o pagamento com cart\xE3o.";
7815
+ setFailed(message);
7816
+ onError?.(message);
7817
+ }
7818
+ })();
7819
+ return () => {
7820
+ cancelled = true;
7821
+ try {
7822
+ controller.current?.unmount();
7823
+ } catch {
7824
+ }
7825
+ controller.current = null;
7826
+ mounted.current = false;
7827
+ };
7828
+ }, []);
7829
+ if (failed) {
7830
+ return /* @__PURE__ */ jsx("p", { className: "rounded-lg border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive", children: failed });
7831
+ }
7832
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
7833
+ !ready && /* @__PURE__ */ jsxs(SkeletonGroup, { label: "Carregando o formul\xE1rio do cart\xE3o", className: "space-y-3", children: [
7834
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full rounded-lg" }),
7835
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
7836
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full rounded-lg" }),
7837
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full rounded-lg" })
7838
+ ] }),
7839
+ /* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full rounded-lg" })
7840
+ ] }),
7841
+ /* @__PURE__ */ jsx("div", { id: CONTAINER_ID, className: ready ? "" : "hidden" })
7842
+ ] });
7843
+ }
7655
7844
  var POLL_MS = 3e3;
7656
- function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable }) {
7845
+ var RESUME_KEY = "fz.checkout.resume";
7846
+ function useExpiry(expiresAt) {
7847
+ const target = React4.useMemo(() => {
7848
+ if (!expiresAt) return null;
7849
+ const t = new Date(expiresAt).getTime();
7850
+ return Number.isFinite(t) ? t : null;
7851
+ }, [expiresAt]);
7852
+ const [now, setNow] = React4.useState(() => Date.now());
7853
+ React4.useEffect(() => {
7854
+ if (target === null) return;
7855
+ const id = setInterval(() => setNow(Date.now()), 1e3);
7856
+ return () => clearInterval(id);
7857
+ }, [target]);
7858
+ if (target === null) return { secondsLeft: null, expired: false };
7859
+ const secondsLeft = Math.max(0, Math.round((target - now) / 1e3));
7860
+ return { secondsLeft, expired: secondsLeft === 0 };
7861
+ }
7862
+ function formatCountdown(seconds) {
7863
+ const m = Math.floor(seconds / 60);
7864
+ const s2 = seconds % 60;
7865
+ return `${m}:${String(s2).padStart(2, "0")}`;
7866
+ }
7867
+ async function copyText(text) {
7868
+ try {
7869
+ if (navigator.clipboard?.writeText) {
7870
+ await navigator.clipboard.writeText(text);
7871
+ return true;
7872
+ }
7873
+ } catch {
7874
+ }
7875
+ try {
7876
+ const area = document.createElement("textarea");
7877
+ area.value = text;
7878
+ area.setAttribute("readonly", "");
7879
+ area.style.position = "fixed";
7880
+ area.style.top = "-1000px";
7881
+ area.style.opacity = "0";
7882
+ document.body.appendChild(area);
7883
+ area.select();
7884
+ area.setSelectionRange(0, text.length);
7885
+ const ok = document.execCommand("copy");
7886
+ document.body.removeChild(area);
7887
+ return ok;
7888
+ } catch {
7889
+ return false;
7890
+ }
7891
+ }
7892
+ function rememberSession(sessionId, provider) {
7893
+ try {
7894
+ sessionStorage.setItem(RESUME_KEY, JSON.stringify({ sessionId, provider }));
7895
+ } catch {
7896
+ }
7897
+ }
7898
+ function recallSessionProvider(sessionId) {
7899
+ try {
7900
+ const raw = JSON.parse(sessionStorage.getItem(RESUME_KEY) ?? "null");
7901
+ return raw?.sessionId === sessionId ? raw.provider ?? null : null;
7902
+ } catch {
7903
+ return null;
7904
+ }
7905
+ }
7906
+ function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable, resume, amount }) {
7657
7907
  const [phase, setPhase] = useState("opening");
7658
7908
  const [charge, setCharge] = useState(null);
7909
+ const expiry = useExpiry(charge?.expiresAt);
7659
7910
  const [total, setTotal] = useState(null);
7660
- const [sessionId, setSessionId] = useState(null);
7661
- const [provider, setProvider] = useState(null);
7911
+ const [redirectUrl, setRedirectUrl] = useState(null);
7912
+ const [brickKey, setBrickKey] = useState(null);
7913
+ const [sessionId, setSessionId] = useState(resume?.sessionId ?? null);
7914
+ const [provider, setProvider] = useState(resume?.provider ?? null);
7662
7915
  const [error, setError] = useState(null);
7663
7916
  const [copied, setCopied] = useState(false);
7664
7917
  const [checking, setChecking] = useState(false);
@@ -7674,15 +7927,37 @@ function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable }) {
7674
7927
  setPhase("opening");
7675
7928
  setError(null);
7676
7929
  try {
7677
- const session = await shop.openCheckoutSession(input);
7930
+ if (input.paymentMethod === "credit_card" && shop.paymentPublicKey) {
7931
+ const providerSlug = input.provider ?? null;
7932
+ const publicKey = providerSlug ? await shop.paymentPublicKey(providerSlug) : null;
7933
+ if (publicKey) {
7934
+ setProvider(providerSlug);
7935
+ setBrickKey(publicKey);
7936
+ setPhase("card");
7937
+ return;
7938
+ }
7939
+ }
7940
+ const session = await shop.openCheckoutSession({
7941
+ ...input,
7942
+ // Para onde o adquirente devolve o comprador. É esta página, com o id da
7943
+ // sessão pendurado: quem volta volta em página nova, sem estado nenhum.
7944
+ returnUrl: typeof window === "undefined" ? null : window.location.href.split("?")[0]
7945
+ });
7678
7946
  setSessionId(session.sessionId);
7679
7947
  setProvider(session.provider ?? input.provider ?? null);
7680
7948
  setTotal(session.total);
7681
7949
  setCharge(session.charge);
7950
+ if (session.paymentUrl && !session.charge) {
7951
+ setRedirectUrl(session.paymentUrl);
7952
+ setPhase("redirect");
7953
+ return;
7954
+ }
7682
7955
  setPhase(session.charge ? "waiting" : "error");
7683
7956
  if (!session.charge) setError("N\xE3o conseguimos abrir a cobran\xE7a agora.");
7684
7957
  } catch (err) {
7685
- if (err?.status === 404) {
7958
+ const status = err?.status;
7959
+ const code = err?.code;
7960
+ if (status === 404 || code === "method_unavailable") {
7686
7961
  onUnavailable?.();
7687
7962
  return;
7688
7963
  }
@@ -7693,14 +7968,18 @@ function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable }) {
7693
7968
  useEffect(() => {
7694
7969
  if (opened.current) return;
7695
7970
  opened.current = true;
7971
+ if (resume?.sessionId) {
7972
+ setPhase("waiting");
7973
+ return;
7974
+ }
7696
7975
  void open();
7697
- }, [open]);
7976
+ }, [open, resume?.sessionId]);
7698
7977
  const check = useCallback(async () => {
7699
7978
  if (!sessionId || settled.current) return false;
7700
7979
  const shop = getShopProvider();
7701
7980
  if (!shop.settleCheckoutSession) return false;
7702
7981
  try {
7703
- const result = await shop.settleCheckoutSession(sessionId, provider);
7982
+ const result = await shop.settleCheckoutSession(sessionId, provider, resume?.chargeToken ?? null);
7704
7983
  if (settled.current) return false;
7705
7984
  if (result.state === "paid" && result.orderId) {
7706
7985
  settled.current = true;
@@ -7718,7 +7997,7 @@ function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable }) {
7718
7997
  } catch {
7719
7998
  return false;
7720
7999
  }
7721
- }, [sessionId, provider, onPaid]);
8000
+ }, [sessionId, provider, resume?.chargeToken, onPaid]);
7722
8001
  useEffect(() => {
7723
8002
  if (phase !== "waiting" || !sessionId) return;
7724
8003
  let stopped = false;
@@ -7737,13 +8016,116 @@ function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable }) {
7737
8016
  }
7738
8017
  async function copy() {
7739
8018
  if (!charge?.emv) return;
7740
- try {
7741
- await navigator.clipboard.writeText(charge.emv);
8019
+ if (await copyText(charge.emv)) {
7742
8020
  setCopied(true);
7743
8021
  setTimeout(() => setCopied(false), 2400);
7744
- } catch {
7745
- toast.error("Copie o c\xF3digo manualmente");
8022
+ return;
7746
8023
  }
8024
+ toast.error("Copie o c\xF3digo manualmente");
8025
+ }
8026
+ if (phase === "card" && brickKey) {
8027
+ const acquirer = gatewayLabel(provider);
8028
+ return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-4 rounded-xl border bg-card p-6", children: [
8029
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
8030
+ /* @__PURE__ */ jsx(CreditCard, { className: "h-5 w-5 text-primary" }),
8031
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Pague com cart\xE3o sem sair da loja" })
8032
+ ] }),
8033
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: acquirer ? `Os campos do cart\xE3o s\xE3o da ${acquirer} e o n\xFAmero n\xE3o passa por esta loja. Seu pedido \xE9 criado assim que o pagamento for aprovado.` : "O n\xFAmero do cart\xE3o n\xE3o passa por esta loja. Seu pedido \xE9 criado assim que o pagamento for aprovado." }),
8034
+ (total ?? amount) != null && /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
8035
+ "Valor: ",
8036
+ /* @__PURE__ */ jsx("strong", { "data-testid": TID.checkoutPaymentTotal, children: money(total ?? amount) })
8037
+ ] }),
8038
+ /* @__PURE__ */ jsx(
8039
+ CardBrick,
8040
+ {
8041
+ publicKey: brickKey,
8042
+ amount: total ?? amount ?? 0,
8043
+ email: input.customer?.email ?? null,
8044
+ onError: (message) => setError(message),
8045
+ onSubmit: async (result) => {
8046
+ const shop = getShopProvider();
8047
+ if (!shop.openCheckoutSession) throw new Error("sem gateway");
8048
+ try {
8049
+ const session = await shop.openCheckoutSession({
8050
+ ...input,
8051
+ cardToken: result.token,
8052
+ installments: result.installments,
8053
+ paymentMethodId: result.paymentMethodId,
8054
+ issuerId: result.issuerId,
8055
+ // Their anti-fraud reads this, and a card charged without it is
8056
+ // declined more often for no visible reason.
8057
+ deviceId: deviceSessionId()
8058
+ });
8059
+ setSessionId(session.sessionId);
8060
+ setProvider(session.provider ?? input.provider ?? null);
8061
+ setTotal(session.total);
8062
+ const orderId = session.orderId ?? null;
8063
+ if (orderId) {
8064
+ settled.current = true;
8065
+ setPhase("settling");
8066
+ onPaid(orderId);
8067
+ return;
8068
+ }
8069
+ setPhase("waiting");
8070
+ } catch (err) {
8071
+ const message = err instanceof Error ? err.message : "O pagamento n\xE3o foi aprovado.";
8072
+ setError(message);
8073
+ throw err;
8074
+ }
8075
+ }
8076
+ }
8077
+ ),
8078
+ error && /* @__PURE__ */ jsx("p", { className: "rounded-lg border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive", children: error }),
8079
+ /* @__PURE__ */ jsx(
8080
+ "button",
8081
+ {
8082
+ type: "button",
8083
+ onClick: onBack,
8084
+ className: "inline-flex min-h-11 items-center rounded-lg border px-4 py-2.5 text-sm font-medium transition hover:bg-muted",
8085
+ children: "Voltar"
8086
+ }
8087
+ )
8088
+ ] });
8089
+ }
8090
+ if (phase === "redirect" && redirectUrl) {
8091
+ const acquirer = gatewayLabel(provider);
8092
+ return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-4 rounded-xl border bg-card p-6", children: [
8093
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
8094
+ /* @__PURE__ */ jsx(CreditCard, { className: "h-5 w-5 text-primary" }),
8095
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: acquirer ? `Pague com cart\xE3o na p\xE1gina da ${acquirer}` : "Pague com cart\xE3o" })
8096
+ ] }),
8097
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: acquirer ? `Os dados do cart\xE3o s\xE3o digitados no ambiente seguro da ${acquirer} \u2014 a loja n\xE3o v\xEA o n\xFAmero. Ao terminar, voc\xEA volta para c\xE1 e o pedido \xE9 criado.` : "Os dados do cart\xE3o s\xE3o digitados no ambiente seguro do provedor \u2014 a loja n\xE3o v\xEA o n\xFAmero. Ao terminar, voc\xEA volta para c\xE1 e o pedido \xE9 criado." }),
8098
+ total != null && /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
8099
+ "Valor: ",
8100
+ /* @__PURE__ */ jsx("strong", { "data-testid": TID.checkoutPaymentTotal, children: money(total) })
8101
+ ] }),
8102
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
8103
+ /* @__PURE__ */ jsxs(
8104
+ "button",
8105
+ {
8106
+ type: "button",
8107
+ onClick: () => {
8108
+ if (sessionId) rememberSession(sessionId, provider);
8109
+ window.location.assign(redirectUrl);
8110
+ },
8111
+ className: "inline-flex min-h-11 items-center gap-2 rounded-lg bg-primary px-5 py-2.5 text-sm font-semibold text-primary-foreground transition hover:opacity-90",
8112
+ children: [
8113
+ /* @__PURE__ */ jsx(Lock, { className: "h-4 w-4" }),
8114
+ acquirer ? `Pagar com ${acquirer}` : "Pagar com cart\xE3o"
8115
+ ]
8116
+ }
8117
+ ),
8118
+ /* @__PURE__ */ jsx(
8119
+ "button",
8120
+ {
8121
+ type: "button",
8122
+ onClick: onBack,
8123
+ className: "inline-flex min-h-11 items-center rounded-lg border px-4 py-2.5 text-sm font-medium transition hover:bg-muted",
8124
+ children: "Voltar"
8125
+ }
8126
+ )
8127
+ ] })
8128
+ ] });
7747
8129
  }
7748
8130
  if (phase === "opening") {
7749
8131
  return (
@@ -7805,6 +8187,30 @@ function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable }) {
7805
8187
  ] })
7806
8188
  ] });
7807
8189
  }
8190
+ if (!charge) {
8191
+ return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-3 rounded-xl border bg-card p-6", "aria-live": "polite", children: [
8192
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
8193
+ /* @__PURE__ */ jsx(PulseDot, {}),
8194
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Confirmando seu pagamento\u2026" })
8195
+ ] }),
8196
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Assim que o pagamento for confirmado, seu pedido aparece aqui. Pode deixar esta tela aberta." }),
8197
+ /* @__PURE__ */ jsxs(
8198
+ "button",
8199
+ {
8200
+ type: "button",
8201
+ onClick: () => {
8202
+ void confirmPaid();
8203
+ },
8204
+ disabled: checking,
8205
+ className: "inline-flex min-h-11 items-center gap-2 rounded-lg border border-primary px-4 py-2.5 text-sm font-semibold text-primary transition hover:bg-primary/5 disabled:opacity-60",
8206
+ children: [
8207
+ checking ? /* @__PURE__ */ jsx(PulseDot, {}) : /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
8208
+ checking ? "Verificando\u2026" : "J\xE1 paguei"
8209
+ ]
8210
+ }
8211
+ )
8212
+ ] });
8213
+ }
7808
8214
  return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-4", "aria-live": "polite", children: [
7809
8215
  /* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-card p-6", children: [
7810
8216
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
@@ -7813,12 +8219,14 @@ function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable }) {
7813
8219
  ] }),
7814
8220
  /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "Seu pedido \xE9 criado assim que o pagamento entrar. Pode deixar esta tela aberta." }),
7815
8221
  /* @__PURE__ */ jsxs("div", { className: "mt-5 grid gap-5 sm:grid-cols-[auto_minmax(0,1fr)] sm:items-start", children: [
7816
- charge?.qrcodeBase64 && /* @__PURE__ */ jsx(
8222
+ (charge?.qrcodeBase64 || charge?.qrcodeUrl) && /* @__PURE__ */ jsx(
7817
8223
  "img",
7818
8224
  {
7819
- src: `data:image/png;base64,${charge.qrcodeBase64}`,
8225
+ src: charge.qrcodeBase64 ? `data:image/png;base64,${charge.qrcodeBase64}` : charge.qrcodeUrl,
7820
8226
  alt: "QR code do Pix",
7821
- className: "mx-auto h-44 w-44 rounded-lg border bg-white p-2"
8227
+ width: 276,
8228
+ height: 276,
8229
+ className: "mx-auto box-content h-[276px] w-[276px] max-w-full rounded-lg border bg-white p-4"
7822
8230
  }
7823
8231
  ),
7824
8232
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
@@ -7835,7 +8243,28 @@ function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable }) {
7835
8243
  children: charge?.emv
7836
8244
  }
7837
8245
  ),
7838
- /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-3", children: [
8246
+ expiry.secondsLeft !== null && !expiry.expired && /* @__PURE__ */ jsxs("p", { className: "mt-2 text-xs text-muted-foreground", children: [
8247
+ "Este c\xF3digo vale por mais",
8248
+ " ",
8249
+ /* @__PURE__ */ jsx("span", { className: "font-medium tabular-nums text-foreground", children: formatCountdown(expiry.secondsLeft) })
8250
+ ] }),
8251
+ expiry.expired && /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", children: [
8252
+ /* @__PURE__ */ jsx("p", { "data-testid": TID.checkoutExpired, className: "text-sm text-muted-foreground", children: "O prazo deste c\xF3digo acabou. Gere outro para pagar." }),
8253
+ /* @__PURE__ */ jsx(
8254
+ "button",
8255
+ {
8256
+ type: "button",
8257
+ onClick: () => {
8258
+ opened.current = false;
8259
+ setCharge(null);
8260
+ void open();
8261
+ },
8262
+ className: "inline-flex min-h-11 items-center gap-2 rounded-lg bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground transition hover:opacity-90",
8263
+ children: "Gerar novo c\xF3digo"
8264
+ }
8265
+ )
8266
+ ] }),
8267
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-3", hidden: expiry.expired, children: [
7839
8268
  /* @__PURE__ */ jsxs(
7840
8269
  "button",
7841
8270
  {
@@ -7914,8 +8343,52 @@ function SuccessMark() {
7914
8343
  ) })
7915
8344
  ] });
7916
8345
  }
8346
+ function useKnownCustomer() {
8347
+ const customerId = useSessionStore((s2) => s2.customerId);
8348
+ const { orders, loading: ordersLoading } = useMyOrders();
8349
+ const [contact, setContact] = useState(null);
8350
+ const [checked, setChecked] = useState(false);
8351
+ useEffect(() => {
8352
+ if (!customerId) {
8353
+ setContact(null);
8354
+ setChecked(true);
8355
+ return;
8356
+ }
8357
+ let cancelled = false;
8358
+ const shop = getShopProvider();
8359
+ Promise.resolve(shop.getCustomer?.(customerId)).then((customer) => {
8360
+ if (cancelled) return;
8361
+ setContact(customer ? { phone: customer.phone ?? null, document: customer.document ?? null } : null);
8362
+ }).catch(() => {
8363
+ if (!cancelled) setContact(null);
8364
+ }).finally(() => {
8365
+ if (!cancelled) setChecked(true);
8366
+ });
8367
+ return () => {
8368
+ cancelled = true;
8369
+ };
8370
+ }, [customerId]);
8371
+ const lastAddress = pickLastAddress(orders);
8372
+ return {
8373
+ phone: contact?.phone ?? null,
8374
+ document: contact?.document ?? null,
8375
+ address: lastAddress,
8376
+ ready: checked && !ordersLoading
8377
+ };
8378
+ }
8379
+ function pickLastAddress(orders) {
8380
+ for (const order of orders) {
8381
+ const address = order.shippingAddress;
8382
+ if (address && (address.postalCode || address.street)) return address;
8383
+ }
8384
+ return null;
8385
+ }
7917
8386
  var PAYMENT_LABELS = {
7918
8387
  pix: { label: "Pix", hint: "Voc\xEA recebe a chave para pagar ap\xF3s confirmar o pedido" },
8388
+ // O cartão tem DOIS significados agora, e o texto muda com a loja: sem
8389
+ // adquirente é a maquininha do entregador; com adquirente é a página segura
8390
+ // dele. Ver PAYMENT_HINTS abaixo — prometer "maquininha" para quem vai ser
8391
+ // redirecionado é a loja mentindo sobre a própria entrega.
7919
8392
  credit_card: { label: "Cart\xE3o de cr\xE9dito", hint: "Maquininha na entrega" },
7920
8393
  debit_card: { label: "Cart\xE3o de d\xE9bito", hint: "Maquininha na entrega" },
7921
8394
  boleto: { label: "Boleto", hint: "Enviado por e-mail ap\xF3s a confirma\xE7\xE3o" },
@@ -7964,12 +8437,26 @@ function CheckoutPage() {
7964
8437
  const fieldPolicy = policy?.checkoutFields ?? {};
7965
8438
  const gatewayCheckout = config.payments.mode === "gateway" || !!policy?.paymentProvider;
7966
8439
  const shows = (key) => key === "document" ? gatewayCheckout || fieldPolicy.document?.visible === true : fieldPolicy[key]?.visible !== false;
7967
- const requires = (key) => key === "document" ? gatewayCheckout || fieldPolicy.document?.visible === true && fieldPolicy.document?.required === true : fieldPolicy[key]?.visible !== false && fieldPolicy[key]?.required === true;
8440
+ const requires = (key) => key === "document" || key === "phone" ? gatewayCheckout || fieldPolicy[key]?.visible === true && fieldPolicy[key]?.required === true : fieldPolicy[key]?.visible !== false && fieldPolicy[key]?.required === true;
7968
8441
  const [selectedAddressId, setSelectedAddressId] = useState("new");
7969
8442
  const [savedAddresses, setSavedAddresses] = useState([]);
7970
8443
  const [addressesLoading, setAddressesLoading] = useState(false);
7971
8444
  const [addressMode, setAddressMode] = useState("new");
7972
- const [paymentMethod, setPaymentMethod] = useState(paymentMethods[0] ?? "pix");
8445
+ const [resume] = useState(() => {
8446
+ if (typeof window === "undefined") return null;
8447
+ const params = new URLSearchParams(window.location.search);
8448
+ const sessionId = params.get("vindi_session") ?? params.get("pay_session");
8449
+ if (!sessionId) return null;
8450
+ return {
8451
+ sessionId,
8452
+ provider: recallSessionProvider(sessionId),
8453
+ chargeToken: params.get("token_transaction")
8454
+ };
8455
+ });
8456
+ const [paymentMethod, setPaymentMethod] = useState(
8457
+ resume ? "credit_card" : paymentMethods[0] ?? "pix"
8458
+ );
8459
+ const known = useKnownCustomer();
7973
8460
  const [form, setForm] = useState({
7974
8461
  email: session.email ?? "",
7975
8462
  name: session.name ?? "",
@@ -7983,7 +8470,22 @@ function CheckoutPage() {
7983
8470
  district: "",
7984
8471
  state: ""
7985
8472
  });
7986
- const [step, setStep] = useState(1);
8473
+ React4.useEffect(() => {
8474
+ if (!known.ready) return;
8475
+ setForm((current) => ({
8476
+ ...current,
8477
+ phone: current.phone || known.phone || "",
8478
+ document: current.document || known.document || "",
8479
+ zip: current.zip || known.address?.postalCode || "",
8480
+ street: current.street || known.address?.street || "",
8481
+ number: current.number || known.address?.number || "",
8482
+ complement: current.complement || known.address?.complement || "",
8483
+ district: current.district || known.address?.district || "",
8484
+ city: current.city || known.address?.city || "",
8485
+ state: current.state || known.address?.state || ""
8486
+ }));
8487
+ }, [known.ready, known.phone, known.document, known.address]);
8488
+ const [step, setStep] = useState(resume ? 3 : 1);
7987
8489
  const [paidOrderId, setPaidOrderId] = useState(null);
7988
8490
  const [error, setError] = useState(null);
7989
8491
  const [discountCode, setDiscountCode] = useState("");
@@ -8006,7 +8508,26 @@ function CheckoutPage() {
8006
8508
  quantity: line.quantity,
8007
8509
  optionsLabel: line.optionsLabel ?? null
8008
8510
  })),
8009
- customer: { name: form.name.trim(), email: form.email.trim() },
8511
+ // The document and the phone travel WITH the buyer, not only inside the
8512
+ // address.
8513
+ //
8514
+ // The acquirer charges a PERSON: Vindi refuses a charge with no document
8515
+ // ("Informe o CPF ou CNPJ do comprador"), and AbacatePay's customer block is
8516
+ // all-or-nothing — name, email, taxId and cellphone, or no customer at all.
8517
+ // With two of the four it had been opening charges while registering nobody,
8518
+ // which is the "no client was created" the store saw in the provider account.
8519
+ //
8520
+ // The field was already asked for on screen: with an acquirer connected,
8521
+ // `requires('document')` is true and nobody gets past step 1 without filling
8522
+ // it in. What was missing was forwarding it to whoever charges. On the ORDER
8523
+ // it still travels inside the address, because of shop_place_order's
8524
+ // signature — different roads.
8525
+ customer: {
8526
+ name: form.name.trim(),
8527
+ email: form.email.trim(),
8528
+ document: documentDigits(form.document) || void 0,
8529
+ phone: form.phone.trim() || void 0
8530
+ },
8010
8531
  shippingAddress: {
8011
8532
  postalCode: form.zip.trim(),
8012
8533
  street: form.street.trim(),
@@ -8026,12 +8547,21 @@ function CheckoutPage() {
8026
8547
  provider: policy?.paymentProvider ?? null
8027
8548
  // eslint-disable-next-line react-hooks/exhaustive-deps
8028
8549
  }), [cart.lines, cart.discountCode, form, paymentMethod, policy?.paymentProvider]);
8029
- const [acquirerMissing, setAcquirerMissing] = useState(false);
8030
- const chargesOnline = paymentMethod === "pix" && !acquirerMissing && (!policy?.paymentProvider || isChargeableProvider(policy.paymentProvider));
8550
+ const [unavailable, setUnavailable] = useState([]);
8551
+ const cardIsCharged = !unavailable.includes("credit_card") && (!policy?.paymentProvider || isChargeableProvider(policy.paymentProvider)) && (!policy?.paymentProvider || supportsCard(policy.paymentProvider));
8552
+ const acquirerMissing = unavailable.includes(paymentMethod);
8553
+ const chargesOnline = (
8554
+ // Pix é cobrado dentro da loja; cartão é pago na página do adquirente e
8555
+ // volta para cá. Débito, dinheiro e "combinar" nunca foram cobrança online.
8556
+ // `cardIsCharged` rather than a bare `credit_card`: card is charged online
8557
+ // only where the acquirer charges cards. Where it does not, this screen
8558
+ // offers the order without a charge — the card-reader road, as it always was.
8559
+ (paymentMethod === "pix" || paymentMethod === "credit_card" && cardIsCharged) && !acquirerMissing && (!policy?.paymentProvider || isChargeableProvider(policy.paymentProvider))
8560
+ );
8031
8561
  const money = (value) => formatMoney(value, config.currency, config.locale);
8032
8562
  useEffect(() => {
8033
- if (cart.lines.length === 0 && !paidOrderId && !leaving.current) navigateTo(config.catalogPath);
8034
- }, [cart.lines.length, config.catalogPath, paidOrderId]);
8563
+ if (cart.lines.length === 0 && !paidOrderId && !leaving.current && !resume) navigateTo(config.catalogPath);
8564
+ }, [cart.lines.length, config.catalogPath, paidOrderId, resume]);
8035
8565
  const set = (key) => (value) => {
8036
8566
  setError(null);
8037
8567
  setForm((current) => ({ ...current, [key]: value }));
@@ -8072,10 +8602,11 @@ function CheckoutPage() {
8072
8602
  const appliedCode = discountCode.trim().toUpperCase();
8073
8603
  const result = await validateDiscount2(discountCode);
8074
8604
  if (result.valid) {
8075
- cart.applyDiscount(appliedCode, result.percent);
8605
+ cart.applyDiscount(appliedCode, result.percent, { freeShipping: result.freeShipping });
8076
8606
  setDiscountCode(appliedCode);
8077
- setDiscountSuccess(`${appliedCode} aplicado: ${result.percent}% de desconto.`);
8078
- toast.success("Cupom aplicado!", `${appliedCode} \u2022 ${result.percent}% de desconto`);
8607
+ const effect = result.freeShipping ? "frete gr\xE1tis" : `${result.percent}% de desconto`;
8608
+ setDiscountSuccess(`${appliedCode} aplicado: ${effect}.`);
8609
+ toast.success("Cupom aplicado!", `${appliedCode} \u2022 ${effect}`);
8079
8610
  } else {
8080
8611
  setDiscountError(result.message ?? "Cupom inv\xE1lido.");
8081
8612
  toast.error("Cupom inv\xE1lido", result.message ?? "Verifique o c\xF3digo e tente novamente.");
@@ -8218,8 +8749,13 @@ function CheckoutPage() {
8218
8749
  if (typeof window !== "undefined") window.scrollTo({ top: 0, behavior: "smooth" });
8219
8750
  }
8220
8751
  function advance() {
8221
- if (step === 1 && validateContact()) goToStep(2);
8222
- else if (step === 2 && validateDelivery()) goToStep(3);
8752
+ if (step === 1 && validateContact()) {
8753
+ void getShopProvider().saveCustomerContact?.({
8754
+ phone: form.phone.trim() || null,
8755
+ document: documentDigits(form.document) || null
8756
+ });
8757
+ goToStep(2);
8758
+ } else if (step === 2 && validateDelivery()) goToStep(3);
8223
8759
  }
8224
8760
  async function confirmWithoutCharge() {
8225
8761
  if (!validate()) return;
@@ -8491,7 +9027,8 @@ function CheckoutPage() {
8491
9027
  step === 3 && !placing && /* @__PURE__ */ jsxs("section", { children: [
8492
9028
  /* @__PURE__ */ jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: "Pagamento" }),
8493
9029
  /* @__PURE__ */ jsx("div", { className: "mb-3 grid gap-3", role: "radiogroup", "aria-label": "Forma de pagamento", children: paymentMethods.map((method) => {
8494
- const copy = PAYMENT_LABELS[method];
9030
+ const base = PAYMENT_LABELS[method];
9031
+ const copy = method === "credit_card" && cardIsCharged ? { ...base, hint: "Voc\xEA paga na p\xE1gina segura do provedor e volta para a loja" } : base;
8495
9032
  const selected = paymentMethod === method;
8496
9033
  return (
8497
9034
  // Cada forma de pagamento abre o que ELA pede, embaixo dela
@@ -8524,8 +9061,10 @@ function CheckoutPage() {
8524
9061
  {
8525
9062
  input: checkoutSessionInput,
8526
9063
  money,
9064
+ amount: total,
8527
9065
  onBack: () => goToStep(2),
8528
- onUnavailable: () => setAcquirerMissing(true),
9066
+ resume,
9067
+ onUnavailable: () => setUnavailable((list) => list.includes(method) ? list : [...list, method]),
8529
9068
  onPaid: (orderId) => {
8530
9069
  leaving.current = true;
8531
9070
  setPaidOrderId(orderId);
@@ -8676,7 +9215,22 @@ function CheckoutPage() {
8676
9215
  ] })
8677
9216
  ] }),
8678
9217
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
8679
- /* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: "Entrega" }),
9218
+ /* @__PURE__ */ jsxs("dt", { className: cart.discountFreeShipping ? "text-emerald-700" : "text-muted-foreground", children: [
9219
+ "Entrega",
9220
+ cart.discountFreeShipping && /* @__PURE__ */ jsxs(
9221
+ "button",
9222
+ {
9223
+ type: "button",
9224
+ onClick: clearDiscountCode,
9225
+ className: "ml-1 text-xs text-muted-foreground underline",
9226
+ children: [
9227
+ "(",
9228
+ cart.discountCode,
9229
+ " \xD7)"
9230
+ ]
9231
+ }
9232
+ )
9233
+ ] }),
8680
9234
  /* @__PURE__ */ jsx("dd", { "data-price": shipping.toFixed(2), children: shipping === 0 ? "Gr\xE1tis" : money(shipping) })
8681
9235
  ] }),
8682
9236
  /* @__PURE__ */ jsxs("div", { className: "flex justify-between border-t pt-5 text-lg font-bold", children: [
@@ -9133,45 +9687,11 @@ function OrderConfirmationPage({ orderId }) {
9133
9687
  }
9134
9688
  );
9135
9689
  }
9136
- function useMyOrders() {
9137
- const customerId = useSessionStore((s2) => s2.customerId);
9138
- const email = useSessionStore((s2) => s2.email);
9139
- const [orders, setOrders] = useState([]);
9140
- const [loading, setLoading] = useState(true);
9141
- const [tick, setTick] = useState(0);
9142
- const refresh = useCallback(() => setTick((t) => t + 1), []);
9143
- useEffect(() => {
9144
- let cancelled = false;
9145
- const local = listLocalOrderIds();
9146
- if (!customerId && !email) {
9147
- if (local.length === 0) {
9148
- setOrders([]);
9149
- setLoading(false);
9150
- return;
9151
- }
9152
- }
9153
- setLoading(true);
9154
- const query = customerId ? { customerId, limit: 50 } : { customerEmail: email, limit: 50 };
9155
- getShopProvider().listOrders(query).then((data) => {
9156
- if (!cancelled) setOrders(data);
9157
- }).catch(async () => {
9158
- const mine = await Promise.all(
9159
- listLocalOrderIds().map((id) => getShopProvider().getOrder(id).catch(() => null))
9160
- );
9161
- if (!cancelled) setOrders(mine.filter((o) => !!o));
9162
- }).finally(() => {
9163
- if (!cancelled) setLoading(false);
9164
- });
9165
- return () => {
9166
- cancelled = true;
9167
- };
9168
- }, [customerId, email, tick]);
9169
- return { orders, loading, refresh };
9170
- }
9171
9690
  function AuthForm() {
9172
9691
  const [mode, setMode] = useState("signin");
9173
9692
  const [email, setEmail] = useState("");
9174
9693
  const [password, setPassword] = useState("");
9694
+ const [confirmPassword, setConfirmPassword] = useState("");
9175
9695
  const [name, setName] = useState("");
9176
9696
  const [busy, setBusy] = useState(false);
9177
9697
  const [error, setError] = useState(null);
@@ -9185,6 +9705,10 @@ function AuthForm() {
9185
9705
  setError("Informe seu nome.");
9186
9706
  return;
9187
9707
  }
9708
+ if (mode === "signup" && password !== confirmPassword) {
9709
+ setError("As senhas n\xE3o s\xE3o iguais.");
9710
+ return;
9711
+ }
9188
9712
  setBusy(true);
9189
9713
  try {
9190
9714
  if (mode === "signup") await signUpCustomer(email, password, name.trim());
@@ -9260,6 +9784,20 @@ function AuthForm() {
9260
9784
  style: { borderRadius: "var(--sf-radius-input)" }
9261
9785
  }
9262
9786
  ),
9787
+ mode === "signup" && /* @__PURE__ */ jsx(
9788
+ "input",
9789
+ {
9790
+ "data-testid": TID.signinPasswordConfirm,
9791
+ type: "password",
9792
+ required: true,
9793
+ placeholder: "Confirmar senha",
9794
+ value: confirmPassword,
9795
+ onChange: (e) => setConfirmPassword(e.target.value),
9796
+ "aria-invalid": confirmPassword.length > 0 && password !== confirmPassword,
9797
+ className: "w-full border bg-background px-3 py-2.5 text-sm",
9798
+ style: { borderRadius: "var(--sf-radius-input)" }
9799
+ }
9800
+ ),
9263
9801
  notice && /* @__PURE__ */ jsx("p", { "data-testid": TID.authNotice, className: "text-sm font-medium text-emerald-700", children: notice }),
9264
9802
  error && /* @__PURE__ */ jsx("p", { "data-testid": TID.authError, className: "text-sm text-destructive", children: error }),
9265
9803
  /* @__PURE__ */ jsx(
@@ -11123,5 +11661,5 @@ ${formatErrors(report.errors)}`;
11123
11661
  }
11124
11662
 
11125
11663
  export { BEFORE_PLACE_ORDER_VALVE, BLOCK_STRUCTURAL_KEYS, BenefitsRow, BlockDataError, CART_LINE_ANNOTATION_POINT, CATALOG_BODY_BLOCK, CHECKOUT_FIELD_POINT, CartDrawer, CatalogPage, CategoryShowcase, CheckoutPage, CollectionCta, CollectionHero, ContentPage, CountdownBand, EmailConfirmationRequiredError, EntityListBlock, FaqSection, FiltersPanel, FormBlock, HeroSection, ImageTiles, Link, ManifestoBlock, MediaCarousel, MotifStrip, MyPurchasesPage, NewsletterBand, ORDER_METADATA_POINT, OrderConfirmationPage, OrderTrackingTimeline, PAGE_DATA_SCRIPT_ID, PLACEHOLDER_ASPECTS, PRODUCT_BODY_BLOCK, PRODUCT_CARD_BADGE_POINT, PageDataScope, PaymentTerms, Price, ProductCard, ProductDetailPage, ProductEnquiryForm, ProductGallery, ProductGrid, ProductOptionSelector, ProductRail, ProductReviews, ProductSlider, ProductSpecs, ProductSpotlight, PromoBanner, QuantityInput, Reveal, STOREFRONT_COMPONENT_KEYS, STOREFRONT_PAGE_KINDS, STOREFRONT_SLOTS, STORE_BACKEND_PROVIDERS, STORE_COMMERCE_MODES, STORE_DOCUMENT_KIND, STORE_PAYMENT_METHOD_KINDS, STORE_PAYMENT_MODES, STORE_ROUTE_CHROMES, STORE_ROUTE_KINDS, STORE_SECTIONS, STORE_SECTION_NAMES, SealsBand, SearchOverlay, Slot, SmoothImage, StepsSection, StorefrontConfigHost, StorefrontConfigProvider, StorefrontFooter, StorefrontHeader, StorefrontPage, StorefrontRouterProvider, StorefrontShell, StorefrontThemeStyle, StoryQuote, THEME_SCALE_FIELDS, THEME_SCALE_VAR_PREFIX, TID, Testimonials, TierCards, applyStoreSettings, bannerPlaceholder, blockInstanceId, buildEnquiryFromForm, checkPageBlocks, collectDataNeeds, collectOrderMetadata, contentPagePath, createStorefront, createStorefrontApp, currentStaticRenderPass, defaultPageBlocks, entityListBlockMeta, establishCustomerSession, exportStore, formatMoney, formatProductOptionSelection, getCartLineAnnotations, getCheckoutFieldContributions, getCustomerAuthAdapter, getProductCardBadges, getProductOptionGroups, hydrateStoreDocument, hydratedEntriesFor, initStorefrontRuntime, isJsonValue, isKnownPath, isNestedBlockRef, isPublicPath, isRecord, listSlots, matchContentPage, matchPath, matchesFacets, mergeCollectionFacet, mergePageTheme, navigateTo, nextMilestone, normalizeProductOptionSelection, orderGalleryImages, overrideComponent, overrideProps, pageSeo, pageThemeSelector, pickProductCardImage, placeStorefrontOrder, prefersReducedMotion, primaryImageUrl, productCardComponentContract, productJsonLd, productOptionSelectionKey, productPlaceholder, productSeo, readHydratedPageData, registerStorefrontBlocks, registerStorefrontEntities, registerStorefrontExtensionPoints, registerStorefrontLoaders, resetPageDiagnostics, resetProductOptionsDeprecationWarnings, resetStorefrontExtensionPoints, resolveAuthAdapter, resolveConfig, resolveDataParams, resolveOverride, resolvePageData, resolvePageDataNeed, resolvePaymentTerms, resolveStorefrontProvider, resolveStorefrontRoute, resolveVariantForSelection, roundCents, routePathParams, runBeforePlaceOrderValve, sectionToJsonSchema, sectionsToBlocks, selectCount, selectDiscountTotal, selectRequiresShipping, selectShipping, selectSubtotal, selectTotal, selectionPrice, serializePageData, shellRouterAdapter, signInByEmail, signOutCustomer, signUpCustomer, slugifyStoreName, storefrontComponentContracts, storefrontPaths, themeToCss, toBlockNodes, toFacetMap, useBlockData, useCartStore, useCatalogStore, useCategories, useDeliveryStore, useDiscountValidator, useEnquiry, useHashPath, useInView, useLoader, useMyOrders, useNavigate, usePopOnChange, useProduct, useProducts, useRoutePath, useRouterAdapter, useScrollToTopOnNavigate, useScrolled, useSessionStore, useSlotContext, useStorefrontActions, useStorefrontConfig, useStorefrontConfigOptional, useStorefrontHead, useStorefrontPage, useStorefrontPageOptional, usedSlots, validateStore, withFacet, withStaticRenderPass };
11126
- //# sourceMappingURL=chunk-GS7TDBF5.js.map
11127
- //# sourceMappingURL=chunk-GS7TDBF5.js.map
11664
+ //# sourceMappingURL=chunk-STL6MATQ.js.map
11665
+ //# sourceMappingURL=chunk-STL6MATQ.js.map