@fayz-ai/storefront 0.83.1 → 0.84.1
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/CheckoutPage-DDY4WLFJ.js +12 -0
- package/dist/CheckoutPage-DDY4WLFJ.js.map +1 -0
- package/dist/MyPurchasesPage-IHCB5QRR.js +11 -0
- package/dist/MyPurchasesPage-IHCB5QRR.js.map +1 -0
- package/dist/OrderConfirmationPage-SAKAGOCC.js +10 -0
- package/dist/OrderConfirmationPage-SAKAGOCC.js.map +1 -0
- package/dist/app/createStorefrontApp.d.ts.map +1 -1
- package/dist/chunk-45R7CIEA.js +234 -0
- package/dist/chunk-45R7CIEA.js.map +1 -0
- package/dist/chunk-BKO2AQ3Q.js +329 -0
- package/dist/chunk-BKO2AQ3Q.js.map +1 -0
- package/dist/chunk-CV6RYJIC.js +454 -0
- package/dist/chunk-CV6RYJIC.js.map +1 -0
- package/dist/chunk-D5C5U2SG.js +267 -0
- package/dist/chunk-D5C5U2SG.js.map +1 -0
- package/dist/chunk-DRMDBXMT.js +2123 -0
- package/dist/chunk-DRMDBXMT.js.map +1 -0
- package/dist/chunk-DVQHKNNX.js +29 -0
- package/dist/chunk-DVQHKNNX.js.map +1 -0
- package/dist/chunk-FCPOPWG3.js +31 -0
- package/dist/chunk-FCPOPWG3.js.map +1 -0
- package/dist/chunk-GQ3TCJVX.js +216 -0
- package/dist/chunk-GQ3TCJVX.js.map +1 -0
- package/dist/{chunk-7N5HKQBM.js → chunk-GVZ2SAOI.js} +5 -552
- package/dist/chunk-GVZ2SAOI.js.map +1 -0
- package/dist/{chunk-G2R6OJQA.js → chunk-H6IOPA3J.js} +5 -4
- package/dist/chunk-H6IOPA3J.js.map +1 -0
- package/dist/{chunk-OV6DVQMB.js → chunk-HRLF5NYO.js} +1202 -6512
- package/dist/chunk-HRLF5NYO.js.map +1 -0
- package/dist/chunk-K27LYMSF.js +435 -0
- package/dist/chunk-K27LYMSF.js.map +1 -0
- package/dist/chunk-MFAU3AWK.js +817 -0
- package/dist/chunk-MFAU3AWK.js.map +1 -0
- package/dist/chunk-OHWX6BEY.js +436 -0
- package/dist/chunk-OHWX6BEY.js.map +1 -0
- package/dist/chunk-PATS4DIF.js +484 -0
- package/dist/chunk-PATS4DIF.js.map +1 -0
- package/dist/chunk-WDQFNHYO.js +41 -0
- package/dist/chunk-WDQFNHYO.js.map +1 -0
- package/dist/chunk-WPWNYBNG.js +68 -0
- package/dist/chunk-WPWNYBNG.js.map +1 -0
- package/dist/chunk-WXXYJZE6.js +99 -0
- package/dist/chunk-WXXYJZE6.js.map +1 -0
- package/dist/components/delivery/StoreCover.d.ts +3 -1
- package/dist/components/delivery/StoreCover.d.ts.map +1 -1
- package/dist/extend/slots.d.ts +48 -0
- package/dist/extend/slots.d.ts.map +1 -1
- package/dist/host.js +4 -2
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -6
- package/dist/index.js.map +1 -1
- package/dist/render.js +11 -3
- package/dist/render.js.map +1 -1
- package/dist/tanstack.js +10 -2
- package/dist/tanstack.js.map +1 -1
- package/dist/views/MenuPage.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/chunk-7N5HKQBM.js.map +0 -1
- package/dist/chunk-G2R6OJQA.js.map +0 -1
- package/dist/chunk-OV6DVQMB.js.map +0 -1
|
@@ -0,0 +1,2123 @@
|
|
|
1
|
+
import { SuccessMark } from './chunk-WDQFNHYO.js';
|
|
2
|
+
import { rememberLocalOrder, useMyOrders } from './chunk-WPWNYBNG.js';
|
|
3
|
+
import { SkeletonGroup, Skeleton, PulseDot } from './chunk-FCPOPWG3.js';
|
|
4
|
+
import { runBeforePlaceOrderValve, collectOrderMetadata, useT, useBuyerCountry, useDiscountValidator, trackStorefront, identifyStorefrontBuyer, Slot, SignInModal, returnPath, toast } from './chunk-MFAU3AWK.js';
|
|
5
|
+
import { prefersReducedMotion } from './chunk-WXXYJZE6.js';
|
|
6
|
+
import { selectShipping, useCartStore, useDeliveryStore, selectSubtotal, selectDiscountTotal, selectTotal, selectRequiresShipping } from './chunk-K27LYMSF.js';
|
|
7
|
+
import { TID, establishCustomerSession, useSessionStore, useStorefrontHead, signOutCustomer, SmoothImage, formatMoney, hasCustomerAuth, relinkCustomer } from './chunk-PATS4DIF.js';
|
|
8
|
+
import { useStorefrontConfig, navigateTo, Link } from './chunk-GVZ2SAOI.js';
|
|
9
|
+
import React4, { useState, useEffect, useRef, useCallback } from 'react';
|
|
10
|
+
import { MapPin, Lock, CreditCard, ShieldCheck, Check, QrCode, Copy } from 'lucide-react';
|
|
11
|
+
import { getShopProvider } from '@fayz-ai/shop/runtime';
|
|
12
|
+
import { documentDigits, isChargeableProvider, supportsCard, formatDocument, isValidDocument, gatewayLabel } from '@fayz-ai/shop';
|
|
13
|
+
import { getCountrySchema, countryName, formatPostalCode, normalizePostalCode, lookupPostalCode, describeError, validateAddress } from '@fayz-ai/core';
|
|
14
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
15
|
+
|
|
16
|
+
// src/lib/errors.ts
|
|
17
|
+
function errorMessage(err) {
|
|
18
|
+
if (err instanceof Error) return err.message;
|
|
19
|
+
if (err && typeof err === "object" && typeof err.message === "string") {
|
|
20
|
+
return err.message;
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/workflows/checkout.ts
|
|
26
|
+
function formatDeliveryNotes(address, shippingAddress) {
|
|
27
|
+
if (address?.notes?.trim()) return address.notes.trim();
|
|
28
|
+
if (shippingAddress) return void 0;
|
|
29
|
+
if (!address) return void 0;
|
|
30
|
+
const parts = [address.street, address.city, address.zip].map((part) => part?.trim()).filter(Boolean);
|
|
31
|
+
return parts.length > 0 ? `Entrega: ${parts.join(", ")}` : void 0;
|
|
32
|
+
}
|
|
33
|
+
function isStaleCustomerError(err) {
|
|
34
|
+
const message = errorMessage(err).toLowerCase();
|
|
35
|
+
return message.includes("customer email mismatch") || message.includes("customer does not belong to this store") || message.includes("customer belongs to another account");
|
|
36
|
+
}
|
|
37
|
+
async function placeStorefrontOrder({
|
|
38
|
+
config,
|
|
39
|
+
cart,
|
|
40
|
+
session,
|
|
41
|
+
customer,
|
|
42
|
+
address,
|
|
43
|
+
shippingAddress,
|
|
44
|
+
fulfillmentType,
|
|
45
|
+
scheduledFor,
|
|
46
|
+
paymentMethod,
|
|
47
|
+
markPaid = false
|
|
48
|
+
}) {
|
|
49
|
+
const email = customer.email.trim().toLowerCase();
|
|
50
|
+
const name = customer.name.trim();
|
|
51
|
+
const provider = getShopProvider();
|
|
52
|
+
let customerId = session?.customerId ?? null;
|
|
53
|
+
if (!customerId || session?.email?.trim().toLowerCase() !== email) {
|
|
54
|
+
const established = await establishCustomerSession(email, { name });
|
|
55
|
+
customerId = established.customerId;
|
|
56
|
+
}
|
|
57
|
+
const notes = formatDeliveryNotes(address, shippingAddress);
|
|
58
|
+
const items = cart.lines.map((line) => ({
|
|
59
|
+
productId: line.productId,
|
|
60
|
+
// The variant the shopper picked (spine M5): the server prices the line
|
|
61
|
+
// from it and decrements ITS stock, so two sizes never share a counter.
|
|
62
|
+
variantId: line.variantId || void 0,
|
|
63
|
+
quantity: line.quantity,
|
|
64
|
+
optionsLabel: line.optionsLabel || void 0,
|
|
65
|
+
// A observação da linha (0094). Vai por item porque é por item: duas
|
|
66
|
+
// unidades do mesmo prato com pedidos diferentes são duas linhas.
|
|
67
|
+
notes: line.notes || void 0
|
|
68
|
+
}));
|
|
69
|
+
const shippingTotal = fulfillmentType === "pickup" ? 0 : selectShipping(cart, config);
|
|
70
|
+
const replaced = await runBeforePlaceOrderValve({
|
|
71
|
+
items,
|
|
72
|
+
customer: { name, email },
|
|
73
|
+
currency: config.currency,
|
|
74
|
+
discountCode: cart.discountCode ?? void 0,
|
|
75
|
+
shippingTotal,
|
|
76
|
+
notes,
|
|
77
|
+
customerId: customerId ?? void 0
|
|
78
|
+
});
|
|
79
|
+
if (replaced) return { order: replaced, customerId: customerId ?? replaced.customerId ?? "" };
|
|
80
|
+
const contributed = await collectOrderMetadata({
|
|
81
|
+
items: cart.lines.map((line) => ({
|
|
82
|
+
productId: line.productId,
|
|
83
|
+
quantity: line.quantity,
|
|
84
|
+
unitPrice: line.unitPrice
|
|
85
|
+
})),
|
|
86
|
+
subtotal: cart.lines.reduce((sum, line) => sum + line.unitPrice * line.quantity, 0),
|
|
87
|
+
customerEmail: email
|
|
88
|
+
});
|
|
89
|
+
const metadata = config.market ? { ...contributed ?? {}, market: config.market.code } : contributed;
|
|
90
|
+
const place = (withCustomerId) => provider.placeOrder({
|
|
91
|
+
customerId: withCustomerId,
|
|
92
|
+
customer: { name, email },
|
|
93
|
+
currency: config.currency,
|
|
94
|
+
notes,
|
|
95
|
+
discountCode: cart.discountCode ?? void 0,
|
|
96
|
+
shippingTotal,
|
|
97
|
+
// Structured address + method are what populate public.addresses and the
|
|
98
|
+
// transactions ledger. `notes` stays for backwards compatibility with
|
|
99
|
+
// checkouts that have not been updated yet.
|
|
100
|
+
shippingAddress,
|
|
101
|
+
paymentMethod,
|
|
102
|
+
metadata,
|
|
103
|
+
fulfillmentType,
|
|
104
|
+
scheduledFor,
|
|
105
|
+
items
|
|
106
|
+
});
|
|
107
|
+
let order;
|
|
108
|
+
try {
|
|
109
|
+
order = await place(customerId ?? void 0);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (!customerId || !isStaleCustomerError(err)) throw err;
|
|
112
|
+
customerId = null;
|
|
113
|
+
order = await place(void 0);
|
|
114
|
+
}
|
|
115
|
+
if (markPaid) {
|
|
116
|
+
if (provider.confirmPayment) await provider.confirmPayment(order.id);
|
|
117
|
+
else await provider.updateOrder(order.id, { financialStatus: "paid" });
|
|
118
|
+
}
|
|
119
|
+
rememberLocalOrder(order.id);
|
|
120
|
+
return { order, customerId: customerId ?? order.customerId ?? "" };
|
|
121
|
+
}
|
|
122
|
+
var LABELS = {
|
|
123
|
+
1: "checkout.steps.1",
|
|
124
|
+
2: "checkout.steps.2",
|
|
125
|
+
3: "checkout.steps.3",
|
|
126
|
+
4: "checkout.steps.4"
|
|
127
|
+
};
|
|
128
|
+
function CheckoutSteps({ current, onGoBackTo, omit }) {
|
|
129
|
+
const t = useT();
|
|
130
|
+
const steps = [1, 2, 3, 4].filter((s) => !omit?.includes(s));
|
|
131
|
+
return /* @__PURE__ */ jsxs("nav", { "data-testid": TID.checkoutSteps, "aria-label": t("checkout.steps.label"), className: "mb-8", children: [
|
|
132
|
+
/* @__PURE__ */ jsx("ol", { className: "flex items-center gap-1.5 sm:gap-2", children: steps.map((step, index) => {
|
|
133
|
+
const done = step < current;
|
|
134
|
+
const active = step === current;
|
|
135
|
+
const canGoBack = done && !!onGoBackTo;
|
|
136
|
+
const Tag = canGoBack ? "button" : "div";
|
|
137
|
+
return (
|
|
138
|
+
// `flex-1` só em quem TEM traço. O último passo não tem, e com
|
|
139
|
+
// flex-1 ele reservava uma fatia inteira para desenhar apenas a
|
|
140
|
+
// bolinha — que então parava no começo da fatia em vez do fim, e a
|
|
141
|
+
// régua saía torta. Sem flex-1, o traço absorve a sobra e as
|
|
142
|
+
// bolinhas encostam nas pontas.
|
|
143
|
+
/* @__PURE__ */ jsxs(
|
|
144
|
+
"li",
|
|
145
|
+
{
|
|
146
|
+
className: `flex min-w-0 items-center gap-1.5 sm:gap-2 ${index < steps.length - 1 ? "flex-1" : ""}`,
|
|
147
|
+
children: [
|
|
148
|
+
/* @__PURE__ */ jsxs(
|
|
149
|
+
Tag,
|
|
150
|
+
{
|
|
151
|
+
...canGoBack ? { type: "button", onClick: () => onGoBackTo(step) } : {},
|
|
152
|
+
"data-testid": TID.checkoutStep(step),
|
|
153
|
+
"data-state": done ? "done" : active ? "current" : "pending",
|
|
154
|
+
"aria-current": active ? "step" : void 0,
|
|
155
|
+
className: [
|
|
156
|
+
"flex min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-left transition",
|
|
157
|
+
canGoBack ? "hover:bg-muted" : "",
|
|
158
|
+
active ? "text-foreground" : done ? "text-foreground" : "text-muted-foreground"
|
|
159
|
+
].join(" "),
|
|
160
|
+
children: [
|
|
161
|
+
/* @__PURE__ */ jsx(
|
|
162
|
+
"span",
|
|
163
|
+
{
|
|
164
|
+
className: [
|
|
165
|
+
"grid h-7 w-7 shrink-0 place-items-center rounded-full border text-xs font-semibold",
|
|
166
|
+
done ? "border-primary bg-primary text-primary-foreground" : active ? "border-primary bg-background text-primary" : "border-muted-foreground/30 bg-background"
|
|
167
|
+
].join(" "),
|
|
168
|
+
children: done ? /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) : index + 1
|
|
169
|
+
}
|
|
170
|
+
),
|
|
171
|
+
/* @__PURE__ */ jsx("span", { className: `hidden truncate text-sm sm:inline ${active ? "font-semibold" : ""}`, children: t(LABELS[step]) })
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
index < steps.length - 1 && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: `h-px flex-1 ${done ? "bg-primary" : "bg-border"}` })
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
step
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
}) }),
|
|
182
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-2 text-sm font-semibold sm:hidden", children: [
|
|
183
|
+
steps.indexOf(current) + 1,
|
|
184
|
+
". ",
|
|
185
|
+
t(LABELS[current])
|
|
186
|
+
] })
|
|
187
|
+
] });
|
|
188
|
+
}
|
|
189
|
+
var SDK_URL = "https://sdk.mercadopago.com/js/v2";
|
|
190
|
+
var CONTAINER_ID = "fayz-mp-card-brick";
|
|
191
|
+
var DEVICE_SCRIPT_URL = "https://www.mercadopago.com/v2/security.js";
|
|
192
|
+
var DEVICE_VIEW = "checkout";
|
|
193
|
+
function loadDeviceScript() {
|
|
194
|
+
if (typeof document === "undefined") return;
|
|
195
|
+
if (document.querySelector(`script[src="${DEVICE_SCRIPT_URL}"]`)) return;
|
|
196
|
+
const script = document.createElement("script");
|
|
197
|
+
script.src = DEVICE_SCRIPT_URL;
|
|
198
|
+
script.setAttribute("view", DEVICE_VIEW);
|
|
199
|
+
script.async = true;
|
|
200
|
+
document.body.appendChild(script);
|
|
201
|
+
}
|
|
202
|
+
function deviceSessionId() {
|
|
203
|
+
if (typeof window === "undefined") return null;
|
|
204
|
+
const id = window.MP_DEVICE_SESSION_ID;
|
|
205
|
+
return typeof id === "string" && id.trim() ? id.trim() : null;
|
|
206
|
+
}
|
|
207
|
+
var sdkPromise = null;
|
|
208
|
+
function loadSdk() {
|
|
209
|
+
if (typeof window === "undefined") return Promise.reject(new Error("no window"));
|
|
210
|
+
if (window.MercadoPago) return Promise.resolve();
|
|
211
|
+
if (sdkPromise) return sdkPromise;
|
|
212
|
+
sdkPromise = new Promise((resolve, reject) => {
|
|
213
|
+
const existing = document.querySelector(`script[src="${SDK_URL}"]`);
|
|
214
|
+
const script = existing ?? document.createElement("script");
|
|
215
|
+
script.src = SDK_URL;
|
|
216
|
+
script.async = true;
|
|
217
|
+
script.addEventListener("load", () => resolve());
|
|
218
|
+
script.addEventListener("error", () => {
|
|
219
|
+
sdkPromise = null;
|
|
220
|
+
reject(new Error("N\xE3o conseguimos carregar o pagamento com cart\xE3o."));
|
|
221
|
+
});
|
|
222
|
+
if (!existing) document.body.appendChild(script);
|
|
223
|
+
});
|
|
224
|
+
return sdkPromise;
|
|
225
|
+
}
|
|
226
|
+
function CardBrick({ publicKey, amount, email, onSubmit, onError }) {
|
|
227
|
+
const [ready, setReady] = useState(false);
|
|
228
|
+
const [failed, setFailed] = useState(null);
|
|
229
|
+
const mounted = useRef(false);
|
|
230
|
+
const controller = useRef(null);
|
|
231
|
+
const submitRef = useRef(onSubmit);
|
|
232
|
+
submitRef.current = onSubmit;
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
if (mounted.current) return;
|
|
235
|
+
mounted.current = true;
|
|
236
|
+
let cancelled = false;
|
|
237
|
+
loadDeviceScript();
|
|
238
|
+
void (async () => {
|
|
239
|
+
try {
|
|
240
|
+
await loadSdk();
|
|
241
|
+
if (cancelled || !window.MercadoPago) return;
|
|
242
|
+
const mp = new window.MercadoPago(publicKey, { locale: "pt-BR" });
|
|
243
|
+
const builder = mp.bricks();
|
|
244
|
+
controller.current = await builder.create("cardPayment", CONTAINER_ID, {
|
|
245
|
+
initialization: {
|
|
246
|
+
amount,
|
|
247
|
+
...email ? { payer: { email } } : {}
|
|
248
|
+
},
|
|
249
|
+
customization: {
|
|
250
|
+
visual: { style: { theme: "default" } },
|
|
251
|
+
paymentMethods: { maxInstallments: 12 }
|
|
252
|
+
},
|
|
253
|
+
callbacks: {
|
|
254
|
+
onReady: () => {
|
|
255
|
+
if (!cancelled) setReady(true);
|
|
256
|
+
},
|
|
257
|
+
onSubmit: (formData) => (
|
|
258
|
+
// Their contract: resolve when the payment is handled, reject to
|
|
259
|
+
// keep the form up. Rejecting is what lets a declined card be
|
|
260
|
+
// corrected in place instead of re-typed from scratch.
|
|
261
|
+
submitRef.current({
|
|
262
|
+
token: String(formData?.token ?? ""),
|
|
263
|
+
paymentMethodId: formData?.payment_method_id ?? null,
|
|
264
|
+
issuerId: formData?.issuer_id == null ? null : String(formData.issuer_id),
|
|
265
|
+
installments: Number(formData?.installments) || 1,
|
|
266
|
+
email: formData?.payer?.email ?? null,
|
|
267
|
+
document: formData?.payer?.identification?.number ? {
|
|
268
|
+
type: String(formData.payer.identification.type ?? "CPF"),
|
|
269
|
+
number: String(formData.payer.identification.number)
|
|
270
|
+
} : null
|
|
271
|
+
})
|
|
272
|
+
),
|
|
273
|
+
onError: (brickError) => {
|
|
274
|
+
const message = brickError?.message ?? "N\xE3o conseguimos carregar o formul\xE1rio do cart\xE3o.";
|
|
275
|
+
if (!cancelled) onError?.(message);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
} catch (err) {
|
|
280
|
+
if (cancelled) return;
|
|
281
|
+
const message = describeError(err, "N\xE3o conseguimos carregar o pagamento com cart\xE3o.");
|
|
282
|
+
setFailed(message);
|
|
283
|
+
onError?.(message);
|
|
284
|
+
}
|
|
285
|
+
})();
|
|
286
|
+
return () => {
|
|
287
|
+
cancelled = true;
|
|
288
|
+
try {
|
|
289
|
+
controller.current?.unmount();
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
controller.current = null;
|
|
293
|
+
mounted.current = false;
|
|
294
|
+
};
|
|
295
|
+
}, []);
|
|
296
|
+
if (failed) {
|
|
297
|
+
return /* @__PURE__ */ jsx("p", { className: "rounded-lg border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive", children: failed });
|
|
298
|
+
}
|
|
299
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
300
|
+
!ready && /* @__PURE__ */ jsxs(SkeletonGroup, { label: "Carregando o formul\xE1rio do cart\xE3o", className: "space-y-3", children: [
|
|
301
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full rounded-lg" }),
|
|
302
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
303
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full rounded-lg" }),
|
|
304
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full rounded-lg" })
|
|
305
|
+
] }),
|
|
306
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-full rounded-lg" })
|
|
307
|
+
] }),
|
|
308
|
+
/* @__PURE__ */ jsx("div", { id: CONTAINER_ID, className: ready ? "" : "hidden" })
|
|
309
|
+
] });
|
|
310
|
+
}
|
|
311
|
+
var POLL_MS = 3e3;
|
|
312
|
+
var RESUME_KEY = "fz.checkout.resume";
|
|
313
|
+
function useExpiry(expiresAt) {
|
|
314
|
+
const target = React4.useMemo(() => {
|
|
315
|
+
if (!expiresAt) return null;
|
|
316
|
+
const t = new Date(expiresAt).getTime();
|
|
317
|
+
return Number.isFinite(t) ? t : null;
|
|
318
|
+
}, [expiresAt]);
|
|
319
|
+
const [now, setNow] = React4.useState(() => Date.now());
|
|
320
|
+
React4.useEffect(() => {
|
|
321
|
+
if (target === null) return;
|
|
322
|
+
const id = setInterval(() => setNow(Date.now()), 1e3);
|
|
323
|
+
return () => clearInterval(id);
|
|
324
|
+
}, [target]);
|
|
325
|
+
if (target === null) return { secondsLeft: null, expired: false };
|
|
326
|
+
const secondsLeft = Math.max(0, Math.round((target - now) / 1e3));
|
|
327
|
+
return { secondsLeft, expired: secondsLeft === 0 };
|
|
328
|
+
}
|
|
329
|
+
function formatCountdown(seconds) {
|
|
330
|
+
const m = Math.floor(seconds / 60);
|
|
331
|
+
const s = seconds % 60;
|
|
332
|
+
return `${m}:${String(s).padStart(2, "0")}`;
|
|
333
|
+
}
|
|
334
|
+
async function copyText(text) {
|
|
335
|
+
try {
|
|
336
|
+
if (navigator.clipboard?.writeText) {
|
|
337
|
+
await navigator.clipboard.writeText(text);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const area = document.createElement("textarea");
|
|
344
|
+
area.value = text;
|
|
345
|
+
area.setAttribute("readonly", "");
|
|
346
|
+
area.style.position = "fixed";
|
|
347
|
+
area.style.top = "-1000px";
|
|
348
|
+
area.style.opacity = "0";
|
|
349
|
+
document.body.appendChild(area);
|
|
350
|
+
area.select();
|
|
351
|
+
area.setSelectionRange(0, text.length);
|
|
352
|
+
const ok = document.execCommand("copy");
|
|
353
|
+
document.body.removeChild(area);
|
|
354
|
+
return ok;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function rememberSession(sessionId, provider) {
|
|
360
|
+
try {
|
|
361
|
+
sessionStorage.setItem(RESUME_KEY, JSON.stringify({ sessionId, provider }));
|
|
362
|
+
} catch {
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function recallSessionProvider(sessionId) {
|
|
366
|
+
try {
|
|
367
|
+
const raw = JSON.parse(sessionStorage.getItem(RESUME_KEY) ?? "null");
|
|
368
|
+
return raw?.sessionId === sessionId ? raw.provider ?? null : null;
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function CheckoutPayment({ input, onPaid, onBack, money, onUnavailable, resume, amount }) {
|
|
374
|
+
const [phase, setPhase] = useState("opening");
|
|
375
|
+
const [charge, setCharge] = useState(null);
|
|
376
|
+
const expiry = useExpiry(charge?.expiresAt);
|
|
377
|
+
const [total, setTotal] = useState(null);
|
|
378
|
+
const [redirectUrl, setRedirectUrl] = useState(null);
|
|
379
|
+
const [brickKey, setBrickKey] = useState(null);
|
|
380
|
+
const [sessionId, setSessionId] = useState(resume?.sessionId ?? null);
|
|
381
|
+
const [provider, setProvider] = useState(resume?.provider ?? null);
|
|
382
|
+
const [error, setError] = useState(null);
|
|
383
|
+
const [copied, setCopied] = useState(false);
|
|
384
|
+
const [checking, setChecking] = useState(false);
|
|
385
|
+
const settled = useRef(false);
|
|
386
|
+
const opened = useRef(false);
|
|
387
|
+
const open = useCallback(async () => {
|
|
388
|
+
const shop = getShopProvider();
|
|
389
|
+
if (!shop.openCheckoutSession) {
|
|
390
|
+
setError("Esta loja ainda n\xE3o recebe pagamento online.");
|
|
391
|
+
setPhase("error");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
setPhase("opening");
|
|
395
|
+
setError(null);
|
|
396
|
+
try {
|
|
397
|
+
if (input.paymentMethod === "credit_card" && shop.paymentPublicKey) {
|
|
398
|
+
const providerSlug = input.provider ?? null;
|
|
399
|
+
const publicKey = providerSlug ? await shop.paymentPublicKey(providerSlug) : null;
|
|
400
|
+
if (publicKey) {
|
|
401
|
+
setProvider(providerSlug);
|
|
402
|
+
setBrickKey(publicKey);
|
|
403
|
+
setPhase("card");
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const session = await shop.openCheckoutSession({
|
|
408
|
+
...input,
|
|
409
|
+
// Para onde o adquirente devolve o comprador. É esta página, com o id da
|
|
410
|
+
// sessão pendurado: quem volta volta em página nova, sem estado nenhum.
|
|
411
|
+
returnUrl: typeof window === "undefined" ? null : window.location.href.split("?")[0]
|
|
412
|
+
});
|
|
413
|
+
setSessionId(session.sessionId);
|
|
414
|
+
setProvider(session.provider ?? input.provider ?? null);
|
|
415
|
+
setTotal(session.total);
|
|
416
|
+
setCharge(session.charge);
|
|
417
|
+
if (session.paymentUrl && !session.charge) {
|
|
418
|
+
setRedirectUrl(session.paymentUrl);
|
|
419
|
+
setPhase("redirect");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
setPhase(session.charge ? "waiting" : "error");
|
|
423
|
+
if (!session.charge) setError("N\xE3o conseguimos abrir a cobran\xE7a agora.");
|
|
424
|
+
} catch (err) {
|
|
425
|
+
const status = err?.status;
|
|
426
|
+
const code = err?.code;
|
|
427
|
+
if (status === 404 || code === "method_unavailable") {
|
|
428
|
+
onUnavailable?.();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
setError(describeError(err, "N\xE3o conseguimos abrir a cobran\xE7a agora."));
|
|
432
|
+
setPhase("error");
|
|
433
|
+
}
|
|
434
|
+
}, [input, onUnavailable]);
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
if (opened.current) return;
|
|
437
|
+
opened.current = true;
|
|
438
|
+
if (resume?.sessionId) {
|
|
439
|
+
setPhase("waiting");
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
void open();
|
|
443
|
+
}, [open, resume?.sessionId]);
|
|
444
|
+
const check = useCallback(async () => {
|
|
445
|
+
if (!sessionId || settled.current) return false;
|
|
446
|
+
const shop = getShopProvider();
|
|
447
|
+
if (!shop.settleCheckoutSession) return false;
|
|
448
|
+
try {
|
|
449
|
+
const result = await shop.settleCheckoutSession(sessionId, provider, resume?.chargeToken ?? null);
|
|
450
|
+
if (settled.current) return false;
|
|
451
|
+
if (result.state === "paid" && result.orderId) {
|
|
452
|
+
settled.current = true;
|
|
453
|
+
setPhase("settling");
|
|
454
|
+
onPaid(result.orderId);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
if (result.state === "failed" || result.state === "expired") {
|
|
458
|
+
setError(
|
|
459
|
+
result.reason ?? (result.state === "expired" ? "O c\xF3digo expirou. Gere um novo para continuar." : "O pagamento n\xE3o foi conclu\xEDdo.")
|
|
460
|
+
);
|
|
461
|
+
setPhase("error");
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
} catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}, [sessionId, provider, resume?.chargeToken, onPaid]);
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
if (phase !== "waiting" || !sessionId) return;
|
|
470
|
+
let stopped = false;
|
|
471
|
+
const timer = setInterval(() => {
|
|
472
|
+
if (!stopped) void check();
|
|
473
|
+
}, POLL_MS);
|
|
474
|
+
return () => {
|
|
475
|
+
stopped = true;
|
|
476
|
+
clearInterval(timer);
|
|
477
|
+
};
|
|
478
|
+
}, [phase, sessionId, check]);
|
|
479
|
+
async function confirmPaid() {
|
|
480
|
+
setChecking(true);
|
|
481
|
+
const paid = await check();
|
|
482
|
+
if (!paid) setChecking(false);
|
|
483
|
+
}
|
|
484
|
+
async function copy() {
|
|
485
|
+
if (!charge?.emv) return;
|
|
486
|
+
if (await copyText(charge.emv)) {
|
|
487
|
+
setCopied(true);
|
|
488
|
+
setTimeout(() => setCopied(false), 2400);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
toast.error("Copie o c\xF3digo manualmente");
|
|
492
|
+
}
|
|
493
|
+
if (phase === "card" && brickKey) {
|
|
494
|
+
const acquirer = gatewayLabel(provider);
|
|
495
|
+
return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-4 rounded-xl border bg-card p-6", children: [
|
|
496
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
497
|
+
/* @__PURE__ */ jsx(CreditCard, { className: "h-5 w-5 text-primary" }),
|
|
498
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Pague com cart\xE3o sem sair da loja" })
|
|
499
|
+
] }),
|
|
500
|
+
/* @__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." }),
|
|
501
|
+
(total ?? amount) != null && /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
|
|
502
|
+
"Valor: ",
|
|
503
|
+
/* @__PURE__ */ jsx("strong", { "data-testid": TID.checkoutPaymentTotal, children: money(total ?? amount) })
|
|
504
|
+
] }),
|
|
505
|
+
/* @__PURE__ */ jsx(
|
|
506
|
+
CardBrick,
|
|
507
|
+
{
|
|
508
|
+
publicKey: brickKey,
|
|
509
|
+
amount: total ?? amount ?? 0,
|
|
510
|
+
email: input.customer?.email ?? null,
|
|
511
|
+
onError: (message) => setError(message),
|
|
512
|
+
onSubmit: async (result) => {
|
|
513
|
+
const shop = getShopProvider();
|
|
514
|
+
if (!shop.openCheckoutSession) throw new Error("sem gateway");
|
|
515
|
+
try {
|
|
516
|
+
const session = await shop.openCheckoutSession({
|
|
517
|
+
...input,
|
|
518
|
+
cardToken: result.token,
|
|
519
|
+
installments: result.installments,
|
|
520
|
+
paymentMethodId: result.paymentMethodId,
|
|
521
|
+
issuerId: result.issuerId,
|
|
522
|
+
// Their anti-fraud reads this, and a card charged without it is
|
|
523
|
+
// declined more often for no visible reason.
|
|
524
|
+
deviceId: deviceSessionId()
|
|
525
|
+
});
|
|
526
|
+
setSessionId(session.sessionId);
|
|
527
|
+
setProvider(session.provider ?? input.provider ?? null);
|
|
528
|
+
setTotal(session.total);
|
|
529
|
+
const orderId = session.orderId ?? null;
|
|
530
|
+
if (orderId) {
|
|
531
|
+
settled.current = true;
|
|
532
|
+
setPhase("settling");
|
|
533
|
+
onPaid(orderId);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
setPhase("waiting");
|
|
537
|
+
} catch (err) {
|
|
538
|
+
const message = describeError(err, "O pagamento n\xE3o foi aprovado.");
|
|
539
|
+
setError(message);
|
|
540
|
+
throw err;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
),
|
|
545
|
+
error && /* @__PURE__ */ jsx("p", { className: "rounded-lg border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive", children: error }),
|
|
546
|
+
/* @__PURE__ */ jsx(
|
|
547
|
+
"button",
|
|
548
|
+
{
|
|
549
|
+
type: "button",
|
|
550
|
+
onClick: onBack,
|
|
551
|
+
className: "inline-flex min-h-11 items-center rounded-lg border px-4 py-2.5 text-sm font-medium transition hover:bg-muted",
|
|
552
|
+
children: "Voltar"
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
] });
|
|
556
|
+
}
|
|
557
|
+
if (phase === "redirect" && redirectUrl) {
|
|
558
|
+
const acquirer = gatewayLabel(provider);
|
|
559
|
+
return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-4 rounded-xl border bg-card p-6", children: [
|
|
560
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
561
|
+
/* @__PURE__ */ jsx(CreditCard, { className: "h-5 w-5 text-primary" }),
|
|
562
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: acquirer ? `Pague com cart\xE3o na p\xE1gina da ${acquirer}` : "Pague com cart\xE3o" })
|
|
563
|
+
] }),
|
|
564
|
+
/* @__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." }),
|
|
565
|
+
total != null && /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
|
|
566
|
+
"Valor: ",
|
|
567
|
+
/* @__PURE__ */ jsx("strong", { "data-testid": TID.checkoutPaymentTotal, children: money(total) })
|
|
568
|
+
] }),
|
|
569
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
570
|
+
/* @__PURE__ */ jsxs(
|
|
571
|
+
"button",
|
|
572
|
+
{
|
|
573
|
+
type: "button",
|
|
574
|
+
onClick: () => {
|
|
575
|
+
if (sessionId) rememberSession(sessionId, provider);
|
|
576
|
+
window.location.assign(redirectUrl);
|
|
577
|
+
},
|
|
578
|
+
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",
|
|
579
|
+
children: [
|
|
580
|
+
/* @__PURE__ */ jsx(Lock, { className: "h-4 w-4" }),
|
|
581
|
+
acquirer ? `Pagar com ${acquirer}` : "Pagar com cart\xE3o"
|
|
582
|
+
]
|
|
583
|
+
}
|
|
584
|
+
),
|
|
585
|
+
/* @__PURE__ */ jsx(
|
|
586
|
+
"button",
|
|
587
|
+
{
|
|
588
|
+
type: "button",
|
|
589
|
+
onClick: onBack,
|
|
590
|
+
className: "inline-flex min-h-11 items-center rounded-lg border px-4 py-2.5 text-sm font-medium transition hover:bg-muted",
|
|
591
|
+
children: "Voltar"
|
|
592
|
+
}
|
|
593
|
+
)
|
|
594
|
+
] })
|
|
595
|
+
] });
|
|
596
|
+
}
|
|
597
|
+
if (phase === "opening") {
|
|
598
|
+
return (
|
|
599
|
+
// Same layout as the charge below, so nothing jumps when it arrives.
|
|
600
|
+
/* @__PURE__ */ jsxs(
|
|
601
|
+
SkeletonGroup,
|
|
602
|
+
{
|
|
603
|
+
label: "Preparando seu pagamento",
|
|
604
|
+
"data-testid": TID.checkoutPayment,
|
|
605
|
+
className: "rounded-xl border bg-card p-6",
|
|
606
|
+
children: [
|
|
607
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
608
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-5 rounded-md" }),
|
|
609
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-5 w-64 max-w-[70%]" })
|
|
610
|
+
] }),
|
|
611
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-2 h-3.5 w-full max-w-sm" }),
|
|
612
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-5 grid gap-5 sm:grid-cols-[auto_minmax(0,1fr)] sm:items-start", children: [
|
|
613
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mx-auto h-44 w-44 rounded-lg" }),
|
|
614
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
615
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-32" }),
|
|
616
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-4 h-3 w-28" }),
|
|
617
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-1 h-16 w-full rounded-lg" }),
|
|
618
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-3", children: [
|
|
619
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-40 rounded-lg" }),
|
|
620
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-11 w-32 rounded-lg" })
|
|
621
|
+
] })
|
|
622
|
+
] })
|
|
623
|
+
] })
|
|
624
|
+
]
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
if (phase === "error") {
|
|
630
|
+
return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-3 rounded-xl border bg-card p-6", children: [
|
|
631
|
+
/* @__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 ?? "N\xE3o conseguimos abrir a cobran\xE7a agora." }),
|
|
632
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
633
|
+
/* @__PURE__ */ jsx(
|
|
634
|
+
"button",
|
|
635
|
+
{
|
|
636
|
+
type: "button",
|
|
637
|
+
onClick: () => {
|
|
638
|
+
opened.current = false;
|
|
639
|
+
void open();
|
|
640
|
+
},
|
|
641
|
+
className: "rounded-lg bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground transition hover:opacity-90",
|
|
642
|
+
children: "Tentar de novo"
|
|
643
|
+
}
|
|
644
|
+
),
|
|
645
|
+
/* @__PURE__ */ jsx(
|
|
646
|
+
"button",
|
|
647
|
+
{
|
|
648
|
+
type: "button",
|
|
649
|
+
onClick: onBack,
|
|
650
|
+
className: "rounded-lg border px-4 py-2.5 text-sm font-medium transition hover:bg-muted",
|
|
651
|
+
children: "Voltar"
|
|
652
|
+
}
|
|
653
|
+
)
|
|
654
|
+
] })
|
|
655
|
+
] });
|
|
656
|
+
}
|
|
657
|
+
if (!charge) {
|
|
658
|
+
return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-3 rounded-xl border bg-card p-6", "aria-live": "polite", children: [
|
|
659
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
660
|
+
/* @__PURE__ */ jsx(PulseDot, {}),
|
|
661
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Confirmando seu pagamento\u2026" })
|
|
662
|
+
] }),
|
|
663
|
+
/* @__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." }),
|
|
664
|
+
/* @__PURE__ */ jsxs(
|
|
665
|
+
"button",
|
|
666
|
+
{
|
|
667
|
+
type: "button",
|
|
668
|
+
onClick: () => {
|
|
669
|
+
void confirmPaid();
|
|
670
|
+
},
|
|
671
|
+
disabled: checking,
|
|
672
|
+
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",
|
|
673
|
+
children: [
|
|
674
|
+
checking ? /* @__PURE__ */ jsx(PulseDot, {}) : /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
|
|
675
|
+
checking ? "Verificando\u2026" : "J\xE1 paguei"
|
|
676
|
+
]
|
|
677
|
+
}
|
|
678
|
+
)
|
|
679
|
+
] });
|
|
680
|
+
}
|
|
681
|
+
return /* @__PURE__ */ jsxs("div", { "data-testid": TID.checkoutPayment, className: "space-y-4", "aria-live": "polite", children: [
|
|
682
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-card p-6", children: [
|
|
683
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
684
|
+
/* @__PURE__ */ jsx(QrCode, { className: "h-5 w-5 text-primary" }),
|
|
685
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Pague com Pix para fechar o pedido" })
|
|
686
|
+
] }),
|
|
687
|
+
/* @__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." }),
|
|
688
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-5 grid gap-5 sm:grid-cols-[auto_minmax(0,1fr)] sm:items-start", children: [
|
|
689
|
+
(charge?.qrcodeBase64 || charge?.qrcodeUrl) && /* @__PURE__ */ jsx(
|
|
690
|
+
"img",
|
|
691
|
+
{
|
|
692
|
+
src: charge.qrcodeBase64 ? `data:image/png;base64,${charge.qrcodeBase64}` : charge.qrcodeUrl,
|
|
693
|
+
alt: "QR code do Pix",
|
|
694
|
+
width: 276,
|
|
695
|
+
height: 276,
|
|
696
|
+
className: "mx-auto box-content h-[276px] w-[276px] max-w-full rounded-lg border bg-white p-4"
|
|
697
|
+
}
|
|
698
|
+
),
|
|
699
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
700
|
+
total != null && /* @__PURE__ */ jsxs("p", { className: "text-sm", children: [
|
|
701
|
+
"Valor: ",
|
|
702
|
+
/* @__PURE__ */ jsx("strong", { "data-testid": TID.checkoutPaymentTotal, children: money(total) })
|
|
703
|
+
] }),
|
|
704
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: "Pix copia e cola" }),
|
|
705
|
+
/* @__PURE__ */ jsx(
|
|
706
|
+
"p",
|
|
707
|
+
{
|
|
708
|
+
"data-testid": TID.pixCode,
|
|
709
|
+
className: "mt-1 max-h-24 overflow-y-auto break-all rounded-lg border bg-muted/40 p-3 font-mono text-xs",
|
|
710
|
+
children: charge?.emv
|
|
711
|
+
}
|
|
712
|
+
),
|
|
713
|
+
expiry.secondsLeft !== null && !expiry.expired && /* @__PURE__ */ jsxs("p", { className: "mt-2 text-xs text-muted-foreground", children: [
|
|
714
|
+
"Este c\xF3digo vale por mais",
|
|
715
|
+
" ",
|
|
716
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium tabular-nums text-foreground", children: formatCountdown(expiry.secondsLeft) })
|
|
717
|
+
] }),
|
|
718
|
+
expiry.expired && /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", children: [
|
|
719
|
+
/* @__PURE__ */ jsx("p", { "data-testid": TID.checkoutExpired, className: "text-sm text-muted-foreground", children: "O prazo deste c\xF3digo acabou. Gere outro para pagar." }),
|
|
720
|
+
/* @__PURE__ */ jsx(
|
|
721
|
+
"button",
|
|
722
|
+
{
|
|
723
|
+
type: "button",
|
|
724
|
+
onClick: () => {
|
|
725
|
+
opened.current = false;
|
|
726
|
+
setCharge(null);
|
|
727
|
+
void open();
|
|
728
|
+
},
|
|
729
|
+
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",
|
|
730
|
+
children: "Gerar novo c\xF3digo"
|
|
731
|
+
}
|
|
732
|
+
)
|
|
733
|
+
] }),
|
|
734
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-3 flex flex-wrap items-center gap-3", hidden: expiry.expired, children: [
|
|
735
|
+
/* @__PURE__ */ jsxs(
|
|
736
|
+
"button",
|
|
737
|
+
{
|
|
738
|
+
type: "button",
|
|
739
|
+
onClick: copy,
|
|
740
|
+
"data-testid": TID.pixCopy,
|
|
741
|
+
className: "inline-flex min-h-11 items-center gap-2 rounded-lg bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground transition hover:opacity-90",
|
|
742
|
+
children: [
|
|
743
|
+
copied ? /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(Copy, { className: "h-4 w-4" }),
|
|
744
|
+
copied ? "C\xF3digo copiado" : "Copiar c\xF3digo"
|
|
745
|
+
]
|
|
746
|
+
}
|
|
747
|
+
),
|
|
748
|
+
/* @__PURE__ */ jsxs(
|
|
749
|
+
"button",
|
|
750
|
+
{
|
|
751
|
+
type: "button",
|
|
752
|
+
onClick: () => {
|
|
753
|
+
void confirmPaid();
|
|
754
|
+
},
|
|
755
|
+
disabled: checking || phase === "settling",
|
|
756
|
+
"data-testid": TID.pixConfirm,
|
|
757
|
+
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",
|
|
758
|
+
children: [
|
|
759
|
+
checking || phase === "settling" ? /* @__PURE__ */ jsx(PulseDot, {}) : /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }),
|
|
760
|
+
phase === "settling" ? "Confirmando\u2026" : checking ? "Verificando\u2026" : "J\xE1 paguei"
|
|
761
|
+
]
|
|
762
|
+
}
|
|
763
|
+
)
|
|
764
|
+
] })
|
|
765
|
+
] })
|
|
766
|
+
] })
|
|
767
|
+
] }),
|
|
768
|
+
/* @__PURE__ */ jsx(
|
|
769
|
+
"button",
|
|
770
|
+
{
|
|
771
|
+
type: "button",
|
|
772
|
+
onClick: onBack,
|
|
773
|
+
className: "text-sm text-muted-foreground underline hover:text-foreground",
|
|
774
|
+
children: "Voltar e revisar o pedido"
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
] });
|
|
778
|
+
}
|
|
779
|
+
function useKnownCustomer() {
|
|
780
|
+
const customerId = useSessionStore((s) => s.customerId);
|
|
781
|
+
const { orders, loading: ordersLoading } = useMyOrders();
|
|
782
|
+
const [contact, setContact] = useState(null);
|
|
783
|
+
const [checked, setChecked] = useState(false);
|
|
784
|
+
useEffect(() => {
|
|
785
|
+
if (!customerId) {
|
|
786
|
+
setContact(null);
|
|
787
|
+
setChecked(true);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
let cancelled = false;
|
|
791
|
+
const shop = getShopProvider();
|
|
792
|
+
Promise.resolve(shop.getCustomer?.(customerId)).then(async (customer) => {
|
|
793
|
+
if (customer || !hasCustomerAuth()) return customer ? { phone: customer.phone ?? null, document: customer.document ?? null } : null;
|
|
794
|
+
const relinked = await relinkCustomer();
|
|
795
|
+
return relinked ? { phone: relinked.phone, document: relinked.document } : null;
|
|
796
|
+
}).then((found) => {
|
|
797
|
+
if (cancelled) return;
|
|
798
|
+
setContact(found);
|
|
799
|
+
}).catch(() => {
|
|
800
|
+
if (!cancelled) setContact(null);
|
|
801
|
+
}).finally(() => {
|
|
802
|
+
if (!cancelled) setChecked(true);
|
|
803
|
+
});
|
|
804
|
+
return () => {
|
|
805
|
+
cancelled = true;
|
|
806
|
+
};
|
|
807
|
+
}, [customerId]);
|
|
808
|
+
const lastAddress = pickLastAddress(orders);
|
|
809
|
+
return {
|
|
810
|
+
phone: contact?.phone ?? null,
|
|
811
|
+
document: contact?.document ?? null,
|
|
812
|
+
address: lastAddress,
|
|
813
|
+
ready: checked && !ordersLoading
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function pickLastAddress(orders) {
|
|
817
|
+
for (const order of orders) {
|
|
818
|
+
const address = order.shippingAddress;
|
|
819
|
+
if (address && (address.postalCode || address.street)) return address;
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
function checkoutField(label, testId, value, onChange, props) {
|
|
824
|
+
return /* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
825
|
+
/* @__PURE__ */ jsx("span", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: label }),
|
|
826
|
+
/* @__PURE__ */ jsx(
|
|
827
|
+
"input",
|
|
828
|
+
{
|
|
829
|
+
"data-testid": testId,
|
|
830
|
+
"aria-label": label,
|
|
831
|
+
value,
|
|
832
|
+
onChange: (event) => onChange(event.target.value),
|
|
833
|
+
className: "w-full rounded-lg border bg-background px-3.5 py-3 text-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10",
|
|
834
|
+
...props
|
|
835
|
+
}
|
|
836
|
+
)
|
|
837
|
+
] });
|
|
838
|
+
}
|
|
839
|
+
function checkoutSelect(label, testId, value, onChange, options, placeholder, props) {
|
|
840
|
+
return /* @__PURE__ */ jsxs("label", { className: "block", children: [
|
|
841
|
+
/* @__PURE__ */ jsx("span", { className: "mb-1.5 block text-xs font-medium text-muted-foreground", children: label }),
|
|
842
|
+
/* @__PURE__ */ jsxs(
|
|
843
|
+
"select",
|
|
844
|
+
{
|
|
845
|
+
"data-testid": testId,
|
|
846
|
+
"aria-label": label,
|
|
847
|
+
value,
|
|
848
|
+
onChange: (event) => onChange(event.target.value),
|
|
849
|
+
className: "w-full rounded-lg border bg-background px-3.5 py-3 text-sm outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10",
|
|
850
|
+
...props,
|
|
851
|
+
children: [
|
|
852
|
+
/* @__PURE__ */ jsx("option", { value: "", children: placeholder }),
|
|
853
|
+
options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
|
|
854
|
+
]
|
|
855
|
+
}
|
|
856
|
+
)
|
|
857
|
+
] });
|
|
858
|
+
}
|
|
859
|
+
var DRAFT_KEY = {
|
|
860
|
+
postalCode: "zip",
|
|
861
|
+
street: "street",
|
|
862
|
+
number: "number",
|
|
863
|
+
complement: "complement",
|
|
864
|
+
district: "district",
|
|
865
|
+
city: "city",
|
|
866
|
+
region: "state"
|
|
867
|
+
};
|
|
868
|
+
var TEST_ID = {
|
|
869
|
+
postalCode: TID.checkoutZip,
|
|
870
|
+
street: TID.checkoutStreet,
|
|
871
|
+
number: "checkout-number",
|
|
872
|
+
complement: "checkout-complement",
|
|
873
|
+
district: "checkout-district",
|
|
874
|
+
city: TID.checkoutCity,
|
|
875
|
+
region: "checkout-state"
|
|
876
|
+
};
|
|
877
|
+
function AddressForm({ country, language, value, onChange, onPostalCode, lookup, complement }) {
|
|
878
|
+
const t = useT();
|
|
879
|
+
const schema = getCountrySchema(country);
|
|
880
|
+
const specs = new Map(schema.fields.map((spec) => [spec.key, spec]));
|
|
881
|
+
const showComplement = complement?.visible !== false;
|
|
882
|
+
const requireComplement = complement?.required === true;
|
|
883
|
+
function input(spec) {
|
|
884
|
+
const key = DRAFT_KEY[spec.key];
|
|
885
|
+
const label = spec.label[language];
|
|
886
|
+
const required = spec.required || spec.key === "complement" && requireComplement;
|
|
887
|
+
const basePlaceholder = spec.placeholder?.[language] ?? label;
|
|
888
|
+
const placeholder = required ? basePlaceholder : t("checkout.address.optional", { label: basePlaceholder });
|
|
889
|
+
if (spec.key === "postalCode") {
|
|
890
|
+
return checkoutField(label, TEST_ID.postalCode, value.zip, onPostalCode, {
|
|
891
|
+
placeholder,
|
|
892
|
+
inputMode: schema.postal.inputMode,
|
|
893
|
+
autoComplete: "postal-code"
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
if (spec.key === "region" && schema.region.input === "select" && schema.regions) {
|
|
897
|
+
return checkoutSelect(
|
|
898
|
+
label,
|
|
899
|
+
TEST_ID.region,
|
|
900
|
+
value.state,
|
|
901
|
+
(next) => onChange("state", next),
|
|
902
|
+
schema.regions.map((region) => ({
|
|
903
|
+
value: schema.region.stores === "code" ? region.code : region.name,
|
|
904
|
+
label: region.name
|
|
905
|
+
})),
|
|
906
|
+
t("checkout.address.choose"),
|
|
907
|
+
{ autoComplete: spec.autoComplete }
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
return checkoutField(label, TEST_ID[spec.key], value[key], (next) => onChange(key, next), {
|
|
911
|
+
placeholder,
|
|
912
|
+
...spec.autoComplete ? { autoComplete: spec.autoComplete } : {}
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function postalHint() {
|
|
916
|
+
if (lookup === "loading") {
|
|
917
|
+
return /* @__PURE__ */ jsxs(SkeletonGroup, { label: t("checkout.address.lookingUp"), className: "grid gap-1.5 px-1", children: [
|
|
918
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-2.5 w-40" }),
|
|
919
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-2.5 w-24" })
|
|
920
|
+
] });
|
|
921
|
+
}
|
|
922
|
+
if (schema.postal.lookup === "viacep") {
|
|
923
|
+
return /* @__PURE__ */ jsx("p", { className: "px-1 text-xs text-muted-foreground", children: lookup === "not-found" ? t("checkout.address.notFound") : t("checkout.address.filled") });
|
|
924
|
+
}
|
|
925
|
+
if (schema.postal.lookup === "prefix" && lookup === "found") {
|
|
926
|
+
return /* @__PURE__ */ jsx("p", { className: "px-1 text-xs text-muted-foreground", children: t("checkout.address.regionFromPostal") });
|
|
927
|
+
}
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
return /* @__PURE__ */ jsx("div", { className: "grid gap-3", children: schema.rows.map((row) => {
|
|
931
|
+
const cells = row.filter((key) => specs.has(key) && (key !== "complement" || showComplement));
|
|
932
|
+
if (cells.length === 0) return null;
|
|
933
|
+
if (cells.length === 1 && cells[0] === "postalCode") {
|
|
934
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-1 sm:grid-cols-[200px_1fr] sm:items-center", children: [
|
|
935
|
+
input(specs.get("postalCode")),
|
|
936
|
+
postalHint()
|
|
937
|
+
] }, "postalCode");
|
|
938
|
+
}
|
|
939
|
+
if (row.length === 1) return /* @__PURE__ */ jsx(React4.Fragment, { children: input(specs.get(cells[0])) }, cells[0]);
|
|
940
|
+
return /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-3", children: cells.map((key) => /* @__PURE__ */ jsx(React4.Fragment, { children: input(specs.get(key)) }, key)) }, cells.join("-"));
|
|
941
|
+
}) });
|
|
942
|
+
}
|
|
943
|
+
function maskDocument(raw) {
|
|
944
|
+
const d = documentDigits(raw);
|
|
945
|
+
if (d.length === 11) return `***.${d.slice(3, 6)}.${d.slice(6, 9)}-**`;
|
|
946
|
+
if (d.length === 14) return `${d.slice(0, 2)}.${d.slice(2, 5)}.${d.slice(5, 8)}/${d.slice(8, 12)}-${d.slice(12)}`;
|
|
947
|
+
return raw;
|
|
948
|
+
}
|
|
949
|
+
function formatBrPhone(raw) {
|
|
950
|
+
let d = raw.replace(/\D/g, "");
|
|
951
|
+
if (d.startsWith("55") && (d.length === 12 || d.length === 13)) d = d.slice(2);
|
|
952
|
+
if (d.length === 11) return `(${d.slice(0, 2)}) ${d.slice(2, 7)}-${d.slice(7)}`;
|
|
953
|
+
if (d.length === 10) return `(${d.slice(0, 2)}) ${d.slice(2, 6)}-${d.slice(6)}`;
|
|
954
|
+
return raw;
|
|
955
|
+
}
|
|
956
|
+
function ContactSummary({ name, email, phone, document: document2, onEdit, onSignOut, labels }) {
|
|
957
|
+
const line = [phone ? formatBrPhone(phone) : null, document2 ? maskDocument(document2) : null].filter(Boolean).join(" \xB7 ");
|
|
958
|
+
return /* @__PURE__ */ jsxs("div", { "data-testid": "checkout-contact-summary", className: "rounded-xl border bg-card p-4", children: [
|
|
959
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
960
|
+
/* @__PURE__ */ jsx("span", { className: "mt-0.5 grid h-6 w-6 shrink-0 place-items-center rounded-full bg-primary/15 text-primary", "aria-hidden": true, children: /* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }) }),
|
|
961
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
962
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-semibold", children: name }),
|
|
963
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm text-muted-foreground", children: email }),
|
|
964
|
+
line && /* @__PURE__ */ jsx("p", { className: "truncate text-sm text-muted-foreground", children: line })
|
|
965
|
+
] }),
|
|
966
|
+
/* @__PURE__ */ jsx(
|
|
967
|
+
"button",
|
|
968
|
+
{
|
|
969
|
+
type: "button",
|
|
970
|
+
onClick: onEdit,
|
|
971
|
+
"data-testid": "checkout-contact-edit",
|
|
972
|
+
className: "shrink-0 text-sm font-medium text-primary hover:underline",
|
|
973
|
+
children: labels.edit
|
|
974
|
+
}
|
|
975
|
+
)
|
|
976
|
+
] }),
|
|
977
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-3 border-t pt-3 text-xs text-muted-foreground", children: [
|
|
978
|
+
labels.notYou,
|
|
979
|
+
" ",
|
|
980
|
+
/* @__PURE__ */ jsx(
|
|
981
|
+
"button",
|
|
982
|
+
{
|
|
983
|
+
type: "button",
|
|
984
|
+
onClick: onSignOut,
|
|
985
|
+
"data-testid": "checkout-contact-sign-out",
|
|
986
|
+
className: "font-medium text-foreground hover:underline",
|
|
987
|
+
children: labels.signOut
|
|
988
|
+
}
|
|
989
|
+
)
|
|
990
|
+
] })
|
|
991
|
+
] });
|
|
992
|
+
}
|
|
993
|
+
function toShippingAddress(form, country) {
|
|
994
|
+
const schema = getCountrySchema(country);
|
|
995
|
+
const isBrazil = schema.iso2 === "BR";
|
|
996
|
+
const has = (key) => schema.fields.some((f) => f.key === key);
|
|
997
|
+
return {
|
|
998
|
+
postalCode: isBrazil ? form.zip.trim() : normalizePostalCode(form.zip, country),
|
|
999
|
+
street: form.street.trim(),
|
|
1000
|
+
number: has("number") ? form.number.trim() || void 0 : void 0,
|
|
1001
|
+
complement: form.complement.trim() || void 0,
|
|
1002
|
+
district: has("district") ? form.district.trim() || void 0 : void 0,
|
|
1003
|
+
city: form.city.trim(),
|
|
1004
|
+
// A UF is a code and is upper-cased; a province is a name and keeps its case.
|
|
1005
|
+
state: schema.region.stores === "code" ? form.state.trim().toUpperCase() : form.state.trim(),
|
|
1006
|
+
country: schema.iso2,
|
|
1007
|
+
phone: form.phone.trim() || void 0,
|
|
1008
|
+
document: (isBrazil ? documentDigits(form.document) : form.document.trim().toUpperCase()) || void 0
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function flag(iso2) {
|
|
1012
|
+
return iso2.toUpperCase().replace(/[^A-Z]/g, "").split("").map((c) => String.fromCodePoint(127462 + c.charCodeAt(0) - 65)).join("");
|
|
1013
|
+
}
|
|
1014
|
+
function CountrySelect({ country, allowed, onChange, className }) {
|
|
1015
|
+
const t = useT();
|
|
1016
|
+
const config = useStorefrontConfig();
|
|
1017
|
+
return /* @__PURE__ */ jsxs("label", { className: `flex items-center gap-2 text-sm ${className ?? ""}`, children: [
|
|
1018
|
+
/* @__PURE__ */ jsx(MapPin, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
|
|
1019
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: t("country.deliverTo") }),
|
|
1020
|
+
/* @__PURE__ */ jsx(
|
|
1021
|
+
"select",
|
|
1022
|
+
{
|
|
1023
|
+
"data-testid": "deliver-to-country",
|
|
1024
|
+
"aria-label": t("country.label"),
|
|
1025
|
+
value: country,
|
|
1026
|
+
onChange: (event) => onChange(event.target.value),
|
|
1027
|
+
className: "rounded-lg border bg-background px-2.5 py-1.5 text-sm font-medium outline-none transition focus:border-primary focus:ring-2 focus:ring-primary/10",
|
|
1028
|
+
children: allowed.map((iso2) => /* @__PURE__ */ jsxs("option", { value: iso2, children: [
|
|
1029
|
+
flag(iso2),
|
|
1030
|
+
" ",
|
|
1031
|
+
countryName(iso2, config.language)
|
|
1032
|
+
] }, iso2))
|
|
1033
|
+
}
|
|
1034
|
+
)
|
|
1035
|
+
] });
|
|
1036
|
+
}
|
|
1037
|
+
function DeliverToPicker({ className }) {
|
|
1038
|
+
const buyer = useBuyerCountry();
|
|
1039
|
+
if (buyer.allowed.length < 2) return null;
|
|
1040
|
+
return /* @__PURE__ */ jsx(CountrySelect, { className, country: buyer.country, allowed: buyer.allowed, onChange: buyer.setCountry });
|
|
1041
|
+
}
|
|
1042
|
+
var PAYMENT_LABELS = {
|
|
1043
|
+
pix: { label: "payment.pix.label", hint: "payment.pix.hint" },
|
|
1044
|
+
// O cartão tem DOIS significados agora, e o texto muda com a loja: sem
|
|
1045
|
+
// adquirente é a maquininha do entregador; com adquirente é a página segura
|
|
1046
|
+
// dele — prometer "maquininha" para quem vai ser redirecionado é a loja
|
|
1047
|
+
// mentindo sobre a própria entrega.
|
|
1048
|
+
credit_card: { label: "payment.credit_card.label", hint: "payment.credit_card.hint" },
|
|
1049
|
+
debit_card: { label: "payment.debit_card.label", hint: "payment.debit_card.hint" },
|
|
1050
|
+
boleto: { label: "payment.boleto.label", hint: "payment.boleto.hint" },
|
|
1051
|
+
cash: { label: "payment.cash.label", hint: "payment.cash.hint" },
|
|
1052
|
+
other: { label: "payment.other.label", hint: "payment.other.hint" }
|
|
1053
|
+
};
|
|
1054
|
+
var BRAZIL_ONLY = ["pix", "boleto"];
|
|
1055
|
+
function placeOrderMessage(err, t) {
|
|
1056
|
+
const message = errorMessage(err);
|
|
1057
|
+
if (/customer (email mismatch|does not belong|belongs to another)/i.test(message)) {
|
|
1058
|
+
return t("error.sessionMismatch");
|
|
1059
|
+
}
|
|
1060
|
+
if (/out of stock|insufficient|estoque/i.test(message)) {
|
|
1061
|
+
return t("error.outOfStock");
|
|
1062
|
+
}
|
|
1063
|
+
if (/delivery not available|postal code/i.test(message)) {
|
|
1064
|
+
return t("error.unserved");
|
|
1065
|
+
}
|
|
1066
|
+
return message.replace(/^shop_[a-z_]+:\s*/i, "") || t("error.placeOrder");
|
|
1067
|
+
}
|
|
1068
|
+
var DEV_BUILD = (() => {
|
|
1069
|
+
try {
|
|
1070
|
+
return import.meta.env?.DEV === true;
|
|
1071
|
+
} catch {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
})();
|
|
1075
|
+
var CHECKOUT_STEP_NAMES = { 1: "contact", 2: "delivery", 3: "payment", 4: "done" };
|
|
1076
|
+
function CheckoutPage() {
|
|
1077
|
+
const config = useStorefrontConfig();
|
|
1078
|
+
const cart = useCartStore();
|
|
1079
|
+
const session = useSessionStore();
|
|
1080
|
+
const delivery = useDeliveryStore();
|
|
1081
|
+
const validateDiscount = useDiscountValidator();
|
|
1082
|
+
const t = useT();
|
|
1083
|
+
useStorefrontHead({ title: t("checkout.title", { store: config.name }) });
|
|
1084
|
+
const buyer = useBuyerCountry();
|
|
1085
|
+
const country = buyer.country;
|
|
1086
|
+
const isBrazil = country === "BR";
|
|
1087
|
+
const countrySchema = getCountrySchema(country);
|
|
1088
|
+
const paymentMethods = isBrazil ? config.payments.methods : config.payments.methods.filter((method) => !BRAZIL_ONLY.includes(method));
|
|
1089
|
+
const policy = config.policy;
|
|
1090
|
+
const requiresAccount = policy?.guestCheckout === false;
|
|
1091
|
+
const fieldPolicy = policy?.checkoutFields ?? {};
|
|
1092
|
+
const gatewayCheckout = config.payments.mode === "gateway" || !!policy?.paymentProvider;
|
|
1093
|
+
const shows = (key) => key === "document" ? isBrazil && gatewayCheckout || fieldPolicy.document?.visible === true : fieldPolicy[key]?.visible !== false;
|
|
1094
|
+
const requires = (key) => (
|
|
1095
|
+
// A CPF is Brazil's; elsewhere the tax id is the buyer's to give or not.
|
|
1096
|
+
key === "document" && !isBrazil ? countrySchema.taxId?.required === "always" : key === "document" || key === "phone" ? gatewayCheckout || fieldPolicy[key]?.visible === true && fieldPolicy[key]?.required === true : fieldPolicy[key]?.visible !== false && fieldPolicy[key]?.required === true
|
|
1097
|
+
);
|
|
1098
|
+
const [selectedAddressId, setSelectedAddressId] = useState("new");
|
|
1099
|
+
const [savedAddresses, setSavedAddresses] = useState([]);
|
|
1100
|
+
const [addressesLoading, setAddressesLoading] = useState(false);
|
|
1101
|
+
const [addressMode, setAddressMode] = useState("new");
|
|
1102
|
+
const [resume] = useState(() => {
|
|
1103
|
+
if (typeof window === "undefined") return null;
|
|
1104
|
+
const params = new URLSearchParams(window.location.search);
|
|
1105
|
+
const sessionId = params.get("vindi_session") ?? params.get("pay_session");
|
|
1106
|
+
if (!sessionId) return null;
|
|
1107
|
+
return {
|
|
1108
|
+
sessionId,
|
|
1109
|
+
provider: recallSessionProvider(sessionId),
|
|
1110
|
+
chargeToken: params.get("token_transaction")
|
|
1111
|
+
};
|
|
1112
|
+
});
|
|
1113
|
+
const [paymentMethod, setPaymentMethod] = useState(
|
|
1114
|
+
resume ? "credit_card" : paymentMethods[0] ?? "pix"
|
|
1115
|
+
);
|
|
1116
|
+
useEffect(() => {
|
|
1117
|
+
if (!paymentMethods.includes(paymentMethod) && paymentMethods[0]) setPaymentMethod(paymentMethods[0]);
|
|
1118
|
+
}, [paymentMethods, paymentMethod]);
|
|
1119
|
+
const known = useKnownCustomer();
|
|
1120
|
+
const [form, setForm] = useState({
|
|
1121
|
+
email: session.email ?? "",
|
|
1122
|
+
name: session.name ?? "",
|
|
1123
|
+
phone: "",
|
|
1124
|
+
document: "",
|
|
1125
|
+
street: "",
|
|
1126
|
+
city: "",
|
|
1127
|
+
zip: "",
|
|
1128
|
+
number: "",
|
|
1129
|
+
complement: "",
|
|
1130
|
+
district: "",
|
|
1131
|
+
state: ""
|
|
1132
|
+
});
|
|
1133
|
+
React4.useEffect(() => {
|
|
1134
|
+
if (!known.ready) return;
|
|
1135
|
+
setForm((current) => ({
|
|
1136
|
+
...current,
|
|
1137
|
+
phone: current.phone || known.phone || "",
|
|
1138
|
+
document: current.document || known.document || "",
|
|
1139
|
+
zip: current.zip || known.address?.postalCode || "",
|
|
1140
|
+
street: current.street || known.address?.street || "",
|
|
1141
|
+
number: current.number || known.address?.number || "",
|
|
1142
|
+
complement: current.complement || known.address?.complement || "",
|
|
1143
|
+
district: current.district || known.address?.district || "",
|
|
1144
|
+
city: current.city || known.address?.city || "",
|
|
1145
|
+
state: current.state || known.address?.state || ""
|
|
1146
|
+
}));
|
|
1147
|
+
}, [known.ready, known.phone, known.document, known.address]);
|
|
1148
|
+
const [step, setStep] = useState(resume ? 3 : 1);
|
|
1149
|
+
const reportedStart = React4.useRef(false);
|
|
1150
|
+
useEffect(() => {
|
|
1151
|
+
if (reportedStart.current) return;
|
|
1152
|
+
reportedStart.current = true;
|
|
1153
|
+
if (!resume) {
|
|
1154
|
+
trackStorefront("checkout_started", {
|
|
1155
|
+
cart_count: cart.lines.reduce((n, l) => n + l.quantity, 0),
|
|
1156
|
+
// What the cart is worth when the checkout opens: the value an ad
|
|
1157
|
+
// platform optimises InitiateCheckout for.
|
|
1158
|
+
cart_subtotal: Math.round(cart.lines.reduce((sum, l) => sum + l.unitPrice * l.quantity, 0) * 100) / 100
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}, []);
|
|
1162
|
+
const reportedStep = React4.useRef(null);
|
|
1163
|
+
useEffect(() => {
|
|
1164
|
+
if (reportedStep.current === step) return;
|
|
1165
|
+
reportedStep.current = step;
|
|
1166
|
+
trackStorefront("checkout_step_viewed", { step, step_name: CHECKOUT_STEP_NAMES[step], resume: Boolean(resume) });
|
|
1167
|
+
if (step >= 2 && /.+@.+\..+/.test(form.email)) identifyStorefrontBuyer(form.email);
|
|
1168
|
+
}, [step]);
|
|
1169
|
+
const [contactKnown, setContactKnown] = useState(false);
|
|
1170
|
+
const [editingContact, setEditingContact] = useState(false);
|
|
1171
|
+
const skippedContact = React4.useRef(false);
|
|
1172
|
+
const fulfillmentType = useDeliveryStore((s) => s.fulfillmentType);
|
|
1173
|
+
const scheduledFor = useDeliveryStore((s) => s.scheduledFor);
|
|
1174
|
+
const deliveryReference = useDeliveryStore((s) => {
|
|
1175
|
+
const kept = s.savedAddresses.find((a) => a.id === s.selectedAddressId);
|
|
1176
|
+
return kept?.reference?.trim() || "";
|
|
1177
|
+
});
|
|
1178
|
+
const isPickup = fulfillmentType === "pickup";
|
|
1179
|
+
const [dbPickup, setDbPickup] = useState(null);
|
|
1180
|
+
useEffect(() => {
|
|
1181
|
+
if (!isPickup) return;
|
|
1182
|
+
let alive = true;
|
|
1183
|
+
void (async () => {
|
|
1184
|
+
const settings = await getShopProvider().getPublicStoreSettings?.().catch(() => null);
|
|
1185
|
+
if (alive && settings?.pickupAddress) setDbPickup(settings.pickupAddress);
|
|
1186
|
+
})();
|
|
1187
|
+
return () => {
|
|
1188
|
+
alive = false;
|
|
1189
|
+
};
|
|
1190
|
+
}, [isPickup]);
|
|
1191
|
+
const pickupAddress = dbPickup ?? config.delivery?.pickupAddress ?? null;
|
|
1192
|
+
const [paidOrderId, setPaidOrderId] = useState(null);
|
|
1193
|
+
const [error, setError] = useState(null);
|
|
1194
|
+
const [discountCode, setDiscountCode] = useState("");
|
|
1195
|
+
const [discountError, setDiscountError] = useState(null);
|
|
1196
|
+
const [discountSuccess, setDiscountSuccess] = useState(null);
|
|
1197
|
+
const [applyingDiscount, setApplyingDiscount] = useState(false);
|
|
1198
|
+
const [showSignin, setShowSignin] = useState(false);
|
|
1199
|
+
const [placing, setPlacing] = useState(false);
|
|
1200
|
+
const leaving = React4.useRef(false);
|
|
1201
|
+
const [zipLookup, setZipLookup] = useState("idle");
|
|
1202
|
+
const subtotal = selectSubtotal(cart);
|
|
1203
|
+
const discountTotal = selectDiscountTotal(cart);
|
|
1204
|
+
const shippingRaw = selectShipping(cart, config);
|
|
1205
|
+
const shipping = isPickup ? 0 : shippingRaw;
|
|
1206
|
+
const total = selectTotal(cart, config) - (isPickup ? shippingRaw : 0);
|
|
1207
|
+
const checkoutSlotContext = { subtotal, total };
|
|
1208
|
+
const checkoutSessionInput = React4.useMemo(() => ({
|
|
1209
|
+
items: cart.lines.map((line) => ({
|
|
1210
|
+
productId: line.productId,
|
|
1211
|
+
variantId: line.variantId ?? null,
|
|
1212
|
+
quantity: line.quantity,
|
|
1213
|
+
optionsLabel: line.optionsLabel ?? null
|
|
1214
|
+
})),
|
|
1215
|
+
// The document and the phone travel WITH the buyer, not only inside the
|
|
1216
|
+
// address.
|
|
1217
|
+
//
|
|
1218
|
+
// The acquirer charges a PERSON: Vindi refuses a charge with no document
|
|
1219
|
+
// ("Informe o CPF ou CNPJ do comprador"), and AbacatePay's customer block is
|
|
1220
|
+
// all-or-nothing — name, email, taxId and cellphone, or no customer at all.
|
|
1221
|
+
// With two of the four it had been opening charges while registering nobody,
|
|
1222
|
+
// which is the "no client was created" the store saw in the provider account.
|
|
1223
|
+
//
|
|
1224
|
+
// The field was already asked for on screen: with an acquirer connected,
|
|
1225
|
+
// `requires('document')` is true and nobody gets past step 1 without filling
|
|
1226
|
+
// it in. What was missing was forwarding it to whoever charges. On the ORDER
|
|
1227
|
+
// it still travels inside the address, because of shop_place_order's
|
|
1228
|
+
// signature — different roads.
|
|
1229
|
+
customer: {
|
|
1230
|
+
name: form.name.trim(),
|
|
1231
|
+
email: form.email.trim(),
|
|
1232
|
+
document: documentDigits(form.document) || void 0,
|
|
1233
|
+
phone: form.phone.trim() || void 0
|
|
1234
|
+
},
|
|
1235
|
+
shippingAddress: deliveryAddress(),
|
|
1236
|
+
discountCode: cart.discountCode ?? null,
|
|
1237
|
+
paymentMethod,
|
|
1238
|
+
// Qual adquirente cobra. Sai dos ajustes da loja e não de uma escolha do
|
|
1239
|
+
// comprador — a função reconfere de qualquer forma, mas mandar daqui é o
|
|
1240
|
+
// que faz o clique bater na função do gateway que a loja realmente conectou.
|
|
1241
|
+
provider: policy?.paymentProvider ?? null,
|
|
1242
|
+
// The market (0116) the acquirer's open function prices and charges in.
|
|
1243
|
+
market: config.market?.code ?? null,
|
|
1244
|
+
country
|
|
1245
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1246
|
+
}), [cart.lines, cart.discountCode, form, paymentMethod, policy?.paymentProvider, country, config.market?.code]);
|
|
1247
|
+
const [unavailable, setUnavailable] = useState([]);
|
|
1248
|
+
const cardIsCharged = (
|
|
1249
|
+
// Outside Brazil a card is charged online only by a real acquirer: with no
|
|
1250
|
+
// provider the online panel is the mock's Pix, which a Spanish buyer cannot
|
|
1251
|
+
// pay. Without one the order is placed pending, as an offline payment is.
|
|
1252
|
+
(isBrazil || !!policy?.paymentProvider) && !unavailable.includes("credit_card") && (!policy?.paymentProvider || isChargeableProvider(policy.paymentProvider)) && (!policy?.paymentProvider || supportsCard(policy.paymentProvider))
|
|
1253
|
+
);
|
|
1254
|
+
const acquirerMissing = unavailable.includes(paymentMethod);
|
|
1255
|
+
const chargesOnline = (
|
|
1256
|
+
// Pix é cobrado dentro da loja; cartão é pago na página do adquirente e
|
|
1257
|
+
// volta para cá. Débito, dinheiro e "combinar" nunca foram cobrança online.
|
|
1258
|
+
// `cardIsCharged` rather than a bare `credit_card`: card is charged online
|
|
1259
|
+
// only where the acquirer charges cards. Where it does not, this screen
|
|
1260
|
+
// offers the order without a charge — the card-reader road, as it always was.
|
|
1261
|
+
(paymentMethod === "pix" || paymentMethod === "credit_card" && cardIsCharged) && !acquirerMissing && (!policy?.paymentProvider || isChargeableProvider(policy.paymentProvider))
|
|
1262
|
+
);
|
|
1263
|
+
const money = (value) => formatMoney(value, config.currency, config.locale);
|
|
1264
|
+
useEffect(() => {
|
|
1265
|
+
if (cart.lines.length === 0 && !paidOrderId && !leaving.current && !resume) navigateTo(config.catalogPath);
|
|
1266
|
+
}, [cart.lines.length, config.catalogPath, paidOrderId, resume]);
|
|
1267
|
+
const set = (key) => (value) => {
|
|
1268
|
+
setError(null);
|
|
1269
|
+
setForm((current) => ({ ...current, [key]: value }));
|
|
1270
|
+
};
|
|
1271
|
+
async function setZip(value) {
|
|
1272
|
+
const masked = formatPostalCode(value, country);
|
|
1273
|
+
setError(null);
|
|
1274
|
+
setForm((current) => ({ ...current, zip: masked }));
|
|
1275
|
+
if (!countrySchema.postal.pattern.test(normalizePostalCode(masked, country))) {
|
|
1276
|
+
setZipLookup("idle");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
setZipLookup("loading");
|
|
1280
|
+
void delivery.resolve(masked, subtotal, country);
|
|
1281
|
+
try {
|
|
1282
|
+
const found = await lookupPostalCode(masked, country);
|
|
1283
|
+
if (!found) {
|
|
1284
|
+
setZipLookup("not-found");
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
setZipLookup("found");
|
|
1288
|
+
setForm((current) => ({
|
|
1289
|
+
...current,
|
|
1290
|
+
street: found.street || current.street,
|
|
1291
|
+
district: found.district || current.district,
|
|
1292
|
+
city: found.city || current.city,
|
|
1293
|
+
state: found.state
|
|
1294
|
+
}));
|
|
1295
|
+
} catch {
|
|
1296
|
+
setZipLookup("idle");
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
async function applyDiscountCode() {
|
|
1300
|
+
setApplyingDiscount(true);
|
|
1301
|
+
setDiscountError(null);
|
|
1302
|
+
setDiscountSuccess(null);
|
|
1303
|
+
try {
|
|
1304
|
+
const appliedCode = discountCode.trim().toUpperCase();
|
|
1305
|
+
const result = await validateDiscount(discountCode);
|
|
1306
|
+
if (result.valid) {
|
|
1307
|
+
cart.applyDiscount(appliedCode, result.percent, { freeShipping: result.freeShipping });
|
|
1308
|
+
setDiscountCode(appliedCode);
|
|
1309
|
+
const effect = result.freeShipping ? t("discount.freeShipping") : t("discount.percentOff", { percent: result.percent });
|
|
1310
|
+
setDiscountSuccess(t("discount.applied", { code: appliedCode, effect }));
|
|
1311
|
+
toast.success(t("discount.appliedToast"), `${appliedCode} \u2022 ${effect}`);
|
|
1312
|
+
} else {
|
|
1313
|
+
setDiscountError(result.message ?? t("discount.invalid"));
|
|
1314
|
+
toast.error(t("discount.invalidToast"), result.message ?? t("discount.checkCode"));
|
|
1315
|
+
}
|
|
1316
|
+
} finally {
|
|
1317
|
+
setApplyingDiscount(false);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function clearDiscountCode() {
|
|
1321
|
+
cart.clearDiscount();
|
|
1322
|
+
setDiscountCode("");
|
|
1323
|
+
setDiscountSuccess(null);
|
|
1324
|
+
setDiscountError(null);
|
|
1325
|
+
}
|
|
1326
|
+
useEffect(() => {
|
|
1327
|
+
const customerId = session.customerId;
|
|
1328
|
+
if (!customerId) {
|
|
1329
|
+
setSavedAddresses([]);
|
|
1330
|
+
setAddressMode("new");
|
|
1331
|
+
setAddressesLoading(false);
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
let cancelled = false;
|
|
1335
|
+
const provider = getShopProvider();
|
|
1336
|
+
const listAddresses = provider.listCustomerAddresses?.bind(provider);
|
|
1337
|
+
if (!listAddresses) {
|
|
1338
|
+
setAddressesLoading(false);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
setAddressesLoading(true);
|
|
1342
|
+
void listAddresses(customerId).then((addresses) => {
|
|
1343
|
+
if (cancelled || addresses.length === 0) return;
|
|
1344
|
+
setSavedAddresses(addresses);
|
|
1345
|
+
const here = addresses.find((address) => (address.country ?? "BR") === country);
|
|
1346
|
+
if (here) applySavedAddress(here);
|
|
1347
|
+
}).catch(() => {
|
|
1348
|
+
}).finally(() => {
|
|
1349
|
+
if (!cancelled) setAddressesLoading(false);
|
|
1350
|
+
});
|
|
1351
|
+
return () => {
|
|
1352
|
+
cancelled = true;
|
|
1353
|
+
};
|
|
1354
|
+
}, [session.customerId]);
|
|
1355
|
+
useEffect(() => {
|
|
1356
|
+
const customerId = session.customerId;
|
|
1357
|
+
setContactKnown(false);
|
|
1358
|
+
if (!customerId) return;
|
|
1359
|
+
const provider = getShopProvider();
|
|
1360
|
+
const getCustomer = provider.getCustomer?.bind(provider);
|
|
1361
|
+
const getMyProfile = provider.getMyProfile?.bind(provider);
|
|
1362
|
+
let cancelled = false;
|
|
1363
|
+
const fill = (c) => {
|
|
1364
|
+
if (cancelled || !c) return;
|
|
1365
|
+
setForm((current) => ({
|
|
1366
|
+
...current,
|
|
1367
|
+
phone: current.phone || c.phone || "",
|
|
1368
|
+
document: current.document || (c.document ? formatDocument(c.document) : ""),
|
|
1369
|
+
email: current.email || c.email || "",
|
|
1370
|
+
name: current.name || [c.firstName, c.lastName].filter(Boolean).join(" ").trim()
|
|
1371
|
+
}));
|
|
1372
|
+
};
|
|
1373
|
+
void (async () => {
|
|
1374
|
+
const profile = getMyProfile ? await getMyProfile().catch(() => null) : null;
|
|
1375
|
+
fill(profile);
|
|
1376
|
+
if (getCustomer && (!profile || !profile.phone || !profile.document)) {
|
|
1377
|
+
fill(await getCustomer(customerId).catch(() => null));
|
|
1378
|
+
}
|
|
1379
|
+
if (!cancelled) setContactKnown(true);
|
|
1380
|
+
})();
|
|
1381
|
+
return () => {
|
|
1382
|
+
cancelled = true;
|
|
1383
|
+
};
|
|
1384
|
+
}, [session.customerId]);
|
|
1385
|
+
const shownCountry = React4.useRef(country);
|
|
1386
|
+
useEffect(() => {
|
|
1387
|
+
if (shownCountry.current === country) return;
|
|
1388
|
+
shownCountry.current = country;
|
|
1389
|
+
setZipLookup("idle");
|
|
1390
|
+
addNewAddress();
|
|
1391
|
+
}, [country]);
|
|
1392
|
+
const addressesHere = savedAddresses.filter((address) => (address.country ?? "BR") === country);
|
|
1393
|
+
useEffect(() => {
|
|
1394
|
+
if (addressMode !== "new") return;
|
|
1395
|
+
const keptAny = delivery.savedAddresses.find((a) => a.id === delivery.selectedAddressId);
|
|
1396
|
+
const kept = keptAny && (keptAny.country ?? "BR") === country ? keptAny : void 0;
|
|
1397
|
+
const looked = delivery.address && (delivery.address.country || "BR") === country ? delivery.address : null;
|
|
1398
|
+
if (!kept && !looked) return;
|
|
1399
|
+
setForm((current) => {
|
|
1400
|
+
if (current.zip.trim() || current.street.trim()) return current;
|
|
1401
|
+
if (kept) {
|
|
1402
|
+
return {
|
|
1403
|
+
...current,
|
|
1404
|
+
zip: formatPostalCode(kept.postalCode, country),
|
|
1405
|
+
street: kept.street,
|
|
1406
|
+
number: kept.number,
|
|
1407
|
+
complement: kept.complement ?? "",
|
|
1408
|
+
district: kept.district,
|
|
1409
|
+
city: kept.city,
|
|
1410
|
+
state: kept.state
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
...current,
|
|
1415
|
+
zip: formatPostalCode(looked.postalCode, country),
|
|
1416
|
+
street: looked.street,
|
|
1417
|
+
district: looked.district,
|
|
1418
|
+
city: looked.city,
|
|
1419
|
+
state: looked.state
|
|
1420
|
+
};
|
|
1421
|
+
});
|
|
1422
|
+
}, [delivery.address, delivery.savedAddresses, delivery.selectedAddressId, addressMode, country]);
|
|
1423
|
+
function applySavedAddress(address) {
|
|
1424
|
+
setAddressMode("saved");
|
|
1425
|
+
setSelectedAddressId(address.id);
|
|
1426
|
+
setForm((current) => ({
|
|
1427
|
+
...current,
|
|
1428
|
+
street: address.street,
|
|
1429
|
+
city: address.city,
|
|
1430
|
+
zip: address.postalCode,
|
|
1431
|
+
number: address.number ?? "",
|
|
1432
|
+
complement: address.complement ?? "",
|
|
1433
|
+
district: address.district ?? "",
|
|
1434
|
+
state: address.state,
|
|
1435
|
+
// Kept from the book when it has one, and NOT cleared when it does not:
|
|
1436
|
+
// a shopper who just typed their phone should not lose it by picking a
|
|
1437
|
+
// saved address that predates the field.
|
|
1438
|
+
phone: address.phone ?? current.phone
|
|
1439
|
+
}));
|
|
1440
|
+
}
|
|
1441
|
+
function selectAddress(addressId) {
|
|
1442
|
+
const address = savedAddresses.find((item) => item.id === addressId);
|
|
1443
|
+
if (address) applySavedAddress(address);
|
|
1444
|
+
}
|
|
1445
|
+
function addNewAddress() {
|
|
1446
|
+
setAddressMode("new");
|
|
1447
|
+
setSelectedAddressId("new");
|
|
1448
|
+
setForm((current) => ({ ...current, street: "", city: "", zip: "", number: "", complement: "", district: "", state: "" }));
|
|
1449
|
+
}
|
|
1450
|
+
function fail(message, testId) {
|
|
1451
|
+
setError(message);
|
|
1452
|
+
trackStorefront("checkout_error", { step, field: testId ?? null, reason: message });
|
|
1453
|
+
if (testId) {
|
|
1454
|
+
const field = document.querySelector(`[data-testid="${testId}"]`);
|
|
1455
|
+
field?.scrollIntoView({ block: "center", behavior: prefersReducedMotion() ? "auto" : "smooth" });
|
|
1456
|
+
field?.focus({ preventScroll: true });
|
|
1457
|
+
}
|
|
1458
|
+
return false;
|
|
1459
|
+
}
|
|
1460
|
+
function validateContact() {
|
|
1461
|
+
setError(null);
|
|
1462
|
+
if (requiresAccount && !session.customerId) {
|
|
1463
|
+
setShowSignin(true);
|
|
1464
|
+
return fail(t("error.requiresAccount"));
|
|
1465
|
+
}
|
|
1466
|
+
if (!form.email.trim() || !/.+@.+\..+/.test(form.email)) {
|
|
1467
|
+
return fail(t("error.email"), TID.checkoutEmail);
|
|
1468
|
+
}
|
|
1469
|
+
if (!form.name.trim()) return fail(t("error.name"), TID.checkoutName);
|
|
1470
|
+
if (requires("phone") && !form.phone.trim()) {
|
|
1471
|
+
return fail(t("error.phone"), "checkout-phone");
|
|
1472
|
+
}
|
|
1473
|
+
return validateDocument();
|
|
1474
|
+
}
|
|
1475
|
+
function contactComplete() {
|
|
1476
|
+
if (!session.customerId) return false;
|
|
1477
|
+
if (!form.email.trim() || !/.+@.+\..+/.test(form.email)) return false;
|
|
1478
|
+
if (!form.name.trim()) return false;
|
|
1479
|
+
const phone = form.phone.replace(/\D/g, "");
|
|
1480
|
+
if (requires("phone") && phone.length < 10) return false;
|
|
1481
|
+
if (shows("phone") && !phone) return false;
|
|
1482
|
+
if (isBrazil) {
|
|
1483
|
+
if (requires("document") && !isValidDocument(form.document)) return false;
|
|
1484
|
+
if (shows("document") && !isValidDocument(form.document)) return false;
|
|
1485
|
+
}
|
|
1486
|
+
return true;
|
|
1487
|
+
}
|
|
1488
|
+
const contactAsCard = !editingContact && contactKnown && contactComplete();
|
|
1489
|
+
useEffect(() => {
|
|
1490
|
+
if (skippedContact.current || step !== 1 || !contactAsCard) return;
|
|
1491
|
+
skippedContact.current = true;
|
|
1492
|
+
setStep(isPickup ? 3 : 2);
|
|
1493
|
+
}, [contactAsCard, step]);
|
|
1494
|
+
function validateDocument() {
|
|
1495
|
+
if (isBrazil) {
|
|
1496
|
+
if (requires("document") && !isValidDocument(form.document)) {
|
|
1497
|
+
return fail(
|
|
1498
|
+
form.document.trim() ? t("error.documentInvalid") : t("error.documentMissing"),
|
|
1499
|
+
TID.checkoutDocument
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
const taxId = countrySchema.taxId;
|
|
1505
|
+
if (!form.document.trim()) {
|
|
1506
|
+
return requires("document") ? fail(t("error.documentMissing"), TID.checkoutDocument) : true;
|
|
1507
|
+
}
|
|
1508
|
+
if (taxId && !taxId.validate(form.document)) return fail(t("error.taxIdInvalid"), TID.checkoutDocument);
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
function validateAddressFields() {
|
|
1512
|
+
if (isBrazil) {
|
|
1513
|
+
if (!form.street.trim() || !form.city.trim() || !form.zip.trim() || !form.district.trim()) {
|
|
1514
|
+
return fail(t("error.address"), TID.checkoutStreet);
|
|
1515
|
+
}
|
|
1516
|
+
if (!form.number.trim()) return fail(t("error.number"), "checkout-number");
|
|
1517
|
+
if (requires("complement") && !form.complement.trim()) {
|
|
1518
|
+
return fail(t("error.complement"), "checkout-complement");
|
|
1519
|
+
}
|
|
1520
|
+
if (!/^[A-Za-z]{2}$/.test(form.state.trim())) {
|
|
1521
|
+
return fail(t("error.state"), "checkout-state");
|
|
1522
|
+
}
|
|
1523
|
+
return true;
|
|
1524
|
+
}
|
|
1525
|
+
const errors = validateAddress(
|
|
1526
|
+
{ postalCode: form.zip, street: form.street, complement: form.complement, city: form.city, state: form.state },
|
|
1527
|
+
country,
|
|
1528
|
+
{ complement: requires("complement") }
|
|
1529
|
+
);
|
|
1530
|
+
if (errors.postalCode === "invalid") {
|
|
1531
|
+
return fail(t("error.postalInvalid", { example: countrySchema.postal.example }), TID.checkoutZip);
|
|
1532
|
+
}
|
|
1533
|
+
if (errors.postalCode || errors.street || errors.city) return fail(t("error.address"), errors.postalCode ? TID.checkoutZip : TID.checkoutStreet);
|
|
1534
|
+
if (errors.complement) return fail(t("error.complement"), "checkout-complement");
|
|
1535
|
+
if (errors.region) {
|
|
1536
|
+
const label = countrySchema.fields.find((f) => f.key === "region")?.label[config.language] ?? "";
|
|
1537
|
+
return fail(t("error.region", { label: label.toLowerCase() }), "checkout-state");
|
|
1538
|
+
}
|
|
1539
|
+
return true;
|
|
1540
|
+
}
|
|
1541
|
+
function validateDelivery() {
|
|
1542
|
+
setError(null);
|
|
1543
|
+
if (isPickup) return true;
|
|
1544
|
+
if (!selectRequiresShipping(cart)) return true;
|
|
1545
|
+
if (!validateAddressFields()) return false;
|
|
1546
|
+
if (delivery.status === "unserved" && delivery.postalCode === normalizePostalCode(form.zip, country)) {
|
|
1547
|
+
return fail(delivery.refusedZone ? t("error.refusedZone") : t("error.unserved"), TID.checkoutZip);
|
|
1548
|
+
}
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
function goToStep(next) {
|
|
1552
|
+
setError(null);
|
|
1553
|
+
setStep(next);
|
|
1554
|
+
if (typeof window !== "undefined") window.scrollTo({ top: 0, behavior: "smooth" });
|
|
1555
|
+
}
|
|
1556
|
+
function orderAddress() {
|
|
1557
|
+
if (isPickup && pickupAddress) {
|
|
1558
|
+
return {
|
|
1559
|
+
...pickupAddress,
|
|
1560
|
+
phone: form.phone.trim() || void 0,
|
|
1561
|
+
document: documentDigits(form.document) || void 0
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
return deliveryAddress();
|
|
1565
|
+
}
|
|
1566
|
+
function deliveryAddress() {
|
|
1567
|
+
return toShippingAddress(form, country);
|
|
1568
|
+
}
|
|
1569
|
+
function advance() {
|
|
1570
|
+
if (step === 1 && validateContact()) {
|
|
1571
|
+
if (editingContact && session.customerId) {
|
|
1572
|
+
void getShopProvider().saveMyProfile?.({
|
|
1573
|
+
name: form.name.trim(),
|
|
1574
|
+
phone: form.phone.trim() || null,
|
|
1575
|
+
document: documentDigits(form.document) || null
|
|
1576
|
+
}).catch(() => {
|
|
1577
|
+
});
|
|
1578
|
+
setEditingContact(false);
|
|
1579
|
+
}
|
|
1580
|
+
void getShopProvider().saveCustomerContact?.({
|
|
1581
|
+
phone: form.phone.trim() || null,
|
|
1582
|
+
document: documentDigits(form.document) || null
|
|
1583
|
+
});
|
|
1584
|
+
goToStep(isPickup ? 3 : 2);
|
|
1585
|
+
} else if (step === 2 && validateDelivery()) goToStep(3);
|
|
1586
|
+
}
|
|
1587
|
+
async function confirmWithoutCharge() {
|
|
1588
|
+
if (!validate()) return;
|
|
1589
|
+
setPlacing(true);
|
|
1590
|
+
try {
|
|
1591
|
+
const { order, customerId } = await placeStorefrontOrder({
|
|
1592
|
+
config,
|
|
1593
|
+
cart,
|
|
1594
|
+
session,
|
|
1595
|
+
customer: { email: form.email, name: form.name },
|
|
1596
|
+
// `notes` é o recado do comprador para a loja, e o ponto de referência
|
|
1597
|
+
// é exatamente isso: "portão azul, ao lado da padaria" não cabe em
|
|
1598
|
+
// nenhum campo estruturado do endereço e é o que o entregador lê.
|
|
1599
|
+
address: {
|
|
1600
|
+
street: form.street,
|
|
1601
|
+
city: form.city,
|
|
1602
|
+
zip: form.zip,
|
|
1603
|
+
notes: deliveryReference || void 0
|
|
1604
|
+
},
|
|
1605
|
+
// Viaja com o `document` dentro: a assinatura do shop_place_order não
|
|
1606
|
+
// pode ganhar parâmetro sem DROP (0050), e uma trigger o levanta para a
|
|
1607
|
+
// coluna. Na retirada este endereço é o balcão — ver orderAddress().
|
|
1608
|
+
shippingAddress: orderAddress(),
|
|
1609
|
+
// Entrega/retirada e agendamento (0094) vão AQUI porque esta tela chama
|
|
1610
|
+
// placeStorefrontOrder direto, sem passar pelo useCheckout — e sem esta
|
|
1611
|
+
// linha o pedido nascia sem `fulfillment_type`, com a retirada
|
|
1612
|
+
// indistinguível de uma entrega e o frete cobrado assim mesmo. Foi um
|
|
1613
|
+
// pedido de teste real (#1379) que mostrou isso.
|
|
1614
|
+
fulfillmentType,
|
|
1615
|
+
scheduledFor: scheduledFor ?? void 0,
|
|
1616
|
+
paymentMethod
|
|
1617
|
+
});
|
|
1618
|
+
if (customerId) session.setSession({ customerId, email: form.email, name: form.name });
|
|
1619
|
+
leaving.current = true;
|
|
1620
|
+
trackStorefront("checkout_completed", {
|
|
1621
|
+
order_id: order.id,
|
|
1622
|
+
payment_method: paymentMethod,
|
|
1623
|
+
fulfillment: fulfillmentType ?? "delivery",
|
|
1624
|
+
total,
|
|
1625
|
+
online_payment: false
|
|
1626
|
+
});
|
|
1627
|
+
cart.clear();
|
|
1628
|
+
navigateTo(`/order/${order.id}`);
|
|
1629
|
+
} catch (err) {
|
|
1630
|
+
setError(placeOrderMessage(err, t));
|
|
1631
|
+
trackStorefront("checkout_error", { step, field: "place_order", reason: placeOrderMessage(err, t) });
|
|
1632
|
+
setPlacing(false);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function validate() {
|
|
1636
|
+
setError(null);
|
|
1637
|
+
if (requiresAccount && !session.customerId) {
|
|
1638
|
+
setShowSignin(true);
|
|
1639
|
+
return fail(t("error.requiresAccount"));
|
|
1640
|
+
}
|
|
1641
|
+
if (!form.email.trim() || !/.+@.+\..+/.test(form.email)) {
|
|
1642
|
+
return fail(t("error.email"), TID.checkoutEmail);
|
|
1643
|
+
}
|
|
1644
|
+
if (requires("phone") && !form.phone.trim()) {
|
|
1645
|
+
return fail(t("error.phone"), "checkout-phone");
|
|
1646
|
+
}
|
|
1647
|
+
if (!form.name.trim()) {
|
|
1648
|
+
return fail(t("error.name"), TID.checkoutName);
|
|
1649
|
+
}
|
|
1650
|
+
if (!validateDocument()) return false;
|
|
1651
|
+
if (isPickup) return true;
|
|
1652
|
+
if (!validateAddressFields()) return false;
|
|
1653
|
+
if (selectRequiresShipping(cart) && delivery.status === "unserved" && delivery.postalCode === normalizePostalCode(form.zip, country)) {
|
|
1654
|
+
return fail(delivery.refusedZone ? t("error.refusedZone") : t("error.unserved"), TID.checkoutZip);
|
|
1655
|
+
}
|
|
1656
|
+
return true;
|
|
1657
|
+
}
|
|
1658
|
+
return /* @__PURE__ */ jsxs("main", { className: "min-h-screen bg-background", children: [
|
|
1659
|
+
/* @__PURE__ */ jsx(Slot, { area: "checkout", name: "beforeForm", context: checkoutSlotContext }),
|
|
1660
|
+
showSignin && !session.email && /* @__PURE__ */ jsx(
|
|
1661
|
+
SignInModal,
|
|
1662
|
+
{
|
|
1663
|
+
defaultEmail: form.email,
|
|
1664
|
+
onClose: () => setShowSignin(false),
|
|
1665
|
+
onSignedIn: (signed) => setForm((current) => ({ ...current, email: signed.email ?? current.email, name: signed.name || current.name }))
|
|
1666
|
+
}
|
|
1667
|
+
),
|
|
1668
|
+
/* @__PURE__ */ jsx("header", { className: "border-b bg-background", children: /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-6xl items-center justify-between gap-4 px-4 py-6 sm:px-6", children: [
|
|
1669
|
+
/* @__PURE__ */ jsx(Link, { to: returnPath(config), className: "text-sm text-muted-foreground hover:text-foreground", children: t("checkout.backToStore") }),
|
|
1670
|
+
/* @__PURE__ */ jsxs("div", { className: "text-center", children: [
|
|
1671
|
+
/* @__PURE__ */ jsx("div", { className: "sf-heading text-2xl font-bold tracking-tight", children: config.logo ?? config.name }),
|
|
1672
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 flex items-center justify-center gap-2 text-xs text-muted-foreground", children: [
|
|
1673
|
+
/* @__PURE__ */ jsx(Lock, { className: "h-3.5 w-3.5" }),
|
|
1674
|
+
t("checkout.secure")
|
|
1675
|
+
] })
|
|
1676
|
+
] }),
|
|
1677
|
+
/* @__PURE__ */ jsx("a", { href: "mailto:suporte@example.com", className: "text-sm text-muted-foreground hover:text-foreground", children: t("checkout.help") })
|
|
1678
|
+
] }) }),
|
|
1679
|
+
/* @__PURE__ */ jsxs("div", { className: "mx-auto grid max-w-6xl grid-cols-1 gap-8 px-4 py-8 sm:px-6 lg:grid-cols-5 lg:px-6", children: [
|
|
1680
|
+
/* @__PURE__ */ jsxs("section", { className: "lg:col-span-3 lg:pr-10", children: [
|
|
1681
|
+
/* @__PURE__ */ jsx(
|
|
1682
|
+
CheckoutSteps,
|
|
1683
|
+
{
|
|
1684
|
+
current: step,
|
|
1685
|
+
omit: isPickup ? [2] : void 0,
|
|
1686
|
+
onGoBackTo: step === 4 ? void 0 : (target) => goToStep(isPickup && target === 2 ? 1 : target)
|
|
1687
|
+
}
|
|
1688
|
+
),
|
|
1689
|
+
/* @__PURE__ */ jsxs(
|
|
1690
|
+
"form",
|
|
1691
|
+
{
|
|
1692
|
+
className: "space-y-8",
|
|
1693
|
+
onSubmit: (event) => {
|
|
1694
|
+
event.preventDefault();
|
|
1695
|
+
advance();
|
|
1696
|
+
},
|
|
1697
|
+
children: [
|
|
1698
|
+
step === 1 && /* @__PURE__ */ jsxs("section", { children: [
|
|
1699
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex items-center justify-between gap-4", children: [
|
|
1700
|
+
/* @__PURE__ */ jsx("h1", { className: "text-xl font-semibold tracking-tight", children: t("checkout.contact.title") }),
|
|
1701
|
+
session.email ? /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: t("checkout.contact.signedInAs", { email: session.email }) }) : /* @__PURE__ */ jsx(
|
|
1702
|
+
"button",
|
|
1703
|
+
{
|
|
1704
|
+
type: "button",
|
|
1705
|
+
onClick: () => setShowSignin((value) => !value),
|
|
1706
|
+
className: "text-sm font-medium text-primary hover:underline",
|
|
1707
|
+
children: t("checkout.contact.haveAccount")
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
] }),
|
|
1711
|
+
contactAsCard ? /* @__PURE__ */ jsx(
|
|
1712
|
+
ContactSummary,
|
|
1713
|
+
{
|
|
1714
|
+
name: form.name,
|
|
1715
|
+
email: form.email,
|
|
1716
|
+
phone: shows("phone") ? form.phone : void 0,
|
|
1717
|
+
document: shows("document") ? form.document : void 0,
|
|
1718
|
+
onEdit: () => setEditingContact(true),
|
|
1719
|
+
onSignOut: () => {
|
|
1720
|
+
void signOutCustomer().finally(() => {
|
|
1721
|
+
setForm((current) => ({ ...current, email: "", name: "", phone: "", document: "" }));
|
|
1722
|
+
setEditingContact(false);
|
|
1723
|
+
});
|
|
1724
|
+
},
|
|
1725
|
+
labels: { edit: t("checkout.contact.edit"), notYou: t("checkout.contact.notYou"), signOut: t("checkout.contact.signOut") }
|
|
1726
|
+
}
|
|
1727
|
+
) : /* @__PURE__ */ jsxs("div", { className: "grid gap-3 sm:grid-cols-2", children: [
|
|
1728
|
+
checkoutField(t("checkout.contact.email"), TID.checkoutEmail, form.email, set("email"), {
|
|
1729
|
+
type: "email",
|
|
1730
|
+
placeholder: t("checkout.contact.email"),
|
|
1731
|
+
// The account's address: an order under another e-mail would
|
|
1732
|
+
// not show up in this buyer's «Meus pedidos».
|
|
1733
|
+
...session.customerId && session.email ? { readOnly: true, title: t("checkout.contact.emailLocked"), className: "w-full rounded-lg border bg-muted/40 px-3.5 py-3 text-sm text-muted-foreground outline-none" } : {}
|
|
1734
|
+
}),
|
|
1735
|
+
checkoutField(t("checkout.contact.name"), TID.checkoutName, form.name, set("name"), {
|
|
1736
|
+
placeholder: t("checkout.contact.name")
|
|
1737
|
+
}),
|
|
1738
|
+
shows("phone") && checkoutField(t("checkout.contact.phone"), "checkout-phone", form.phone, set("phone"), {
|
|
1739
|
+
type: "tel",
|
|
1740
|
+
autoComplete: "tel",
|
|
1741
|
+
placeholder: requires("phone") ? t("checkout.contact.phone") : t("checkout.contact.phoneOptional")
|
|
1742
|
+
}),
|
|
1743
|
+
shows("document") && (isBrazil ? checkoutField(
|
|
1744
|
+
t("checkout.contact.document"),
|
|
1745
|
+
TID.checkoutDocument,
|
|
1746
|
+
form.document,
|
|
1747
|
+
(value) => {
|
|
1748
|
+
setError(null);
|
|
1749
|
+
setForm((current) => ({ ...current, document: formatDocument(value) }));
|
|
1750
|
+
},
|
|
1751
|
+
{
|
|
1752
|
+
// `inputMode` and not `type="number"`: the mask writes dots
|
|
1753
|
+
// and a dash, which a number input silently refuses to hold.
|
|
1754
|
+
inputMode: "numeric",
|
|
1755
|
+
autoComplete: "off",
|
|
1756
|
+
placeholder: requires("document") ? t("checkout.contact.document") : t("checkout.contact.documentOptional")
|
|
1757
|
+
}
|
|
1758
|
+
) : checkoutField(
|
|
1759
|
+
// The country's own tax id (Spain: NIF/NIE), unmasked: it has letters.
|
|
1760
|
+
countrySchema.taxId?.label[config.language] ?? t("checkout.contact.documentOptional"),
|
|
1761
|
+
TID.checkoutDocument,
|
|
1762
|
+
form.document,
|
|
1763
|
+
set("document"),
|
|
1764
|
+
{ autoComplete: "off", placeholder: countrySchema.taxId?.label[config.language] ?? "" }
|
|
1765
|
+
))
|
|
1766
|
+
] }),
|
|
1767
|
+
requiresAccount && !session.customerId && /* @__PURE__ */ jsxs("p", { className: "mt-3 rounded-lg border border-primary/30 bg-primary/5 px-3 py-2 text-sm", children: [
|
|
1768
|
+
t("checkout.contact.requiresAccount"),
|
|
1769
|
+
" ",
|
|
1770
|
+
/* @__PURE__ */ jsx(
|
|
1771
|
+
"button",
|
|
1772
|
+
{
|
|
1773
|
+
type: "button",
|
|
1774
|
+
onClick: () => setShowSignin(true),
|
|
1775
|
+
className: "font-semibold text-primary hover:underline",
|
|
1776
|
+
children: t("checkout.contact.signInOrCreate")
|
|
1777
|
+
}
|
|
1778
|
+
)
|
|
1779
|
+
] })
|
|
1780
|
+
] }),
|
|
1781
|
+
step === 2 && /* @__PURE__ */ jsxs("section", { children: [
|
|
1782
|
+
/* @__PURE__ */ jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: t("checkout.address.title") }),
|
|
1783
|
+
buyer.allowed.length > 1 && /* @__PURE__ */ jsx(CountrySelect, { className: "mb-3", country, allowed: buyer.allowed, onChange: buyer.setCountry }),
|
|
1784
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-3 grid gap-3", children: [
|
|
1785
|
+
addressesLoading && savedAddresses.length === 0 && /* @__PURE__ */ jsx(SkeletonGroup, { label: t("checkout.address.loadingSaved"), className: "grid gap-3", children: [0, 1].map((row) => /* @__PURE__ */ jsxs("div", { className: "rounded-lg border p-3", children: [
|
|
1786
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-28" }),
|
|
1787
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-2 h-3 w-3/4" }),
|
|
1788
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-1.5 h-3 w-1/2" })
|
|
1789
|
+
] }, row)) }),
|
|
1790
|
+
addressesHere.map((address) => /* @__PURE__ */ jsx(
|
|
1791
|
+
"div",
|
|
1792
|
+
{
|
|
1793
|
+
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"}`,
|
|
1794
|
+
children: /* @__PURE__ */ jsxs("button", { type: "button", onClick: () => selectAddress(address.id), className: "block w-full text-left", children: [
|
|
1795
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
|
|
1796
|
+
address.label,
|
|
1797
|
+
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: t("checkout.address.selected") })
|
|
1798
|
+
] }),
|
|
1799
|
+
/* @__PURE__ */ jsxs("span", { className: "mt-1 block text-muted-foreground", children: [
|
|
1800
|
+
[address.street, address.number].filter(Boolean).join(", "),
|
|
1801
|
+
address.complement ? ` \u2014 ${address.complement}` : ""
|
|
1802
|
+
] }),
|
|
1803
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-muted-foreground", children: [
|
|
1804
|
+
[address.district, address.city, address.state].filter(Boolean).join(" \xB7 "),
|
|
1805
|
+
" \u2014 ",
|
|
1806
|
+
address.postalCode
|
|
1807
|
+
] })
|
|
1808
|
+
] })
|
|
1809
|
+
},
|
|
1810
|
+
address.id
|
|
1811
|
+
)),
|
|
1812
|
+
/* @__PURE__ */ jsxs(
|
|
1813
|
+
"button",
|
|
1814
|
+
{
|
|
1815
|
+
type: "button",
|
|
1816
|
+
onClick: addNewAddress,
|
|
1817
|
+
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"}`,
|
|
1818
|
+
children: [
|
|
1819
|
+
/* @__PURE__ */ jsx("span", { className: "block font-semibold text-primary", children: addressesHere.length > 0 ? t("checkout.address.addNew") : t("checkout.address.enter") }),
|
|
1820
|
+
/* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: t("checkout.address.enterHint") })
|
|
1821
|
+
]
|
|
1822
|
+
}
|
|
1823
|
+
)
|
|
1824
|
+
] }),
|
|
1825
|
+
addressMode === "new" && /* @__PURE__ */ jsx(
|
|
1826
|
+
AddressForm,
|
|
1827
|
+
{
|
|
1828
|
+
country,
|
|
1829
|
+
language: config.language,
|
|
1830
|
+
value: form,
|
|
1831
|
+
onChange: (key, value) => set(key)(value),
|
|
1832
|
+
onPostalCode: setZip,
|
|
1833
|
+
lookup: zipLookup,
|
|
1834
|
+
complement: { visible: shows("complement"), required: requires("complement") }
|
|
1835
|
+
}
|
|
1836
|
+
)
|
|
1837
|
+
] }),
|
|
1838
|
+
step === 3 && placing && /* @__PURE__ */ jsxs("section", { children: [
|
|
1839
|
+
/* @__PURE__ */ jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: t("checkout.confirming.title") }),
|
|
1840
|
+
/* @__PURE__ */ jsxs(SkeletonGroup, { label: t("checkout.confirming.label"), className: "rounded-xl border bg-card p-6 sm:p-8", children: [
|
|
1841
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-6 border-b pb-6 sm:grid-cols-[120px_minmax(0,1fr)]", children: [
|
|
1842
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mx-auto h-24 w-24 rounded-full" }),
|
|
1843
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1844
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-7 w-56 max-w-full" }),
|
|
1845
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-2 h-3.5 w-full max-w-sm" }),
|
|
1846
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-4 h-5 w-40" })
|
|
1847
|
+
] })
|
|
1848
|
+
] }),
|
|
1849
|
+
/* @__PURE__ */ jsx("div", { className: "mt-6 grid gap-4 rounded-xl border bg-muted/20 p-4 sm:grid-cols-3", children: [0, 1, 2].map((column) => /* @__PURE__ */ jsxs("div", { children: [
|
|
1850
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3 w-16" }),
|
|
1851
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "mt-2 h-4 w-24" })
|
|
1852
|
+
] }, column)) }),
|
|
1853
|
+
/* @__PURE__ */ jsx("div", { className: "mt-8 space-y-3", children: cart.lines.slice(0, 3).map((line) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1854
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-12 w-12 rounded-lg" }),
|
|
1855
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 flex-1" }),
|
|
1856
|
+
/* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-16" })
|
|
1857
|
+
] }, line.lineId ?? line.productId)) })
|
|
1858
|
+
] }),
|
|
1859
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3 text-center text-sm text-muted-foreground", children: t("checkout.confirming.keepOpen") })
|
|
1860
|
+
] }),
|
|
1861
|
+
step === 3 && !placing && /* @__PURE__ */ jsxs("section", { children: [
|
|
1862
|
+
/* @__PURE__ */ jsx("h2", { className: "mb-3 text-xl font-semibold tracking-tight", children: t("checkout.payment.title") }),
|
|
1863
|
+
/* @__PURE__ */ jsx("div", { className: "mb-3 grid gap-3", role: "radiogroup", "aria-label": t("checkout.payment.label"), children: paymentMethods.map((method) => {
|
|
1864
|
+
const base = { label: t(PAYMENT_LABELS[method].label), hint: t(PAYMENT_LABELS[method].hint) };
|
|
1865
|
+
const copy = method === "credit_card" && cardIsCharged ? { ...base, hint: t("checkout.payment.cardOnline") } : base;
|
|
1866
|
+
const selected = paymentMethod === method;
|
|
1867
|
+
return (
|
|
1868
|
+
// Cada forma de pagamento abre o que ELA pede, embaixo dela
|
|
1869
|
+
// mesma. Antes o painel do Pix ficava no fim da lista, longe
|
|
1870
|
+
// do cartão escolhido, e escolher parecia não fazer nada.
|
|
1871
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1872
|
+
/* @__PURE__ */ jsxs(
|
|
1873
|
+
"button",
|
|
1874
|
+
{
|
|
1875
|
+
type: "button",
|
|
1876
|
+
role: "radio",
|
|
1877
|
+
"aria-checked": selected,
|
|
1878
|
+
"data-testid": `checkout-payment-${method}`,
|
|
1879
|
+
onClick: () => {
|
|
1880
|
+
setPaymentMethod(method);
|
|
1881
|
+
if (method !== paymentMethod) trackStorefront("checkout_payment_selected", { payment_method: method });
|
|
1882
|
+
},
|
|
1883
|
+
className: `w-full rounded-lg border p-3 text-left text-sm transition hover:bg-muted/40 ${selected ? "border-primary bg-primary/5" : "border-border bg-background"}`,
|
|
1884
|
+
children: [
|
|
1885
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between gap-3 font-semibold", children: [
|
|
1886
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
1887
|
+
/* @__PURE__ */ jsx(CreditCard, { className: "h-4 w-4" }),
|
|
1888
|
+
copy.label
|
|
1889
|
+
] }),
|
|
1890
|
+
selected && /* @__PURE__ */ jsx("span", { className: "rounded-full bg-primary/10 px-2 py-0.5 text-[11px] font-bold text-primary", children: t("checkout.address.selected") })
|
|
1891
|
+
] }),
|
|
1892
|
+
/* @__PURE__ */ jsx("span", { className: "mt-1 block text-muted-foreground", children: copy.hint })
|
|
1893
|
+
]
|
|
1894
|
+
}
|
|
1895
|
+
),
|
|
1896
|
+
selected && chargesOnline && /* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsx(
|
|
1897
|
+
CheckoutPayment,
|
|
1898
|
+
{
|
|
1899
|
+
input: checkoutSessionInput,
|
|
1900
|
+
money,
|
|
1901
|
+
amount: total,
|
|
1902
|
+
onBack: () => goToStep(2),
|
|
1903
|
+
resume,
|
|
1904
|
+
onUnavailable: () => setUnavailable((list) => list.includes(method) ? list : [...list, method]),
|
|
1905
|
+
onPaid: (orderId) => {
|
|
1906
|
+
leaving.current = true;
|
|
1907
|
+
setPaidOrderId(orderId);
|
|
1908
|
+
trackStorefront("checkout_completed", {
|
|
1909
|
+
order_id: orderId,
|
|
1910
|
+
payment_method: method,
|
|
1911
|
+
fulfillment: fulfillmentType ?? "delivery",
|
|
1912
|
+
total,
|
|
1913
|
+
online_payment: true
|
|
1914
|
+
});
|
|
1915
|
+
goToStep(4);
|
|
1916
|
+
cart.clear();
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
) })
|
|
1920
|
+
] }, method)
|
|
1921
|
+
);
|
|
1922
|
+
}) }),
|
|
1923
|
+
/* @__PURE__ */ jsx(Slot, { area: "checkout", name: "afterPaymentMethods", context: checkoutSlotContext }),
|
|
1924
|
+
!chargesOnline && /* @__PURE__ */ jsxs("div", { className: "mt-5", children: [
|
|
1925
|
+
/* @__PURE__ */ jsx(
|
|
1926
|
+
"button",
|
|
1927
|
+
{
|
|
1928
|
+
type: "button",
|
|
1929
|
+
"data-testid": TID.placeOrder,
|
|
1930
|
+
disabled: placing,
|
|
1931
|
+
onClick: () => {
|
|
1932
|
+
void confirmWithoutCharge();
|
|
1933
|
+
},
|
|
1934
|
+
className: "min-h-12 w-full rounded-xl bg-primary px-6 py-3 font-semibold text-primary-foreground transition hover:opacity-90 disabled:opacity-60",
|
|
1935
|
+
children: t("checkout.payment.placeOrder", { total: money(total) })
|
|
1936
|
+
}
|
|
1937
|
+
),
|
|
1938
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-center text-xs text-muted-foreground", children: t("checkout.payment.pendingNote") })
|
|
1939
|
+
] }),
|
|
1940
|
+
DEV_BUILD && chargesOnline && /* @__PURE__ */ jsxs("div", { className: "mt-6 rounded-xl border border-dashed border-amber-500/60 bg-amber-500/5 p-4", children: [
|
|
1941
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs font-semibold uppercase tracking-wide text-amber-600", children: "Somente em desenvolvimento" }),
|
|
1942
|
+
/* @__PURE__ */ jsx(
|
|
1943
|
+
"button",
|
|
1944
|
+
{
|
|
1945
|
+
type: "button",
|
|
1946
|
+
"data-testid": TID.devPlaceOrderWithoutCharge,
|
|
1947
|
+
disabled: placing,
|
|
1948
|
+
onClick: () => {
|
|
1949
|
+
void confirmWithoutCharge();
|
|
1950
|
+
},
|
|
1951
|
+
className: "mt-2 min-h-11 w-full rounded-lg border border-amber-500/60 px-4 py-2.5 text-sm font-medium text-amber-700 transition hover:bg-amber-500/10 disabled:opacity-60",
|
|
1952
|
+
children: `Criar pedido sem cobrar \u2014 ${money(total)}`
|
|
1953
|
+
}
|
|
1954
|
+
),
|
|
1955
|
+
/* @__PURE__ */ jsxs("p", { className: "mt-2 text-xs text-muted-foreground", children: [
|
|
1956
|
+
"Pula o adquirente e grava o pedido como ",
|
|
1957
|
+
/* @__PURE__ */ jsx("code", { children: "pending" }),
|
|
1958
|
+
", igual a uma loja que confirma o pagamento \xE0 m\xE3o. Este bloco n\xE3o vai para produ\xE7\xE3o."
|
|
1959
|
+
] })
|
|
1960
|
+
] })
|
|
1961
|
+
] }),
|
|
1962
|
+
step === 4 && paidOrderId && /* @__PURE__ */ jsxs("section", { "data-testid": TID.checkoutDone, className: "rounded-xl border bg-card p-8 text-center", children: [
|
|
1963
|
+
/* @__PURE__ */ jsx(SuccessMark, {}),
|
|
1964
|
+
/* @__PURE__ */ jsx("h2", { className: "mt-4 text-2xl font-bold tracking-tight", children: t("checkout.done.title") }),
|
|
1965
|
+
/* @__PURE__ */ jsx("p", { className: "mt-2 text-muted-foreground", children: t("checkout.done.text") }),
|
|
1966
|
+
/* @__PURE__ */ jsx(
|
|
1967
|
+
"button",
|
|
1968
|
+
{
|
|
1969
|
+
type: "button",
|
|
1970
|
+
onClick: () => navigateTo(`/order/${paidOrderId}`),
|
|
1971
|
+
className: "mt-6 inline-flex min-h-11 items-center rounded-lg bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground transition hover:opacity-90",
|
|
1972
|
+
children: t("checkout.done.viewOrder")
|
|
1973
|
+
}
|
|
1974
|
+
)
|
|
1975
|
+
] }),
|
|
1976
|
+
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 }),
|
|
1977
|
+
(step === 1 || step === 2) && /* @__PURE__ */ jsxs("div", { className: "grid gap-3 border-t pt-5", children: [
|
|
1978
|
+
/* @__PURE__ */ jsx(
|
|
1979
|
+
"button",
|
|
1980
|
+
{
|
|
1981
|
+
type: "submit",
|
|
1982
|
+
"data-testid": TID.checkoutContinue,
|
|
1983
|
+
className: "min-h-12 rounded-lg bg-primary px-6 py-4 text-sm font-bold text-primary-foreground transition hover:opacity-90",
|
|
1984
|
+
children: step === 1 ? isPickup ? t("checkout.goToPayment", { total: money(total) }) : t("checkout.continueToDelivery") : t("checkout.goToPayment", { total: money(total) })
|
|
1985
|
+
}
|
|
1986
|
+
),
|
|
1987
|
+
step === 2 && /* @__PURE__ */ jsx(
|
|
1988
|
+
"button",
|
|
1989
|
+
{
|
|
1990
|
+
type: "button",
|
|
1991
|
+
"data-testid": TID.checkoutBack,
|
|
1992
|
+
onClick: () => goToStep(1),
|
|
1993
|
+
className: "min-h-11 rounded-lg border px-6 py-3 text-sm font-medium transition hover:bg-muted",
|
|
1994
|
+
children: t("checkout.backToContact")
|
|
1995
|
+
}
|
|
1996
|
+
),
|
|
1997
|
+
/* @__PURE__ */ jsx(Link, { to: returnPath(config), className: "text-center text-sm text-muted-foreground hover:text-foreground", children: t("checkout.backToCart") })
|
|
1998
|
+
] })
|
|
1999
|
+
]
|
|
2000
|
+
}
|
|
2001
|
+
)
|
|
2002
|
+
] }),
|
|
2003
|
+
/* @__PURE__ */ jsxs("aside", { className: "rounded-xl border bg-muted/20 p-5 lg:sticky lg:top-6 lg:col-span-2 lg:self-start", children: [
|
|
2004
|
+
/* @__PURE__ */ jsx("h2", { className: "sr-only", children: t("summary.title") }),
|
|
2005
|
+
/* @__PURE__ */ jsx("ul", { className: "space-y-4", children: cart.lines.map((line) => /* @__PURE__ */ jsxs("li", { className: "flex gap-3 text-sm", children: [
|
|
2006
|
+
/* @__PURE__ */ jsxs("div", { className: "relative h-16 w-16 flex-none overflow-hidden rounded-lg border bg-background", children: [
|
|
2007
|
+
line.imageUrl && /* @__PURE__ */ jsx(SmoothImage, { src: line.imageUrl, alt: line.name, className: "h-full w-full object-cover", variant: { width: 96 } }),
|
|
2008
|
+
/* @__PURE__ */ jsx("span", { className: "absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-muted-foreground px-1.5 text-[10px] font-bold text-background", children: line.quantity })
|
|
2009
|
+
] }),
|
|
2010
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
2011
|
+
/* @__PURE__ */ jsx("p", { className: "truncate font-medium", children: line.name }),
|
|
2012
|
+
line.sku && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: line.sku }),
|
|
2013
|
+
line.optionsLabel && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: line.optionsLabel }),
|
|
2014
|
+
line.notes && /* @__PURE__ */ jsxs("p", { className: "text-xs italic text-muted-foreground", children: [
|
|
2015
|
+
"\u201C",
|
|
2016
|
+
line.notes,
|
|
2017
|
+
"\u201D"
|
|
2018
|
+
] })
|
|
2019
|
+
] }),
|
|
2020
|
+
/* @__PURE__ */ jsx("span", { "data-price": (line.unitPrice * line.quantity).toFixed(2), children: money(line.unitPrice * line.quantity) })
|
|
2021
|
+
] }, line.lineId ?? line.productId)) }),
|
|
2022
|
+
config.features.discounts && /* @__PURE__ */ jsxs("div", { className: "mt-5 space-y-2 border-t pt-5", children: [
|
|
2023
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
2024
|
+
/* @__PURE__ */ jsx(
|
|
2025
|
+
"input",
|
|
2026
|
+
{
|
|
2027
|
+
"data-testid": TID.discountInput,
|
|
2028
|
+
type: "text",
|
|
2029
|
+
placeholder: t("discount.placeholder"),
|
|
2030
|
+
value: discountCode,
|
|
2031
|
+
onChange: (event) => {
|
|
2032
|
+
setDiscountCode(event.target.value);
|
|
2033
|
+
setDiscountSuccess(null);
|
|
2034
|
+
setDiscountError(null);
|
|
2035
|
+
},
|
|
2036
|
+
className: "min-w-0 flex-1 rounded-lg border bg-background px-3 py-2 text-sm uppercase"
|
|
2037
|
+
}
|
|
2038
|
+
),
|
|
2039
|
+
/* @__PURE__ */ jsx(
|
|
2040
|
+
"button",
|
|
2041
|
+
{
|
|
2042
|
+
type: "button",
|
|
2043
|
+
"data-testid": TID.discountApply,
|
|
2044
|
+
onClick: applyDiscountCode,
|
|
2045
|
+
disabled: applyingDiscount || !discountCode.trim(),
|
|
2046
|
+
className: "rounded-lg border px-3 py-2 text-sm font-semibold hover:bg-muted disabled:opacity-50",
|
|
2047
|
+
children: t("discount.apply")
|
|
2048
|
+
}
|
|
2049
|
+
)
|
|
2050
|
+
] }),
|
|
2051
|
+
discountError && /* @__PURE__ */ jsx("p", { "data-testid": TID.discountError, className: "text-xs text-destructive", children: discountError }),
|
|
2052
|
+
discountSuccess && /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold text-emerald-700", children: discountSuccess })
|
|
2053
|
+
] }),
|
|
2054
|
+
/* @__PURE__ */ jsxs("dl", { className: "mt-8 space-y-3 border-t pt-6 text-sm", children: [
|
|
2055
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
2056
|
+
/* @__PURE__ */ jsx("dt", { className: "text-muted-foreground", children: t("summary.subtotal") }),
|
|
2057
|
+
/* @__PURE__ */ jsx("dd", { "data-price": subtotal.toFixed(2), children: money(subtotal) })
|
|
2058
|
+
] }),
|
|
2059
|
+
discountTotal > 0 && /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-emerald-700", "data-testid": TID.discountRow, children: [
|
|
2060
|
+
/* @__PURE__ */ jsxs("dt", { children: [
|
|
2061
|
+
t("summary.discount"),
|
|
2062
|
+
" ",
|
|
2063
|
+
/* @__PURE__ */ jsxs(
|
|
2064
|
+
"button",
|
|
2065
|
+
{
|
|
2066
|
+
type: "button",
|
|
2067
|
+
onClick: clearDiscountCode,
|
|
2068
|
+
className: "text-xs text-muted-foreground underline",
|
|
2069
|
+
children: [
|
|
2070
|
+
"(",
|
|
2071
|
+
cart.discountCode,
|
|
2072
|
+
" \xD7)"
|
|
2073
|
+
]
|
|
2074
|
+
}
|
|
2075
|
+
)
|
|
2076
|
+
] }),
|
|
2077
|
+
/* @__PURE__ */ jsxs("dd", { "data-price": discountTotal.toFixed(2), children: [
|
|
2078
|
+
"-",
|
|
2079
|
+
money(discountTotal)
|
|
2080
|
+
] })
|
|
2081
|
+
] }),
|
|
2082
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between", children: [
|
|
2083
|
+
/* @__PURE__ */ jsxs("dt", { className: cart.discountFreeShipping ? "text-emerald-700" : "text-muted-foreground", children: [
|
|
2084
|
+
isPickup ? t("summary.pickup") : t("summary.delivery"),
|
|
2085
|
+
cart.discountFreeShipping && /* @__PURE__ */ jsxs(
|
|
2086
|
+
"button",
|
|
2087
|
+
{
|
|
2088
|
+
type: "button",
|
|
2089
|
+
onClick: clearDiscountCode,
|
|
2090
|
+
className: "ml-1 text-xs text-muted-foreground underline",
|
|
2091
|
+
children: [
|
|
2092
|
+
"(",
|
|
2093
|
+
cart.discountCode,
|
|
2094
|
+
" \xD7)"
|
|
2095
|
+
]
|
|
2096
|
+
}
|
|
2097
|
+
)
|
|
2098
|
+
] }),
|
|
2099
|
+
/* @__PURE__ */ jsx("dd", { "data-price": shipping.toFixed(2), children: isPickup ? t("summary.noFee") : shipping === 0 ? t("summary.free") : money(shipping) })
|
|
2100
|
+
] }),
|
|
2101
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between border-t pt-5 text-lg font-bold", children: [
|
|
2102
|
+
/* @__PURE__ */ jsx("dt", { children: t("summary.total") }),
|
|
2103
|
+
/* @__PURE__ */ jsx("dd", { "data-price": total.toFixed(2), children: money(total) })
|
|
2104
|
+
] })
|
|
2105
|
+
] }),
|
|
2106
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-6 flex items-center gap-2 text-xs text-muted-foreground", children: [
|
|
2107
|
+
/* @__PURE__ */ jsx(ShieldCheck, { className: "h-4 w-4 text-primary" }),
|
|
2108
|
+
t("summary.protected")
|
|
2109
|
+
] }),
|
|
2110
|
+
/* @__PURE__ */ jsx("div", { className: "mt-6 border-t pt-5", children: /* @__PURE__ */ jsx(Slot, { area: "checkout", name: "afterForm", context: checkoutSlotContext }) })
|
|
2111
|
+
] }),
|
|
2112
|
+
/* @__PURE__ */ jsxs("footer", { className: "flex flex-wrap gap-4 border-t pt-5 text-xs text-muted-foreground lg:col-span-5", children: [
|
|
2113
|
+
/* @__PURE__ */ jsx(Link, { to: "/privacy", className: "hover:text-foreground", children: t("legal.privacy") }),
|
|
2114
|
+
/* @__PURE__ */ jsx(Link, { to: "/terms", className: "hover:text-foreground", children: t("legal.terms") }),
|
|
2115
|
+
/* @__PURE__ */ jsx(Link, { to: "/refunds", className: "hover:text-foreground", children: t("legal.refunds") })
|
|
2116
|
+
] })
|
|
2117
|
+
] })
|
|
2118
|
+
] });
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
export { AddressForm, CheckoutPage, CountrySelect, DeliverToPicker, placeStorefrontOrder };
|
|
2122
|
+
//# sourceMappingURL=chunk-DRMDBXMT.js.map
|
|
2123
|
+
//# sourceMappingURL=chunk-DRMDBXMT.js.map
|