@sazito/checkout 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -265,6 +265,7 @@ interface Strings {
265
265
  shippingMethods: string;
266
266
  shippingMethodsHint: string;
267
267
  digitalNoShipping: string;
268
+ digitalNoShippingHint: string;
268
269
  errorRequired: string;
269
270
  errorMobilePhone: string;
270
271
  errorEmail: string;
@@ -265,6 +265,7 @@ interface Strings {
265
265
  shippingMethods: string;
266
266
  shippingMethodsHint: string;
267
267
  digitalNoShipping: string;
268
+ digitalNoShippingHint: string;
268
269
  errorRequired: string;
269
270
  errorMobilePhone: string;
270
271
  errorEmail: string;
@@ -1,9 +1,9 @@
1
1
  'use client';
2
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';
3
+ import { isValidElement, cloneElement, useMemo, useState, useRef, useEffect, useCallback } from 'react';
4
+ import { u as useCheckout, b as useSazitoClient, C as CheckoutProvider } from '../chunks/use-checkout-gAjnCzae.js';
5
+ export { S as SazitoProvider } from '../chunks/use-checkout-gAjnCzae.js';
6
+ import { m as formatPercent, E as sortCartItemsNewestFirst, H as toPersianDigits, r as isDigitalServiceGroup, t as isValidIranianMobile, z as normalizeIranianPhone, A as paymentMethodInfo, l as formatNumber } from '../chunks/labels-ByA_yGKl.js';
7
7
  import '@sazito/client-sdk';
8
8
 
9
9
  /**
@@ -139,6 +139,7 @@ function ClipboardIcon() {
139
139
 
140
140
  function OrderSummary() {
141
141
  const { state, summary, money, t } = useCheckout();
142
+ const updating = state.flags.updatingCart;
142
143
  const percentSign = state.locale === 'fa' ? '٪' : '%';
143
144
  const lineLabels = {
144
145
  subtotal: t.subtotal,
@@ -147,14 +148,14 @@ function OrderSummary() {
147
148
  credit: t.credit,
148
149
  vat: t.vat
149
150
  };
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
+ return (jsxs("aside", { className: `szc-summary${updating ? ' szc-summary--loading' : ''}`, "aria-busy": updating, 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
152
  ? ` (${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
+ : ''] }), 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) })] }), updating ? (jsxs("div", { className: "szc-summary__loading", role: "status", "aria-live": "polite", children: [jsx(Spinner, { className: "szc-summary__spinner" }), jsx("span", { className: "szc-visually-hidden", children: t.loading })] })) : null] }));
153
154
  }
154
155
 
155
156
  const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹';
156
157
  const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩';
157
- const QUANTITY_DEBOUNCE_MS = 600;
158
+ const QUANTITY_DEBOUNCE_MS = 500;
158
159
  function toLatinDigits(value) {
159
160
  return value
160
161
  .replace(/[۰-۹]/g, (digit) => String(PERSIAN_DIGITS.indexOf(digit)))
@@ -188,6 +189,7 @@ function CartStep({ continueShoppingUrl, renderEmpty, }) {
188
189
  const { state, actions, money, t } = useCheckout();
189
190
  const cart = state.cart;
190
191
  const busy = state.flags.updatingCart;
192
+ const newestCartItems = useMemo(() => sortCartItemsNewestFirst(cart?.items ?? []), [cart?.items]);
191
193
  const invoiceItemsByVariant = new Map((state.invoice?.items ?? []).map((item) => [item.productVariantId, item]));
192
194
  if (!cart || cart.items.length === 0) {
193
195
  if (renderEmpty) {
@@ -195,7 +197,7 @@ function CartStep({ continueShoppingUrl, renderEmpty, }) {
195
197
  }
196
198
  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
199
  }
198
- return (jsx("section", { className: "szc-step-panel", children: jsx("ul", { className: "szc-cart-list", children: cart.items.map((item) => {
200
+ return (jsx("section", { className: "szc-step-panel", children: jsx("ul", { className: "szc-cart-list", children: newestCartItems.map((item) => {
199
201
  const max = item.product.maxOrderQuantity;
200
202
  const min = item.product.minOrderQuantity ?? 1;
201
203
  const invoiceItem = invoiceItemsByVariant.get(item.product.variantId);
@@ -214,6 +216,7 @@ function CartStep({ continueShoppingUrl, renderEmpty, }) {
214
216
  function QuantityControl({ value, min, max, label, onChange }) {
215
217
  const [draft, setDraft] = useState(() => toPersianDigits(String(value)));
216
218
  const [pending, setPending] = useState(false);
219
+ const [updating, setUpdating] = useState(false);
217
220
  const draftRef = useRef(draft);
218
221
  const pendingValueRef = useRef(null);
219
222
  const timerRef = useRef(null);
@@ -248,11 +251,13 @@ function QuantityControl({ value, min, max, label, onChange }) {
248
251
  return;
249
252
  }
250
253
  inFlightRef.current = true;
254
+ setUpdating(true);
251
255
  try {
252
256
  await onChangeRef.current(target);
253
257
  }
254
258
  finally {
255
259
  inFlightRef.current = false;
260
+ setUpdating(false);
256
261
  if (pendingValueRef.current === target) {
257
262
  pendingValueRef.current = null;
258
263
  setPending(false);
@@ -287,7 +292,7 @@ function QuantityControl({ value, min, max, label, onChange }) {
287
292
  };
288
293
  const parsedDraft = Number(toLatinDigits(draft));
289
294
  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) => {
295
+ return (jsxs("div", { className: `szc-qty${pending ? ' szc-qty--pending' : ''}`, "aria-busy": updating, children: [jsx("button", { type: "button", className: "szc-qty__btn", "aria-label": `${label} −`, disabled: updating || 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, disabled: updating, onChange: (event) => {
291
296
  const digits = toLatinDigits(event.target.value).replace(/\D/g, '');
292
297
  if (!digits) {
293
298
  if (timerRef.current)
@@ -318,7 +323,7 @@ function QuantityControl({ value, min, max, label, onChange }) {
318
323
  event.preventDefault();
319
324
  step(-1);
320
325
  }
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, {}) })] }));
326
+ } }), jsx("button", { type: "button", className: "szc-qty__btn", "aria-label": `${label} +`, disabled: updating || (max != null && displayedValue >= max), onMouseDown: (event) => event.preventDefault(), onClick: () => step(1), children: jsx(PlusIcon, {}) })] }));
322
327
  }
323
328
  function MinusIcon() {
324
329
  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" }) }));
@@ -365,6 +370,8 @@ function ShippingStep() {
365
370
  const { addressForm, regions, shippingGroups } = state;
366
371
  const [touched, setTouched] = useState({});
367
372
  const needsShipping = state.invoice?.needsShipping ?? true;
373
+ const physicalShippingGroups = shippingGroups.filter((group) => !isDigitalServiceGroup(group));
374
+ const physicalItemCount = Math.max(0, (state.invoice?.items.length ?? 0) - digitalItems.length);
368
375
  const touch = useCallback((key) => {
369
376
  setTouched((prev) => ({ ...prev, [key]: true }));
370
377
  }, []);
@@ -387,7 +394,7 @@ function ShippingStep() {
387
394
  }, 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
395
  actions.setAddressField('cityId', v ? Number(v) : null);
389
396
  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] }));
397
+ }, 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] }), digitalItems.length > 0 ? (jsxs("section", { className: "szc-digital-products", "aria-labelledby": "szc-digital-title", children: [jsxs("header", { className: "szc-digital-products__head", children: [jsx("span", { className: "szc-digital-products__icon", "aria-hidden": "true", children: jsx(DigitalIcon, {}) }), jsxs("div", { className: "szc-digital-products__heading", children: [jsxs("div", { className: "szc-digital-products__title-row", children: [jsx("h2", { id: "szc-digital-title", className: "szc-digital-products__title", children: t.digitalNoShipping }), jsx("span", { className: "szc-digital-products__count", children: t.productCount(toPersianDigits(String(digitalItems.length))) })] }), jsx("p", { children: t.digitalNoShippingHint })] })] }), jsx("div", { className: "szc-digital-products__list", children: digitalItems.map((item) => (jsxs("article", { className: "szc-digital-product", children: [jsxs("div", { className: "szc-digital-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)) })] }), jsx("strong", { className: "szc-digital-product__name", children: item.name })] }, item.id))) })] })) : null, physicalShippingGroups.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: physicalShippingGroups.map((group) => (jsx(ShippingGroupCard, { group: group }, group.key))) })] })) : needsShipping && physicalItemCount > 0 ? (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] }));
391
398
  }
392
399
  function ShippingSectionHeader({ id, title, icon }) {
393
400
  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 })] }));
@@ -398,6 +405,9 @@ function ContactIcon() {
398
405
  function AddressIcon() {
399
406
  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
407
  }
408
+ function DigitalIcon() {
409
+ return (jsxs("svg", { viewBox: "0 0 24 24", width: "22", height: "22", fill: "none", "aria-hidden": "true", children: [jsx("path", { d: "M12 3v11m0 0 4-4m-4 4-4-4", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }), jsx("path", { d: "M5 16.5v1.75A1.75 1.75 0 0 0 6.75 20h10.5A1.75 1.75 0 0 0 19 18.25V16.5", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round" })] }));
410
+ }
401
411
  function EnhancedSelect({ id, label, value, placeholder, options, required, optionalLabel, disabled, error, onChange }) {
402
412
  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
413
  }