@sazito/checkout 0.1.1 → 0.2.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.
- package/README.md +4 -0
- package/dist/chunks/{labels-BDeABGTD.js → labels-ByA_yGKl.js} +131 -29
- package/dist/chunks/labels-ByA_yGKl.js.map +1 -0
- package/dist/chunks/{labels-CWfNSAYm.cjs → labels-DJ_RnkCQ.cjs} +132 -28
- package/dist/chunks/labels-DJ_RnkCQ.cjs.map +1 -0
- package/dist/chunks/{use-checkout-CKFla5or.cjs → use-checkout-CQBSONj1.cjs} +2 -2
- package/dist/chunks/{use-checkout-CKFla5or.cjs.map → use-checkout-CQBSONj1.cjs.map} +1 -1
- package/dist/chunks/{use-checkout-DIkxFBnD.js → use-checkout-gAjnCzae.js} +2 -2
- package/dist/chunks/{use-checkout-DIkxFBnD.js.map → use-checkout-gAjnCzae.js.map} +1 -1
- package/dist/core/index.cjs +3 -1
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +17 -4
- package/dist/core/index.d.ts +17 -4
- package/dist/core/index.js +1 -1
- package/dist/next/index.cjs +29 -11
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +9 -0
- package/dist/next/index.d.ts +9 -0
- package/dist/next/index.js +31 -13
- package/dist/next/index.js.map +1 -1
- package/dist/react/index.cjs +2 -2
- package/dist/react/index.d.cts +9 -0
- package/dist/react/index.d.ts +9 -0
- package/dist/react/index.js +2 -2
- package/dist/styles.css +147 -29
- package/package.json +1 -1
- package/dist/chunks/labels-BDeABGTD.js.map +0 -1
- package/dist/chunks/labels-CWfNSAYm.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -77,6 +77,10 @@ export default function Checkout() {
|
|
|
77
77
|
| `summaryBackground` | `--szc-summary-bg` | Summary sidebar |
|
|
78
78
|
| `danger` | `--szc-danger` | Errors and destructive states |
|
|
79
79
|
| `success` | `--szc-success` | Completed and success states |
|
|
80
|
+
| `successForeground` | `--szc-success-foreground` | Text/icons on solid success surfaces |
|
|
81
|
+
| `logoBackground` | `--szc-logo-bg` | Plate behind shipping-provider logos |
|
|
82
|
+
| `shippingNeutral` | `--szc-shipping-neutral` | Neutral surface for white shipping colors |
|
|
83
|
+
| `shippingNeutralForeground` | `--szc-shipping-neutral-foreground` | Foreground on the neutral shipping surface |
|
|
80
84
|
|
|
81
85
|
`radius` and `fontFamily` remain available for shape and typography. Shipping-rate icon colors continue to come from the shipping API.
|
|
82
86
|
|
|
@@ -200,6 +200,30 @@ function createBrowserEffectExecutor(config) {
|
|
|
200
200
|
const noopEffectExecutor = () => { };
|
|
201
201
|
|
|
202
202
|
const idStr = (value) => String(value);
|
|
203
|
+
function cartItemCreatedAt(item) {
|
|
204
|
+
if (!item.createdAt)
|
|
205
|
+
return null;
|
|
206
|
+
const timestamp = Date.parse(item.createdAt);
|
|
207
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
208
|
+
}
|
|
209
|
+
/** Return cart lines newest-first without mutating SDK state. */
|
|
210
|
+
function sortCartItemsNewestFirst(items) {
|
|
211
|
+
return [...items].sort((a, b) => {
|
|
212
|
+
const aCreatedAt = cartItemCreatedAt(a);
|
|
213
|
+
const bCreatedAt = cartItemCreatedAt(b);
|
|
214
|
+
if (aCreatedAt != null || bCreatedAt != null) {
|
|
215
|
+
if (aCreatedAt == null)
|
|
216
|
+
return 1;
|
|
217
|
+
if (bCreatedAt == null)
|
|
218
|
+
return -1;
|
|
219
|
+
if (aCreatedAt !== bCreatedAt)
|
|
220
|
+
return bCreatedAt - aCreatedAt;
|
|
221
|
+
}
|
|
222
|
+
const aId = Number(a.id);
|
|
223
|
+
const bId = Number(b.id);
|
|
224
|
+
return Number.isFinite(aId) && Number.isFinite(bId) ? bId - aId : 0;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
203
227
|
function emptyAddressForm() {
|
|
204
228
|
return {
|
|
205
229
|
firstName: '',
|
|
@@ -298,6 +322,28 @@ function uniqueRates(rates) {
|
|
|
298
322
|
}
|
|
299
323
|
return out;
|
|
300
324
|
}
|
|
325
|
+
function isDigitalServiceLabel(value) {
|
|
326
|
+
if (!value)
|
|
327
|
+
return false;
|
|
328
|
+
const normalized = value.trim().toLocaleLowerCase('en-US');
|
|
329
|
+
return (normalized.includes('digital') ||
|
|
330
|
+
normalized.includes('service') ||
|
|
331
|
+
normalized.includes('دیجیتال') ||
|
|
332
|
+
normalized.includes('خدمات') ||
|
|
333
|
+
normalized.includes('خدمت'));
|
|
334
|
+
}
|
|
335
|
+
function isDigitalServiceItem(item) {
|
|
336
|
+
return isDigitalServiceLabel(item.productType);
|
|
337
|
+
}
|
|
338
|
+
/** True for the API's automatic digital/service delivery group. */
|
|
339
|
+
function isDigitalServiceGroup(group) {
|
|
340
|
+
if (isDigitalServiceLabel(group.key) || isDigitalServiceLabel(group.title)) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
const ratesAreDigitalService = group.rates.length > 0 &&
|
|
344
|
+
group.rates.every((rate) => isDigitalServiceLabel(rate.type) || isDigitalServiceLabel(rate.name));
|
|
345
|
+
return ratesAreDigitalService || (group.items.length > 0 && group.items.every(isDigitalServiceItem));
|
|
346
|
+
}
|
|
301
347
|
/**
|
|
302
348
|
* Group invoice items into shippable bundles with their switchable rates and
|
|
303
349
|
* the currently selected rate. Digital-only invoices yield an empty list.
|
|
@@ -317,10 +363,12 @@ function deriveShippingGroups(invoice, applicable) {
|
|
|
317
363
|
return [];
|
|
318
364
|
}
|
|
319
365
|
const itemById = new Map(invoice.items.map((item) => [idStr(item.id), item]));
|
|
320
|
-
//
|
|
366
|
+
// The API's plain `digital` products never receive a shipping assignment.
|
|
367
|
+
// Other digital/service variants may arrive through an automatic fulfillment
|
|
368
|
+
// rate; retain those groups for the API assignment and hide them in the UI.
|
|
321
369
|
const physicalItemsRate = (applicable.itemsShippingRate ?? []).filter((isr) => {
|
|
322
370
|
const item = itemById.get(idStr(isr.invoiceItemId));
|
|
323
|
-
return !item || item.productType !== 'digital';
|
|
371
|
+
return !item || item.productType?.trim().toLocaleLowerCase('en-US') !== 'digital';
|
|
324
372
|
});
|
|
325
373
|
if (physicalItemsRate.length === 0)
|
|
326
374
|
return [];
|
|
@@ -419,24 +467,27 @@ function deriveShippingGroups(invoice, applicable) {
|
|
|
419
467
|
}
|
|
420
468
|
];
|
|
421
469
|
}
|
|
422
|
-
/** Build the API payload from
|
|
470
|
+
/** Build the API payload from physical shipping selections. */
|
|
423
471
|
function buildShippingAssignments(groups) {
|
|
424
472
|
return groups
|
|
425
|
-
.filter((group) => group.selectedRateId != null)
|
|
473
|
+
.filter((group) => !isDigitalServiceGroup(group) && group.selectedRateId != null)
|
|
426
474
|
.map((group) => ({
|
|
427
475
|
rateId: group.selectedRateId,
|
|
428
476
|
invoiceItemIds: group.itemIds
|
|
429
477
|
}));
|
|
430
478
|
}
|
|
431
|
-
/** True once every
|
|
479
|
+
/** True once every physical shipping group has a selected rate. */
|
|
432
480
|
function isShippingComplete(invoice, groups) {
|
|
433
481
|
if (!invoice?.needsShipping) {
|
|
434
482
|
return true;
|
|
435
483
|
}
|
|
436
|
-
|
|
437
|
-
|
|
484
|
+
const physicalGroups = groups.filter((group) => !isDigitalServiceGroup(group));
|
|
485
|
+
if (physicalGroups.length === 0) {
|
|
486
|
+
// The API may set needsShipping for an automatic Digital/Service group.
|
|
487
|
+
// It is fulfillment metadata, not a customer-selectable shipping method.
|
|
488
|
+
return groups.length > 0 && groups.every(isDigitalServiceGroup);
|
|
438
489
|
}
|
|
439
|
-
return
|
|
490
|
+
return physicalGroups.every((group) => group.selectedRateId != null);
|
|
440
491
|
}
|
|
441
492
|
const codeDiscountTotal = (invoice) => (invoice.couponTotal || 0) + (invoice.discountTotal || 0);
|
|
442
493
|
/**
|
|
@@ -540,14 +591,22 @@ function selectDigitalItems(invoice, groups, applicable) {
|
|
|
540
591
|
// Before shipping methods are loaded, only explicitly-digital items are known.
|
|
541
592
|
// Showing everything as "digital" when applicable is null would be wrong.
|
|
542
593
|
if (applicable == null) {
|
|
543
|
-
return invoice.items.filter(
|
|
594
|
+
return invoice.items.filter(isDigitalServiceItem);
|
|
544
595
|
}
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
596
|
+
// The backend may model digital/service fulfillment as an automatic zero-cost
|
|
597
|
+
// shipping group. Treat its products as non-shippable in the UI while keeping
|
|
598
|
+
// the group in state so its assignment can still be submitted.
|
|
599
|
+
const digitalGroupItems = new Set(groups
|
|
600
|
+
.filter(isDigitalServiceGroup)
|
|
601
|
+
.flatMap((group) => group.itemIds.map(idStr)));
|
|
602
|
+
const groupedItems = new Set(groups.flatMap((group) => group.itemIds.map(idStr)));
|
|
603
|
+
return invoice.items.filter((item) => isDigitalServiceItem(item) ||
|
|
604
|
+
digitalGroupItems.has(idStr(item.id)) ||
|
|
605
|
+
!groupedItems.has(idStr(item.id)));
|
|
548
606
|
}
|
|
549
607
|
|
|
550
608
|
const STEP_ORDER = ['cart', 'shipping', 'payment', 'result'];
|
|
609
|
+
const ADDRESS_AUTOSUBMIT_DEBOUNCE_MS = 350;
|
|
551
610
|
function initialFlags() {
|
|
552
611
|
return {
|
|
553
612
|
bootstrapping: false,
|
|
@@ -594,6 +653,7 @@ function createCheckoutEngine(options) {
|
|
|
594
653
|
let effectExecutor = noopEffectExecutor;
|
|
595
654
|
let lock = Promise.resolve();
|
|
596
655
|
let addressRevision = 0;
|
|
656
|
+
let addressAutoSubmitTimer = null;
|
|
597
657
|
// ---- internal helpers -------------------------------------------------
|
|
598
658
|
const get = () => store.getState();
|
|
599
659
|
const set = store.setState;
|
|
@@ -614,6 +674,19 @@ function createCheckoutEngine(options) {
|
|
|
614
674
|
lock = next.then(() => undefined, () => undefined);
|
|
615
675
|
return next;
|
|
616
676
|
}
|
|
677
|
+
function clearAddressAutoSubmit() {
|
|
678
|
+
if (addressAutoSubmitTimer) {
|
|
679
|
+
clearTimeout(addressAutoSubmitTimer);
|
|
680
|
+
addressAutoSubmitTimer = null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function scheduleAddressAutoSubmit(delay = ADDRESS_AUTOSUBMIT_DEBOUNCE_MS) {
|
|
684
|
+
clearAddressAutoSubmit();
|
|
685
|
+
addressAutoSubmitTimer = setTimeout(() => {
|
|
686
|
+
addressAutoSubmitTimer = null;
|
|
687
|
+
void withLock(submitAddressInternal);
|
|
688
|
+
}, delay);
|
|
689
|
+
}
|
|
617
690
|
function recomputeGroups() {
|
|
618
691
|
const { invoice, applicable } = get();
|
|
619
692
|
const groups = deriveShippingGroups(invoice, applicable);
|
|
@@ -809,13 +882,17 @@ function createCheckoutEngine(options) {
|
|
|
809
882
|
}
|
|
810
883
|
// ---- public actions ---------------------------------------------------
|
|
811
884
|
function goToStep(step) {
|
|
885
|
+
if (step !== 'shipping') {
|
|
886
|
+
clearAddressAutoSubmit();
|
|
887
|
+
}
|
|
812
888
|
set({ step, error: null });
|
|
813
889
|
emit('step_viewed', { step });
|
|
814
890
|
// When landing on shipping with a complete address but no methods fetched yet,
|
|
815
891
|
// auto-submit so methods appear without requiring a manual Continue press.
|
|
816
892
|
if (step === 'shipping') {
|
|
817
|
-
const { addressForm, invoice, applicable } = get();
|
|
818
|
-
|
|
893
|
+
const { addressForm, addressDirty, invoice, applicable, shippingGroups } = get();
|
|
894
|
+
const methodsUnresolved = Boolean(invoice?.needsShipping) && shippingGroups.length === 0;
|
|
895
|
+
if ((addressDirty || !applicable || methodsUnresolved) && isAddressComplete(addressForm, invoice?.needsShipping ?? true, get().postalCodeMandatory, get().emailMandatory)) {
|
|
819
896
|
void withLock(submitAddressInternal);
|
|
820
897
|
}
|
|
821
898
|
}
|
|
@@ -918,13 +995,18 @@ function createCheckoutEngine(options) {
|
|
|
918
995
|
// the Shipping step becomes visible.
|
|
919
996
|
const invoice = get().invoice;
|
|
920
997
|
if (isUsableSavedAddress(invoice?.shippingAddress)) {
|
|
921
|
-
|
|
998
|
+
const addressForm = reconcileAddressWithRegions(addressFormFromInvoice(invoice), get().regions, savedAddressCityName(invoice?.shippingAddress));
|
|
999
|
+
set({
|
|
1000
|
+
addressForm,
|
|
1001
|
+
addressDirty: isAddressDirty(addressForm, invoice)
|
|
1002
|
+
});
|
|
922
1003
|
}
|
|
923
1004
|
goToStep('shipping');
|
|
924
1005
|
}
|
|
925
1006
|
return;
|
|
926
1007
|
}
|
|
927
1008
|
if (state.step === 'shipping') {
|
|
1009
|
+
clearAddressAutoSubmit();
|
|
928
1010
|
// Phase 1: address not saved yet (or changed) → save it and reveal the
|
|
929
1011
|
// shipping methods, staying on the shipping step.
|
|
930
1012
|
const needsSave = state.addressDirty || !state.applicable;
|
|
@@ -994,6 +1076,7 @@ function createCheckoutEngine(options) {
|
|
|
994
1076
|
setAddressField(key, value) {
|
|
995
1077
|
const before = get().addressForm;
|
|
996
1078
|
const changed = before[key] !== value || (key === 'regionId' && before.cityId !== null);
|
|
1079
|
+
const destinationChanged = changed && (key === 'regionId' || key === 'cityId');
|
|
997
1080
|
if (changed) {
|
|
998
1081
|
addressRevision += 1;
|
|
999
1082
|
}
|
|
@@ -1007,22 +1090,28 @@ function createCheckoutEngine(options) {
|
|
|
1007
1090
|
return {
|
|
1008
1091
|
addressForm,
|
|
1009
1092
|
addressDirty,
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
...(
|
|
1093
|
+
// Shipping rates depend on region/city. Contact and street edits must
|
|
1094
|
+
// not discard valid methods for the same destination.
|
|
1095
|
+
...(destinationChanged ? { applicable: null, shippingGroups: [] } : {})
|
|
1013
1096
|
};
|
|
1014
1097
|
});
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
if (changed &&
|
|
1098
|
+
// Browser autofill often populates the selects before the final required
|
|
1099
|
+
// text field. Once the form becomes complete, debounce text changes and
|
|
1100
|
+
// submit automatically; a discrete city selection can submit immediately.
|
|
1101
|
+
if (changed && get().step === 'shipping') {
|
|
1019
1102
|
const state = get();
|
|
1020
|
-
|
|
1021
|
-
|
|
1103
|
+
const methodsUnresolved = !state.applicable
|
|
1104
|
+
|| (Boolean(state.invoice?.needsShipping) && state.shippingGroups.length === 0);
|
|
1105
|
+
if (isAddressComplete(state.addressForm, state.invoice?.needsShipping ?? true, state.postalCodeMandatory, state.emailMandatory) && (destinationChanged || methodsUnresolved)) {
|
|
1106
|
+
scheduleAddressAutoSubmit(key === 'cityId' ? 0 : undefined);
|
|
1107
|
+
}
|
|
1108
|
+
else if (!isAddressComplete(state.addressForm, state.invoice?.needsShipping ?? true, state.postalCodeMandatory, state.emailMandatory)) {
|
|
1109
|
+
clearAddressAutoSubmit();
|
|
1022
1110
|
}
|
|
1023
1111
|
}
|
|
1024
1112
|
},
|
|
1025
1113
|
async submitAddress() {
|
|
1114
|
+
clearAddressAutoSubmit();
|
|
1026
1115
|
return withLock(submitAddressInternal);
|
|
1027
1116
|
},
|
|
1028
1117
|
async selectShippingRate(groupKey, rateId) {
|
|
@@ -1144,6 +1233,8 @@ function createCheckoutEngine(options) {
|
|
|
1144
1233
|
const state = get();
|
|
1145
1234
|
const submittedAddressRevision = addressRevision;
|
|
1146
1235
|
const needsShipping = state.invoice?.needsShipping ?? true;
|
|
1236
|
+
const shouldReloadShipping = needsShipping
|
|
1237
|
+
&& (!state.applicable || state.shippingGroups.length === 0);
|
|
1147
1238
|
const abandonStaleSubmission = (invoice) => {
|
|
1148
1239
|
set({
|
|
1149
1240
|
...(invoice ? { invoice } : {}),
|
|
@@ -1188,7 +1279,15 @@ function createCheckoutEngine(options) {
|
|
|
1188
1279
|
}
|
|
1189
1280
|
set({ invoice: linkRes.data, addressDirty: false });
|
|
1190
1281
|
if (needsShipping) {
|
|
1191
|
-
|
|
1282
|
+
let ok = true;
|
|
1283
|
+
if (shouldReloadShipping) {
|
|
1284
|
+
ok = await loadApplicableShipping(submittedAddressRevision);
|
|
1285
|
+
}
|
|
1286
|
+
else if (recomputeGroups().length === 0) {
|
|
1287
|
+
// Invoice item IDs can change when the new address snapshot is linked.
|
|
1288
|
+
// Reload only if the existing applicable data can no longer form groups.
|
|
1289
|
+
ok = await loadApplicableShipping(submittedAddressRevision);
|
|
1290
|
+
}
|
|
1192
1291
|
if (ok) {
|
|
1193
1292
|
await assignCurrentShipping();
|
|
1194
1293
|
if (addressRevision !== submittedAddressRevision) {
|
|
@@ -1215,6 +1314,7 @@ function createCheckoutEngine(options) {
|
|
|
1215
1314
|
effectExecutor = executor;
|
|
1216
1315
|
},
|
|
1217
1316
|
destroy() {
|
|
1317
|
+
clearAddressAutoSubmit();
|
|
1218
1318
|
effectExecutor = noopEffectExecutor;
|
|
1219
1319
|
}
|
|
1220
1320
|
};
|
|
@@ -1427,7 +1527,8 @@ const fa = {
|
|
|
1427
1527
|
shippingMethod: 'روش ارسال',
|
|
1428
1528
|
shippingMethods: 'روشهای ارسال',
|
|
1429
1529
|
shippingMethodsHint: 'پس از ثبت نشانی، روشهای قابل استفاده نمایش داده میشوند.',
|
|
1430
|
-
digitalNoShipping: '
|
|
1530
|
+
digitalNoShipping: 'محصولات دیجیتال و خدمات',
|
|
1531
|
+
digitalNoShippingHint: 'این موارد به ارسال فیزیکی نیاز ندارند.',
|
|
1431
1532
|
errorRequired: 'این فیلد اجباری است',
|
|
1432
1533
|
errorMobilePhone: 'شماره موبایل معتبر وارد کنید (مثلاً ۰۹۱۲۳۴۵۶۷۸۹)',
|
|
1433
1534
|
errorEmail: 'آدرس ایمیل معتبر نیست',
|
|
@@ -1507,7 +1608,8 @@ const en = {
|
|
|
1507
1608
|
shippingMethod: 'Shipping method',
|
|
1508
1609
|
shippingMethods: 'Shipping methods',
|
|
1509
1610
|
shippingMethodsHint: 'Available delivery options will appear after you save this address.',
|
|
1510
|
-
digitalNoShipping: 'Digital
|
|
1611
|
+
digitalNoShipping: 'Digital products and services',
|
|
1612
|
+
digitalNoShippingHint: 'These items do not require physical delivery.',
|
|
1511
1613
|
errorRequired: 'This field is required',
|
|
1512
1614
|
errorMobilePhone: 'Enter a valid mobile number (e.g. 09123456789)',
|
|
1513
1615
|
errorEmail: 'Enter a valid email address',
|
|
@@ -1585,5 +1687,5 @@ function paymentMethodInfo(code, locale) {
|
|
|
1585
1687
|
};
|
|
1586
1688
|
}
|
|
1587
1689
|
|
|
1588
|
-
export {
|
|
1589
|
-
//# sourceMappingURL=labels-
|
|
1690
|
+
export { paymentMethodInfo as A, paymentMethodLabel as B, selectDigitalItems as C, selectSummary as D, sortCartItemsNewestFirst as E, strings as F, toEnglishDigits as G, toPersianDigits as H, addressFormFromInvoice as a, buildShippingAssignments as b, classifyAppliedDiscount as c, createBrowserEffectExecutor as d, createCheckoutEngine as e, createSdkBinding as f, createStore as g, defaultCurrencyLabel as h, deriveShippingGroups as i, emptyAddressForm as j, formatMoney as k, formatNumber as l, formatPercent as m, formatPrice as n, fromSdkError as o, isAddressComplete as p, isAddressDirty as q, isDigitalServiceGroup as r, isShippingComplete as s, isValidIranianMobile as t, isValidIranianPhone as u, makeError as v, makeEvent as w, messageForCode as x, noopEffectExecutor as y, normalizeIranianPhone as z };
|
|
1691
|
+
//# sourceMappingURL=labels-ByA_yGKl.js.map
|