@sazito/checkout 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,834 @@
1
+ 'use client';
2
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
+ import { isValidElement, cloneElement, useState, useRef, useEffect, useCallback } from 'react';
4
+ import { u as useCheckout, b as useSazitoClient, C as CheckoutProvider } from '../chunks/use-checkout-E0O8HsSs.js';
5
+ export { S as SazitoProvider } from '../chunks/use-checkout-E0O8HsSs.js';
6
+ import { m as formatPercent, F as toPersianDigits, s as isValidIranianMobile, y as normalizeIranianPhone, z as paymentMethodInfo, l as formatNumber } from '../chunks/labels-ChywPk2i.js';
7
+ import '@sazito/client-sdk';
8
+
9
+ /**
10
+ * When `asChild` is true, merges the parent's props onto the single React child
11
+ * element instead of rendering a wrapper element — the same pattern Radix UI /
12
+ * shadcn/ui uses. Class names and event handlers are merged; all other props
13
+ * from the parent win (child props act as defaults).
14
+ */
15
+ function Slot({ children, ...slotProps }) {
16
+ if (!isValidElement(children)) {
17
+ if (process.env.NODE_ENV !== 'production') {
18
+ console.warn('[Slot] Must receive exactly one valid React element as child.');
19
+ }
20
+ return jsx(Fragment, { children: children });
21
+ }
22
+ const child = children;
23
+ const childProps = child.props;
24
+ const mergedClassName = [slotProps.className, childProps.className].filter(Boolean).join(' ') || undefined;
25
+ // Merge event handlers: call both if both exist.
26
+ const merged = { ...childProps, ...slotProps };
27
+ if (mergedClassName)
28
+ merged.className = mergedClassName;
29
+ for (const key of Object.keys(childProps)) {
30
+ if (key.startsWith('on') && typeof childProps[key] === 'function' && typeof slotProps[key] === 'function') {
31
+ const slotHandler = slotProps[key];
32
+ const childHandler = childProps[key];
33
+ merged[key] = (...args) => {
34
+ slotHandler(...args);
35
+ childHandler(...args);
36
+ };
37
+ }
38
+ }
39
+ return cloneElement(child, merged);
40
+ }
41
+
42
+ function cx(...parts) {
43
+ return parts.filter(Boolean).join(' ');
44
+ }
45
+ function Button({ variant = 'primary', block, loading, asChild, className, children, disabled, ...rest }) {
46
+ const props = {
47
+ className: cx('szc-btn', `szc-btn--${variant}`, block && 'szc-btn--block', className),
48
+ disabled: disabled || loading,
49
+ 'aria-busy': loading || undefined,
50
+ ...rest,
51
+ };
52
+ if (asChild) {
53
+ return jsx(Slot, { ...props, children: children });
54
+ }
55
+ return (jsxs("button", { type: "button", ...props, children: [loading ? jsx(Spinner, {}) : null, jsx("span", { children: children })] }));
56
+ }
57
+ function Spinner({ asChild, className, children } = {}) {
58
+ if (asChild && children) {
59
+ return jsx(Slot, { className: cx('szc-spinner', className), children: children });
60
+ }
61
+ return jsx("span", { className: cx('szc-spinner', className), "aria-hidden": "true", children: children });
62
+ }
63
+ function ProductPlaceholder({ asChild, className, children } = {}) {
64
+ const props = { className: cx('szc-product-placeholder', className) };
65
+ if (asChild && children) {
66
+ return jsx(Slot, { ...props, children: children });
67
+ }
68
+ return (jsx("span", { ...props, children: children ?? (jsxs("svg", { viewBox: "0 0 64 64", width: "30", height: "30", fill: "none", "aria-hidden": "true", children: [jsx("path", { d: "M32 12 49 21.5v21L32 52 15 42.5v-21L32 12Z", stroke: "currentColor", strokeWidth: "2.25", strokeLinejoin: "round" }), jsx("path", { d: "m15 21.5 17 9.5 17-9.5M32 31v21", stroke: "currentColor", strokeWidth: "2.25", strokeLinecap: "round", strokeLinejoin: "round" })] })) }));
69
+ }
70
+ function FieldLabel({ label, required, optionalLabel }) {
71
+ return (jsxs("span", { className: "szc-field__label", children: [label, !required && optionalLabel ? (jsxs("span", { className: "szc-field__optional", children: ["(", optionalLabel, ")"] })) : null] }));
72
+ }
73
+ function Field({ label, id, className, error, required, optionalLabel, asChild, children, ...rest }) {
74
+ const inputProps = {
75
+ id,
76
+ required,
77
+ className: cx('szc-input', error && 'szc-input--error', className),
78
+ ...rest,
79
+ };
80
+ return (jsxs("label", { className: "szc-field", htmlFor: id, children: [jsx(FieldLabel, { label: label, required: required, optionalLabel: optionalLabel }), asChild && children
81
+ ? jsx(Slot, { ...inputProps, children: children })
82
+ : jsx("input", { ...inputProps }), error ? jsx("span", { className: "szc-field__error", children: error }) : null] }));
83
+ }
84
+ function SectionTitle({ children, asChild, className }) {
85
+ const props = { className: cx('szc-section-title', className) };
86
+ if (asChild && children) {
87
+ return jsx(Slot, { ...props, children: children });
88
+ }
89
+ return jsx("h3", { ...props, children: children });
90
+ }
91
+ function ErrorBanner({ message, asChild, children, className }) {
92
+ const props = {
93
+ className: cx('szc-error', className),
94
+ role: 'alert',
95
+ };
96
+ if (asChild && children) {
97
+ return jsx(Slot, { ...props, children: children });
98
+ }
99
+ return jsx("div", { ...props, children: message });
100
+ }
101
+
102
+ const STEPS = ['cart', 'shipping', 'payment', 'result'];
103
+ const STEP_ICONS = {
104
+ cart: CartIcon,
105
+ shipping: TruckIcon$1,
106
+ payment: CardIcon,
107
+ result: ClipboardIcon
108
+ };
109
+ function Stepper() {
110
+ const { state, t } = useCheckout();
111
+ const labels = {
112
+ cart: t.stepCart,
113
+ shipping: t.stepShipping,
114
+ payment: t.stepPayment,
115
+ result: t.stepResult
116
+ };
117
+ const currentIndex = STEPS.indexOf(state.step);
118
+ return (jsx("ol", { className: "szc-stepper", children: STEPS.map((step, index) => {
119
+ const status = index < currentIndex ? 'done' : index === currentIndex ? 'active' : 'todo';
120
+ const Icon = STEP_ICONS[step];
121
+ return (jsxs("li", { className: `szc-step szc-step--${status}`, children: [jsx("span", { className: "szc-step__dot", children: status === 'done' ? jsx(CheckIcon$2, {}) : jsx(Icon, {}) }), jsx("span", { className: "szc-step__label", children: labels[step] }), index < STEPS.length - 1 ? jsx("span", { className: "szc-step__line" }) : null] }, step));
122
+ }) }));
123
+ }
124
+ function CheckIcon$2() {
125
+ return (jsx("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M3.5 8.5l3 3 6-6.5", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
126
+ }
127
+ function CartIcon() {
128
+ return (jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: [jsx("path", { d: "M1.5 2h1.8l1.9 7.5h6.3l1.5-5H4.8", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }), jsx("circle", { cx: "6.5", cy: "13", r: "1", fill: "currentColor" }), jsx("circle", { cx: "11", cy: "13", r: "1", fill: "currentColor" })] }));
129
+ }
130
+ function TruckIcon$1() {
131
+ return (jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: [jsx("rect", { x: "1", y: "4", width: "9", height: "7", rx: "1", stroke: "currentColor", strokeWidth: "1.6" }), jsx("path", { d: "M10 6.5h2.5l2 2.5V11h-4.5V6.5z", stroke: "currentColor", strokeWidth: "1.6", strokeLinejoin: "round" }), jsx("circle", { cx: "4", cy: "12.5", r: "1.2", stroke: "currentColor", strokeWidth: "1.3" }), jsx("circle", { cx: "12", cy: "12.5", r: "1.2", stroke: "currentColor", strokeWidth: "1.3" })] }));
132
+ }
133
+ function CardIcon() {
134
+ return (jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: [jsx("rect", { x: "1.5", y: "3.5", width: "13", height: "9", rx: "1.5", stroke: "currentColor", strokeWidth: "1.6" }), jsx("path", { d: "M1.5 6.5h13", stroke: "currentColor", strokeWidth: "1.6" }), jsx("rect", { x: "3", y: "8.5", width: "3", height: "1.5", rx: ".5", fill: "currentColor" })] }));
135
+ }
136
+ function ClipboardIcon() {
137
+ return (jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: [jsx("rect", { x: "2.5", y: "2.5", width: "11", height: "12", rx: "1.5", stroke: "currentColor", strokeWidth: "1.6" }), jsx("path", { d: "M5.5 2.5A1.5 1.5 0 0 1 7 1h2a1.5 1.5 0 0 1 1.5 1.5", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }), jsx("path", { d: "M5 7h6M5 9.5h4", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" })] }));
138
+ }
139
+
140
+ function OrderSummary() {
141
+ const { state, summary, money, t } = useCheckout();
142
+ const percentSign = state.locale === 'fa' ? '٪' : '%';
143
+ const lineLabels = {
144
+ subtotal: t.subtotal,
145
+ discount: t.yourSavings,
146
+ shipping: t.shipping,
147
+ credit: t.credit,
148
+ vat: t.vat
149
+ };
150
+ return (jsxs("aside", { className: "szc-summary", children: [jsxs("div", { className: "szc-summary__total-head", children: [jsx("span", { className: "szc-summary__total-label", children: t.totalAmount }), jsx("span", { className: "szc-summary__total-value", children: money(summary.total) })] }), jsx("dl", { className: "szc-summary__lines", children: summary.lines.map((line) => (jsxs("div", { className: `szc-summary__line${line.key === 'discount' ? ' szc-summary__line--discount' : ''}`, children: [jsxs("dt", { children: [lineLabels[line.key], (line.percent ?? 0) > 0
151
+ ? ` (${formatPercent(line.percent, state.locale)}${percentSign})`
152
+ : ''] }), jsx("dd", { className: line.negative ? 'szc-summary__line--neg' : undefined, children: line.free ? (jsx("span", { className: "szc-summary__free", children: t.free })) : (`${line.negative ? '−' : ''}${money(line.amount)}`) })] }, line.key))) }), jsxs("div", { className: "szc-summary__grand", children: [jsx("span", { children: t.total }), jsx("span", { children: money(summary.total) })] })] }));
153
+ }
154
+
155
+ const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹';
156
+ const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩';
157
+ const QUANTITY_DEBOUNCE_MS = 600;
158
+ function toLatinDigits(value) {
159
+ return value
160
+ .replace(/[۰-۹]/g, (digit) => String(PERSIAN_DIGITS.indexOf(digit)))
161
+ .replace(/[٠-٩]/g, (digit) => String(ARABIC_DIGITS.indexOf(digit)));
162
+ }
163
+ const HEX_COLOR_RE = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
164
+ function attrValueToString(value) {
165
+ if (value == null)
166
+ return '';
167
+ if (typeof value === 'object') {
168
+ const v = value.value;
169
+ return v == null ? '' : String(v);
170
+ }
171
+ return String(value);
172
+ }
173
+ function toHexColor(value) {
174
+ if (typeof value !== 'string')
175
+ return null;
176
+ const trimmed = value.trim();
177
+ return HEX_COLOR_RE.test(trimmed) ? (trimmed.startsWith('#') ? trimmed : `#${trimmed}`) : null;
178
+ }
179
+ // Hex can arrive as the raw value, or nested in `extra`/`value` of an object attribute.
180
+ function attrHexColor(value) {
181
+ if (value && typeof value === 'object') {
182
+ const obj = value;
183
+ return toHexColor(obj.extra) ?? toHexColor(obj.value);
184
+ }
185
+ return toHexColor(value);
186
+ }
187
+ function CartStep({ continueShoppingUrl, renderEmpty, }) {
188
+ const { state, actions, money, t } = useCheckout();
189
+ const cart = state.cart;
190
+ const busy = state.flags.updatingCart;
191
+ const invoiceItemsByVariant = new Map((state.invoice?.items ?? []).map((item) => [item.productVariantId, item]));
192
+ if (!cart || cart.items.length === 0) {
193
+ if (renderEmpty) {
194
+ return renderEmpty({ continueShoppingUrl });
195
+ }
196
+ return (jsx("section", { className: "szc-empty", role: "status", "aria-live": "polite", children: jsxs("div", { className: "szc-empty__card", children: [jsx("span", { className: "szc-empty__badge", children: jsx(CartIcon, {}) }), jsxs("div", { className: "szc-empty__copy", children: [jsx("h3", { className: "szc-empty__title", children: t.cartEmpty }), jsx("p", { className: "szc-empty__hint", children: t.cartEmptyHint })] }), continueShoppingUrl ? (jsx(Button, { className: "szc-empty__cta", variant: "primary", onClick: () => { window.location.href = continueShoppingUrl; }, children: t.continueShopping })) : null] }) }));
197
+ }
198
+ return (jsx("section", { className: "szc-step-panel", children: jsx("ul", { className: "szc-cart-list", children: cart.items.map((item) => {
199
+ const max = item.product.maxOrderQuantity;
200
+ const min = item.product.minOrderQuantity ?? 1;
201
+ const invoiceItem = invoiceItemsByVariant.get(item.product.variantId);
202
+ const rawLineTotal = (invoiceItem?.rawPrice ?? item.unitPrice) * item.quantity;
203
+ const itemDiscount = invoiceItem?.customerProfit
204
+ || Math.max(0, rawLineTotal - item.lineTotal);
205
+ return (jsxs("li", { className: "szc-cart-row", children: [item.product.image?.url ? (
206
+ // eslint-disable-next-line @next/next/no-img-element
207
+ jsx("img", { className: "szc-cart-row__thumb", src: item.product.image.url, alt: "" })) : (jsx(ProductPlaceholder, {})), jsxs("div", { className: "szc-cart-row__main", children: [jsx("span", { className: "szc-cart-row__name", children: item.product.name }), item.product.attributes?.length ? (jsx("span", { className: "szc-cart-row__attrs", children: item.product.attributes.map((a, i) => {
208
+ const value = attrValueToString(a.value);
209
+ const hex = attrHexColor(a.value);
210
+ return (jsxs("span", { className: "szc-cart-row__attr", children: [i > 0 ? jsx("span", { className: "szc-cart-row__attr-sep", children: "\u00B7" }) : null, jsxs("span", { children: [a.name, ":"] }), hex ? (jsx("span", { className: "szc-cart-row__swatch", style: { backgroundColor: hex }, title: hex, "aria-hidden": "true" })) : null, value ? jsx("span", { children: value }) : null] }, `${a.name}-${i}`));
211
+ }) })) : null, jsx("span", { className: "szc-cart-row__price", children: money(item.unitPrice) })] }), jsxs("div", { className: "szc-cart-row__side", children: [jsx(QuantityControl, { value: item.quantity, min: min, max: max, label: t.quantity, onChange: (quantity) => actions.updateItemQuantity(item.id, item.product.variantId, quantity) }), itemDiscount > 0 ? (jsx("span", { className: "szc-cart-row__discount", children: t.itemDiscount(money(itemDiscount)) })) : null, jsx("span", { className: "szc-cart-row__line-total", children: money(item.lineTotal) }), jsx("button", { type: "button", className: "szc-cart-row__remove", "aria-label": t.remove, disabled: busy, onClick: () => actions.removeItem(item.id, item.product.variantId), children: jsx(TrashIcon, {}) })] })] }, item.id));
212
+ }) }) }));
213
+ }
214
+ function QuantityControl({ value, min, max, label, onChange }) {
215
+ const [draft, setDraft] = useState(() => toPersianDigits(String(value)));
216
+ const [pending, setPending] = useState(false);
217
+ const draftRef = useRef(draft);
218
+ const pendingValueRef = useRef(null);
219
+ const timerRef = useRef(null);
220
+ const inFlightRef = useRef(false);
221
+ const serverValueRef = useRef(value);
222
+ const onChangeRef = useRef(onChange);
223
+ serverValueRef.current = value;
224
+ onChangeRef.current = onChange;
225
+ const updateDraft = (next) => {
226
+ draftRef.current = next;
227
+ setDraft(next);
228
+ };
229
+ useEffect(() => {
230
+ if (!pending || pendingValueRef.current === value) {
231
+ updateDraft(toPersianDigits(String(value)));
232
+ }
233
+ }, [value, pending]);
234
+ useEffect(() => () => {
235
+ if (timerRef.current)
236
+ clearTimeout(timerRef.current);
237
+ }, []);
238
+ const normalize = (next) => Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min, Math.trunc(next)));
239
+ async function flushPendingUpdate() {
240
+ if (inFlightRef.current)
241
+ return;
242
+ const target = pendingValueRef.current;
243
+ if (target == null)
244
+ return;
245
+ if (target === serverValueRef.current) {
246
+ pendingValueRef.current = null;
247
+ setPending(false);
248
+ return;
249
+ }
250
+ inFlightRef.current = true;
251
+ try {
252
+ await onChangeRef.current(target);
253
+ }
254
+ finally {
255
+ inFlightRef.current = false;
256
+ if (pendingValueRef.current === target) {
257
+ pendingValueRef.current = null;
258
+ setPending(false);
259
+ }
260
+ else if (timerRef.current == null) {
261
+ timerRef.current = setTimeout(() => {
262
+ timerRef.current = null;
263
+ void flushPendingUpdate();
264
+ }, QUANTITY_DEBOUNCE_MS);
265
+ }
266
+ }
267
+ }
268
+ const scheduleUpdate = (rawValue) => {
269
+ const parsed = typeof rawValue === 'number' ? rawValue : Number(toLatinDigits(rawValue));
270
+ const next = normalize(Number.isFinite(parsed) ? parsed : value);
271
+ updateDraft(toPersianDigits(String(next)));
272
+ if (next === serverValueRef.current && !pending && !inFlightRef.current)
273
+ return;
274
+ pendingValueRef.current = next;
275
+ setPending(true);
276
+ if (timerRef.current)
277
+ clearTimeout(timerRef.current);
278
+ timerRef.current = setTimeout(() => {
279
+ timerRef.current = null;
280
+ void flushPendingUpdate();
281
+ }, QUANTITY_DEBOUNCE_MS);
282
+ };
283
+ const step = (amount) => {
284
+ const parsedDraft = Number(toLatinDigits(draftRef.current));
285
+ const base = Number.isFinite(parsedDraft) ? parsedDraft : value;
286
+ scheduleUpdate(base + amount);
287
+ };
288
+ const parsedDraft = Number(toLatinDigits(draft));
289
+ const displayedValue = Number.isFinite(parsedDraft) ? normalize(parsedDraft) : value;
290
+ return (jsxs("div", { className: `szc-qty${pending ? ' szc-qty--pending' : ''}`, "aria-busy": pending, children: [jsx("button", { type: "button", className: "szc-qty__btn", "aria-label": `${label} −`, disabled: displayedValue <= min, onMouseDown: (event) => event.preventDefault(), onClick: () => step(-1), children: jsx(MinusIcon, {}) }), jsx("input", { className: "szc-qty__input", type: "text", inputMode: "numeric", pattern: "[\u06F0-\u06F9\u0660-\u06690-9]*", role: "spinbutton", value: draft, "aria-label": label, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": displayedValue, onChange: (event) => {
291
+ const digits = toLatinDigits(event.target.value).replace(/\D/g, '');
292
+ if (!digits) {
293
+ if (timerRef.current)
294
+ clearTimeout(timerRef.current);
295
+ timerRef.current = null;
296
+ updateDraft('');
297
+ return;
298
+ }
299
+ scheduleUpdate(Number(digits));
300
+ }, onBlur: () => scheduleUpdate(draftRef.current), onKeyDown: (event) => {
301
+ if (event.key === 'Enter') {
302
+ event.currentTarget.blur();
303
+ }
304
+ else if (event.key === 'Escape') {
305
+ event.preventDefault();
306
+ if (timerRef.current)
307
+ clearTimeout(timerRef.current);
308
+ timerRef.current = null;
309
+ pendingValueRef.current = null;
310
+ setPending(false);
311
+ updateDraft(toPersianDigits(String(value)));
312
+ }
313
+ else if (event.key === 'ArrowUp') {
314
+ event.preventDefault();
315
+ step(1);
316
+ }
317
+ else if (event.key === 'ArrowDown') {
318
+ event.preventDefault();
319
+ step(-1);
320
+ }
321
+ } }), jsx("button", { type: "button", className: "szc-qty__btn", "aria-label": `${label} +`, disabled: max != null && displayedValue >= max, onMouseDown: (event) => event.preventDefault(), onClick: () => step(1), children: jsx(PlusIcon, {}) })] }));
322
+ }
323
+ function MinusIcon() {
324
+ return (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M3.5 8h9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
325
+ }
326
+ function PlusIcon() {
327
+ return (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M8 3.5v9M3.5 8h9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
328
+ }
329
+ function TrashIcon() {
330
+ return (jsxs("svg", { viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsx("path", { d: "M3 6h18" }), jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }), jsx("path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }), jsx("path", { d: "M10 11v6M14 11v6" })] }));
331
+ }
332
+
333
+ function validate(form, needsShipping, postalCodeMandatory, emailMandatory, t) {
334
+ const e = {};
335
+ if (!form.firstName.trim())
336
+ e.firstName = t.errorRequired;
337
+ if (!form.lastName.trim())
338
+ e.lastName = t.errorRequired;
339
+ if (!form.mobilePhone.trim()) {
340
+ e.mobilePhone = t.errorRequired;
341
+ }
342
+ else if (!isValidIranianMobile(form.mobilePhone)) {
343
+ e.mobilePhone = t.errorMobilePhone;
344
+ }
345
+ if (emailMandatory && !form.email.trim()) {
346
+ e.email = t.errorRequired;
347
+ }
348
+ else if (form.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) {
349
+ e.email = t.errorEmail;
350
+ }
351
+ if (needsShipping) {
352
+ if (!form.regionId)
353
+ e.regionId = t.errorRequired;
354
+ if (!form.cityId)
355
+ e.cityId = t.errorRequired;
356
+ if (postalCodeMandatory && !form.postalCode.trim())
357
+ e.postalCode = t.errorRequired;
358
+ if (!form.address.trim())
359
+ e.address = t.errorRequired;
360
+ }
361
+ return e;
362
+ }
363
+ function ShippingStep() {
364
+ const { state, actions, t, digitalItems } = useCheckout();
365
+ const { addressForm, regions, shippingGroups } = state;
366
+ const [touched, setTouched] = useState({});
367
+ const needsShipping = state.invoice?.needsShipping ?? true;
368
+ const touch = useCallback((key) => {
369
+ setTouched((prev) => ({ ...prev, [key]: true }));
370
+ }, []);
371
+ const allErrors = validate(addressForm, needsShipping, state.postalCodeMandatory, state.emailMandatory, t);
372
+ const errors = Object.fromEntries(Object.entries(allErrors).filter(([k]) => touched[k]));
373
+ const selectedRegion = regions.find((r) => r.id === addressForm.regionId);
374
+ const handleBlurPhone = (key) => {
375
+ const raw = addressForm[key];
376
+ if (raw) {
377
+ const normalized = normalizeIranianPhone(raw);
378
+ if (normalized !== raw)
379
+ actions.setAddressField(key, normalized);
380
+ }
381
+ touch(key);
382
+ };
383
+ const busy = state.flags.savingAddress || state.flags.loadingShipping;
384
+ return (jsxs("section", { className: `szc-step-panel${busy ? ' szc-step-panel--loading' : ''}`, children: [jsxs("div", { className: "szc-shipping-form-card", children: [jsxs("section", { className: "szc-shipping-section", "aria-labelledby": "szc-contact-title", children: [jsx(ShippingSectionHeader, { id: "szc-contact-title", title: t.contactInfo, icon: "contact" }), jsxs("div", { className: "szc-form-grid", children: [jsx(Field, { id: "szc-first-name", label: t.firstName, required: true, optionalLabel: t.optional, value: addressForm.firstName, error: errors.firstName, onChange: (e) => actions.setAddressField('firstName', e.target.value), onBlur: () => touch('firstName') }), jsx(Field, { id: "szc-last-name", label: t.lastName, required: true, optionalLabel: t.optional, value: addressForm.lastName, error: errors.lastName, onChange: (e) => actions.setAddressField('lastName', e.target.value), onBlur: () => touch('lastName') }), jsx(Field, { id: "szc-mobile", label: t.mobilePhone, required: true, optionalLabel: t.optional, className: "szc-input--ltr", dir: "ltr", inputMode: "tel", value: addressForm.mobilePhone, error: errors.mobilePhone, onChange: (e) => actions.setAddressField('mobilePhone', e.target.value), onBlur: () => handleBlurPhone('mobilePhone') }), jsx(Field, { id: "szc-email", label: t.email, required: state.emailMandatory, optionalLabel: t.optional, className: "szc-input--ltr", dir: "ltr", inputMode: "email", value: addressForm.email, error: errors.email, onChange: (e) => actions.setAddressField('email', e.target.value), onBlur: () => touch('email') })] })] }), needsShipping ? (jsxs("section", { className: "szc-shipping-section", "aria-labelledby": "szc-address-title", children: [jsx(ShippingSectionHeader, { id: "szc-address-title", title: t.reviewAddress, icon: "address" }), jsxs("div", { className: "szc-form-grid", children: [jsx(EnhancedSelect, { id: "szc-region", label: t.region, required: true, optionalLabel: t.optional, value: addressForm.regionId ?? '', error: errors.regionId, onChange: (v) => {
385
+ actions.setAddressField('regionId', v ? Number(v) : null);
386
+ touch('regionId');
387
+ }, placeholder: t.selectRegion, options: regions.map((r) => ({ value: r.id, label: r.name })) }), jsx(EnhancedSelect, { id: "szc-city", label: t.city, required: true, optionalLabel: t.optional, value: addressForm.cityId ?? '', disabled: !selectedRegion, error: errors.cityId, onChange: (v) => {
388
+ actions.setAddressField('cityId', v ? Number(v) : null);
389
+ touch('cityId');
390
+ }, placeholder: t.selectCity, options: selectedRegion?.cities.map((c) => ({ value: c.id, label: c.name })) ?? [] }), jsx(Field, { id: "szc-postal", label: t.postalCode, required: state.postalCodeMandatory, optionalLabel: t.optional, className: "szc-input--ltr", dir: "ltr", inputMode: "numeric", value: addressForm.postalCode, error: errors.postalCode, onChange: (e) => actions.setAddressField('postalCode', e.target.value), onBlur: () => touch('postalCode') }), jsx(Field, { id: "szc-phone", label: t.phoneNumber, optionalLabel: t.optional, className: "szc-input--ltr", dir: "ltr", inputMode: "tel", value: addressForm.phoneNumber, onChange: (e) => actions.setAddressField('phoneNumber', e.target.value), onBlur: () => handleBlurPhone('phoneNumber') })] }), jsx(Field, { id: "szc-address", label: t.addressLine, required: true, optionalLabel: t.optional, value: addressForm.address, error: errors.address, onChange: (e) => actions.setAddressField('address', e.target.value), onBlur: () => touch('address') }), jsx(Field, { id: "szc-description", label: t.description, optionalLabel: t.optional, value: addressForm.description, onChange: (e) => actions.setAddressField('description', e.target.value), onBlur: () => touch('description') })] })) : null] }), shippingGroups.length > 0 ? (jsxs("section", { className: "szc-shipping-methods", "aria-labelledby": "szc-methods-title", children: [jsx(ShippingSectionHeader, { id: "szc-methods-title", title: t.shippingMethods, icon: "shipping" }), jsx("div", { className: "szc-ship-groups", children: shippingGroups.map((group) => (jsx(ShippingGroupCard, { group: group }, group.key))) })] })) : needsShipping ? (jsxs("div", { className: "szc-shipping-pending", role: "note", children: [jsx("span", { className: "szc-shipping-pending__icon", "aria-hidden": "true", children: jsx(TruckIcon, {}) }), jsxs("div", { className: "szc-shipping-pending__copy", children: [jsx("strong", { children: t.shippingMethods }), jsx("span", { children: t.shippingMethodsHint })] })] })) : null, digitalItems.length > 0 ? (jsxs("div", { className: "szc-digital-note", children: [jsx("span", { className: "szc-digital-note__icon", children: "\u2B07\uFE0E" }), jsxs("div", { children: [jsx("strong", { children: t.digitalNoShipping }), jsx("ul", { children: digitalItems.map((item) => (jsx("li", { children: item.name }, item.id))) })] })] })) : null] }));
391
+ }
392
+ function ShippingSectionHeader({ id, title, icon }) {
393
+ return (jsxs("header", { className: "szc-shipping-section__head", children: [jsx("span", { className: "szc-shipping-section__icon", "aria-hidden": "true", children: icon === 'contact' ? jsx(ContactIcon, {}) : icon === 'address' ? jsx(AddressIcon, {}) : jsx(TruckIcon, {}) }), jsx("h2", { id: id, className: "szc-shipping-section__title", children: title })] }));
394
+ }
395
+ function ContactIcon() {
396
+ return (jsxs("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: [jsx("circle", { cx: "12", cy: "8", r: "3.25", stroke: "currentColor", strokeWidth: "1.7" }), jsx("path", { d: "M5.5 19c.65-3.35 3-5.25 6.5-5.25s5.85 1.9 6.5 5.25", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" })] }));
397
+ }
398
+ function AddressIcon() {
399
+ return (jsxs("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: [jsx("path", { d: "M19 10c0 4.7-7 10-7 10S5 14.7 5 10a7 7 0 1 1 14 0Z", stroke: "currentColor", strokeWidth: "1.7", strokeLinejoin: "round" }), jsx("circle", { cx: "12", cy: "10", r: "2.25", stroke: "currentColor", strokeWidth: "1.7" })] }));
400
+ }
401
+ function EnhancedSelect({ id, label, value, placeholder, options, required, optionalLabel, disabled, error, onChange }) {
402
+ return (jsxs("label", { className: "szc-field", htmlFor: id, children: [jsx(FieldLabel, { label: label, required: required, optionalLabel: optionalLabel }), jsxs("div", { className: "szc-select-wrap", children: [jsxs("select", { id: id, className: `szc-input szc-select-enhanced${error ? ' szc-input--error' : ''}`, value: value, required: required, disabled: disabled, onChange: (e) => onChange(e.target.value), children: [jsx("option", { value: "", children: placeholder }), options.map((opt) => (jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }), jsx("span", { className: "szc-select-chevron", "aria-hidden": "true", children: jsx(ChevronIcon, {}) })] }), error ? jsx("span", { className: "szc-field__error", children: error }) : null] }));
403
+ }
404
+ function ChevronIcon() {
405
+ return (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }));
406
+ }
407
+ /**
408
+ * Click-and-drag horizontal scrolling for a container with a hidden scrollbar.
409
+ * Also maps vertical mouse-wheel deltas to horizontal scroll so a plain mouse
410
+ * (no horizontal wheel) can still pan the list.
411
+ */
412
+ function useDragScroll() {
413
+ const ref = useRef(null);
414
+ const drag = useRef({ down: false, startX: 0, startLeft: 0 });
415
+ useEffect(() => {
416
+ const el = ref.current;
417
+ if (!el)
418
+ return;
419
+ const onWheel = (e) => {
420
+ if (el.scrollWidth <= el.clientWidth)
421
+ return; // nothing to pan
422
+ if (Math.abs(e.deltaY) <= Math.abs(e.deltaX))
423
+ return; // already horizontal
424
+ e.preventDefault();
425
+ el.scrollLeft += e.deltaY;
426
+ };
427
+ el.addEventListener('wheel', onWheel, { passive: false });
428
+ return () => el.removeEventListener('wheel', onWheel);
429
+ }, []);
430
+ const onPointerDown = (e) => {
431
+ const el = ref.current;
432
+ if (!el)
433
+ return;
434
+ drag.current = { down: true, startX: e.clientX, startLeft: el.scrollLeft };
435
+ el.setPointerCapture(e.pointerId);
436
+ el.classList.add('szc-dragging');
437
+ };
438
+ const onPointerMove = (e) => {
439
+ const el = ref.current;
440
+ if (!el || !drag.current.down)
441
+ return;
442
+ el.scrollLeft = drag.current.startLeft - (e.clientX - drag.current.startX);
443
+ };
444
+ const onPointerUp = (e) => {
445
+ const el = ref.current;
446
+ if (!el || !drag.current.down)
447
+ return;
448
+ drag.current.down = false;
449
+ el.classList.remove('szc-dragging');
450
+ try {
451
+ el.releasePointerCapture(e.pointerId);
452
+ }
453
+ catch {
454
+ /* pointer already released */
455
+ }
456
+ };
457
+ return { ref, onPointerDown, onPointerMove, onPointerUp };
458
+ }
459
+ function ShippingGroupCard({ group }) {
460
+ const { actions, price, state, t } = useCheckout();
461
+ const busy = state.flags.selectingRate;
462
+ const selectedRate = group.rates.find((rate) => rate.id === group.selectedRateId);
463
+ const showRateSelector = group.items.length !== 1 || group.rates.length !== 1;
464
+ const itemsScroll = useDragScroll();
465
+ return (jsxs("div", { className: "szc-ship-group", children: [jsxs("header", { className: "szc-ship-group__head", children: [jsx("span", { className: "szc-ship-group__mark", style: shippingRateIconStyle(selectedRate?.color), "aria-hidden": "true", children: selectedRate ? jsx(ShippingRateIcon, { rate: selectedRate }) : jsx(TruckIcon, {}) }), jsxs("div", { className: "szc-ship-group__heading", children: [jsxs("div", { className: "szc-ship-group__title-row", children: [jsx("strong", { className: "szc-ship-group__title", children: group.title || selectedRate?.name || t.shippingMethod }), jsx("span", { className: "szc-ship-group__count", children: t.productCount(toPersianDigits(String(group.items.length))) })] }), selectedRate ? (jsxs("span", { className: "szc-ship-group__hint", children: [selectedRate.description ? (jsx("span", { className: "szc-ship-group__desc", children: selectedRate.description })) : null, jsx("span", { className: "szc-ship-group__rate-price", children: selectedRate.price > 0 ? price(selectedRate.price) : t.free })] })) : (jsx("span", { className: "szc-ship-group__hint", children: t.shippingMethods }))] })] }), jsx("div", { className: "szc-ship-group__items", ref: itemsScroll.ref, onPointerDown: itemsScroll.onPointerDown, onPointerMove: itemsScroll.onPointerMove, onPointerUp: itemsScroll.onPointerUp, onPointerCancel: itemsScroll.onPointerUp, children: group.items.map((item) => (jsxs("article", { className: "szc-ship-product", children: [jsxs("div", { className: "szc-ship-product__media", children: [item.image?.url ? (jsx("img", { src: item.image.url, alt: item.image.alt || item.name })) : (jsx(ProductPlaceholder, {})), jsx("span", { className: "szc-ship-product__quantity", children: toPersianDigits(String(item.quantity)) })] }), jsxs("div", { className: "szc-ship-product__copy", children: [jsx("strong", { className: "szc-ship-product__name", children: item.name }), item.attributes.length > 0 ? (jsx("span", { className: "szc-ship-product__attrs", children: item.attributes.map((attribute) => `${attribute.name}: ${attribute.value}`).join(' · ') })) : null] })] }, item.id))) }), !showRateSelector && selectedRate?.description?.trim() ? (jsx("div", { className: "szc-ship-group__description", children: selectedRate.description.trim() })) : null, showRateSelector ? (jsx("div", { className: "szc-rate-list", role: "radiogroup", children: group.rates.map((rate) => {
466
+ const selected = group.selectedRateId === rate.id;
467
+ const description = rate.description?.trim();
468
+ return (jsxs("button", { type: "button", role: "radio", "aria-checked": selected, "aria-label": description ? `${rate.name} — ${description}` : rate.name, disabled: busy, className: `szc-rate${selected ? ' szc-rate--selected' : ''}`, onClick: () => actions.selectShippingRate(group.key, rate.id), children: [jsx("span", { className: "szc-rate__icon", style: shippingRateIconStyle(rate.color), "aria-hidden": "true", children: jsx(ShippingRateIcon, { rate: rate }) }), jsxs("span", { className: "szc-rate__copy", children: [jsx("span", { className: "szc-rate__name", children: rate.name }), rate.price > 0 ? (jsx("span", { className: "szc-rate__price", children: price(rate.price) })) : null] }), jsx("span", { className: "szc-rate__radio", "aria-hidden": "true", children: selected ? jsx(CheckIcon$1, {}) : null }), description ? (jsx("span", { className: "szc-rate__description", children: description })) : null] }, rate.id));
469
+ }) })) : null] }));
470
+ }
471
+ const ICON_PROPS$1 = {
472
+ viewBox: '0 0 24 24',
473
+ width: 26,
474
+ height: 26,
475
+ fill: 'none',
476
+ stroke: 'currentColor',
477
+ strokeWidth: 1.8,
478
+ strokeLinecap: 'round',
479
+ strokeLinejoin: 'round',
480
+ 'aria-hidden': true,
481
+ };
482
+ function TruckIcon() {
483
+ return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("path", { d: "M14 17V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h1" }), jsx("path", { d: "M9 18h5" }), jsx("path", { d: "M18 18h1a1 1 0 0 0 1-1v-3.28a1 1 0 0 0-.21-.62l-2.7-3.39A1 1 0 0 0 16.3 9H14" }), jsx("circle", { cx: "7", cy: "18", r: "2" }), jsx("circle", { cx: "16", cy: "18", r: "2" })] }));
484
+ }
485
+ function ShippingRateIcon({ rate }) {
486
+ const iconKey = shippingRateIconKey(rate);
487
+ switch (iconKey) {
488
+ case 'post':
489
+ return jsx(PostIcon, {});
490
+ case 'free-delivery':
491
+ return jsx(FreeDeliveryIcon, {});
492
+ case 'courier':
493
+ return jsx(CourierIcon, {});
494
+ case 'delivery-man':
495
+ return jsx(DeliveryManIcon, {});
496
+ default:
497
+ return rate.icon && isImageUrl(rate.icon) ? jsx("img", { src: rate.icon, alt: "" }) : jsx(TruckIcon, {});
498
+ }
499
+ }
500
+ function shippingRateIconKey(rate) {
501
+ const explicit = (rate.icon || rate.type || '').trim().toLowerCase().replace(/_/g, '-');
502
+ if (explicit)
503
+ return explicit;
504
+ const name = rate.name.trim().toLowerCase();
505
+ if (name.includes('پست') || name.includes('post'))
506
+ return 'post';
507
+ if (name.includes('رایگان') || name.includes('free'))
508
+ return 'free-delivery';
509
+ if (name.includes('پیک') || name.includes('courier'))
510
+ return 'courier';
511
+ if (name.includes('حضوری') || name.includes('delivery man'))
512
+ return 'delivery-man';
513
+ return '';
514
+ }
515
+ /**
516
+ * The API returns `color` as a palette *token* (e.g. `mainColor`, `green`) — not
517
+ * always a CSS color. Map the known tokens to concrete CSS colors so each rate
518
+ * renders its own chip color instead of every icon falling back to the accent.
519
+ */
520
+ const SHIPPING_COLOR_TOKENS = {
521
+ green: '#16a34a',
522
+ red: '#dc2626',
523
+ blue: '#2563eb',
524
+ orange: '#ea580c',
525
+ yellow: '#eab308',
526
+ purple: '#7c3aed',
527
+ pink: '#db2777',
528
+ teal: '#0d9488',
529
+ cyan: '#0891b2',
530
+ indigo: '#4f46e5',
531
+ gray: '#6b7280',
532
+ grey: '#6b7280',
533
+ black: '#0f172a',
534
+ brown: '#92400e'
535
+ };
536
+ function resolveShippingColor(raw) {
537
+ const token = raw.trim().toLowerCase();
538
+ if (!token)
539
+ return undefined;
540
+ // The shop's "main color" is the theme accent → let the CSS fallback handle it.
541
+ if (token === 'maincolor' || token === 'main')
542
+ return undefined;
543
+ if (token === 'transparent' || token === 'white')
544
+ return '#ffffff';
545
+ if (SHIPPING_COLOR_TOKENS[token])
546
+ return SHIPPING_COLOR_TOKENS[token];
547
+ // Already a concrete CSS color value (hex / rgb()).
548
+ if (parseColorChannels(token))
549
+ return token;
550
+ return undefined;
551
+ }
552
+ function shippingRateIconStyle(color) {
553
+ if (!color)
554
+ return undefined;
555
+ const resolved = resolveShippingColor(color);
556
+ // Unknown token / unparseable → let CSS use the theme's soft accent treatment.
557
+ if (!resolved)
558
+ return undefined;
559
+ const rgb = parseColorChannels(resolved);
560
+ if (!rgb)
561
+ return undefined;
562
+ const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
563
+ // A white / near-white brand color (common for "post") would vanish against
564
+ // the card. Render a neutral chip with a slate glyph instead.
565
+ if (luminance > 0.9) {
566
+ return {
567
+ '--szc-rate-color': '#eef0f5',
568
+ '--szc-rate-foreground': '#475569'
569
+ };
570
+ }
571
+ const foreground = luminance > 0.62
572
+ ? `rgb(${rgb.map((channel) => Math.round(channel * 0.58)).join(', ')})`
573
+ : resolved;
574
+ return {
575
+ '--szc-rate-color': `color-mix(in srgb, ${resolved} 13%, var(--szc-card))`,
576
+ '--szc-rate-foreground': foreground
577
+ };
578
+ }
579
+ function parseColorChannels(value) {
580
+ const hex = value.match(/^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i)?.[1];
581
+ if (hex?.length === 3) {
582
+ return [0, 1, 2].map((index) => parseInt(hex[index] + hex[index], 16));
583
+ }
584
+ if (hex && hex.length >= 6) {
585
+ return [
586
+ parseInt(hex.slice(0, 2), 16),
587
+ parseInt(hex.slice(2, 4), 16),
588
+ parseInt(hex.slice(4, 6), 16)
589
+ ];
590
+ }
591
+ const channels = value.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
592
+ if (channels)
593
+ return [Number(channels[1]), Number(channels[2]), Number(channels[3])];
594
+ return null;
595
+ }
596
+ function isImageUrl(value) {
597
+ return /^(https?:)?\/\//i.test(value) || value.startsWith('/') || value.startsWith('data:');
598
+ }
599
+ function PostIcon() {
600
+ return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("path", { d: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" }), jsx("path", { d: "M3.3 7 12 12l8.7-5" }), jsx("path", { d: "M12 22V12" }), jsx("path", { d: "m7.5 4.6 9 5.2" })] }));
601
+ }
602
+ function FreeDeliveryIcon() {
603
+ return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("path", { d: "M21 11V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l1.5-.86" }), jsx("path", { d: "M3.3 7 12 12l8.7-5" }), jsx("path", { d: "M12 22V12" }), jsx("path", { d: "m15.5 17 2 2 4-4" })] }));
604
+ }
605
+ function CourierIcon() {
606
+ return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("circle", { cx: "5.5", cy: "17.5", r: "3.5" }), jsx("circle", { cx: "18.5", cy: "17.5", r: "3.5" }), jsx("circle", { cx: "15", cy: "5", r: "1" }), jsx("path", { d: "M12 17.5V14l-3-3 4-3 2 3h2" })] }));
607
+ }
608
+ function DeliveryManIcon() {
609
+ return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("circle", { cx: "9", cy: "4.5", r: "2.5" }), jsx("path", { d: "M5.5 21v-6.5a3.5 3.5 0 0 1 7 0V21" }), jsx("rect", { x: "14", y: "11.5", width: "6.5", height: "7", rx: "1.2" }), jsx("path", { d: "M14 15h6.5" }), jsx("path", { d: "M17.2 11.5v3.5" })] }));
610
+ }
611
+ function CheckIcon$1() {
612
+ return (jsx("svg", { viewBox: "0 0 16 16", width: "12", height: "12", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "m3.2 8.2 3 3 6.6-6.4", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
613
+ }
614
+
615
+ /** Human line for the applied-code card: "20% off (50,000)", "Free shipping", … */
616
+ function describeDiscount(discount, t, money, locale) {
617
+ const parts = [];
618
+ if (discount.kind === 'percentage' && (discount.percent ?? 0) > 0) {
619
+ const percentOff = t.discountPercentOff(formatPercent(discount.percent, locale));
620
+ parts.push(discount.amount > 0 ? `${percentOff} (${money(discount.amount)})` : percentOff);
621
+ }
622
+ else if (discount.amount > 0) {
623
+ parts.push(t.discountAmountOff(money(discount.amount)));
624
+ }
625
+ if (discount.kind === 'free_shipping' || (discount.shippingSaved ?? 0) > 0) {
626
+ parts.push(t.discountFreeShipping);
627
+ }
628
+ return parts.length > 0 ? parts.join(' + ') : null;
629
+ }
630
+ function PaymentStep() {
631
+ const { state, actions, t, money } = useCheckout();
632
+ const { paymentMethods, selectedPaymentMethodId, appliedDiscount, discountCode, discountError, flags, locale } = state;
633
+ return (jsxs("section", { className: "szc-step-panel", children: [jsx(SectionTitle, { children: t.paymentMethod }), flags.loadingPayments ? (jsxs("p", { className: "szc-inline-loading", children: [jsx(Spinner, {}), " ", t.loading] })) : (jsx("div", { className: "szc-pay-list", role: "radiogroup", "aria-label": t.paymentMethod, children: paymentMethods.map((method) => {
634
+ const selected = selectedPaymentMethodId === method.id;
635
+ const info = paymentMethodInfo(method.code, locale);
636
+ // Prefer backend-provided title/description; fall back to derived labels.
637
+ const backendTitle = locale === 'fa' ? method.titleFa : method.title;
638
+ const title = backendTitle?.trim() || info.title;
639
+ const description = method.description?.trim() || info.description;
640
+ return (jsxs("button", { type: "button", role: "radio", "aria-checked": selected, className: `szc-pay${selected ? ' szc-pay--selected' : ''}`, onClick: () => actions.selectPaymentMethod(method.id), children: [jsx("span", { className: "szc-pay__icon", "aria-hidden": "true", children: jsx(PaymentIcon, { kind: info.kind }) }), jsxs("span", { className: "szc-pay__body", children: [jsx("span", { className: "szc-pay__name", children: title }), jsx("span", { className: "szc-pay__desc", children: description })] }), jsx("span", { className: "szc-pay__radio", "aria-hidden": "true", children: selected ? jsx(CheckIcon, {}) : null })] }, method.id));
641
+ }) })), jsx(SectionTitle, { children: t.discountCode }), appliedDiscount ? (jsxs("div", { className: "szc-discount szc-discount--applied", children: [jsx("span", { className: "szc-discount__icon", "aria-hidden": "true", children: jsx(TagIcon, {}) }), jsxs("span", { className: "szc-discount__body", children: [jsx("code", { className: "szc-discount__code", children: appliedDiscount.code }), jsx("span", { className: "szc-discount__desc", children: describeDiscount(appliedDiscount, t, money, locale) ?? t.applied })] }), jsxs("span", { className: "szc-discount__badge", children: [jsx(CheckIcon, {}), " ", t.applied] }), jsx(Button, { variant: "ghost", onClick: () => actions.removeDiscount(), loading: flags.applyingDiscount, children: t.remove })] })) : (jsxs("div", { className: "szc-discount-field", children: [jsxs("div", { className: "szc-discount", children: [jsxs("span", { className: "szc-discount__inputwrap", children: [jsx(TagIcon, {}), jsx("input", { className: `szc-input${discountError ? ' szc-input--error' : ''}`, placeholder: t.discountPlaceholder, value: discountCode, "aria-invalid": discountError ? true : undefined, onChange: (e) => actions.setDiscountCode(e.target.value), onKeyDown: (e) => {
642
+ if (e.key === 'Enter')
643
+ actions.applyDiscount();
644
+ } })] }), jsx(Button, { variant: "outline", onClick: () => actions.applyDiscount(), loading: flags.applyingDiscount, disabled: !discountCode.trim(), children: t.apply })] }), discountError ? (jsxs("p", { className: "szc-discount__error", role: "alert", children: [jsx(AlertIcon, {}), discountError] })) : null] }))] }));
645
+ }
646
+ function TagIcon() {
647
+ return (jsxs("svg", { viewBox: "0 0 20 20", width: "18", height: "18", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsx("path", { d: "M10.5 2.5H16v5.5l-7.5 7.5a1.8 1.8 0 0 1-2.5 0L3 13a1.8 1.8 0 0 1 0-2.5z" }), jsx("circle", { cx: "12.8", cy: "5.7", r: "1", fill: "currentColor", stroke: "none" })] }));
648
+ }
649
+ const ICON_PROPS = {
650
+ viewBox: '0 0 20 20',
651
+ width: 20,
652
+ height: 20,
653
+ fill: 'none',
654
+ stroke: 'currentColor',
655
+ strokeWidth: 1.4,
656
+ strokeLinecap: 'round',
657
+ strokeLinejoin: 'round',
658
+ 'aria-hidden': true
659
+ };
660
+ function AlertIcon() {
661
+ return (jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: [jsx("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeWidth: "1.4" }), jsx("path", { d: "M8 4.8v3.4", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" }), jsx("circle", { cx: "8", cy: "11", r: "0.85", fill: "currentColor" })] }));
662
+ }
663
+ function CheckIcon() {
664
+ return (jsx("svg", { viewBox: "0 0 16 16", width: "12", height: "12", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "m3.2 8.2 3 3 6.6-6.4", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
665
+ }
666
+ function PaymentIcon({ kind }) {
667
+ switch (kind) {
668
+ case 'card':
669
+ // two cards / transfer
670
+ return (jsxs("svg", { ...ICON_PROPS, children: [jsx("rect", { x: "2", y: "5", width: "13", height: "9", rx: "2" }), jsx("path", { d: "M2 8h13M5.5 11.5h3" }), jsx("path", { d: "M15.5 6.5h2.5v9h-9v-1.5" })] }));
671
+ case 'cod':
672
+ // package / hand
673
+ return (jsxs("svg", { ...ICON_PROPS, children: [jsx("path", { d: "M10 2.5 17 6v8l-7 3.5L3 14V6z" }), jsx("path", { d: "M3 6l7 3.5L17 6M10 9.5v8" })] }));
674
+ case 'bnpl':
675
+ // calendar / installments
676
+ return (jsxs("svg", { ...ICON_PROPS, children: [jsx("rect", { x: "3", y: "4", width: "14", height: "13", rx: "2" }), jsx("path", { d: "M3 8h14M7 2.5v3M13 2.5v3M6.5 12h3" })] }));
677
+ case 'wallet':
678
+ return (jsxs("svg", { ...ICON_PROPS, children: [jsx("rect", { x: "2.5", y: "5", width: "15", height: "11", rx: "2.5" }), jsx("path", { d: "M2.5 9h15" }), jsx("circle", { cx: "14", cy: "12.5", r: "1.1", fill: "currentColor", stroke: "none" })] }));
679
+ case 'free':
680
+ // tag
681
+ return (jsxs("svg", { ...ICON_PROPS, children: [jsx("path", { d: "M10.5 2.5H16v5.5l-7.5 7.5a1.8 1.8 0 0 1-2.5 0L3 13a1.8 1.8 0 0 1 0-2.5z" }), jsx("circle", { cx: "12.8", cy: "5.7", r: "1", fill: "currentColor", stroke: "none" })] }));
682
+ case 'gateway':
683
+ default:
684
+ // single card / bank
685
+ return (jsxs("svg", { ...ICON_PROPS, children: [jsx("rect", { x: "2.5", y: "4.5", width: "15", height: "11", rx: "2" }), jsx("path", { d: "M2.5 8.5h15M5.5 12.5h4" })] }));
686
+ }
687
+ }
688
+
689
+ function ResultStep({ continueShoppingUrl }) {
690
+ const { state, actions, t } = useCheckout();
691
+ const result = state.result;
692
+ const status = result?.status ?? 'pending';
693
+ const titleMap = {
694
+ success: t.paymentSuccess,
695
+ failed: t.paymentFailed,
696
+ pending: t.paymentPending,
697
+ stock_violated: t.paymentFailed
698
+ };
699
+ return (jsxs("section", { className: `szc-result szc-result--${status}`, children: [jsx("div", { className: "szc-result__icon", children: status === 'success' ? (jsx(CheckCircle, {})) : status === 'pending' ? (jsx(Spinner, {})) : (jsx(CrossCircle, {})) }), jsx("h2", { className: "szc-result__title", children: titleMap[status] }), status === 'pending' ? jsx("p", { className: "szc-muted", children: t.paymentPendingHint }) : null, result?.message ? jsx("p", { className: "szc-muted", children: result.message }) : null, result?.order ? (jsxs("p", { className: "szc-result__order", children: [t.orderNumber, ": ", jsx("strong", { children: result.order.orderNumber })] })) : null, jsxs("div", { className: "szc-result__actions", children: [status === 'failed' || status === 'stock_violated' ? (jsx(Button, { onClick: () => actions.goToStep('payment'), children: t.tryAgain })) : null, continueShoppingUrl ? (jsx(Button, { variant: "outline", onClick: () => (window.location.href = continueShoppingUrl), children: t.continueShopping })) : null] })] }));
700
+ }
701
+ function CheckCircle() {
702
+ return (jsxs("svg", { viewBox: "0 0 48 48", width: "48", height: "48", fill: "none", "aria-hidden": "true", children: [jsx("circle", { cx: "24", cy: "24", r: "22", stroke: "currentColor", strokeWidth: "2.5" }), jsx("path", { d: "M15 24.5l6 6 12-13", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" })] }));
703
+ }
704
+ function CrossCircle() {
705
+ return (jsxs("svg", { viewBox: "0 0 48 48", width: "48", height: "48", fill: "none", "aria-hidden": "true", children: [jsx("circle", { cx: "24", cy: "24", r: "22", stroke: "currentColor", strokeWidth: "2.5" }), jsx("path", { d: "M17 17l14 14M31 17L17 31", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" })] }));
706
+ }
707
+
708
+ function themeVars(theme) {
709
+ const vars = {};
710
+ if (theme?.accent)
711
+ vars['--szc-accent'] = theme.accent;
712
+ if (theme?.accentForeground)
713
+ vars['--szc-accent-foreground'] = theme.accentForeground;
714
+ if (theme?.accentSoft)
715
+ vars['--szc-accent-soft'] = theme.accentSoft;
716
+ if (theme?.background)
717
+ vars['--szc-bg'] = theme.background;
718
+ if (theme?.foreground)
719
+ vars['--szc-fg'] = theme.foreground;
720
+ if (theme?.muted)
721
+ vars['--szc-muted'] = theme.muted;
722
+ if (theme?.mutedForeground)
723
+ vars['--szc-muted-fg'] = theme.mutedForeground;
724
+ if (theme?.border)
725
+ vars['--szc-border'] = theme.border;
726
+ if (theme?.card)
727
+ vars['--szc-card'] = theme.card;
728
+ if (theme?.summaryBackground)
729
+ vars['--szc-summary-bg'] = theme.summaryBackground;
730
+ if (theme?.danger)
731
+ vars['--szc-danger'] = theme.danger;
732
+ if (theme?.success)
733
+ vars['--szc-success'] = theme.success;
734
+ if (theme?.radius != null)
735
+ vars['--szc-radius'] = `${theme.radius}px`;
736
+ if (theme?.fontFamily)
737
+ vars['--szc-font'] = theme.fontFamily;
738
+ return vars;
739
+ }
740
+ function CheckoutSkeleton({ label }) {
741
+ return (jsxs("div", { className: "szc-layout szc-skeleton-layout", role: "status", "aria-label": label, children: [jsxs("main", { className: "szc-main", "aria-hidden": "true", children: [jsxs("div", { className: "szc-skeleton-cart-row", children: [jsx("span", { className: "szc-skeleton szc-skeleton--thumb" }), jsxs("div", { className: "szc-skeleton-copy", children: [jsx("span", { className: "szc-skeleton szc-skeleton--title" }), jsx("span", { className: "szc-skeleton szc-skeleton--text" }), jsx("span", { className: "szc-skeleton szc-skeleton--price" })] }), jsx("span", { className: "szc-skeleton szc-skeleton--counter" })] }), jsxs("div", { className: "szc-skeleton-cart-row", children: [jsx("span", { className: "szc-skeleton szc-skeleton--thumb" }), jsxs("div", { className: "szc-skeleton-copy", children: [jsx("span", { className: "szc-skeleton szc-skeleton--title" }), jsx("span", { className: "szc-skeleton szc-skeleton--text" }), jsx("span", { className: "szc-skeleton szc-skeleton--price" })] }), jsx("span", { className: "szc-skeleton szc-skeleton--counter" })] })] }), jsxs("div", { className: "szc-side", "aria-hidden": "true", children: [jsxs("aside", { className: "szc-summary szc-summary--skeleton", children: [jsxs("div", { className: "szc-summary__total-head", children: [jsx("span", { className: "szc-skeleton szc-skeleton--summary-label" }), jsx("span", { className: "szc-skeleton szc-skeleton--summary-total" })] }), jsxs("div", { className: "szc-skeleton-summary-lines", children: [jsxs("div", { children: [jsx("span", { className: "szc-skeleton" }), jsx("span", { className: "szc-skeleton" })] }), jsxs("div", { children: [jsx("span", { className: "szc-skeleton" }), jsx("span", { className: "szc-skeleton" })] })] }), jsxs("div", { className: "szc-skeleton-summary-grand", children: [jsx("span", { className: "szc-skeleton" }), jsx("span", { className: "szc-skeleton" })] })] }), jsx("div", { className: "szc-side__cta", children: jsx("span", { className: "szc-skeleton szc-skeleton--button" }) })] })] }));
742
+ }
743
+ function SazitoCheckout({ theme, continueShoppingUrl, className, renderNextButton, renderBackButton, renderEmptyCart, }) {
744
+ const { state, actions, t, summary, money } = useCheckout();
745
+ const { step, direction, flags, error } = state;
746
+ const isResult = step === 'result';
747
+ const bootstrapping = step === 'cart' && !error && (!state.cart || !state.invoice || flags.bootstrapping);
748
+ const fatal = error && !state.cart && step === 'cart';
749
+ const cartEmpty = !state.cart || state.cart.items.length === 0;
750
+ const footerVisible = (step === 'cart' && !cartEmpty) || step === 'shipping' || step === 'payment';
751
+ const nextBusy = state.status === 'working' ||
752
+ flags.savingAddress ||
753
+ flags.loadingShipping ||
754
+ flags.loadingPayments ||
755
+ flags.placingOrder;
756
+ const needsShipping = state.invoice?.needsShipping !== false;
757
+ const shippingSavePhase = state.addressDirty || !state.applicable;
758
+ const shippingReady = !needsShipping ||
759
+ (state.shippingGroups.length > 0 &&
760
+ state.shippingGroups.every((g) => g.selectedRateId != null));
761
+ const paymentReady = state.paymentMethods.length > 0 && state.selectedPaymentMethodId != null;
762
+ const nextDisabled = step === 'cart'
763
+ ? cartEmpty
764
+ : step === 'shipping'
765
+ ? !shippingSavePhase && !shippingReady
766
+ : step === 'payment'
767
+ ? !paymentReady
768
+ : false;
769
+ const nextLabel = step === 'cart'
770
+ ? t.placeOrder
771
+ : step === 'shipping'
772
+ ? needsShipping && shippingSavePhase
773
+ ? t.saveShippingDetails
774
+ : t.continueToPayment
775
+ : t.finishPurchase;
776
+ const backOnClick = step === 'cart' && continueShoppingUrl
777
+ ? () => { window.location.href = continueShoppingUrl; }
778
+ : () => actions.back();
779
+ const backVisible = step !== 'cart' || !!continueShoppingUrl;
780
+ const nextButtonProps = {
781
+ loading: nextBusy,
782
+ disabled: nextDisabled,
783
+ onClick: () => actions.next(),
784
+ children: nextLabel,
785
+ };
786
+ const backButtonProps = {
787
+ loading: false,
788
+ disabled: false,
789
+ onClick: backOnClick,
790
+ children: t.back,
791
+ };
792
+ /* Rendered twice: under the order summary (desktop) and in the sticky
793
+ bottom bar (mobile). CSS shows exactly one of the two. */
794
+ const nextButtonNode = renderNextButton ? (renderNextButton(nextButtonProps)) : (jsx(Button, { className: "szc-footer__next", loading: nextBusy, disabled: nextDisabled, onClick: () => actions.next(), children: nextLabel }));
795
+ const backButtonNode = backVisible ? (renderBackButton ? (renderBackButton(backButtonProps)) : (jsxs(Button, { variant: "ghost", className: "szc-back-btn", onClick: backOnClick, children: [jsx("svg", { className: "szc-back-arrow", viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M9.5 3.5 5 8l4.5 4.5", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }), t.back] }))) : null;
796
+ const actionsVisible = !bootstrapping && !fatal && !isResult && footerVisible;
797
+ /* Mobile header (replaces the stepper on small screens): back chevron at the
798
+ inline start, current step title centered, "step n of m" at the end. */
799
+ const flowSteps = ['cart', 'shipping', 'payment'];
800
+ const flowIndex = flowSteps.indexOf(step);
801
+ const headTitle = step === 'cart'
802
+ ? t.stepCart
803
+ : step === 'shipping'
804
+ ? t.stepShippingInfo
805
+ : step === 'payment'
806
+ ? t.stepPayment
807
+ : t.stepResult;
808
+ const HeadIcon = step === 'cart'
809
+ ? CartIcon
810
+ : step === 'shipping'
811
+ ? TruckIcon$1
812
+ : step === 'payment'
813
+ ? CardIcon
814
+ : ClipboardIcon;
815
+ const mobileHead = (jsxs("div", { className: "szc-mobile-head", children: [footerVisible && backVisible ? (jsx("button", { type: "button", className: "szc-mobile-head__back", "aria-label": t.back, onClick: backOnClick, children: jsx("svg", { className: "szc-back-arrow", viewBox: "0 0 16 16", width: "18", height: "18", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M9.5 3.5 5 8l4.5 4.5", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) })) : (jsx("span", {})), jsxs("span", { className: "szc-mobile-head__title", children: [jsx("span", { className: "szc-mobile-head__dot", children: jsx(HeadIcon, {}) }), headTitle] }), flowIndex >= 0 ? (jsx("span", { className: "szc-mobile-head__count", children: t.stepOf(formatNumber(flowIndex + 1, state.locale), formatNumber(flowSteps.length, state.locale)) })) : (jsx("span", {}))] }));
816
+ return (jsxs("div", { className: `szc-root${className ? ` ${className}` : ''}`, dir: direction, style: themeVars(theme), children: [mobileHead, jsx(Stepper, {}), bootstrapping ? (jsx(CheckoutSkeleton, { label: t.loading })) : fatal ? (jsx(ErrorBanner, { message: error.message })) : isResult ? (jsx("div", { className: "szc-result-wrap", children: jsx(ResultStep, { continueShoppingUrl: continueShoppingUrl }) })) : (jsx(Fragment, { children: jsxs("div", { className: `szc-layout${cartEmpty ? ' szc-layout--full' : ''}`, children: [jsxs("main", { className: "szc-main", children: [error ? jsx(ErrorBanner, { message: error.message }) : null, step === 'cart' ? jsx(CartStep, { continueShoppingUrl: continueShoppingUrl, renderEmpty: renderEmptyCart }) : null, step === 'shipping' ? jsx(ShippingStep, {}) : null, step === 'payment' ? jsx(PaymentStep, {}) : null, actionsVisible && backButtonNode ? (jsx("div", { className: "szc-back-row", children: backButtonNode })) : null] }), !cartEmpty ? (jsxs("div", { className: "szc-side", children: [jsx(OrderSummary, {}), actionsVisible ? jsx("div", { className: "szc-side__cta", children: nextButtonNode }) : null] })) : null] }) })), actionsVisible ? (jsx("div", { className: "szc-footer-bar", children: jsxs("div", { className: "szc-footer", children: [jsxs("div", { className: "szc-footer__total", children: [jsx("span", { className: "szc-footer__total-label", children: t.total }), jsx("span", { className: "szc-footer__total-value", children: money(summary.total) })] }), jsx("div", { className: "szc-footer__actions", children: nextButtonNode })] }) })) : null] }));
817
+ }
818
+
819
+ function SazitoCheckoutPage({ credentials, config, paymentReturnParams, className, renderNextButton, renderBackButton, renderEmptyCart, }) {
820
+ const isReturn = paymentReturnParams != null;
821
+ const client = useSazitoClient();
822
+ return (jsxs(CheckoutProvider, { client: client, credentials: credentials, config: config, autoStart: !isReturn, children: [isReturn ? jsx(ResolveReturn, { params: paymentReturnParams }) : null, jsx(SazitoCheckout, { theme: config?.theme, continueShoppingUrl: config?.continueShoppingUrl, className: className, renderNextButton: renderNextButton, renderBackButton: renderBackButton, renderEmptyCart: renderEmptyCart })] }));
823
+ }
824
+ function ResolveReturn({ params }) {
825
+ const { actions } = useCheckout();
826
+ useEffect(() => {
827
+ void actions.resolvePaymentReturn(params);
828
+ // eslint-disable-next-line react-hooks/exhaustive-deps
829
+ }, []);
830
+ return null;
831
+ }
832
+
833
+ export { Button, CheckoutProvider, ErrorBanner, Field, ProductPlaceholder, SazitoCheckout, SazitoCheckoutPage, SectionTitle, Spinner, useCheckout };
834
+ //# sourceMappingURL=index.js.map