@cimplify/sdk 0.15.7 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/advanced.d.mts +1 -1
- package/dist/advanced.d.ts +1 -1
- package/dist/{client-eY00I7H1.d.ts → client-NdvQpCxU.d.ts} +25 -2
- package/dist/{client-Cx9Mfh6U.d.mts → client-wOQ8P1Ks.d.mts} +25 -2
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +213 -0
- package/dist/index.mjs +210 -1
- package/dist/react.d.mts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +63 -1
- package/dist/react.mjs +63 -1
- package/dist/styles.css +1 -1
- package/dist/utils.d.mts +10 -1
- package/dist/utils.d.ts +10 -1
- package/dist/utils.js +166 -0
- package/dist/utils.mjs +164 -1
- package/package.json +13 -12
- package/registry/product-customizer.json +1 -1
package/dist/utils.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var libphonenumberJs = require('libphonenumber-js');
|
|
4
|
+
|
|
3
5
|
// src/utils/price.ts
|
|
4
6
|
var CURRENCY_SYMBOLS = {
|
|
5
7
|
USD: "$",
|
|
@@ -529,6 +531,167 @@ function detectMobileMoneyProvider(phoneNumber) {
|
|
|
529
531
|
}
|
|
530
532
|
return null;
|
|
531
533
|
}
|
|
534
|
+
var ISO3_TO_COUNTRY_CODE = {
|
|
535
|
+
AGO: "AO",
|
|
536
|
+
ARE: "AE",
|
|
537
|
+
AUS: "AU",
|
|
538
|
+
BDI: "BI",
|
|
539
|
+
BRA: "BR",
|
|
540
|
+
BWA: "BW",
|
|
541
|
+
CAN: "CA",
|
|
542
|
+
CHE: "CH",
|
|
543
|
+
CHN: "CN",
|
|
544
|
+
COD: "CD",
|
|
545
|
+
EGY: "EG",
|
|
546
|
+
ETH: "ET",
|
|
547
|
+
GHA: "GH",
|
|
548
|
+
GIN: "GN",
|
|
549
|
+
GMB: "GM",
|
|
550
|
+
GBR: "GB",
|
|
551
|
+
HKG: "HK",
|
|
552
|
+
IDN: "ID",
|
|
553
|
+
IND: "IN",
|
|
554
|
+
ISR: "IL",
|
|
555
|
+
JPN: "JP",
|
|
556
|
+
KEN: "KE",
|
|
557
|
+
KOR: "KR",
|
|
558
|
+
LBR: "LR",
|
|
559
|
+
MUS: "MU",
|
|
560
|
+
MEX: "MX",
|
|
561
|
+
MOZ: "MZ",
|
|
562
|
+
MWI: "MW",
|
|
563
|
+
MYS: "MY",
|
|
564
|
+
NAM: "NA",
|
|
565
|
+
NGA: "NG",
|
|
566
|
+
PHL: "PH",
|
|
567
|
+
RWA: "RW",
|
|
568
|
+
SAU: "SA",
|
|
569
|
+
SGP: "SG",
|
|
570
|
+
SLE: "SL",
|
|
571
|
+
THA: "TH",
|
|
572
|
+
TUR: "TR",
|
|
573
|
+
TWN: "TW",
|
|
574
|
+
TZA: "TZ",
|
|
575
|
+
UGA: "UG",
|
|
576
|
+
USA: "US",
|
|
577
|
+
VNM: "VN",
|
|
578
|
+
ZAF: "ZA",
|
|
579
|
+
ZMB: "ZM"
|
|
580
|
+
};
|
|
581
|
+
var CURRENCY_TO_COUNTRY_CODE = {
|
|
582
|
+
AED: "AE",
|
|
583
|
+
AOA: "AO",
|
|
584
|
+
AUD: "AU",
|
|
585
|
+
BIF: "BI",
|
|
586
|
+
BWP: "BW",
|
|
587
|
+
BRL: "BR",
|
|
588
|
+
CAD: "CA",
|
|
589
|
+
CDF: "CD",
|
|
590
|
+
CHF: "CH",
|
|
591
|
+
CNY: "CN",
|
|
592
|
+
EGP: "EG",
|
|
593
|
+
ETB: "ET",
|
|
594
|
+
GHS: "GH",
|
|
595
|
+
GMD: "GM",
|
|
596
|
+
GNF: "GN",
|
|
597
|
+
GBP: "GB",
|
|
598
|
+
HKD: "HK",
|
|
599
|
+
IDR: "ID",
|
|
600
|
+
ILS: "IL",
|
|
601
|
+
INR: "IN",
|
|
602
|
+
JPY: "JP",
|
|
603
|
+
KES: "KE",
|
|
604
|
+
KRW: "KR",
|
|
605
|
+
LRD: "LR",
|
|
606
|
+
MUR: "MU",
|
|
607
|
+
MWK: "MW",
|
|
608
|
+
MXN: "MX",
|
|
609
|
+
MYR: "MY",
|
|
610
|
+
MZN: "MZ",
|
|
611
|
+
NAD: "NA",
|
|
612
|
+
NGN: "NG",
|
|
613
|
+
PHP: "PH",
|
|
614
|
+
RWF: "RW",
|
|
615
|
+
SAR: "SA",
|
|
616
|
+
SGD: "SG",
|
|
617
|
+
SLL: "SL",
|
|
618
|
+
THB: "TH",
|
|
619
|
+
TRY: "TR",
|
|
620
|
+
TWD: "TW",
|
|
621
|
+
TZS: "TZ",
|
|
622
|
+
UGX: "UG",
|
|
623
|
+
USD: "US",
|
|
624
|
+
VND: "VN",
|
|
625
|
+
ZAR: "ZA",
|
|
626
|
+
ZMW: "ZM"
|
|
627
|
+
};
|
|
628
|
+
function normalizeCountryCode(value) {
|
|
629
|
+
const normalized = value?.trim().toUpperCase();
|
|
630
|
+
if (!normalized) {
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
if (normalized.length === 2) {
|
|
634
|
+
return normalized;
|
|
635
|
+
}
|
|
636
|
+
return ISO3_TO_COUNTRY_CODE[normalized];
|
|
637
|
+
}
|
|
638
|
+
function parsePhoneCandidate(value, country) {
|
|
639
|
+
try {
|
|
640
|
+
return country ? libphonenumberJs.parsePhoneNumberFromString(value, country) : libphonenumberJs.parsePhoneNumberFromString(value);
|
|
641
|
+
} catch {
|
|
642
|
+
return void 0;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function resolvePhoneCountryCode(options = {}) {
|
|
646
|
+
const explicitCountry = normalizeCountryCode(options.country);
|
|
647
|
+
if (explicitCountry) {
|
|
648
|
+
return explicitCountry;
|
|
649
|
+
}
|
|
650
|
+
const currency = options.currency?.trim().toUpperCase();
|
|
651
|
+
if (!currency) {
|
|
652
|
+
return void 0;
|
|
653
|
+
}
|
|
654
|
+
return CURRENCY_TO_COUNTRY_CODE[currency];
|
|
655
|
+
}
|
|
656
|
+
function normalizePhoneToE164(raw, options = {}) {
|
|
657
|
+
const trimmed = raw.trim();
|
|
658
|
+
if (!trimmed) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
const country = resolvePhoneCountryCode(options);
|
|
662
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
663
|
+
const internationalDigits = digits.startsWith("00") ? digits.slice(2) : digits;
|
|
664
|
+
const attempts = [];
|
|
665
|
+
const seen = /* @__PURE__ */ new Set();
|
|
666
|
+
function addAttempt(value, nextCountry) {
|
|
667
|
+
const key = `${nextCountry || ""}:${value}`;
|
|
668
|
+
if (seen.has(key)) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
seen.add(key);
|
|
672
|
+
attempts.push({ value, country: nextCountry });
|
|
673
|
+
}
|
|
674
|
+
if (country) {
|
|
675
|
+
addAttempt(trimmed, country);
|
|
676
|
+
}
|
|
677
|
+
addAttempt(trimmed);
|
|
678
|
+
if (country && digits) {
|
|
679
|
+
addAttempt(digits, country);
|
|
680
|
+
}
|
|
681
|
+
if (internationalDigits) {
|
|
682
|
+
addAttempt(`+${internationalDigits}`);
|
|
683
|
+
}
|
|
684
|
+
for (const attempt of attempts) {
|
|
685
|
+
const parsed = parsePhoneCandidate(attempt.value, attempt.country);
|
|
686
|
+
if (parsed?.isValid()) {
|
|
687
|
+
return parsed.format("E.164");
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
function isValidPhoneForContext(raw, options = {}) {
|
|
693
|
+
return normalizePhoneToE164(raw, options) !== null;
|
|
694
|
+
}
|
|
532
695
|
|
|
533
696
|
exports.CURRENCY_SYMBOLS = CURRENCY_SYMBOLS;
|
|
534
697
|
exports.MOBILE_MONEY_PROVIDERS = MOBILE_MONEY_PROVIDERS;
|
|
@@ -554,6 +717,9 @@ exports.isPaymentStatusFailure = isPaymentStatusFailure;
|
|
|
554
717
|
exports.isPaymentStatusRequiresAction = isPaymentStatusRequiresAction;
|
|
555
718
|
exports.isPaymentStatusSuccess = isPaymentStatusSuccess;
|
|
556
719
|
exports.isTaxInclusive = isTaxInclusive;
|
|
720
|
+
exports.isValidPhoneForContext = isValidPhoneForContext;
|
|
557
721
|
exports.normalizePaymentResponse = normalizePaymentResponse;
|
|
722
|
+
exports.normalizePhoneToE164 = normalizePhoneToE164;
|
|
558
723
|
exports.normalizeStatusResponse = normalizeStatusResponse;
|
|
559
724
|
exports.parsePrice = parsePrice;
|
|
725
|
+
exports.resolvePhoneCountryCode = resolvePhoneCountryCode;
|
package/dist/utils.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
|
2
|
+
|
|
1
3
|
// src/utils/price.ts
|
|
2
4
|
var CURRENCY_SYMBOLS = {
|
|
3
5
|
USD: "$",
|
|
@@ -527,5 +529,166 @@ function detectMobileMoneyProvider(phoneNumber) {
|
|
|
527
529
|
}
|
|
528
530
|
return null;
|
|
529
531
|
}
|
|
532
|
+
var ISO3_TO_COUNTRY_CODE = {
|
|
533
|
+
AGO: "AO",
|
|
534
|
+
ARE: "AE",
|
|
535
|
+
AUS: "AU",
|
|
536
|
+
BDI: "BI",
|
|
537
|
+
BRA: "BR",
|
|
538
|
+
BWA: "BW",
|
|
539
|
+
CAN: "CA",
|
|
540
|
+
CHE: "CH",
|
|
541
|
+
CHN: "CN",
|
|
542
|
+
COD: "CD",
|
|
543
|
+
EGY: "EG",
|
|
544
|
+
ETH: "ET",
|
|
545
|
+
GHA: "GH",
|
|
546
|
+
GIN: "GN",
|
|
547
|
+
GMB: "GM",
|
|
548
|
+
GBR: "GB",
|
|
549
|
+
HKG: "HK",
|
|
550
|
+
IDN: "ID",
|
|
551
|
+
IND: "IN",
|
|
552
|
+
ISR: "IL",
|
|
553
|
+
JPN: "JP",
|
|
554
|
+
KEN: "KE",
|
|
555
|
+
KOR: "KR",
|
|
556
|
+
LBR: "LR",
|
|
557
|
+
MUS: "MU",
|
|
558
|
+
MEX: "MX",
|
|
559
|
+
MOZ: "MZ",
|
|
560
|
+
MWI: "MW",
|
|
561
|
+
MYS: "MY",
|
|
562
|
+
NAM: "NA",
|
|
563
|
+
NGA: "NG",
|
|
564
|
+
PHL: "PH",
|
|
565
|
+
RWA: "RW",
|
|
566
|
+
SAU: "SA",
|
|
567
|
+
SGP: "SG",
|
|
568
|
+
SLE: "SL",
|
|
569
|
+
THA: "TH",
|
|
570
|
+
TUR: "TR",
|
|
571
|
+
TWN: "TW",
|
|
572
|
+
TZA: "TZ",
|
|
573
|
+
UGA: "UG",
|
|
574
|
+
USA: "US",
|
|
575
|
+
VNM: "VN",
|
|
576
|
+
ZAF: "ZA",
|
|
577
|
+
ZMB: "ZM"
|
|
578
|
+
};
|
|
579
|
+
var CURRENCY_TO_COUNTRY_CODE = {
|
|
580
|
+
AED: "AE",
|
|
581
|
+
AOA: "AO",
|
|
582
|
+
AUD: "AU",
|
|
583
|
+
BIF: "BI",
|
|
584
|
+
BWP: "BW",
|
|
585
|
+
BRL: "BR",
|
|
586
|
+
CAD: "CA",
|
|
587
|
+
CDF: "CD",
|
|
588
|
+
CHF: "CH",
|
|
589
|
+
CNY: "CN",
|
|
590
|
+
EGP: "EG",
|
|
591
|
+
ETB: "ET",
|
|
592
|
+
GHS: "GH",
|
|
593
|
+
GMD: "GM",
|
|
594
|
+
GNF: "GN",
|
|
595
|
+
GBP: "GB",
|
|
596
|
+
HKD: "HK",
|
|
597
|
+
IDR: "ID",
|
|
598
|
+
ILS: "IL",
|
|
599
|
+
INR: "IN",
|
|
600
|
+
JPY: "JP",
|
|
601
|
+
KES: "KE",
|
|
602
|
+
KRW: "KR",
|
|
603
|
+
LRD: "LR",
|
|
604
|
+
MUR: "MU",
|
|
605
|
+
MWK: "MW",
|
|
606
|
+
MXN: "MX",
|
|
607
|
+
MYR: "MY",
|
|
608
|
+
MZN: "MZ",
|
|
609
|
+
NAD: "NA",
|
|
610
|
+
NGN: "NG",
|
|
611
|
+
PHP: "PH",
|
|
612
|
+
RWF: "RW",
|
|
613
|
+
SAR: "SA",
|
|
614
|
+
SGD: "SG",
|
|
615
|
+
SLL: "SL",
|
|
616
|
+
THB: "TH",
|
|
617
|
+
TRY: "TR",
|
|
618
|
+
TWD: "TW",
|
|
619
|
+
TZS: "TZ",
|
|
620
|
+
UGX: "UG",
|
|
621
|
+
USD: "US",
|
|
622
|
+
VND: "VN",
|
|
623
|
+
ZAR: "ZA",
|
|
624
|
+
ZMW: "ZM"
|
|
625
|
+
};
|
|
626
|
+
function normalizeCountryCode(value) {
|
|
627
|
+
const normalized = value?.trim().toUpperCase();
|
|
628
|
+
if (!normalized) {
|
|
629
|
+
return void 0;
|
|
630
|
+
}
|
|
631
|
+
if (normalized.length === 2) {
|
|
632
|
+
return normalized;
|
|
633
|
+
}
|
|
634
|
+
return ISO3_TO_COUNTRY_CODE[normalized];
|
|
635
|
+
}
|
|
636
|
+
function parsePhoneCandidate(value, country) {
|
|
637
|
+
try {
|
|
638
|
+
return country ? parsePhoneNumberFromString(value, country) : parsePhoneNumberFromString(value);
|
|
639
|
+
} catch {
|
|
640
|
+
return void 0;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function resolvePhoneCountryCode(options = {}) {
|
|
644
|
+
const explicitCountry = normalizeCountryCode(options.country);
|
|
645
|
+
if (explicitCountry) {
|
|
646
|
+
return explicitCountry;
|
|
647
|
+
}
|
|
648
|
+
const currency = options.currency?.trim().toUpperCase();
|
|
649
|
+
if (!currency) {
|
|
650
|
+
return void 0;
|
|
651
|
+
}
|
|
652
|
+
return CURRENCY_TO_COUNTRY_CODE[currency];
|
|
653
|
+
}
|
|
654
|
+
function normalizePhoneToE164(raw, options = {}) {
|
|
655
|
+
const trimmed = raw.trim();
|
|
656
|
+
if (!trimmed) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
const country = resolvePhoneCountryCode(options);
|
|
660
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
661
|
+
const internationalDigits = digits.startsWith("00") ? digits.slice(2) : digits;
|
|
662
|
+
const attempts = [];
|
|
663
|
+
const seen = /* @__PURE__ */ new Set();
|
|
664
|
+
function addAttempt(value, nextCountry) {
|
|
665
|
+
const key = `${nextCountry || ""}:${value}`;
|
|
666
|
+
if (seen.has(key)) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
seen.add(key);
|
|
670
|
+
attempts.push({ value, country: nextCountry });
|
|
671
|
+
}
|
|
672
|
+
if (country) {
|
|
673
|
+
addAttempt(trimmed, country);
|
|
674
|
+
}
|
|
675
|
+
addAttempt(trimmed);
|
|
676
|
+
if (country && digits) {
|
|
677
|
+
addAttempt(digits, country);
|
|
678
|
+
}
|
|
679
|
+
if (internationalDigits) {
|
|
680
|
+
addAttempt(`+${internationalDigits}`);
|
|
681
|
+
}
|
|
682
|
+
for (const attempt of attempts) {
|
|
683
|
+
const parsed = parsePhoneCandidate(attempt.value, attempt.country);
|
|
684
|
+
if (parsed?.isValid()) {
|
|
685
|
+
return parsed.format("E.164");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
function isValidPhoneForContext(raw, options = {}) {
|
|
691
|
+
return normalizePhoneToE164(raw, options) !== null;
|
|
692
|
+
}
|
|
530
693
|
|
|
531
|
-
export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, normalizePaymentResponse, normalizeStatusResponse, parsePrice };
|
|
694
|
+
export { CURRENCY_SYMBOLS, MOBILE_MONEY_PROVIDERS, categorizePaymentError, detectMobileMoneyProvider, formatMoney, formatNumberCompact, formatPrice, formatPriceAdjustment, formatPriceCompact, formatPriceWithTax, formatProductPrice, getBasePrice, getCurrencySymbol, getDiscountPercentage, getDisplayPrice, getMarkupPercentage, getProductCurrency, getTaxAmount, hasTaxInfo, isOnSale, isPaymentStatusFailure, isPaymentStatusRequiresAction, isPaymentStatusSuccess, isTaxInclusive, isValidPhoneForContext, normalizePaymentResponse, normalizePhoneToE164, normalizeStatusResponse, parsePrice, resolvePhoneCountryCode };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cimplify/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Cimplify Commerce SDK for storefronts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cimplify",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"bin": {
|
|
13
|
-
"cimplify-sdk": "
|
|
13
|
+
"cimplify-sdk": "dist/cli.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"build:css": "bunx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify",
|
|
48
48
|
"build:registry": "bun scripts/build-registry.ts",
|
|
49
49
|
"dev": "tsup --watch",
|
|
50
|
-
"typecheck": "tsgo --noEmit",
|
|
50
|
+
"typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.node.json && tsgo --noEmit -p tsconfig.test.json",
|
|
51
51
|
"clean": "rm -rf dist",
|
|
52
52
|
"lint:ox": "oxlint --fix .",
|
|
53
53
|
"format": "oxfmt . --write",
|
|
@@ -69,23 +69,24 @@
|
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@tailwindcss/cli": "4.
|
|
72
|
+
"@tailwindcss/cli": "4.2.2",
|
|
73
73
|
"@testing-library/dom": "^10.4.1",
|
|
74
74
|
"@testing-library/react": "^16.3.2",
|
|
75
|
-
"@types/node": "^25.
|
|
75
|
+
"@types/node": "^25.5.0",
|
|
76
76
|
"@types/react": "^19.2.7",
|
|
77
|
-
"@typescript/native-preview": "^7.0.0-dev.
|
|
78
|
-
"jsdom": "^
|
|
77
|
+
"@typescript/native-preview": "^7.0.0-dev.20260321.1",
|
|
78
|
+
"jsdom": "^29.0.1",
|
|
79
79
|
"react": "^19.2.4",
|
|
80
80
|
"react-dom": "^19.2.4",
|
|
81
|
-
"tailwindcss": "4.
|
|
81
|
+
"tailwindcss": "4.2.2",
|
|
82
82
|
"tsup": "^8.5.1",
|
|
83
83
|
"typescript": "5.9.3",
|
|
84
|
-
"vitest": "^4.0
|
|
84
|
+
"vitest": "^4.1.0"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@base-ui/react": "^1.
|
|
88
|
-
"clsx": "^2.1.
|
|
89
|
-
"
|
|
87
|
+
"@base-ui/react": "^1.3.0",
|
|
88
|
+
"clsx": "^2.1.1",
|
|
89
|
+
"libphonenumber-js": "1.12.40",
|
|
90
|
+
"tailwind-merge": "^3.5.0"
|
|
90
91
|
}
|
|
91
92
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "product-customizer.tsx",
|
|
17
|
-
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n ProductVariant,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<ProductVariant | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n }, [product.id]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const quoteEnabled = isComposite\n ? compositeReady\n : isBundle\n ? bundleReady\n : requiredAddOnsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price ?? priceInfo.final_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: ProductVariant | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const handleAddToCart = async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n setTimeout(() => {\n setIsAdded(false);\n setQuantity(1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n />\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={1}\n />\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart · <Price amount={displayTotalPrice} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
|
|
17
|
+
"content": "\"use client\";\n\nimport React, { useState, useCallback, useMemo, useEffect } from \"react\";\nimport { Button } from \"@base-ui/react/button\";\nimport type {\n ProductWithDetails,\n ProductVariant,\n AddOnOption,\n ComponentSelectionInput,\n CompositePriceResult,\n} from \"@cimplify/sdk\";\nimport type { BundleSelectionInput } from \"@cimplify/sdk\";\nimport { parsePrice } from \"@cimplify/sdk\";\nimport { useCart, useQuote } from \"@cimplify/sdk/react\";\nimport type { AddToCartOptions } from \"@cimplify/sdk/react\";\nimport { Price } from \"@cimplify/sdk/react\";\nimport { QuantitySelector } from \"@cimplify/sdk/react\";\nimport { VariantSelector } from \"@cimplify/sdk/react\";\nimport { AddOnSelector } from \"@cimplify/sdk/react\";\nimport { CompositeSelector } from \"@cimplify/sdk/react\";\nimport { BundleSelector } from \"@cimplify/sdk/react\";\nimport { cn } from \"@cimplify/sdk/react\";\n\nexport interface ProductCustomizerClassNames {\n root?: string;\n actions?: string;\n submitButton?: string;\n submitButtonAdded?: string;\n validation?: string;\n}\n\nexport interface ProductCustomizerProps {\n product: ProductWithDetails;\n onAddToCart?: (\n product: ProductWithDetails,\n quantity: number,\n options: AddToCartOptions,\n ) => void | Promise<void>;\n className?: string;\n classNames?: ProductCustomizerClassNames;\n}\n\nexport function ProductCustomizer({\n product,\n onAddToCart,\n className,\n classNames,\n}: ProductCustomizerProps): React.ReactElement {\n const [quantity, setQuantity] = useState(1);\n const [isAdded, setIsAdded] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [selectedVariantId, setSelectedVariantId] = useState<string | undefined>();\n const [selectedVariant, setSelectedVariant] = useState<ProductVariant | undefined>();\n const [selectedAddOnOptionIds, setSelectedAddOnOptionIds] = useState<string[]>([]);\n\n const [compositeSelections, setCompositeSelections] = useState<ComponentSelectionInput[]>([]);\n const [compositePrice, setCompositePrice] = useState<CompositePriceResult | null>(null);\n const [compositeReady, setCompositeReady] = useState(false);\n\n const [bundleSelections, setBundleSelections] = useState<BundleSelectionInput[]>([]);\n const [bundleTotalPrice, setBundleTotalPrice] = useState<number | null>(null);\n const [bundleReady, setBundleReady] = useState(false);\n\n const cart = useCart();\n\n const productType = product.type || \"product\";\n const isComposite = productType === \"composite\";\n const isBundle = productType === \"bundle\";\n const isStandard = !isComposite && !isBundle;\n\n const hasVariants = isStandard && product.variants && product.variants.length > 0;\n const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;\n\n useEffect(() => {\n setQuantity(1);\n setIsAdded(false);\n setIsSubmitting(false);\n setSelectedVariantId(undefined);\n setSelectedVariant(undefined);\n setSelectedAddOnOptionIds([]);\n setCompositeSelections([]);\n setCompositePrice(null);\n setCompositeReady(false);\n setBundleSelections([]);\n setBundleTotalPrice(null);\n setBundleReady(false);\n }, [product.id]);\n\n const selectedAddOnOptions = useMemo(() => {\n if (!product.add_ons) return [];\n const options: AddOnOption[] = [];\n for (const addOn of product.add_ons) {\n for (const option of addOn.options) {\n if (selectedAddOnOptionIds.includes(option.id)) {\n options.push(option);\n }\n }\n }\n return options;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const normalizedAddOnOptionIds = useMemo(() => {\n if (selectedAddOnOptionIds.length === 0) return [];\n return Array.from(new Set(selectedAddOnOptionIds.map((id) => id.trim()).filter(Boolean))).sort();\n }, [selectedAddOnOptionIds]);\n\n const localTotalPrice = useMemo(() => {\n if (isComposite && compositePrice) {\n return parsePrice(compositePrice.final_price) * quantity;\n }\n\n if (isBundle && bundleTotalPrice != null) {\n return bundleTotalPrice * quantity;\n }\n\n let price = parsePrice(product.default_price);\n\n if (selectedVariant?.price_adjustment) {\n price += parsePrice(selectedVariant.price_adjustment);\n }\n\n for (const option of selectedAddOnOptions) {\n if (option.default_price) {\n price += parsePrice(option.default_price);\n }\n }\n\n return price * quantity;\n }, [product.default_price, selectedVariant, selectedAddOnOptions, quantity, isComposite, compositePrice, isBundle, bundleTotalPrice]);\n\n const requiredAddOnsSatisfied = useMemo(() => {\n if (!product.add_ons) return true;\n\n for (const addOn of product.add_ons) {\n if (addOn.is_required) {\n const selectedInGroup = selectedAddOnOptionIds.filter((id) =>\n addOn.options.some((opt) => opt.id === id),\n ).length;\n\n const minRequired = addOn.min_selections || 1;\n if (selectedInGroup < minRequired) {\n return false;\n }\n }\n }\n return true;\n }, [product.add_ons, selectedAddOnOptionIds]);\n\n const quoteInput = useMemo(\n () => ({\n productId: product.id,\n quantity,\n variantId: selectedVariantId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n }),\n [product.id, quantity, selectedVariantId, normalizedAddOnOptionIds, isBundle, bundleSelections, isComposite, compositeSelections],\n );\n\n const quoteEnabled = isComposite\n ? compositeReady\n : isBundle\n ? bundleReady\n : requiredAddOnsSatisfied;\n\n const { quote } = useQuote(quoteInput, {\n enabled: quoteEnabled,\n });\n\n const quoteId = quote?.quote_id;\n const quotedTotalPrice = useMemo(() => {\n if (!quote) return undefined;\n\n const priceInfo = quote.quoted_total_price_info ?? quote.final_price_info;\n const perUnit = priceInfo.pre_tax_price;\n if (perUnit === undefined || perUnit === null) return undefined;\n return parsePrice(perUnit) * quantity;\n }, [quote, quantity]);\n\n const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;\n\n const handleVariantChange = useCallback(\n (variantId: string | undefined, variant: ProductVariant | undefined) => {\n setSelectedVariantId(variantId);\n setSelectedVariant(variant);\n },\n [],\n );\n\n const handleAddToCart = async () => {\n if (isSubmitting) return;\n\n setIsSubmitting(true);\n\n const options: AddToCartOptions = {\n variantId: selectedVariantId,\n variant: selectedVariant\n ? { id: selectedVariant.id, name: selectedVariant.name || \"\", price_adjustment: selectedVariant.price_adjustment }\n : undefined,\n quoteId,\n addOnOptionIds: normalizedAddOnOptionIds.length > 0 ? normalizedAddOnOptionIds : undefined,\n addOnOptions: selectedAddOnOptions.length > 0\n ? selectedAddOnOptions.map((opt) => ({\n id: opt.id,\n name: opt.name,\n add_on_id: opt.add_on_id,\n default_price: opt.default_price,\n }))\n : undefined,\n compositeSelections: isComposite && compositeSelections.length > 0 ? compositeSelections : undefined,\n bundleSelections: isBundle && bundleSelections.length > 0 ? bundleSelections : undefined,\n };\n\n try {\n if (onAddToCart) {\n await onAddToCart(product, quantity, options);\n } else {\n await cart.addItem(product, quantity, options);\n }\n setIsAdded(true);\n setTimeout(() => {\n setIsAdded(false);\n setQuantity(1);\n }, 2000);\n } catch {\n // Caller handles errors via onAddToCart or cart hook error state\n } finally {\n setIsSubmitting(false);\n }\n };\n\n return (\n <div data-cimplify-customizer className={cn(\"space-y-6\", className, classNames?.root)}>\n {isComposite && product.groups && product.composite_id && (\n <CompositeSelector\n compositeId={product.composite_id}\n groups={product.groups}\n onSelectionsChange={setCompositeSelections}\n onPriceChange={setCompositePrice}\n onReady={setCompositeReady}\n skipPriceFetch\n />\n )}\n\n {isBundle && product.components && (\n <BundleSelector\n components={product.components}\n bundlePrice={product.bundle_price}\n discountValue={product.discount_value}\n pricingType={product.pricing_type}\n onSelectionsChange={setBundleSelections}\n onPriceChange={setBundleTotalPrice}\n onReady={setBundleReady}\n />\n )}\n\n {hasVariants && (\n <VariantSelector\n variants={product.variants!}\n variantAxes={product.variant_axes}\n basePrice={product.default_price}\n selectedVariantId={selectedVariantId}\n onVariantChange={handleVariantChange}\n productName={product.name}\n />\n )}\n\n {hasAddOns && (\n <AddOnSelector\n addOns={product.add_ons!}\n selectedOptions={selectedAddOnOptionIds}\n onOptionsChange={setSelectedAddOnOptionIds}\n />\n )}\n\n <div\n data-cimplify-customizer-actions\n className={cn(\"pt-4 border-t border-border\", classNames?.actions)}\n >\n {!quoteEnabled && (\n <p\n id=\"cimplify-customizer-validation\"\n data-cimplify-customizer-validation\n className={cn(\"text-sm text-destructive mb-3\", classNames?.validation)}\n >\n Please select all required options\n </p>\n )}\n <div className=\"flex items-center gap-4\">\n <QuantitySelector\n value={quantity}\n onChange={setQuantity}\n min={1}\n />\n\n <Button\n onClick={handleAddToCart}\n disabled={isAdded || isSubmitting || !quoteEnabled}\n aria-describedby={!quoteEnabled ? \"cimplify-customizer-validation\" : undefined}\n data-cimplify-customizer-submit\n className={cn(\n \"flex-1 h-14 text-base bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed rounded-full\",\n isAdded && classNames?.submitButtonAdded,\n classNames?.submitButton,\n )}\n >\n {isAdded ? \"Added to Cart\" : (\n <>\n Add to Cart · <Price amount={displayTotalPrice} />\n </>\n )}\n </Button>\n </div>\n </div>\n\n </div>\n );\n}\n"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
20
|
}
|