@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
|
@@ -202,6 +202,30 @@ function createBrowserEffectExecutor(config) {
|
|
|
202
202
|
const noopEffectExecutor = () => { };
|
|
203
203
|
|
|
204
204
|
const idStr = (value) => String(value);
|
|
205
|
+
function cartItemCreatedAt(item) {
|
|
206
|
+
if (!item.createdAt)
|
|
207
|
+
return null;
|
|
208
|
+
const timestamp = Date.parse(item.createdAt);
|
|
209
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
210
|
+
}
|
|
211
|
+
/** Return cart lines newest-first without mutating SDK state. */
|
|
212
|
+
function sortCartItemsNewestFirst(items) {
|
|
213
|
+
return [...items].sort((a, b) => {
|
|
214
|
+
const aCreatedAt = cartItemCreatedAt(a);
|
|
215
|
+
const bCreatedAt = cartItemCreatedAt(b);
|
|
216
|
+
if (aCreatedAt != null || bCreatedAt != null) {
|
|
217
|
+
if (aCreatedAt == null)
|
|
218
|
+
return 1;
|
|
219
|
+
if (bCreatedAt == null)
|
|
220
|
+
return -1;
|
|
221
|
+
if (aCreatedAt !== bCreatedAt)
|
|
222
|
+
return bCreatedAt - aCreatedAt;
|
|
223
|
+
}
|
|
224
|
+
const aId = Number(a.id);
|
|
225
|
+
const bId = Number(b.id);
|
|
226
|
+
return Number.isFinite(aId) && Number.isFinite(bId) ? bId - aId : 0;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
205
229
|
function emptyAddressForm() {
|
|
206
230
|
return {
|
|
207
231
|
firstName: '',
|
|
@@ -300,6 +324,28 @@ function uniqueRates(rates) {
|
|
|
300
324
|
}
|
|
301
325
|
return out;
|
|
302
326
|
}
|
|
327
|
+
function isDigitalServiceLabel(value) {
|
|
328
|
+
if (!value)
|
|
329
|
+
return false;
|
|
330
|
+
const normalized = value.trim().toLocaleLowerCase('en-US');
|
|
331
|
+
return (normalized.includes('digital') ||
|
|
332
|
+
normalized.includes('service') ||
|
|
333
|
+
normalized.includes('دیجیتال') ||
|
|
334
|
+
normalized.includes('خدمات') ||
|
|
335
|
+
normalized.includes('خدمت'));
|
|
336
|
+
}
|
|
337
|
+
function isDigitalServiceItem(item) {
|
|
338
|
+
return isDigitalServiceLabel(item.productType);
|
|
339
|
+
}
|
|
340
|
+
/** True for the API's automatic digital/service delivery group. */
|
|
341
|
+
function isDigitalServiceGroup(group) {
|
|
342
|
+
if (isDigitalServiceLabel(group.key) || isDigitalServiceLabel(group.title)) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
const ratesAreDigitalService = group.rates.length > 0 &&
|
|
346
|
+
group.rates.every((rate) => isDigitalServiceLabel(rate.type) || isDigitalServiceLabel(rate.name));
|
|
347
|
+
return ratesAreDigitalService || (group.items.length > 0 && group.items.every(isDigitalServiceItem));
|
|
348
|
+
}
|
|
303
349
|
/**
|
|
304
350
|
* Group invoice items into shippable bundles with their switchable rates and
|
|
305
351
|
* the currently selected rate. Digital-only invoices yield an empty list.
|
|
@@ -319,10 +365,12 @@ function deriveShippingGroups(invoice, applicable) {
|
|
|
319
365
|
return [];
|
|
320
366
|
}
|
|
321
367
|
const itemById = new Map(invoice.items.map((item) => [idStr(item.id), item]));
|
|
322
|
-
//
|
|
368
|
+
// The API's plain `digital` products never receive a shipping assignment.
|
|
369
|
+
// Other digital/service variants may arrive through an automatic fulfillment
|
|
370
|
+
// rate; retain those groups for the API assignment and hide them in the UI.
|
|
323
371
|
const physicalItemsRate = (applicable.itemsShippingRate ?? []).filter((isr) => {
|
|
324
372
|
const item = itemById.get(idStr(isr.invoiceItemId));
|
|
325
|
-
return !item || item.productType !== 'digital';
|
|
373
|
+
return !item || item.productType?.trim().toLocaleLowerCase('en-US') !== 'digital';
|
|
326
374
|
});
|
|
327
375
|
if (physicalItemsRate.length === 0)
|
|
328
376
|
return [];
|
|
@@ -421,24 +469,27 @@ function deriveShippingGroups(invoice, applicable) {
|
|
|
421
469
|
}
|
|
422
470
|
];
|
|
423
471
|
}
|
|
424
|
-
/** Build the API payload from
|
|
472
|
+
/** Build the API payload from physical shipping selections. */
|
|
425
473
|
function buildShippingAssignments(groups) {
|
|
426
474
|
return groups
|
|
427
|
-
.filter((group) => group.selectedRateId != null)
|
|
475
|
+
.filter((group) => !isDigitalServiceGroup(group) && group.selectedRateId != null)
|
|
428
476
|
.map((group) => ({
|
|
429
477
|
rateId: group.selectedRateId,
|
|
430
478
|
invoiceItemIds: group.itemIds
|
|
431
479
|
}));
|
|
432
480
|
}
|
|
433
|
-
/** True once every
|
|
481
|
+
/** True once every physical shipping group has a selected rate. */
|
|
434
482
|
function isShippingComplete(invoice, groups) {
|
|
435
483
|
if (!invoice?.needsShipping) {
|
|
436
484
|
return true;
|
|
437
485
|
}
|
|
438
|
-
|
|
439
|
-
|
|
486
|
+
const physicalGroups = groups.filter((group) => !isDigitalServiceGroup(group));
|
|
487
|
+
if (physicalGroups.length === 0) {
|
|
488
|
+
// The API may set needsShipping for an automatic Digital/Service group.
|
|
489
|
+
// It is fulfillment metadata, not a customer-selectable shipping method.
|
|
490
|
+
return groups.length > 0 && groups.every(isDigitalServiceGroup);
|
|
440
491
|
}
|
|
441
|
-
return
|
|
492
|
+
return physicalGroups.every((group) => group.selectedRateId != null);
|
|
442
493
|
}
|
|
443
494
|
const codeDiscountTotal = (invoice) => (invoice.couponTotal || 0) + (invoice.discountTotal || 0);
|
|
444
495
|
/**
|
|
@@ -542,14 +593,22 @@ function selectDigitalItems(invoice, groups, applicable) {
|
|
|
542
593
|
// Before shipping methods are loaded, only explicitly-digital items are known.
|
|
543
594
|
// Showing everything as "digital" when applicable is null would be wrong.
|
|
544
595
|
if (applicable == null) {
|
|
545
|
-
return invoice.items.filter(
|
|
596
|
+
return invoice.items.filter(isDigitalServiceItem);
|
|
546
597
|
}
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
|
|
598
|
+
// The backend may model digital/service fulfillment as an automatic zero-cost
|
|
599
|
+
// shipping group. Treat its products as non-shippable in the UI while keeping
|
|
600
|
+
// the group in state so its assignment can still be submitted.
|
|
601
|
+
const digitalGroupItems = new Set(groups
|
|
602
|
+
.filter(isDigitalServiceGroup)
|
|
603
|
+
.flatMap((group) => group.itemIds.map(idStr)));
|
|
604
|
+
const groupedItems = new Set(groups.flatMap((group) => group.itemIds.map(idStr)));
|
|
605
|
+
return invoice.items.filter((item) => isDigitalServiceItem(item) ||
|
|
606
|
+
digitalGroupItems.has(idStr(item.id)) ||
|
|
607
|
+
!groupedItems.has(idStr(item.id)));
|
|
550
608
|
}
|
|
551
609
|
|
|
552
610
|
const STEP_ORDER = ['cart', 'shipping', 'payment', 'result'];
|
|
611
|
+
const ADDRESS_AUTOSUBMIT_DEBOUNCE_MS = 350;
|
|
553
612
|
function initialFlags() {
|
|
554
613
|
return {
|
|
555
614
|
bootstrapping: false,
|
|
@@ -596,6 +655,7 @@ function createCheckoutEngine(options) {
|
|
|
596
655
|
let effectExecutor = noopEffectExecutor;
|
|
597
656
|
let lock = Promise.resolve();
|
|
598
657
|
let addressRevision = 0;
|
|
658
|
+
let addressAutoSubmitTimer = null;
|
|
599
659
|
// ---- internal helpers -------------------------------------------------
|
|
600
660
|
const get = () => store.getState();
|
|
601
661
|
const set = store.setState;
|
|
@@ -616,6 +676,19 @@ function createCheckoutEngine(options) {
|
|
|
616
676
|
lock = next.then(() => undefined, () => undefined);
|
|
617
677
|
return next;
|
|
618
678
|
}
|
|
679
|
+
function clearAddressAutoSubmit() {
|
|
680
|
+
if (addressAutoSubmitTimer) {
|
|
681
|
+
clearTimeout(addressAutoSubmitTimer);
|
|
682
|
+
addressAutoSubmitTimer = null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
function scheduleAddressAutoSubmit(delay = ADDRESS_AUTOSUBMIT_DEBOUNCE_MS) {
|
|
686
|
+
clearAddressAutoSubmit();
|
|
687
|
+
addressAutoSubmitTimer = setTimeout(() => {
|
|
688
|
+
addressAutoSubmitTimer = null;
|
|
689
|
+
void withLock(submitAddressInternal);
|
|
690
|
+
}, delay);
|
|
691
|
+
}
|
|
619
692
|
function recomputeGroups() {
|
|
620
693
|
const { invoice, applicable } = get();
|
|
621
694
|
const groups = deriveShippingGroups(invoice, applicable);
|
|
@@ -811,13 +884,17 @@ function createCheckoutEngine(options) {
|
|
|
811
884
|
}
|
|
812
885
|
// ---- public actions ---------------------------------------------------
|
|
813
886
|
function goToStep(step) {
|
|
887
|
+
if (step !== 'shipping') {
|
|
888
|
+
clearAddressAutoSubmit();
|
|
889
|
+
}
|
|
814
890
|
set({ step, error: null });
|
|
815
891
|
emit('step_viewed', { step });
|
|
816
892
|
// When landing on shipping with a complete address but no methods fetched yet,
|
|
817
893
|
// auto-submit so methods appear without requiring a manual Continue press.
|
|
818
894
|
if (step === 'shipping') {
|
|
819
|
-
const { addressForm, invoice, applicable } = get();
|
|
820
|
-
|
|
895
|
+
const { addressForm, addressDirty, invoice, applicable, shippingGroups } = get();
|
|
896
|
+
const methodsUnresolved = Boolean(invoice?.needsShipping) && shippingGroups.length === 0;
|
|
897
|
+
if ((addressDirty || !applicable || methodsUnresolved) && isAddressComplete(addressForm, invoice?.needsShipping ?? true, get().postalCodeMandatory, get().emailMandatory)) {
|
|
821
898
|
void withLock(submitAddressInternal);
|
|
822
899
|
}
|
|
823
900
|
}
|
|
@@ -920,13 +997,18 @@ function createCheckoutEngine(options) {
|
|
|
920
997
|
// the Shipping step becomes visible.
|
|
921
998
|
const invoice = get().invoice;
|
|
922
999
|
if (isUsableSavedAddress(invoice?.shippingAddress)) {
|
|
923
|
-
|
|
1000
|
+
const addressForm = reconcileAddressWithRegions(addressFormFromInvoice(invoice), get().regions, savedAddressCityName(invoice?.shippingAddress));
|
|
1001
|
+
set({
|
|
1002
|
+
addressForm,
|
|
1003
|
+
addressDirty: isAddressDirty(addressForm, invoice)
|
|
1004
|
+
});
|
|
924
1005
|
}
|
|
925
1006
|
goToStep('shipping');
|
|
926
1007
|
}
|
|
927
1008
|
return;
|
|
928
1009
|
}
|
|
929
1010
|
if (state.step === 'shipping') {
|
|
1011
|
+
clearAddressAutoSubmit();
|
|
930
1012
|
// Phase 1: address not saved yet (or changed) → save it and reveal the
|
|
931
1013
|
// shipping methods, staying on the shipping step.
|
|
932
1014
|
const needsSave = state.addressDirty || !state.applicable;
|
|
@@ -996,6 +1078,7 @@ function createCheckoutEngine(options) {
|
|
|
996
1078
|
setAddressField(key, value) {
|
|
997
1079
|
const before = get().addressForm;
|
|
998
1080
|
const changed = before[key] !== value || (key === 'regionId' && before.cityId !== null);
|
|
1081
|
+
const destinationChanged = changed && (key === 'regionId' || key === 'cityId');
|
|
999
1082
|
if (changed) {
|
|
1000
1083
|
addressRevision += 1;
|
|
1001
1084
|
}
|
|
@@ -1009,22 +1092,28 @@ function createCheckoutEngine(options) {
|
|
|
1009
1092
|
return {
|
|
1010
1093
|
addressForm,
|
|
1011
1094
|
addressDirty,
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
...(
|
|
1095
|
+
// Shipping rates depend on region/city. Contact and street edits must
|
|
1096
|
+
// not discard valid methods for the same destination.
|
|
1097
|
+
...(destinationChanged ? { applicable: null, shippingGroups: [] } : {})
|
|
1015
1098
|
};
|
|
1016
1099
|
});
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
if (changed &&
|
|
1100
|
+
// Browser autofill often populates the selects before the final required
|
|
1101
|
+
// text field. Once the form becomes complete, debounce text changes and
|
|
1102
|
+
// submit automatically; a discrete city selection can submit immediately.
|
|
1103
|
+
if (changed && get().step === 'shipping') {
|
|
1021
1104
|
const state = get();
|
|
1022
|
-
|
|
1023
|
-
|
|
1105
|
+
const methodsUnresolved = !state.applicable
|
|
1106
|
+
|| (Boolean(state.invoice?.needsShipping) && state.shippingGroups.length === 0);
|
|
1107
|
+
if (isAddressComplete(state.addressForm, state.invoice?.needsShipping ?? true, state.postalCodeMandatory, state.emailMandatory) && (destinationChanged || methodsUnresolved)) {
|
|
1108
|
+
scheduleAddressAutoSubmit(key === 'cityId' ? 0 : undefined);
|
|
1109
|
+
}
|
|
1110
|
+
else if (!isAddressComplete(state.addressForm, state.invoice?.needsShipping ?? true, state.postalCodeMandatory, state.emailMandatory)) {
|
|
1111
|
+
clearAddressAutoSubmit();
|
|
1024
1112
|
}
|
|
1025
1113
|
}
|
|
1026
1114
|
},
|
|
1027
1115
|
async submitAddress() {
|
|
1116
|
+
clearAddressAutoSubmit();
|
|
1028
1117
|
return withLock(submitAddressInternal);
|
|
1029
1118
|
},
|
|
1030
1119
|
async selectShippingRate(groupKey, rateId) {
|
|
@@ -1146,6 +1235,8 @@ function createCheckoutEngine(options) {
|
|
|
1146
1235
|
const state = get();
|
|
1147
1236
|
const submittedAddressRevision = addressRevision;
|
|
1148
1237
|
const needsShipping = state.invoice?.needsShipping ?? true;
|
|
1238
|
+
const shouldReloadShipping = needsShipping
|
|
1239
|
+
&& (!state.applicable || state.shippingGroups.length === 0);
|
|
1149
1240
|
const abandonStaleSubmission = (invoice) => {
|
|
1150
1241
|
set({
|
|
1151
1242
|
...(invoice ? { invoice } : {}),
|
|
@@ -1190,7 +1281,15 @@ function createCheckoutEngine(options) {
|
|
|
1190
1281
|
}
|
|
1191
1282
|
set({ invoice: linkRes.data, addressDirty: false });
|
|
1192
1283
|
if (needsShipping) {
|
|
1193
|
-
|
|
1284
|
+
let ok = true;
|
|
1285
|
+
if (shouldReloadShipping) {
|
|
1286
|
+
ok = await loadApplicableShipping(submittedAddressRevision);
|
|
1287
|
+
}
|
|
1288
|
+
else if (recomputeGroups().length === 0) {
|
|
1289
|
+
// Invoice item IDs can change when the new address snapshot is linked.
|
|
1290
|
+
// Reload only if the existing applicable data can no longer form groups.
|
|
1291
|
+
ok = await loadApplicableShipping(submittedAddressRevision);
|
|
1292
|
+
}
|
|
1194
1293
|
if (ok) {
|
|
1195
1294
|
await assignCurrentShipping();
|
|
1196
1295
|
if (addressRevision !== submittedAddressRevision) {
|
|
@@ -1217,6 +1316,7 @@ function createCheckoutEngine(options) {
|
|
|
1217
1316
|
effectExecutor = executor;
|
|
1218
1317
|
},
|
|
1219
1318
|
destroy() {
|
|
1319
|
+
clearAddressAutoSubmit();
|
|
1220
1320
|
effectExecutor = noopEffectExecutor;
|
|
1221
1321
|
}
|
|
1222
1322
|
};
|
|
@@ -1429,7 +1529,8 @@ const fa = {
|
|
|
1429
1529
|
shippingMethod: 'روش ارسال',
|
|
1430
1530
|
shippingMethods: 'روشهای ارسال',
|
|
1431
1531
|
shippingMethodsHint: 'پس از ثبت نشانی، روشهای قابل استفاده نمایش داده میشوند.',
|
|
1432
|
-
digitalNoShipping: '
|
|
1532
|
+
digitalNoShipping: 'محصولات دیجیتال و خدمات',
|
|
1533
|
+
digitalNoShippingHint: 'این موارد به ارسال فیزیکی نیاز ندارند.',
|
|
1433
1534
|
errorRequired: 'این فیلد اجباری است',
|
|
1434
1535
|
errorMobilePhone: 'شماره موبایل معتبر وارد کنید (مثلاً ۰۹۱۲۳۴۵۶۷۸۹)',
|
|
1435
1536
|
errorEmail: 'آدرس ایمیل معتبر نیست',
|
|
@@ -1509,7 +1610,8 @@ const en = {
|
|
|
1509
1610
|
shippingMethod: 'Shipping method',
|
|
1510
1611
|
shippingMethods: 'Shipping methods',
|
|
1511
1612
|
shippingMethodsHint: 'Available delivery options will appear after you save this address.',
|
|
1512
|
-
digitalNoShipping: 'Digital
|
|
1613
|
+
digitalNoShipping: 'Digital products and services',
|
|
1614
|
+
digitalNoShippingHint: 'These items do not require physical delivery.',
|
|
1513
1615
|
errorRequired: 'This field is required',
|
|
1514
1616
|
errorMobilePhone: 'Enter a valid mobile number (e.g. 09123456789)',
|
|
1515
1617
|
errorEmail: 'Enter a valid email address',
|
|
@@ -1604,6 +1706,7 @@ exports.formatPrice = formatPrice;
|
|
|
1604
1706
|
exports.fromSdkError = fromSdkError;
|
|
1605
1707
|
exports.isAddressComplete = isAddressComplete;
|
|
1606
1708
|
exports.isAddressDirty = isAddressDirty;
|
|
1709
|
+
exports.isDigitalServiceGroup = isDigitalServiceGroup;
|
|
1607
1710
|
exports.isShippingComplete = isShippingComplete;
|
|
1608
1711
|
exports.isValidIranianMobile = isValidIranianMobile;
|
|
1609
1712
|
exports.isValidIranianPhone = isValidIranianPhone;
|
|
@@ -1616,7 +1719,8 @@ exports.paymentMethodInfo = paymentMethodInfo;
|
|
|
1616
1719
|
exports.paymentMethodLabel = paymentMethodLabel;
|
|
1617
1720
|
exports.selectDigitalItems = selectDigitalItems;
|
|
1618
1721
|
exports.selectSummary = selectSummary;
|
|
1722
|
+
exports.sortCartItemsNewestFirst = sortCartItemsNewestFirst;
|
|
1619
1723
|
exports.strings = strings;
|
|
1620
1724
|
exports.toEnglishDigits = toEnglishDigits;
|
|
1621
1725
|
exports.toPersianDigits = toPersianDigits;
|
|
1622
|
-
//# sourceMappingURL=labels-
|
|
1726
|
+
//# sourceMappingURL=labels-DJ_RnkCQ.cjs.map
|