@cimplify/sdk 0.6.6 → 0.6.8
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 +19 -0
- package/dist/advanced.d.mts +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.js +228 -101
- package/dist/advanced.mjs +228 -101
- package/dist/{client-CUFdFugo.d.mts → client-D4vA6FY_.d.ts} +40 -34
- package/dist/{client-CjqNbEM6.d.ts → client-FQUyv41r.d.mts} +40 -34
- package/dist/{index-CJ9GkIXf.d.mts → index-DaKJxoEh.d.mts} +13 -12
- package/dist/{index-B5Bj-Ikg.d.ts → index-pztT_bcJ.d.ts} +13 -12
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +315 -131
- package/dist/index.mjs +309 -132
- package/dist/{payment-D-u3asA8.d.mts → payment-Cu75tmUc.d.mts} +34 -8
- package/dist/{payment-D-u3asA8.d.ts → payment-Cu75tmUc.d.ts} +34 -8
- package/dist/react.d.mts +225 -4
- package/dist/react.d.ts +225 -4
- package/dist/react.js +2097 -63
- package/dist/react.mjs +2086 -65
- package/dist/utils.d.mts +2 -2
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +76 -4
- package/dist/utils.mjs +76 -4
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -406,9 +406,11 @@ function CimplifyCheckout({
|
|
|
406
406
|
const elementsRef = react.useRef(null);
|
|
407
407
|
const activeCheckoutRef = react.useRef(null);
|
|
408
408
|
const initialAppearanceRef = react.useRef(appearance);
|
|
409
|
+
const hasWarnedInlineAppearanceRef = react.useRef(false);
|
|
409
410
|
const isMountedRef = react.useRef(true);
|
|
410
411
|
const demoRunRef = react.useRef(0);
|
|
411
412
|
const isDemoCheckout = demoMode ?? client.getPublicKey().trim().length === 0;
|
|
413
|
+
const isTestMode = client.isTestMode();
|
|
412
414
|
const emitStatus = react.useCallback(
|
|
413
415
|
(nextStatus, context = {}) => {
|
|
414
416
|
setStatus(nextStatus);
|
|
@@ -422,6 +424,14 @@ function CimplifyCheckout({
|
|
|
422
424
|
setOrderType(resolvedOrderTypes[0] || "pickup");
|
|
423
425
|
}
|
|
424
426
|
}, [resolvedOrderTypes, orderType]);
|
|
427
|
+
react.useEffect(() => {
|
|
428
|
+
if (appearance && appearance !== initialAppearanceRef.current && !hasWarnedInlineAppearanceRef.current) {
|
|
429
|
+
hasWarnedInlineAppearanceRef.current = true;
|
|
430
|
+
console.warn(
|
|
431
|
+
"[Cimplify] `appearance` prop reference changed after mount. Elements keep the initial appearance to avoid iframe remount. Memoize appearance with useMemo() to remove this warning."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}, [appearance]);
|
|
425
435
|
react.useEffect(() => {
|
|
426
436
|
let cancelled = false;
|
|
427
437
|
async function bootstrap() {
|
|
@@ -628,6 +638,19 @@ function CimplifyCheckout({
|
|
|
628
638
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, "data-cimplify-checkout": "", children: /* @__PURE__ */ jsxRuntime.jsx("p", { "data-cimplify-error": "", style: { fontSize: "14px", color: "#b91c1c" }, children: errorMessage || "Unable to initialize checkout. Please refresh and try again." }) });
|
|
629
639
|
}
|
|
630
640
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, "data-cimplify-checkout": "", children: [
|
|
641
|
+
isTestMode && !isDemoCheckout && /* @__PURE__ */ jsxRuntime.jsx(
|
|
642
|
+
"p",
|
|
643
|
+
{
|
|
644
|
+
"data-cimplify-test-mode": "",
|
|
645
|
+
style: {
|
|
646
|
+
marginBottom: "10px",
|
|
647
|
+
fontSize: "12px",
|
|
648
|
+
fontWeight: 600,
|
|
649
|
+
color: "#92400e"
|
|
650
|
+
},
|
|
651
|
+
children: "Test mode - no real charges"
|
|
652
|
+
}
|
|
653
|
+
),
|
|
631
654
|
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-cimplify-section": "auth", children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: isDemoCheckout ? void 0 : authMountRef }) }),
|
|
632
655
|
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-cimplify-section": "order-type", style: { marginTop: "12px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
633
656
|
"div",
|
|
@@ -690,17 +713,88 @@ function CimplifyCheckout({
|
|
|
690
713
|
}
|
|
691
714
|
|
|
692
715
|
// src/types/common.ts
|
|
716
|
+
function money(value) {
|
|
717
|
+
return value;
|
|
718
|
+
}
|
|
719
|
+
function moneyFromNumber(value) {
|
|
720
|
+
return value.toFixed(2);
|
|
721
|
+
}
|
|
722
|
+
function currencyCode(value) {
|
|
723
|
+
return value;
|
|
724
|
+
}
|
|
693
725
|
var ErrorCode = {
|
|
694
726
|
// General
|
|
695
727
|
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
696
728
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
697
729
|
TIMEOUT: "TIMEOUT",
|
|
730
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
698
731
|
NOT_FOUND: "NOT_FOUND"};
|
|
732
|
+
var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
|
|
733
|
+
function docsUrlForCode(code) {
|
|
734
|
+
return `${DOCS_ERROR_BASE_URL}#${code.toLowerCase().replace(/_/g, "-")}`;
|
|
735
|
+
}
|
|
736
|
+
var ERROR_SUGGESTIONS = {
|
|
737
|
+
UNKNOWN_ERROR: "An unexpected error occurred. Capture the request/response payload and retry with exponential backoff.",
|
|
738
|
+
NETWORK_ERROR: "Check the shopper's connection and retry. If this persists, inspect CORS, DNS, and API reachability.",
|
|
739
|
+
TIMEOUT: "The request exceeded the timeout. Retry once, then poll order status before charging again.",
|
|
740
|
+
UNAUTHORIZED: "Authentication is missing or expired. Ensure a valid access token is set and refresh the session if needed.",
|
|
741
|
+
FORBIDDEN: "The key/session lacks permission for this resource. Verify business ownership and API key scope.",
|
|
742
|
+
NOT_FOUND: "The requested resource does not exist or is not visible in this environment.",
|
|
743
|
+
VALIDATION_ERROR: "One or more fields are invalid. Validate required fields and enum values before retrying.",
|
|
744
|
+
CART_EMPTY: "The cart has no items. Redirect back to menu/catalogue and require at least one line item.",
|
|
745
|
+
CART_EXPIRED: "This cart is no longer active. Recreate a new cart and re-add shopper selections.",
|
|
746
|
+
CART_NOT_FOUND: "Cart could not be located. It may have expired or belongs to a different key/location.",
|
|
747
|
+
ITEM_UNAVAILABLE: "The selected item is unavailable at this location/time. Prompt the shopper to pick an alternative.",
|
|
748
|
+
VARIANT_NOT_FOUND: "The requested variant no longer exists. Refresh product data and require re-selection.",
|
|
749
|
+
VARIANT_OUT_OF_STOCK: "The selected variant is out of stock. Show in-stock variants and block checkout for this line.",
|
|
750
|
+
ADDON_REQUIRED: "A required add-on is missing. Ensure required modifier groups are completed before add-to-cart.",
|
|
751
|
+
ADDON_MAX_EXCEEDED: "Too many add-ons were selected. Enforce max selections client-side before submission.",
|
|
752
|
+
CHECKOUT_VALIDATION_FAILED: "Checkout payload failed validation. Verify customer, order type, and address fields are complete.",
|
|
753
|
+
DELIVERY_ADDRESS_REQUIRED: "Delivery orders require an address. Collect and pass address info before processing checkout.",
|
|
754
|
+
CUSTOMER_INFO_REQUIRED: "Customer details are required. Ensure name/email/phone are available before checkout.",
|
|
755
|
+
PAYMENT_FAILED: "Payment provider rejected or failed processing. Show retry/change-method options to the shopper.",
|
|
756
|
+
PAYMENT_CANCELLED: "Payment was cancelled by the shopper or provider flow. Allow a safe retry path.",
|
|
757
|
+
INSUFFICIENT_FUNDS: "Payment method has insufficient funds. Prompt shopper to use another method.",
|
|
758
|
+
CARD_DECLINED: "Card was declined. Ask shopper to retry or switch payment method.",
|
|
759
|
+
INVALID_OTP: "Authorization code is invalid. Let shopper re-enter OTP/PIN and retry.",
|
|
760
|
+
OTP_EXPIRED: "Authorization code expired. Request a new OTP and re-submit authorization.",
|
|
761
|
+
AUTHORIZATION_FAILED: "Additional payment authorization failed. Retry authorization or change payment method.",
|
|
762
|
+
PAYMENT_ACTION_NOT_COMPLETED: "Required payment action was not completed. Resume provider flow and poll for status.",
|
|
763
|
+
SLOT_UNAVAILABLE: "Selected schedule slot is unavailable. Refresh available slots and ask shopper to reselect.",
|
|
764
|
+
BOOKING_CONFLICT: "The requested booking conflicts with an existing reservation. Pick another slot/resource.",
|
|
765
|
+
SERVICE_NOT_FOUND: "Requested service no longer exists. Refresh service catalogue and retry selection.",
|
|
766
|
+
OUT_OF_STOCK: "Inventory is depleted for this item. Remove it or reduce quantity before checkout.",
|
|
767
|
+
INSUFFICIENT_QUANTITY: "Requested quantity exceeds available inventory. Reduce quantity and retry.",
|
|
768
|
+
BUSINESS_ID_REQUIRED: "Business context could not be resolved. Verify the public key and business bootstrap call.",
|
|
769
|
+
INVALID_CART: "Cart is invalid for checkout. Sync cart state, ensure items exist, then retry.",
|
|
770
|
+
ORDER_TYPE_REQUIRED: "Order type is required. Provide one of delivery, pickup, or dine_in before checkout.",
|
|
771
|
+
NO_PAYMENT_ELEMENT: "PaymentElement is required for processCheckout(). Mount it before triggering checkout.",
|
|
772
|
+
PAYMENT_NOT_MOUNTED: "PaymentElement iframe is not mounted. Mount it in the DOM before processCheckout().",
|
|
773
|
+
AUTH_INCOMPLETE: "AuthElement has not completed authentication. Wait for AUTHENTICATED before checkout.",
|
|
774
|
+
AUTH_LOST: "Session was cleared during checkout. Re-authenticate and restart checkout safely.",
|
|
775
|
+
ALREADY_PROCESSING: "Checkout is already in progress. Disable duplicate submits until completion.",
|
|
776
|
+
CHECKOUT_NOT_READY: "Checkout elements are still initializing. Wait for readiness before submit.",
|
|
777
|
+
CANCELLED: "Checkout was cancelled. Preserve cart state and allow shopper to retry.",
|
|
778
|
+
REQUEST_TIMEOUT: "Provider call timed out. Poll payment/order status before issuing another charge attempt.",
|
|
779
|
+
POPUP_BLOCKED: "Browser blocked provider popup. Ask shopper to enable popups and retry.",
|
|
780
|
+
FX_QUOTE_FAILED: "Failed to lock FX quote. Retry currency quote or fallback to base currency."
|
|
781
|
+
};
|
|
782
|
+
var ERROR_HINTS = Object.fromEntries(
|
|
783
|
+
Object.entries(ERROR_SUGGESTIONS).map(([code, suggestion]) => [
|
|
784
|
+
code,
|
|
785
|
+
{
|
|
786
|
+
docs_url: docsUrlForCode(code),
|
|
787
|
+
suggestion
|
|
788
|
+
}
|
|
789
|
+
])
|
|
790
|
+
);
|
|
699
791
|
var CimplifyError = class extends Error {
|
|
700
|
-
constructor(code, message, retryable = false) {
|
|
792
|
+
constructor(code, message, retryable = false, docs_url, suggestion) {
|
|
701
793
|
super(message);
|
|
702
794
|
this.code = code;
|
|
703
795
|
this.retryable = retryable;
|
|
796
|
+
this.docs_url = docs_url;
|
|
797
|
+
this.suggestion = suggestion;
|
|
704
798
|
this.name = "CimplifyError";
|
|
705
799
|
}
|
|
706
800
|
/** User-friendly message safe to display */
|
|
@@ -708,6 +802,28 @@ var CimplifyError = class extends Error {
|
|
|
708
802
|
return this.message;
|
|
709
803
|
}
|
|
710
804
|
};
|
|
805
|
+
function getErrorHint(code) {
|
|
806
|
+
return ERROR_HINTS[code];
|
|
807
|
+
}
|
|
808
|
+
function enrichError(error, options = {}) {
|
|
809
|
+
const hint = getErrorHint(error.code);
|
|
810
|
+
if (hint) {
|
|
811
|
+
if (!error.docs_url) {
|
|
812
|
+
error.docs_url = hint.docs_url;
|
|
813
|
+
}
|
|
814
|
+
if (!error.suggestion) {
|
|
815
|
+
error.suggestion = hint.suggestion;
|
|
816
|
+
}
|
|
817
|
+
} else if (!error.docs_url) {
|
|
818
|
+
error.docs_url = docsUrlForCode(error.code || ErrorCode.UNKNOWN_ERROR);
|
|
819
|
+
}
|
|
820
|
+
if (options.isTestMode && !error.message.includes("pk_test_")) {
|
|
821
|
+
error.message = `${error.message}
|
|
822
|
+
|
|
823
|
+
\u2139 Your API key is a test-mode key (pk_test_...). Verify test data/session before retrying.`;
|
|
824
|
+
}
|
|
825
|
+
return error;
|
|
826
|
+
}
|
|
711
827
|
|
|
712
828
|
// src/types/result.ts
|
|
713
829
|
function ok(value) {
|
|
@@ -717,13 +833,18 @@ function err(error) {
|
|
|
717
833
|
return { ok: false, error };
|
|
718
834
|
}
|
|
719
835
|
|
|
836
|
+
// src/query/builder.ts
|
|
837
|
+
function escapeQueryValue(value) {
|
|
838
|
+
return value.replace(/'/g, "\\'");
|
|
839
|
+
}
|
|
840
|
+
|
|
720
841
|
// src/catalogue.ts
|
|
721
842
|
function toCimplifyError(error) {
|
|
722
|
-
if (error instanceof CimplifyError) return error;
|
|
843
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
723
844
|
if (error instanceof Error) {
|
|
724
|
-
return new CimplifyError(
|
|
845
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
725
846
|
}
|
|
726
|
-
return new CimplifyError(
|
|
847
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
727
848
|
}
|
|
728
849
|
async function safe(promise) {
|
|
729
850
|
try {
|
|
@@ -797,7 +918,7 @@ var CatalogueQueries = class {
|
|
|
797
918
|
let query = "products";
|
|
798
919
|
const filters = [];
|
|
799
920
|
if (options?.category) {
|
|
800
|
-
filters.push(`@.category_id=='${options.category}'`);
|
|
921
|
+
filters.push(`@.category_id=='${escapeQueryValue(options.category)}'`);
|
|
801
922
|
}
|
|
802
923
|
if (options?.featured !== void 0) {
|
|
803
924
|
filters.push(`@.featured==${options.featured}`);
|
|
@@ -806,7 +927,7 @@ var CatalogueQueries = class {
|
|
|
806
927
|
filters.push(`@.in_stock==${options.in_stock}`);
|
|
807
928
|
}
|
|
808
929
|
if (options?.search) {
|
|
809
|
-
filters.push(`@.name contains '${options.search}'`);
|
|
930
|
+
filters.push(`@.name contains '${escapeQueryValue(options.search)}'`);
|
|
810
931
|
}
|
|
811
932
|
if (options?.min_price !== void 0) {
|
|
812
933
|
filters.push(`@.price>=${options.min_price}`);
|
|
@@ -837,7 +958,9 @@ var CatalogueQueries = class {
|
|
|
837
958
|
}
|
|
838
959
|
async getProductBySlug(slug) {
|
|
839
960
|
const filteredResult = await safe(
|
|
840
|
-
this.client.query(
|
|
961
|
+
this.client.query(
|
|
962
|
+
`products[?(@.slug=='${escapeQueryValue(slug)}')]`
|
|
963
|
+
)
|
|
841
964
|
);
|
|
842
965
|
if (!filteredResult.ok) return filteredResult;
|
|
843
966
|
const exactMatch = findProductBySlug(filteredResult.value, slug);
|
|
@@ -889,7 +1012,7 @@ var CatalogueQueries = class {
|
|
|
889
1012
|
}
|
|
890
1013
|
async getCategoryBySlug(slug) {
|
|
891
1014
|
const result = await safe(
|
|
892
|
-
this.client.query(`categories[?(@.slug=='${slug}')]`)
|
|
1015
|
+
this.client.query(`categories[?(@.slug=='${escapeQueryValue(slug)}')]`)
|
|
893
1016
|
);
|
|
894
1017
|
if (!result.ok) return result;
|
|
895
1018
|
if (!result.value.length) {
|
|
@@ -898,7 +1021,11 @@ var CatalogueQueries = class {
|
|
|
898
1021
|
return ok(result.value[0]);
|
|
899
1022
|
}
|
|
900
1023
|
async getCategoryProducts(categoryId) {
|
|
901
|
-
return safe(
|
|
1024
|
+
return safe(
|
|
1025
|
+
this.client.query(
|
|
1026
|
+
`products[?(@.category_id=='${escapeQueryValue(categoryId)}')]`
|
|
1027
|
+
)
|
|
1028
|
+
);
|
|
902
1029
|
}
|
|
903
1030
|
async getCollections() {
|
|
904
1031
|
return safe(this.client.query("collections"));
|
|
@@ -908,7 +1035,9 @@ var CatalogueQueries = class {
|
|
|
908
1035
|
}
|
|
909
1036
|
async getCollectionBySlug(slug) {
|
|
910
1037
|
const result = await safe(
|
|
911
|
-
this.client.query(
|
|
1038
|
+
this.client.query(
|
|
1039
|
+
`collections[?(@.slug=='${escapeQueryValue(slug)}')]`
|
|
1040
|
+
)
|
|
912
1041
|
);
|
|
913
1042
|
if (!result.ok) return result;
|
|
914
1043
|
if (!result.value.length) {
|
|
@@ -921,7 +1050,9 @@ var CatalogueQueries = class {
|
|
|
921
1050
|
}
|
|
922
1051
|
async searchCollections(query, limit = 20) {
|
|
923
1052
|
return safe(
|
|
924
|
-
this.client.query(
|
|
1053
|
+
this.client.query(
|
|
1054
|
+
`collections[?(@.name contains '${escapeQueryValue(query)}')]#limit(${limit})`
|
|
1055
|
+
)
|
|
925
1056
|
);
|
|
926
1057
|
}
|
|
927
1058
|
async getBundles() {
|
|
@@ -932,7 +1063,9 @@ var CatalogueQueries = class {
|
|
|
932
1063
|
}
|
|
933
1064
|
async getBundleBySlug(slug) {
|
|
934
1065
|
const result = await safe(
|
|
935
|
-
this.client.query(
|
|
1066
|
+
this.client.query(
|
|
1067
|
+
`bundles[?(@.slug=='${escapeQueryValue(slug)}')]`
|
|
1068
|
+
)
|
|
936
1069
|
);
|
|
937
1070
|
if (!result.ok) return result;
|
|
938
1071
|
if (!result.value.length) {
|
|
@@ -942,7 +1075,9 @@ var CatalogueQueries = class {
|
|
|
942
1075
|
}
|
|
943
1076
|
async searchBundles(query, limit = 20) {
|
|
944
1077
|
return safe(
|
|
945
|
-
this.client.query(
|
|
1078
|
+
this.client.query(
|
|
1079
|
+
`bundles[?(@.name contains '${escapeQueryValue(query)}')]#limit(${limit})`
|
|
1080
|
+
)
|
|
946
1081
|
);
|
|
947
1082
|
}
|
|
948
1083
|
async getComposites(options) {
|
|
@@ -982,9 +1117,9 @@ var CatalogueQueries = class {
|
|
|
982
1117
|
}
|
|
983
1118
|
async search(query, options) {
|
|
984
1119
|
const limit = options?.limit ?? 20;
|
|
985
|
-
let searchQuery = `products[?(@.name contains '${query}')]`;
|
|
1120
|
+
let searchQuery = `products[?(@.name contains '${escapeQueryValue(query)}')]`;
|
|
986
1121
|
if (options?.category) {
|
|
987
|
-
searchQuery = `products[?(@.name contains '${query}' && @.category_id=='${options.category}')]`;
|
|
1122
|
+
searchQuery = `products[?(@.name contains '${escapeQueryValue(query)}' && @.category_id=='${escapeQueryValue(options.category)}')]`;
|
|
988
1123
|
}
|
|
989
1124
|
searchQuery += `#limit(${limit})`;
|
|
990
1125
|
return safe(this.client.query(searchQuery));
|
|
@@ -1001,7 +1136,7 @@ var CatalogueQueries = class {
|
|
|
1001
1136
|
async getMenu(options) {
|
|
1002
1137
|
let query = "menu";
|
|
1003
1138
|
if (options?.category) {
|
|
1004
|
-
query = `menu[?(@.category=='${options.category}')]`;
|
|
1139
|
+
query = `menu[?(@.category=='${escapeQueryValue(options.category)}')]`;
|
|
1005
1140
|
}
|
|
1006
1141
|
if (options?.limit) {
|
|
1007
1142
|
query += `#limit(${options.limit})`;
|
|
@@ -1018,11 +1153,11 @@ var CatalogueQueries = class {
|
|
|
1018
1153
|
|
|
1019
1154
|
// src/cart.ts
|
|
1020
1155
|
function toCimplifyError2(error) {
|
|
1021
|
-
if (error instanceof CimplifyError) return error;
|
|
1156
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
1022
1157
|
if (error instanceof Error) {
|
|
1023
|
-
return new CimplifyError(
|
|
1158
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
1024
1159
|
}
|
|
1025
|
-
return new CimplifyError(
|
|
1160
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
1026
1161
|
}
|
|
1027
1162
|
async function safe2(promise) {
|
|
1028
1163
|
try {
|
|
@@ -1182,6 +1317,19 @@ var ORDER_MUTATION = {
|
|
|
1182
1317
|
UPDATE_CUSTOMER: "order.update_order_customer"
|
|
1183
1318
|
};
|
|
1184
1319
|
|
|
1320
|
+
// src/utils/price.ts
|
|
1321
|
+
function parsePrice(value) {
|
|
1322
|
+
if (value === void 0 || value === null) {
|
|
1323
|
+
return 0;
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof value === "number") {
|
|
1326
|
+
return isNaN(value) ? 0 : value;
|
|
1327
|
+
}
|
|
1328
|
+
const cleaned = value.replace(/[^\d.-]/g, "");
|
|
1329
|
+
const parsed = parseFloat(cleaned);
|
|
1330
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1185
1333
|
// src/utils/payment.ts
|
|
1186
1334
|
var PAYMENT_SUCCESS_STATUSES = /* @__PURE__ */ new Set([
|
|
1187
1335
|
"success",
|
|
@@ -1282,6 +1430,8 @@ function normalizeStatusResponse(response) {
|
|
|
1282
1430
|
}
|
|
1283
1431
|
const res = response;
|
|
1284
1432
|
const normalizedStatus = normalizePaymentStatusValue(res.status ?? void 0);
|
|
1433
|
+
const normalizedAmount = typeof res.amount === "string" ? money(res.amount) : typeof res.amount === "number" && Number.isFinite(res.amount) ? moneyFromNumber(res.amount) : void 0;
|
|
1434
|
+
const normalizedCurrency = typeof res.currency === "string" && res.currency.trim().length > 0 ? currencyCode(res.currency) : void 0;
|
|
1285
1435
|
const paidValue = res.paid === true;
|
|
1286
1436
|
const derivedPaid = paidValue || [
|
|
1287
1437
|
"success",
|
|
@@ -1294,8 +1444,8 @@ function normalizeStatusResponse(response) {
|
|
|
1294
1444
|
return {
|
|
1295
1445
|
status: normalizedStatus,
|
|
1296
1446
|
paid: derivedPaid,
|
|
1297
|
-
amount:
|
|
1298
|
-
currency:
|
|
1447
|
+
amount: normalizedAmount,
|
|
1448
|
+
currency: normalizedCurrency,
|
|
1299
1449
|
reference: res.reference,
|
|
1300
1450
|
message: res.message || ""
|
|
1301
1451
|
};
|
|
@@ -1861,11 +2011,11 @@ var CheckoutResolver = class {
|
|
|
1861
2011
|
|
|
1862
2012
|
// src/checkout.ts
|
|
1863
2013
|
function toCimplifyError3(error) {
|
|
1864
|
-
if (error instanceof CimplifyError) return error;
|
|
2014
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
1865
2015
|
if (error instanceof Error) {
|
|
1866
|
-
return new CimplifyError(
|
|
2016
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
1867
2017
|
}
|
|
1868
|
-
return new CimplifyError(
|
|
2018
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
1869
2019
|
}
|
|
1870
2020
|
async function safe3(promise) {
|
|
1871
2021
|
try {
|
|
@@ -1987,14 +2137,17 @@ var CheckoutService = class {
|
|
|
1987
2137
|
pay_currency: data.pay_currency,
|
|
1988
2138
|
fx_quote_id: data.fx_quote_id
|
|
1989
2139
|
};
|
|
1990
|
-
const baseCurrency = (
|
|
2140
|
+
const baseCurrency = currencyCode(
|
|
2141
|
+
(cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase()
|
|
2142
|
+
);
|
|
1991
2143
|
const payCurrency = data.pay_currency?.trim().toUpperCase();
|
|
2144
|
+
const payCurrencyCode = payCurrency ? currencyCode(payCurrency) : void 0;
|
|
1992
2145
|
const cartTotalAmount = Number.parseFloat(cart.pricing.total_price || "0");
|
|
1993
|
-
if (
|
|
2146
|
+
if (payCurrencyCode && payCurrencyCode !== baseCurrency && !checkoutData.fx_quote_id && Number.isFinite(cartTotalAmount) && cartTotalAmount > 0) {
|
|
1994
2147
|
const fxQuoteResult = await this.client.fx.lockQuote({
|
|
1995
2148
|
from: baseCurrency,
|
|
1996
|
-
to:
|
|
1997
|
-
amount:
|
|
2149
|
+
to: payCurrencyCode,
|
|
2150
|
+
amount: cart.pricing.total_price
|
|
1998
2151
|
});
|
|
1999
2152
|
if (!fxQuoteResult.ok) {
|
|
2000
2153
|
return ok(
|
|
@@ -2005,7 +2158,7 @@ var CheckoutService = class {
|
|
|
2005
2158
|
)
|
|
2006
2159
|
);
|
|
2007
2160
|
}
|
|
2008
|
-
checkoutData.pay_currency =
|
|
2161
|
+
checkoutData.pay_currency = payCurrencyCode;
|
|
2009
2162
|
checkoutData.fx_quote_id = fxQuoteResult.value.id;
|
|
2010
2163
|
}
|
|
2011
2164
|
data.on_status_change?.("processing", {});
|
|
@@ -2041,11 +2194,11 @@ var CheckoutService = class {
|
|
|
2041
2194
|
|
|
2042
2195
|
// src/orders.ts
|
|
2043
2196
|
function toCimplifyError4(error) {
|
|
2044
|
-
if (error instanceof CimplifyError) return error;
|
|
2197
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
2045
2198
|
if (error instanceof Error) {
|
|
2046
|
-
return new CimplifyError(
|
|
2199
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
2047
2200
|
}
|
|
2048
|
-
return new CimplifyError(
|
|
2201
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
2049
2202
|
}
|
|
2050
2203
|
async function safe4(promise) {
|
|
2051
2204
|
try {
|
|
@@ -2302,11 +2455,11 @@ var AuthService = class {
|
|
|
2302
2455
|
|
|
2303
2456
|
// src/business.ts
|
|
2304
2457
|
function toCimplifyError7(error) {
|
|
2305
|
-
if (error instanceof CimplifyError) return error;
|
|
2458
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
2306
2459
|
if (error instanceof Error) {
|
|
2307
|
-
return new CimplifyError(
|
|
2460
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
2308
2461
|
}
|
|
2309
|
-
return new CimplifyError(
|
|
2462
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
2310
2463
|
}
|
|
2311
2464
|
async function safe7(promise) {
|
|
2312
2465
|
try {
|
|
@@ -2653,12 +2806,15 @@ var FxService = class {
|
|
|
2653
2806
|
|
|
2654
2807
|
// src/elements.ts
|
|
2655
2808
|
function toCheckoutError(code, message, recoverable) {
|
|
2809
|
+
const hint = getErrorHint(code);
|
|
2656
2810
|
return {
|
|
2657
2811
|
success: false,
|
|
2658
2812
|
error: {
|
|
2659
2813
|
code,
|
|
2660
2814
|
message,
|
|
2661
|
-
recoverable
|
|
2815
|
+
recoverable,
|
|
2816
|
+
docs_url: hint?.docs_url,
|
|
2817
|
+
suggestion: hint?.suggestion
|
|
2662
2818
|
}
|
|
2663
2819
|
};
|
|
2664
2820
|
}
|
|
@@ -2699,6 +2855,7 @@ var CimplifyElements = class {
|
|
|
2699
2855
|
this.paymentData = null;
|
|
2700
2856
|
this.checkoutInProgress = false;
|
|
2701
2857
|
this.activeCheckoutAbort = null;
|
|
2858
|
+
this.hasWarnedMissingAuthElement = false;
|
|
2702
2859
|
this.businessIdResolvePromise = null;
|
|
2703
2860
|
this.client = client;
|
|
2704
2861
|
this.businessId = businessId ?? null;
|
|
@@ -2794,6 +2951,12 @@ var CimplifyElements = class {
|
|
|
2794
2951
|
);
|
|
2795
2952
|
}
|
|
2796
2953
|
const authElement = this.elements.get(ELEMENT_TYPES.AUTH);
|
|
2954
|
+
if (!authElement && !this.hasWarnedMissingAuthElement) {
|
|
2955
|
+
this.hasWarnedMissingAuthElement = true;
|
|
2956
|
+
console.warn(
|
|
2957
|
+
"[Cimplify] processCheckout() called without AuthElement mounted. For best conversion and Link enrollment, mount <AuthElement> before checkout."
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2797
2960
|
if (authElement && !this.accessToken) {
|
|
2798
2961
|
return toCheckoutError(
|
|
2799
2962
|
"AUTH_INCOMPLETE",
|
|
@@ -3230,28 +3393,37 @@ function isRetryable(error) {
|
|
|
3230
3393
|
}
|
|
3231
3394
|
return false;
|
|
3232
3395
|
}
|
|
3233
|
-
function toNetworkError(error) {
|
|
3396
|
+
function toNetworkError(error, isTestMode) {
|
|
3234
3397
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
3235
|
-
return
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3398
|
+
return enrichError(
|
|
3399
|
+
new CimplifyError(
|
|
3400
|
+
ErrorCode.TIMEOUT,
|
|
3401
|
+
"Request timed out. Please check your connection and try again.",
|
|
3402
|
+
true
|
|
3403
|
+
),
|
|
3404
|
+
{ isTestMode }
|
|
3239
3405
|
);
|
|
3240
3406
|
}
|
|
3241
3407
|
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
3242
|
-
return
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3408
|
+
return enrichError(
|
|
3409
|
+
new CimplifyError(
|
|
3410
|
+
ErrorCode.NETWORK_ERROR,
|
|
3411
|
+
"Network error. Please check your internet connection.",
|
|
3412
|
+
true
|
|
3413
|
+
),
|
|
3414
|
+
{ isTestMode }
|
|
3246
3415
|
);
|
|
3247
3416
|
}
|
|
3248
3417
|
if (error instanceof CimplifyError) {
|
|
3249
|
-
return error;
|
|
3418
|
+
return enrichError(error, { isTestMode });
|
|
3250
3419
|
}
|
|
3251
|
-
return
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3420
|
+
return enrichError(
|
|
3421
|
+
new CimplifyError(
|
|
3422
|
+
ErrorCode.UNKNOWN_ERROR,
|
|
3423
|
+
error instanceof Error ? error.message : "An unknown error occurred",
|
|
3424
|
+
false
|
|
3425
|
+
),
|
|
3426
|
+
{ isTestMode }
|
|
3255
3427
|
);
|
|
3256
3428
|
}
|
|
3257
3429
|
function deriveUrls() {
|
|
@@ -3313,6 +3485,9 @@ var CimplifyClient = class {
|
|
|
3313
3485
|
getPublicKey() {
|
|
3314
3486
|
return this.publicKey;
|
|
3315
3487
|
}
|
|
3488
|
+
isTestMode() {
|
|
3489
|
+
return this.publicKey.trim().startsWith("pk_test_");
|
|
3490
|
+
}
|
|
3316
3491
|
setAccessToken(token) {
|
|
3317
3492
|
const previous = this.accessToken;
|
|
3318
3493
|
this.accessToken = token;
|
|
@@ -3434,7 +3609,7 @@ var CimplifyClient = class {
|
|
|
3434
3609
|
signal: controller.signal
|
|
3435
3610
|
});
|
|
3436
3611
|
clearTimeout(timeoutId);
|
|
3437
|
-
if (response.ok
|
|
3612
|
+
if (response.ok) {
|
|
3438
3613
|
this.hooks.onRequestSuccess?.({
|
|
3439
3614
|
...context,
|
|
3440
3615
|
status: response.status,
|
|
@@ -3442,6 +3617,21 @@ var CimplifyClient = class {
|
|
|
3442
3617
|
});
|
|
3443
3618
|
return response;
|
|
3444
3619
|
}
|
|
3620
|
+
if (response.status >= 400 && response.status < 500) {
|
|
3621
|
+
this.hooks.onRequestError?.({
|
|
3622
|
+
...context,
|
|
3623
|
+
error: new CimplifyError(
|
|
3624
|
+
`HTTP_${response.status}`,
|
|
3625
|
+
`Request failed with status ${response.status}`,
|
|
3626
|
+
false
|
|
3627
|
+
),
|
|
3628
|
+
status: response.status,
|
|
3629
|
+
durationMs: Date.now() - startTime,
|
|
3630
|
+
retryCount,
|
|
3631
|
+
retryable: false
|
|
3632
|
+
});
|
|
3633
|
+
return response;
|
|
3634
|
+
}
|
|
3445
3635
|
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
3446
3636
|
retryCount++;
|
|
3447
3637
|
const delay = this.retryDelay * Math.pow(2, attempt);
|
|
@@ -3454,15 +3644,22 @@ var CimplifyClient = class {
|
|
|
3454
3644
|
await sleep(delay);
|
|
3455
3645
|
continue;
|
|
3456
3646
|
}
|
|
3457
|
-
this.hooks.
|
|
3647
|
+
this.hooks.onRequestError?.({
|
|
3458
3648
|
...context,
|
|
3649
|
+
error: new CimplifyError(
|
|
3650
|
+
"SERVER_ERROR",
|
|
3651
|
+
`Server error ${response.status} after ${retryCount} retries`,
|
|
3652
|
+
false
|
|
3653
|
+
),
|
|
3459
3654
|
status: response.status,
|
|
3460
|
-
durationMs: Date.now() - startTime
|
|
3655
|
+
durationMs: Date.now() - startTime,
|
|
3656
|
+
retryCount,
|
|
3657
|
+
retryable: false
|
|
3461
3658
|
});
|
|
3462
3659
|
return response;
|
|
3463
3660
|
} catch (error) {
|
|
3464
3661
|
lastError = error;
|
|
3465
|
-
const networkError = toNetworkError(error);
|
|
3662
|
+
const networkError = toNetworkError(error, this.isTestMode());
|
|
3466
3663
|
const errorRetryable = isRetryable(error);
|
|
3467
3664
|
if (!errorRetryable || attempt >= this.maxRetries) {
|
|
3468
3665
|
this.hooks.onRequestError?.({
|
|
@@ -3485,7 +3682,7 @@ var CimplifyClient = class {
|
|
|
3485
3682
|
await sleep(delay);
|
|
3486
3683
|
}
|
|
3487
3684
|
}
|
|
3488
|
-
const finalError = toNetworkError(lastError);
|
|
3685
|
+
const finalError = toNetworkError(lastError, this.isTestMode());
|
|
3489
3686
|
this.hooks.onRequestError?.({
|
|
3490
3687
|
...context,
|
|
3491
3688
|
error: finalError,
|
|
@@ -3603,22 +3800,40 @@ var CimplifyClient = class {
|
|
|
3603
3800
|
async handleRestResponse(response) {
|
|
3604
3801
|
const json = await response.json();
|
|
3605
3802
|
if (!response.ok) {
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3803
|
+
const error = enrichError(
|
|
3804
|
+
new CimplifyError(
|
|
3805
|
+
json.error?.error_code || "API_ERROR",
|
|
3806
|
+
json.error?.error_message || "An error occurred",
|
|
3807
|
+
false
|
|
3808
|
+
),
|
|
3809
|
+
{ isTestMode: this.isTestMode() }
|
|
3610
3810
|
);
|
|
3811
|
+
if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
|
|
3812
|
+
console.warn(
|
|
3813
|
+
"[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
|
|
3814
|
+
);
|
|
3815
|
+
}
|
|
3816
|
+
throw error;
|
|
3611
3817
|
}
|
|
3612
3818
|
return json.data;
|
|
3613
3819
|
}
|
|
3614
3820
|
async handleResponse(response) {
|
|
3615
3821
|
const json = await response.json();
|
|
3616
3822
|
if (!json.success || json.error) {
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3823
|
+
const error = enrichError(
|
|
3824
|
+
new CimplifyError(
|
|
3825
|
+
json.error?.code || "UNKNOWN_ERROR",
|
|
3826
|
+
json.error?.message || "An unknown error occurred",
|
|
3827
|
+
json.error?.retryable || false
|
|
3828
|
+
),
|
|
3829
|
+
{ isTestMode: this.isTestMode() }
|
|
3621
3830
|
);
|
|
3831
|
+
if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
|
|
3832
|
+
console.warn(
|
|
3833
|
+
"[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
throw error;
|
|
3622
3837
|
}
|
|
3623
3838
|
return json.data;
|
|
3624
3839
|
}
|
|
@@ -3805,6 +4020,9 @@ function CimplifyProvider({
|
|
|
3805
4020
|
const nextLocations = locationsResult.ok && Array.isArray(locationsResult.value) ? locationsResult.value : [];
|
|
3806
4021
|
const initialLocation = resolveInitialLocation(nextLocations);
|
|
3807
4022
|
setBusiness(nextBusiness);
|
|
4023
|
+
if (nextBusiness?.id) {
|
|
4024
|
+
resolvedClient.setBusinessId(nextBusiness.id);
|
|
4025
|
+
}
|
|
3808
4026
|
setLocations(nextLocations);
|
|
3809
4027
|
if (initialLocation) {
|
|
3810
4028
|
setCurrentLocationState(initialLocation);
|
|
@@ -3863,6 +4081,1809 @@ function useCimplify() {
|
|
|
3863
4081
|
}
|
|
3864
4082
|
return context;
|
|
3865
4083
|
}
|
|
4084
|
+
function useOptionalCimplify() {
|
|
4085
|
+
return react.useContext(CimplifyContext);
|
|
4086
|
+
}
|
|
4087
|
+
var productsCache = /* @__PURE__ */ new Map();
|
|
4088
|
+
var productsInflight = /* @__PURE__ */ new Map();
|
|
4089
|
+
function buildProductsCacheKey(client, locationId, options) {
|
|
4090
|
+
return JSON.stringify({
|
|
4091
|
+
key: client.getPublicKey(),
|
|
4092
|
+
location_id: locationId || "__none__",
|
|
4093
|
+
options
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4096
|
+
function useProducts(options = {}) {
|
|
4097
|
+
const context = useOptionalCimplify();
|
|
4098
|
+
const client = options.client ?? context?.client;
|
|
4099
|
+
if (!client) {
|
|
4100
|
+
throw new Error("useProducts must be used within CimplifyProvider or passed { client }.");
|
|
4101
|
+
}
|
|
4102
|
+
const enabled = options.enabled ?? true;
|
|
4103
|
+
const locationId = client.getLocationId();
|
|
4104
|
+
const previousLocationIdRef = react.useRef(locationId);
|
|
4105
|
+
const requestIdRef = react.useRef(0);
|
|
4106
|
+
const queryOptions = react.useMemo(
|
|
4107
|
+
() => ({
|
|
4108
|
+
category: options.category,
|
|
4109
|
+
collection: options.collection,
|
|
4110
|
+
search: options.search,
|
|
4111
|
+
featured: options.featured,
|
|
4112
|
+
limit: options.limit
|
|
4113
|
+
}),
|
|
4114
|
+
[options.category, options.collection, options.featured, options.limit, options.search]
|
|
4115
|
+
);
|
|
4116
|
+
const cacheKey = react.useMemo(
|
|
4117
|
+
() => buildProductsCacheKey(client, locationId, queryOptions),
|
|
4118
|
+
[client, locationId, queryOptions]
|
|
4119
|
+
);
|
|
4120
|
+
const cached = productsCache.get(cacheKey);
|
|
4121
|
+
const [products, setProducts] = react.useState(cached?.products ?? []);
|
|
4122
|
+
const [isLoading, setIsLoading] = react.useState(enabled && !cached);
|
|
4123
|
+
const [error, setError] = react.useState(null);
|
|
4124
|
+
react.useEffect(() => {
|
|
4125
|
+
if (previousLocationIdRef.current !== locationId) {
|
|
4126
|
+
productsCache.clear();
|
|
4127
|
+
productsInflight.clear();
|
|
4128
|
+
previousLocationIdRef.current = locationId;
|
|
4129
|
+
}
|
|
4130
|
+
}, [locationId]);
|
|
4131
|
+
const load = react.useCallback(
|
|
4132
|
+
async (force = false) => {
|
|
4133
|
+
if (!enabled) {
|
|
4134
|
+
setIsLoading(false);
|
|
4135
|
+
return;
|
|
4136
|
+
}
|
|
4137
|
+
const nextRequestId = ++requestIdRef.current;
|
|
4138
|
+
setError(null);
|
|
4139
|
+
if (!force) {
|
|
4140
|
+
const cacheEntry = productsCache.get(cacheKey);
|
|
4141
|
+
if (cacheEntry) {
|
|
4142
|
+
setProducts(cacheEntry.products);
|
|
4143
|
+
setIsLoading(false);
|
|
4144
|
+
return;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
setIsLoading(true);
|
|
4148
|
+
try {
|
|
4149
|
+
const existing = productsInflight.get(cacheKey);
|
|
4150
|
+
const promise = existing ?? (async () => {
|
|
4151
|
+
const result = await client.catalogue.getProducts(queryOptions);
|
|
4152
|
+
if (!result.ok) {
|
|
4153
|
+
throw result.error;
|
|
4154
|
+
}
|
|
4155
|
+
return result.value;
|
|
4156
|
+
})();
|
|
4157
|
+
if (!existing) {
|
|
4158
|
+
productsInflight.set(
|
|
4159
|
+
cacheKey,
|
|
4160
|
+
promise.finally(() => {
|
|
4161
|
+
productsInflight.delete(cacheKey);
|
|
4162
|
+
})
|
|
4163
|
+
);
|
|
4164
|
+
}
|
|
4165
|
+
const value = await promise;
|
|
4166
|
+
productsCache.set(cacheKey, { products: value });
|
|
4167
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4168
|
+
setProducts(value);
|
|
4169
|
+
setError(null);
|
|
4170
|
+
}
|
|
4171
|
+
} catch (loadError) {
|
|
4172
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4173
|
+
setError(loadError);
|
|
4174
|
+
}
|
|
4175
|
+
} finally {
|
|
4176
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4177
|
+
setIsLoading(false);
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
},
|
|
4181
|
+
[cacheKey, client, enabled, queryOptions]
|
|
4182
|
+
);
|
|
4183
|
+
react.useEffect(() => {
|
|
4184
|
+
void load(false);
|
|
4185
|
+
}, [load]);
|
|
4186
|
+
const refetch = react.useCallback(async () => {
|
|
4187
|
+
productsCache.delete(cacheKey);
|
|
4188
|
+
await load(true);
|
|
4189
|
+
}, [cacheKey, load]);
|
|
4190
|
+
return { products, isLoading, error, refetch };
|
|
4191
|
+
}
|
|
4192
|
+
var productCache = /* @__PURE__ */ new Map();
|
|
4193
|
+
var productInflight = /* @__PURE__ */ new Map();
|
|
4194
|
+
function isLikelySlug(value) {
|
|
4195
|
+
return /^[a-z0-9-]+$/.test(value);
|
|
4196
|
+
}
|
|
4197
|
+
function buildProductCacheKey(client, locationId, slugOrId) {
|
|
4198
|
+
return JSON.stringify({
|
|
4199
|
+
key: client.getPublicKey(),
|
|
4200
|
+
location_id: locationId || "__none__",
|
|
4201
|
+
slug_or_id: slugOrId
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
function useProduct(slugOrId, options = {}) {
|
|
4205
|
+
const context = useOptionalCimplify();
|
|
4206
|
+
const client = options.client ?? context?.client;
|
|
4207
|
+
if (!client) {
|
|
4208
|
+
throw new Error("useProduct must be used within CimplifyProvider or passed { client }.");
|
|
4209
|
+
}
|
|
4210
|
+
const enabled = options.enabled ?? true;
|
|
4211
|
+
const locationId = client.getLocationId();
|
|
4212
|
+
const previousLocationIdRef = react.useRef(locationId);
|
|
4213
|
+
const requestIdRef = react.useRef(0);
|
|
4214
|
+
const normalizedSlugOrId = react.useMemo(() => (slugOrId || "").trim(), [slugOrId]);
|
|
4215
|
+
const cacheKey = react.useMemo(
|
|
4216
|
+
() => buildProductCacheKey(client, locationId, normalizedSlugOrId),
|
|
4217
|
+
[client, locationId, normalizedSlugOrId]
|
|
4218
|
+
);
|
|
4219
|
+
const cached = productCache.get(cacheKey);
|
|
4220
|
+
const [product, setProduct] = react.useState(cached?.product ?? null);
|
|
4221
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
4222
|
+
enabled && normalizedSlugOrId.length > 0 && !cached
|
|
4223
|
+
);
|
|
4224
|
+
const [error, setError] = react.useState(null);
|
|
4225
|
+
react.useEffect(() => {
|
|
4226
|
+
if (previousLocationIdRef.current !== locationId) {
|
|
4227
|
+
productCache.clear();
|
|
4228
|
+
productInflight.clear();
|
|
4229
|
+
previousLocationIdRef.current = locationId;
|
|
4230
|
+
}
|
|
4231
|
+
}, [locationId]);
|
|
4232
|
+
const load = react.useCallback(
|
|
4233
|
+
async (force = false) => {
|
|
4234
|
+
if (!enabled || normalizedSlugOrId.length === 0) {
|
|
4235
|
+
setProduct(null);
|
|
4236
|
+
setIsLoading(false);
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
4239
|
+
const nextRequestId = ++requestIdRef.current;
|
|
4240
|
+
setError(null);
|
|
4241
|
+
if (!force) {
|
|
4242
|
+
const cacheEntry = productCache.get(cacheKey);
|
|
4243
|
+
if (cacheEntry) {
|
|
4244
|
+
setProduct(cacheEntry.product);
|
|
4245
|
+
setIsLoading(false);
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
setIsLoading(true);
|
|
4250
|
+
try {
|
|
4251
|
+
const existing = productInflight.get(cacheKey);
|
|
4252
|
+
const promise = existing ?? (async () => {
|
|
4253
|
+
const result = isLikelySlug(normalizedSlugOrId) ? await client.catalogue.getProductBySlug(normalizedSlugOrId) : await client.catalogue.getProduct(normalizedSlugOrId);
|
|
4254
|
+
if (!result.ok) {
|
|
4255
|
+
throw result.error;
|
|
4256
|
+
}
|
|
4257
|
+
return result.value;
|
|
4258
|
+
})();
|
|
4259
|
+
if (!existing) {
|
|
4260
|
+
productInflight.set(
|
|
4261
|
+
cacheKey,
|
|
4262
|
+
promise.finally(() => {
|
|
4263
|
+
productInflight.delete(cacheKey);
|
|
4264
|
+
})
|
|
4265
|
+
);
|
|
4266
|
+
}
|
|
4267
|
+
const value = await promise;
|
|
4268
|
+
productCache.set(cacheKey, { product: value });
|
|
4269
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4270
|
+
setProduct(value);
|
|
4271
|
+
setError(null);
|
|
4272
|
+
}
|
|
4273
|
+
} catch (loadError) {
|
|
4274
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4275
|
+
setError(loadError);
|
|
4276
|
+
}
|
|
4277
|
+
} finally {
|
|
4278
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4279
|
+
setIsLoading(false);
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
},
|
|
4283
|
+
[cacheKey, client, enabled, normalizedSlugOrId]
|
|
4284
|
+
);
|
|
4285
|
+
react.useEffect(() => {
|
|
4286
|
+
void load(false);
|
|
4287
|
+
}, [load]);
|
|
4288
|
+
const refetch = react.useCallback(async () => {
|
|
4289
|
+
productCache.delete(cacheKey);
|
|
4290
|
+
await load(true);
|
|
4291
|
+
}, [cacheKey, load]);
|
|
4292
|
+
return { product, isLoading, error, refetch };
|
|
4293
|
+
}
|
|
4294
|
+
var categoriesCache = /* @__PURE__ */ new Map();
|
|
4295
|
+
var categoriesInflight = /* @__PURE__ */ new Map();
|
|
4296
|
+
function buildCategoriesCacheKey(client) {
|
|
4297
|
+
return client.getPublicKey() || "__demo__";
|
|
4298
|
+
}
|
|
4299
|
+
function useCategories(options = {}) {
|
|
4300
|
+
const context = useOptionalCimplify();
|
|
4301
|
+
const client = options.client ?? context?.client;
|
|
4302
|
+
if (!client) {
|
|
4303
|
+
throw new Error("useCategories must be used within CimplifyProvider or passed { client }.");
|
|
4304
|
+
}
|
|
4305
|
+
const enabled = options.enabled ?? true;
|
|
4306
|
+
const cacheKey = react.useMemo(() => buildCategoriesCacheKey(client), [client]);
|
|
4307
|
+
const cached = categoriesCache.get(cacheKey);
|
|
4308
|
+
const [categories, setCategories] = react.useState(cached ?? []);
|
|
4309
|
+
const [isLoading, setIsLoading] = react.useState(enabled && !cached);
|
|
4310
|
+
const [error, setError] = react.useState(null);
|
|
4311
|
+
const load = react.useCallback(
|
|
4312
|
+
async (force = false) => {
|
|
4313
|
+
if (!enabled) {
|
|
4314
|
+
setIsLoading(false);
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
setError(null);
|
|
4318
|
+
if (!force) {
|
|
4319
|
+
const cachedCategories = categoriesCache.get(cacheKey);
|
|
4320
|
+
if (cachedCategories) {
|
|
4321
|
+
setCategories(cachedCategories);
|
|
4322
|
+
setIsLoading(false);
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
setIsLoading(true);
|
|
4327
|
+
try {
|
|
4328
|
+
const existing = categoriesInflight.get(cacheKey);
|
|
4329
|
+
const promise = existing ?? (async () => {
|
|
4330
|
+
const result = await client.catalogue.getCategories();
|
|
4331
|
+
if (!result.ok) {
|
|
4332
|
+
throw result.error;
|
|
4333
|
+
}
|
|
4334
|
+
return result.value;
|
|
4335
|
+
})();
|
|
4336
|
+
if (!existing) {
|
|
4337
|
+
categoriesInflight.set(
|
|
4338
|
+
cacheKey,
|
|
4339
|
+
promise.finally(() => {
|
|
4340
|
+
categoriesInflight.delete(cacheKey);
|
|
4341
|
+
})
|
|
4342
|
+
);
|
|
4343
|
+
}
|
|
4344
|
+
const value = await promise;
|
|
4345
|
+
categoriesCache.set(cacheKey, value);
|
|
4346
|
+
setCategories(value);
|
|
4347
|
+
} catch (loadError) {
|
|
4348
|
+
setError(loadError);
|
|
4349
|
+
} finally {
|
|
4350
|
+
setIsLoading(false);
|
|
4351
|
+
}
|
|
4352
|
+
},
|
|
4353
|
+
[cacheKey, client, enabled]
|
|
4354
|
+
);
|
|
4355
|
+
react.useEffect(() => {
|
|
4356
|
+
void load(false);
|
|
4357
|
+
}, [load]);
|
|
4358
|
+
const refetch = react.useCallback(async () => {
|
|
4359
|
+
categoriesCache.delete(cacheKey);
|
|
4360
|
+
await load(true);
|
|
4361
|
+
}, [cacheKey, load]);
|
|
4362
|
+
return { categories, isLoading, error, refetch };
|
|
4363
|
+
}
|
|
4364
|
+
var CART_STORAGE_PREFIX = "cimplify:cart:v2";
|
|
4365
|
+
var DEFAULT_TAX_RATE = 0.08875;
|
|
4366
|
+
var cartStores = /* @__PURE__ */ new Map();
|
|
4367
|
+
function toNumber(value) {
|
|
4368
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
4369
|
+
return value;
|
|
4370
|
+
}
|
|
4371
|
+
if (typeof value === "string") {
|
|
4372
|
+
const parsed = Number.parseFloat(value);
|
|
4373
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
4374
|
+
}
|
|
4375
|
+
return 0;
|
|
4376
|
+
}
|
|
4377
|
+
function roundMoney(value) {
|
|
4378
|
+
return Math.max(0, Number(value.toFixed(2)));
|
|
4379
|
+
}
|
|
4380
|
+
function clampQuantity(quantity) {
|
|
4381
|
+
if (!Number.isFinite(quantity)) {
|
|
4382
|
+
return 1;
|
|
4383
|
+
}
|
|
4384
|
+
return Math.max(1, Math.floor(quantity));
|
|
4385
|
+
}
|
|
4386
|
+
function normalizeOptionIds(value) {
|
|
4387
|
+
if (!value || value.length === 0) {
|
|
4388
|
+
return void 0;
|
|
4389
|
+
}
|
|
4390
|
+
const normalized = Array.from(
|
|
4391
|
+
new Set(
|
|
4392
|
+
value.map((entry) => entry.trim()).filter((entry) => entry.length > 0)
|
|
4393
|
+
)
|
|
4394
|
+
).sort();
|
|
4395
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
4396
|
+
}
|
|
4397
|
+
function buildLineKey(productId, options) {
|
|
4398
|
+
const parts = [productId];
|
|
4399
|
+
if (options.locationId) {
|
|
4400
|
+
parts.push(`l:${options.locationId}`);
|
|
4401
|
+
}
|
|
4402
|
+
if (options.variantId) {
|
|
4403
|
+
parts.push(`v:${options.variantId}`);
|
|
4404
|
+
}
|
|
4405
|
+
const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
|
|
4406
|
+
if (addOnOptionIds && addOnOptionIds.length > 0) {
|
|
4407
|
+
parts.push(`a:${addOnOptionIds.join(",")}`);
|
|
4408
|
+
}
|
|
4409
|
+
if (options.bundleSelections && options.bundleSelections.length > 0) {
|
|
4410
|
+
parts.push(`b:${JSON.stringify(options.bundleSelections)}`);
|
|
4411
|
+
}
|
|
4412
|
+
if (options.compositeSelections && options.compositeSelections.length > 0) {
|
|
4413
|
+
parts.push(`c:${JSON.stringify(options.compositeSelections)}`);
|
|
4414
|
+
}
|
|
4415
|
+
if (options.quoteId) {
|
|
4416
|
+
parts.push(`q:${options.quoteId}`);
|
|
4417
|
+
}
|
|
4418
|
+
return parts.join("|");
|
|
4419
|
+
}
|
|
4420
|
+
function calculateLineSubtotal(item) {
|
|
4421
|
+
let unitPrice = parsePrice(item.product.default_price);
|
|
4422
|
+
if (item.variant?.price_adjustment) {
|
|
4423
|
+
unitPrice += parsePrice(item.variant.price_adjustment);
|
|
4424
|
+
}
|
|
4425
|
+
for (const option of item.addOnOptions || []) {
|
|
4426
|
+
if (option.default_price) {
|
|
4427
|
+
unitPrice += parsePrice(option.default_price);
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
return roundMoney(unitPrice * item.quantity);
|
|
4431
|
+
}
|
|
4432
|
+
function calculateSummary(items) {
|
|
4433
|
+
const subtotal = roundMoney(items.reduce((sum, item) => sum + calculateLineSubtotal(item), 0));
|
|
4434
|
+
const tax = roundMoney(subtotal * DEFAULT_TAX_RATE);
|
|
4435
|
+
return {
|
|
4436
|
+
subtotal,
|
|
4437
|
+
tax,
|
|
4438
|
+
total: roundMoney(subtotal + tax)
|
|
4439
|
+
};
|
|
4440
|
+
}
|
|
4441
|
+
function toProductFromServerItem(item, businessId) {
|
|
4442
|
+
return {
|
|
4443
|
+
id: item.item_id,
|
|
4444
|
+
business_id: businessId,
|
|
4445
|
+
category_id: item.category_id,
|
|
4446
|
+
name: item.name,
|
|
4447
|
+
slug: item.item_id,
|
|
4448
|
+
description: item.description,
|
|
4449
|
+
image_url: item.image_url,
|
|
4450
|
+
default_price: item.base_price,
|
|
4451
|
+
product_type: "product",
|
|
4452
|
+
inventory_type: "none",
|
|
4453
|
+
variant_strategy: "fetch_all",
|
|
4454
|
+
is_active: item.is_available,
|
|
4455
|
+
created_at: item.created_at,
|
|
4456
|
+
updated_at: item.updated_at,
|
|
4457
|
+
metadata: {
|
|
4458
|
+
from_sdk: true
|
|
4459
|
+
}
|
|
4460
|
+
};
|
|
4461
|
+
}
|
|
4462
|
+
function mapServerCart(serverCart) {
|
|
4463
|
+
const items = serverCart.items.map((item) => {
|
|
4464
|
+
const variant = item.variant_info || item.variant_name ? {
|
|
4465
|
+
id: item.variant_info?.id || item.variant_id || "",
|
|
4466
|
+
name: item.variant_info?.name || item.variant_name || "Variant",
|
|
4467
|
+
price_adjustment: item.variant_info?.price_adjustment
|
|
4468
|
+
} : void 0;
|
|
4469
|
+
const quoteId = typeof item.metadata?.quote_id === "string" ? item.metadata.quote_id : void 0;
|
|
4470
|
+
return {
|
|
4471
|
+
id: item.id,
|
|
4472
|
+
product: toProductFromServerItem(item, serverCart.business_id),
|
|
4473
|
+
quantity: clampQuantity(item.quantity),
|
|
4474
|
+
locationId: serverCart.location_id,
|
|
4475
|
+
variantId: item.variant_id,
|
|
4476
|
+
variant,
|
|
4477
|
+
quoteId,
|
|
4478
|
+
addOnOptionIds: item.add_on_option_ids || void 0,
|
|
4479
|
+
addOnOptions: (item.add_on_options || []).map((option) => ({
|
|
4480
|
+
id: option.id,
|
|
4481
|
+
name: option.name,
|
|
4482
|
+
default_price: option.price
|
|
4483
|
+
})),
|
|
4484
|
+
bundleSelections: item.bundle_selections || void 0,
|
|
4485
|
+
compositeSelections: item.composite_selections || void 0,
|
|
4486
|
+
specialInstructions: item.special_instructions
|
|
4487
|
+
};
|
|
4488
|
+
});
|
|
4489
|
+
return {
|
|
4490
|
+
items,
|
|
4491
|
+
subtotal: roundMoney(toNumber(serverCart.pricing.subtotal)),
|
|
4492
|
+
tax: roundMoney(toNumber(serverCart.pricing.tax_amount)),
|
|
4493
|
+
total: roundMoney(toNumber(serverCart.pricing.total_price)),
|
|
4494
|
+
currency: serverCart.pricing.currency || "USD"
|
|
4495
|
+
};
|
|
4496
|
+
}
|
|
4497
|
+
function buildStorageKey(storeKey) {
|
|
4498
|
+
return `${CART_STORAGE_PREFIX}:${storeKey}`;
|
|
4499
|
+
}
|
|
4500
|
+
function createEmptySnapshot(currency) {
|
|
4501
|
+
return {
|
|
4502
|
+
items: [],
|
|
4503
|
+
subtotal: 0,
|
|
4504
|
+
tax: 0,
|
|
4505
|
+
total: 0,
|
|
4506
|
+
currency,
|
|
4507
|
+
isLoading: true
|
|
4508
|
+
};
|
|
4509
|
+
}
|
|
4510
|
+
function createCartStore(params) {
|
|
4511
|
+
const { client, storeKey, locationId, isDemoMode, currency } = params;
|
|
4512
|
+
const storageKey = buildStorageKey(storeKey);
|
|
4513
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4514
|
+
let snapshot = createEmptySnapshot(currency);
|
|
4515
|
+
let initialized = false;
|
|
4516
|
+
let initializePromise = null;
|
|
4517
|
+
function emit() {
|
|
4518
|
+
listeners.forEach((listener) => {
|
|
4519
|
+
listener();
|
|
4520
|
+
});
|
|
4521
|
+
}
|
|
4522
|
+
function setSnapshot(nextSnapshot) {
|
|
4523
|
+
snapshot = nextSnapshot;
|
|
4524
|
+
persistSnapshot();
|
|
4525
|
+
emit();
|
|
4526
|
+
}
|
|
4527
|
+
function updateSnapshot(updater) {
|
|
4528
|
+
setSnapshot(updater(snapshot));
|
|
4529
|
+
}
|
|
4530
|
+
function persistSnapshot() {
|
|
4531
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
try {
|
|
4535
|
+
window.localStorage.setItem(
|
|
4536
|
+
storageKey,
|
|
4537
|
+
JSON.stringify({
|
|
4538
|
+
items: snapshot.items,
|
|
4539
|
+
subtotal: snapshot.subtotal,
|
|
4540
|
+
tax: snapshot.tax,
|
|
4541
|
+
total: snapshot.total,
|
|
4542
|
+
currency: snapshot.currency
|
|
4543
|
+
})
|
|
4544
|
+
);
|
|
4545
|
+
} catch {
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
function hydrateFromStorage() {
|
|
4549
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
4550
|
+
return;
|
|
4551
|
+
}
|
|
4552
|
+
try {
|
|
4553
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
4554
|
+
if (!raw) {
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4557
|
+
const parsed = JSON.parse(raw);
|
|
4558
|
+
if (!parsed || !Array.isArray(parsed.items)) {
|
|
4559
|
+
return;
|
|
4560
|
+
}
|
|
4561
|
+
snapshot = {
|
|
4562
|
+
items: parsed.items,
|
|
4563
|
+
subtotal: toNumber(parsed.subtotal),
|
|
4564
|
+
tax: toNumber(parsed.tax),
|
|
4565
|
+
total: toNumber(parsed.total),
|
|
4566
|
+
currency: typeof parsed.currency === "string" && parsed.currency ? parsed.currency : currency,
|
|
4567
|
+
isLoading: !isDemoMode
|
|
4568
|
+
};
|
|
4569
|
+
emit();
|
|
4570
|
+
} catch {
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
async function sync() {
|
|
4574
|
+
if (isDemoMode) {
|
|
4575
|
+
updateSnapshot((current) => ({
|
|
4576
|
+
...current,
|
|
4577
|
+
isLoading: false
|
|
4578
|
+
}));
|
|
4579
|
+
return;
|
|
4580
|
+
}
|
|
4581
|
+
updateSnapshot((current) => ({
|
|
4582
|
+
...current,
|
|
4583
|
+
isLoading: true
|
|
4584
|
+
}));
|
|
4585
|
+
const result = await client.cart.get();
|
|
4586
|
+
if (!result.ok) {
|
|
4587
|
+
updateSnapshot((current) => ({
|
|
4588
|
+
...current,
|
|
4589
|
+
isLoading: false
|
|
4590
|
+
}));
|
|
4591
|
+
throw result.error;
|
|
4592
|
+
}
|
|
4593
|
+
const next = mapServerCart(result.value);
|
|
4594
|
+
setSnapshot({
|
|
4595
|
+
...next,
|
|
4596
|
+
isLoading: false
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
async function maybeResolveQuoteId(product, quantity, options) {
|
|
4600
|
+
if (options.quoteId) {
|
|
4601
|
+
return options.quoteId;
|
|
4602
|
+
}
|
|
4603
|
+
const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
|
|
4604
|
+
const requiresQuote = Boolean(
|
|
4605
|
+
options.variantId || addOnOptionIds && addOnOptionIds.length > 0 || options.bundleSelections && options.bundleSelections.length > 0 || options.compositeSelections && options.compositeSelections.length > 0
|
|
4606
|
+
);
|
|
4607
|
+
if (!requiresQuote || isDemoMode) {
|
|
4608
|
+
return void 0;
|
|
4609
|
+
}
|
|
4610
|
+
const quoteResult = await client.catalogue.fetchQuote({
|
|
4611
|
+
product_id: product.id,
|
|
4612
|
+
quantity,
|
|
4613
|
+
location_id: options.locationId || locationId || void 0,
|
|
4614
|
+
variant_id: options.variantId,
|
|
4615
|
+
add_on_option_ids: addOnOptionIds,
|
|
4616
|
+
bundle_selections: options.bundleSelections,
|
|
4617
|
+
composite_selections: options.compositeSelections
|
|
4618
|
+
});
|
|
4619
|
+
if (!quoteResult.ok) {
|
|
4620
|
+
throw quoteResult.error;
|
|
4621
|
+
}
|
|
4622
|
+
return quoteResult.value.quote_id;
|
|
4623
|
+
}
|
|
4624
|
+
async function addItem(product, quantity = 1, options = {}) {
|
|
4625
|
+
const resolvedQuantity = clampQuantity(quantity);
|
|
4626
|
+
const normalizedOptions = {
|
|
4627
|
+
...options,
|
|
4628
|
+
locationId: options.locationId || locationId || void 0,
|
|
4629
|
+
addOnOptionIds: normalizeOptionIds(options.addOnOptionIds)
|
|
4630
|
+
};
|
|
4631
|
+
const maybeProductWithVariants = product;
|
|
4632
|
+
if (Array.isArray(maybeProductWithVariants.variants) && maybeProductWithVariants.variants.length > 0 && !normalizedOptions.variantId) {
|
|
4633
|
+
console.warn(
|
|
4634
|
+
"[Cimplify] addItem() called without variantId for a product that has variants. Default variant pricing will be used."
|
|
4635
|
+
);
|
|
4636
|
+
}
|
|
4637
|
+
const previousSnapshot = snapshot;
|
|
4638
|
+
const lineKey = buildLineKey(product.id, normalizedOptions);
|
|
4639
|
+
const optimisticItem = {
|
|
4640
|
+
id: `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
4641
|
+
product,
|
|
4642
|
+
quantity: resolvedQuantity,
|
|
4643
|
+
locationId: normalizedOptions.locationId,
|
|
4644
|
+
variantId: normalizedOptions.variantId,
|
|
4645
|
+
variant: normalizedOptions.variant,
|
|
4646
|
+
quoteId: normalizedOptions.quoteId,
|
|
4647
|
+
addOnOptionIds: normalizedOptions.addOnOptionIds,
|
|
4648
|
+
addOnOptions: normalizedOptions.addOnOptions,
|
|
4649
|
+
bundleSelections: normalizedOptions.bundleSelections,
|
|
4650
|
+
compositeSelections: normalizedOptions.compositeSelections,
|
|
4651
|
+
specialInstructions: normalizedOptions.specialInstructions
|
|
4652
|
+
};
|
|
4653
|
+
updateSnapshot((current) => {
|
|
4654
|
+
const existingIndex = current.items.findIndex((item) => {
|
|
4655
|
+
const itemKey = buildLineKey(item.product.id, {
|
|
4656
|
+
locationId: item.locationId,
|
|
4657
|
+
variantId: item.variantId,
|
|
4658
|
+
quoteId: item.quoteId,
|
|
4659
|
+
addOnOptionIds: item.addOnOptionIds,
|
|
4660
|
+
bundleSelections: item.bundleSelections,
|
|
4661
|
+
compositeSelections: item.compositeSelections
|
|
4662
|
+
});
|
|
4663
|
+
return itemKey === lineKey;
|
|
4664
|
+
});
|
|
4665
|
+
const nextItems = [...current.items];
|
|
4666
|
+
if (existingIndex >= 0) {
|
|
4667
|
+
const existing = nextItems[existingIndex];
|
|
4668
|
+
nextItems[existingIndex] = {
|
|
4669
|
+
...existing,
|
|
4670
|
+
quantity: existing.quantity + resolvedQuantity
|
|
4671
|
+
};
|
|
4672
|
+
} else {
|
|
4673
|
+
nextItems.push(optimisticItem);
|
|
4674
|
+
}
|
|
4675
|
+
const summary = calculateSummary(nextItems);
|
|
4676
|
+
return {
|
|
4677
|
+
...current,
|
|
4678
|
+
items: nextItems,
|
|
4679
|
+
subtotal: summary.subtotal,
|
|
4680
|
+
tax: summary.tax,
|
|
4681
|
+
total: summary.total
|
|
4682
|
+
};
|
|
4683
|
+
});
|
|
4684
|
+
if (isDemoMode) {
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4687
|
+
try {
|
|
4688
|
+
const quoteId = await maybeResolveQuoteId(product, resolvedQuantity, normalizedOptions);
|
|
4689
|
+
const result = await client.cart.addItem({
|
|
4690
|
+
item_id: product.id,
|
|
4691
|
+
quantity: resolvedQuantity,
|
|
4692
|
+
variant_id: normalizedOptions.variantId,
|
|
4693
|
+
quote_id: quoteId,
|
|
4694
|
+
add_on_options: normalizedOptions.addOnOptionIds,
|
|
4695
|
+
special_instructions: normalizedOptions.specialInstructions,
|
|
4696
|
+
bundle_selections: normalizedOptions.bundleSelections,
|
|
4697
|
+
composite_selections: normalizedOptions.compositeSelections
|
|
4698
|
+
});
|
|
4699
|
+
if (!result.ok) {
|
|
4700
|
+
throw result.error;
|
|
4701
|
+
}
|
|
4702
|
+
await sync();
|
|
4703
|
+
} catch (error) {
|
|
4704
|
+
snapshot = previousSnapshot;
|
|
4705
|
+
persistSnapshot();
|
|
4706
|
+
emit();
|
|
4707
|
+
throw error;
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
async function removeItem(itemId) {
|
|
4711
|
+
const previousSnapshot = snapshot;
|
|
4712
|
+
updateSnapshot((current) => {
|
|
4713
|
+
const nextItems = current.items.filter((item) => item.id !== itemId);
|
|
4714
|
+
const summary = calculateSummary(nextItems);
|
|
4715
|
+
return {
|
|
4716
|
+
...current,
|
|
4717
|
+
items: nextItems,
|
|
4718
|
+
subtotal: summary.subtotal,
|
|
4719
|
+
tax: summary.tax,
|
|
4720
|
+
total: summary.total
|
|
4721
|
+
};
|
|
4722
|
+
});
|
|
4723
|
+
if (isDemoMode) {
|
|
4724
|
+
return;
|
|
4725
|
+
}
|
|
4726
|
+
try {
|
|
4727
|
+
const result = await client.cart.removeItem(itemId);
|
|
4728
|
+
if (!result.ok) {
|
|
4729
|
+
throw result.error;
|
|
4730
|
+
}
|
|
4731
|
+
await sync();
|
|
4732
|
+
} catch (error) {
|
|
4733
|
+
snapshot = previousSnapshot;
|
|
4734
|
+
persistSnapshot();
|
|
4735
|
+
emit();
|
|
4736
|
+
throw error;
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
async function updateQuantity(itemId, quantity) {
|
|
4740
|
+
if (quantity <= 0) {
|
|
4741
|
+
await removeItem(itemId);
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
const resolvedQuantity = clampQuantity(quantity);
|
|
4745
|
+
const previousSnapshot = snapshot;
|
|
4746
|
+
updateSnapshot((current) => {
|
|
4747
|
+
const nextItems = current.items.map(
|
|
4748
|
+
(item) => item.id === itemId ? {
|
|
4749
|
+
...item,
|
|
4750
|
+
quantity: resolvedQuantity
|
|
4751
|
+
} : item
|
|
4752
|
+
);
|
|
4753
|
+
const summary = calculateSummary(nextItems);
|
|
4754
|
+
return {
|
|
4755
|
+
...current,
|
|
4756
|
+
items: nextItems,
|
|
4757
|
+
subtotal: summary.subtotal,
|
|
4758
|
+
tax: summary.tax,
|
|
4759
|
+
total: summary.total
|
|
4760
|
+
};
|
|
4761
|
+
});
|
|
4762
|
+
if (isDemoMode) {
|
|
4763
|
+
return;
|
|
4764
|
+
}
|
|
4765
|
+
try {
|
|
4766
|
+
const result = await client.cart.updateQuantity(itemId, resolvedQuantity);
|
|
4767
|
+
if (!result.ok) {
|
|
4768
|
+
throw result.error;
|
|
4769
|
+
}
|
|
4770
|
+
await sync();
|
|
4771
|
+
} catch (error) {
|
|
4772
|
+
snapshot = previousSnapshot;
|
|
4773
|
+
persistSnapshot();
|
|
4774
|
+
emit();
|
|
4775
|
+
throw error;
|
|
4776
|
+
}
|
|
4777
|
+
}
|
|
4778
|
+
async function clearCart() {
|
|
4779
|
+
const previousSnapshot = snapshot;
|
|
4780
|
+
updateSnapshot((current) => ({
|
|
4781
|
+
...current,
|
|
4782
|
+
items: [],
|
|
4783
|
+
subtotal: 0,
|
|
4784
|
+
tax: 0,
|
|
4785
|
+
total: 0
|
|
4786
|
+
}));
|
|
4787
|
+
if (isDemoMode) {
|
|
4788
|
+
return;
|
|
4789
|
+
}
|
|
4790
|
+
try {
|
|
4791
|
+
const result = await client.cart.clear();
|
|
4792
|
+
if (!result.ok) {
|
|
4793
|
+
throw result.error;
|
|
4794
|
+
}
|
|
4795
|
+
await sync();
|
|
4796
|
+
} catch (error) {
|
|
4797
|
+
snapshot = previousSnapshot;
|
|
4798
|
+
persistSnapshot();
|
|
4799
|
+
emit();
|
|
4800
|
+
throw error;
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
async function initialize() {
|
|
4804
|
+
if (initialized) {
|
|
4805
|
+
if (initializePromise) {
|
|
4806
|
+
await initializePromise;
|
|
4807
|
+
}
|
|
4808
|
+
return;
|
|
4809
|
+
}
|
|
4810
|
+
initialized = true;
|
|
4811
|
+
hydrateFromStorage();
|
|
4812
|
+
if (isDemoMode) {
|
|
4813
|
+
updateSnapshot((current) => ({
|
|
4814
|
+
...current,
|
|
4815
|
+
isLoading: false
|
|
4816
|
+
}));
|
|
4817
|
+
return;
|
|
4818
|
+
}
|
|
4819
|
+
initializePromise = sync().catch(() => {
|
|
4820
|
+
updateSnapshot((current) => ({
|
|
4821
|
+
...current,
|
|
4822
|
+
isLoading: false
|
|
4823
|
+
}));
|
|
4824
|
+
});
|
|
4825
|
+
await initializePromise;
|
|
4826
|
+
initializePromise = null;
|
|
4827
|
+
}
|
|
4828
|
+
return {
|
|
4829
|
+
subscribe(listener) {
|
|
4830
|
+
listeners.add(listener);
|
|
4831
|
+
return () => {
|
|
4832
|
+
listeners.delete(listener);
|
|
4833
|
+
};
|
|
4834
|
+
},
|
|
4835
|
+
getSnapshot() {
|
|
4836
|
+
return snapshot;
|
|
4837
|
+
},
|
|
4838
|
+
initialize,
|
|
4839
|
+
addItem,
|
|
4840
|
+
removeItem,
|
|
4841
|
+
updateQuantity,
|
|
4842
|
+
clearCart,
|
|
4843
|
+
sync
|
|
4844
|
+
};
|
|
4845
|
+
}
|
|
4846
|
+
function getStoreKey(client, locationId, isDemoMode) {
|
|
4847
|
+
return [
|
|
4848
|
+
client.getPublicKey() || "__demo__",
|
|
4849
|
+
locationId || "__no_location__",
|
|
4850
|
+
isDemoMode ? "demo" : "live"
|
|
4851
|
+
].join(":");
|
|
4852
|
+
}
|
|
4853
|
+
function getOrCreateStore(params) {
|
|
4854
|
+
const { client, locationId, isDemoMode, currency } = params;
|
|
4855
|
+
const storeKey = getStoreKey(client, locationId, isDemoMode);
|
|
4856
|
+
const existing = cartStores.get(storeKey);
|
|
4857
|
+
if (existing) {
|
|
4858
|
+
return existing;
|
|
4859
|
+
}
|
|
4860
|
+
const created = createCartStore({
|
|
4861
|
+
client,
|
|
4862
|
+
storeKey,
|
|
4863
|
+
locationId,
|
|
4864
|
+
isDemoMode,
|
|
4865
|
+
currency
|
|
4866
|
+
});
|
|
4867
|
+
cartStores.set(storeKey, created);
|
|
4868
|
+
return created;
|
|
4869
|
+
}
|
|
4870
|
+
function useCart(options = {}) {
|
|
4871
|
+
const context = useOptionalCimplify();
|
|
4872
|
+
const client = options.client ?? context?.client;
|
|
4873
|
+
if (!client) {
|
|
4874
|
+
throw new Error("useCart must be used within CimplifyProvider or passed { client }.");
|
|
4875
|
+
}
|
|
4876
|
+
const locationId = options.locationId ?? client.getLocationId();
|
|
4877
|
+
const isDemoMode = options.demoMode ?? context?.isDemoMode ?? client.getPublicKey().trim().length === 0;
|
|
4878
|
+
const currency = options.currency ?? context?.currency ?? "USD";
|
|
4879
|
+
const store = react.useMemo(
|
|
4880
|
+
() => getOrCreateStore({
|
|
4881
|
+
client,
|
|
4882
|
+
locationId,
|
|
4883
|
+
isDemoMode,
|
|
4884
|
+
currency
|
|
4885
|
+
}),
|
|
4886
|
+
[client, currency, isDemoMode, locationId]
|
|
4887
|
+
);
|
|
4888
|
+
const snapshot = react.useSyncExternalStore(
|
|
4889
|
+
store.subscribe,
|
|
4890
|
+
store.getSnapshot,
|
|
4891
|
+
store.getSnapshot
|
|
4892
|
+
);
|
|
4893
|
+
react.useEffect(() => {
|
|
4894
|
+
void store.initialize();
|
|
4895
|
+
}, [store]);
|
|
4896
|
+
const addItem = react.useCallback(
|
|
4897
|
+
async (product, quantity, addOptions) => {
|
|
4898
|
+
await store.addItem(product, quantity, addOptions);
|
|
4899
|
+
},
|
|
4900
|
+
[store]
|
|
4901
|
+
);
|
|
4902
|
+
const removeItem = react.useCallback(
|
|
4903
|
+
async (itemId) => {
|
|
4904
|
+
await store.removeItem(itemId);
|
|
4905
|
+
},
|
|
4906
|
+
[store]
|
|
4907
|
+
);
|
|
4908
|
+
const updateQuantity = react.useCallback(
|
|
4909
|
+
async (itemId, quantity) => {
|
|
4910
|
+
await store.updateQuantity(itemId, quantity);
|
|
4911
|
+
},
|
|
4912
|
+
[store]
|
|
4913
|
+
);
|
|
4914
|
+
const clearCart = react.useCallback(async () => {
|
|
4915
|
+
await store.clearCart();
|
|
4916
|
+
}, [store]);
|
|
4917
|
+
const sync = react.useCallback(async () => {
|
|
4918
|
+
try {
|
|
4919
|
+
await store.sync();
|
|
4920
|
+
} catch (syncError) {
|
|
4921
|
+
throw syncError;
|
|
4922
|
+
}
|
|
4923
|
+
}, [store]);
|
|
4924
|
+
const itemCount = react.useMemo(
|
|
4925
|
+
() => snapshot.items.reduce((sum, item) => sum + item.quantity, 0),
|
|
4926
|
+
[snapshot.items]
|
|
4927
|
+
);
|
|
4928
|
+
return {
|
|
4929
|
+
items: snapshot.items,
|
|
4930
|
+
itemCount,
|
|
4931
|
+
subtotal: snapshot.subtotal,
|
|
4932
|
+
tax: snapshot.tax,
|
|
4933
|
+
total: snapshot.total,
|
|
4934
|
+
currency: snapshot.currency,
|
|
4935
|
+
isEmpty: itemCount === 0,
|
|
4936
|
+
isLoading: snapshot.isLoading,
|
|
4937
|
+
addItem,
|
|
4938
|
+
removeItem,
|
|
4939
|
+
updateQuantity,
|
|
4940
|
+
clearCart,
|
|
4941
|
+
sync
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
var orderCache = /* @__PURE__ */ new Map();
|
|
4945
|
+
var orderInflight = /* @__PURE__ */ new Map();
|
|
4946
|
+
function buildOrderCacheKey(client, orderId) {
|
|
4947
|
+
return `${client.getPublicKey() || "__demo__"}:${orderId}`;
|
|
4948
|
+
}
|
|
4949
|
+
function useOrder(orderId, options = {}) {
|
|
4950
|
+
const context = useOptionalCimplify();
|
|
4951
|
+
const client = options.client ?? context?.client;
|
|
4952
|
+
if (!client) {
|
|
4953
|
+
throw new Error("useOrder must be used within CimplifyProvider or passed { client }.");
|
|
4954
|
+
}
|
|
4955
|
+
const normalizedOrderId = react.useMemo(() => (orderId || "").trim(), [orderId]);
|
|
4956
|
+
const enabled = options.enabled ?? true;
|
|
4957
|
+
const poll = options.poll ?? false;
|
|
4958
|
+
const pollInterval = options.pollInterval ?? 5e3;
|
|
4959
|
+
const requestIdRef = react.useRef(0);
|
|
4960
|
+
const cacheKey = react.useMemo(
|
|
4961
|
+
() => buildOrderCacheKey(client, normalizedOrderId),
|
|
4962
|
+
[client, normalizedOrderId]
|
|
4963
|
+
);
|
|
4964
|
+
const cached = orderCache.get(cacheKey);
|
|
4965
|
+
const [order, setOrder] = react.useState(cached?.order ?? null);
|
|
4966
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
4967
|
+
enabled && normalizedOrderId.length > 0 && !cached
|
|
4968
|
+
);
|
|
4969
|
+
const [error, setError] = react.useState(null);
|
|
4970
|
+
const load = react.useCallback(
|
|
4971
|
+
async (force = false) => {
|
|
4972
|
+
if (!enabled || normalizedOrderId.length === 0) {
|
|
4973
|
+
setOrder(null);
|
|
4974
|
+
setIsLoading(false);
|
|
4975
|
+
return;
|
|
4976
|
+
}
|
|
4977
|
+
const nextRequestId = ++requestIdRef.current;
|
|
4978
|
+
setError(null);
|
|
4979
|
+
if (!force) {
|
|
4980
|
+
const cacheEntry = orderCache.get(cacheKey);
|
|
4981
|
+
if (cacheEntry) {
|
|
4982
|
+
setOrder(cacheEntry.order);
|
|
4983
|
+
setIsLoading(false);
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
}
|
|
4987
|
+
setIsLoading(true);
|
|
4988
|
+
try {
|
|
4989
|
+
const existing = orderInflight.get(cacheKey);
|
|
4990
|
+
const promise = existing ?? (async () => {
|
|
4991
|
+
const result = await client.orders.get(normalizedOrderId);
|
|
4992
|
+
if (!result.ok) {
|
|
4993
|
+
throw result.error;
|
|
4994
|
+
}
|
|
4995
|
+
return result.value;
|
|
4996
|
+
})();
|
|
4997
|
+
if (!existing) {
|
|
4998
|
+
orderInflight.set(
|
|
4999
|
+
cacheKey,
|
|
5000
|
+
promise.finally(() => {
|
|
5001
|
+
orderInflight.delete(cacheKey);
|
|
5002
|
+
})
|
|
5003
|
+
);
|
|
5004
|
+
}
|
|
5005
|
+
const value = await promise;
|
|
5006
|
+
orderCache.set(cacheKey, { order: value });
|
|
5007
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5008
|
+
setOrder(value);
|
|
5009
|
+
setError(null);
|
|
5010
|
+
}
|
|
5011
|
+
} catch (loadError) {
|
|
5012
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5013
|
+
setError(loadError);
|
|
5014
|
+
}
|
|
5015
|
+
} finally {
|
|
5016
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5017
|
+
setIsLoading(false);
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
},
|
|
5021
|
+
[cacheKey, client, enabled, normalizedOrderId]
|
|
5022
|
+
);
|
|
5023
|
+
react.useEffect(() => {
|
|
5024
|
+
void load(false);
|
|
5025
|
+
}, [load]);
|
|
5026
|
+
react.useEffect(() => {
|
|
5027
|
+
if (!poll || !enabled || normalizedOrderId.length === 0) {
|
|
5028
|
+
return;
|
|
5029
|
+
}
|
|
5030
|
+
const timer = window.setInterval(() => {
|
|
5031
|
+
void load(true);
|
|
5032
|
+
}, Math.max(1e3, pollInterval));
|
|
5033
|
+
return () => {
|
|
5034
|
+
window.clearInterval(timer);
|
|
5035
|
+
};
|
|
5036
|
+
}, [enabled, load, normalizedOrderId.length, poll, pollInterval]);
|
|
5037
|
+
const refetch = react.useCallback(async () => {
|
|
5038
|
+
orderCache.delete(cacheKey);
|
|
5039
|
+
await load(true);
|
|
5040
|
+
}, [cacheKey, load]);
|
|
5041
|
+
return { order, isLoading, error, refetch };
|
|
5042
|
+
}
|
|
5043
|
+
var LOCATION_STORAGE_KEY2 = "cimplify_location_id";
|
|
5044
|
+
function readStoredLocationId() {
|
|
5045
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
5046
|
+
return null;
|
|
5047
|
+
}
|
|
5048
|
+
const value = window.localStorage.getItem(LOCATION_STORAGE_KEY2);
|
|
5049
|
+
if (!value) {
|
|
5050
|
+
return null;
|
|
5051
|
+
}
|
|
5052
|
+
const normalized = value.trim();
|
|
5053
|
+
return normalized.length > 0 ? normalized : null;
|
|
5054
|
+
}
|
|
5055
|
+
function writeStoredLocationId(locationId) {
|
|
5056
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
5057
|
+
return;
|
|
5058
|
+
}
|
|
5059
|
+
if (!locationId) {
|
|
5060
|
+
window.localStorage.removeItem(LOCATION_STORAGE_KEY2);
|
|
5061
|
+
return;
|
|
5062
|
+
}
|
|
5063
|
+
window.localStorage.setItem(LOCATION_STORAGE_KEY2, locationId);
|
|
5064
|
+
}
|
|
5065
|
+
function resolveLocation(locations, preferredId) {
|
|
5066
|
+
if (locations.length === 0) {
|
|
5067
|
+
return null;
|
|
5068
|
+
}
|
|
5069
|
+
if (preferredId) {
|
|
5070
|
+
const found = locations.find((location) => location.id === preferredId);
|
|
5071
|
+
if (found) {
|
|
5072
|
+
return found;
|
|
5073
|
+
}
|
|
5074
|
+
}
|
|
5075
|
+
return locations[0];
|
|
5076
|
+
}
|
|
5077
|
+
function useLocations(options = {}) {
|
|
5078
|
+
const context = useOptionalCimplify();
|
|
5079
|
+
if (context && (!options.client || context.client === options.client)) {
|
|
5080
|
+
return {
|
|
5081
|
+
locations: context.locations,
|
|
5082
|
+
currentLocation: context.currentLocation,
|
|
5083
|
+
setCurrentLocation: context.setCurrentLocation,
|
|
5084
|
+
isLoading: !context.isReady
|
|
5085
|
+
};
|
|
5086
|
+
}
|
|
5087
|
+
const client = options.client;
|
|
5088
|
+
const [locations, setLocations] = react.useState([]);
|
|
5089
|
+
const [currentLocation, setCurrentLocationState] = react.useState(null);
|
|
5090
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
5091
|
+
const setCurrentLocation = react.useCallback(
|
|
5092
|
+
(location) => {
|
|
5093
|
+
setCurrentLocationState(location);
|
|
5094
|
+
if (client) {
|
|
5095
|
+
client.setLocationId(location.id);
|
|
5096
|
+
}
|
|
5097
|
+
writeStoredLocationId(location.id);
|
|
5098
|
+
},
|
|
5099
|
+
[client]
|
|
5100
|
+
);
|
|
5101
|
+
react.useEffect(() => {
|
|
5102
|
+
if (!client) {
|
|
5103
|
+
setLocations([]);
|
|
5104
|
+
setCurrentLocationState(null);
|
|
5105
|
+
setIsLoading(false);
|
|
5106
|
+
return;
|
|
5107
|
+
}
|
|
5108
|
+
const activeClient = client;
|
|
5109
|
+
let cancelled = false;
|
|
5110
|
+
async function loadLocations() {
|
|
5111
|
+
setIsLoading(true);
|
|
5112
|
+
const result = await activeClient.business.getLocations();
|
|
5113
|
+
if (cancelled) {
|
|
5114
|
+
return;
|
|
5115
|
+
}
|
|
5116
|
+
if (!result.ok) {
|
|
5117
|
+
setLocations([]);
|
|
5118
|
+
setCurrentLocationState(null);
|
|
5119
|
+
setIsLoading(false);
|
|
5120
|
+
return;
|
|
5121
|
+
}
|
|
5122
|
+
const nextLocations = result.value;
|
|
5123
|
+
const preferredId = activeClient.getLocationId() ?? readStoredLocationId();
|
|
5124
|
+
const resolved = resolveLocation(nextLocations, preferredId);
|
|
5125
|
+
setLocations(nextLocations);
|
|
5126
|
+
setCurrentLocationState(resolved);
|
|
5127
|
+
activeClient.setLocationId(resolved?.id || null);
|
|
5128
|
+
writeStoredLocationId(resolved?.id || null);
|
|
5129
|
+
setIsLoading(false);
|
|
5130
|
+
}
|
|
5131
|
+
void loadLocations();
|
|
5132
|
+
return () => {
|
|
5133
|
+
cancelled = true;
|
|
5134
|
+
};
|
|
5135
|
+
}, [client]);
|
|
5136
|
+
return {
|
|
5137
|
+
locations,
|
|
5138
|
+
currentLocation,
|
|
5139
|
+
setCurrentLocation,
|
|
5140
|
+
isLoading
|
|
5141
|
+
};
|
|
5142
|
+
}
|
|
5143
|
+
var collectionsCache = /* @__PURE__ */ new Map();
|
|
5144
|
+
var collectionsInflight = /* @__PURE__ */ new Map();
|
|
5145
|
+
function buildCollectionsCacheKey(client, locationId) {
|
|
5146
|
+
return JSON.stringify({
|
|
5147
|
+
key: client.getPublicKey(),
|
|
5148
|
+
location_id: locationId || "__none__"
|
|
5149
|
+
});
|
|
5150
|
+
}
|
|
5151
|
+
function useCollections(options = {}) {
|
|
5152
|
+
const context = useOptionalCimplify();
|
|
5153
|
+
const client = options.client ?? context?.client;
|
|
5154
|
+
if (!client) {
|
|
5155
|
+
throw new Error("useCollections must be used within CimplifyProvider or passed { client }.");
|
|
5156
|
+
}
|
|
5157
|
+
const enabled = options.enabled ?? true;
|
|
5158
|
+
const locationId = client.getLocationId();
|
|
5159
|
+
const previousLocationIdRef = react.useRef(locationId);
|
|
5160
|
+
const requestIdRef = react.useRef(0);
|
|
5161
|
+
const cacheKey = react.useMemo(
|
|
5162
|
+
() => buildCollectionsCacheKey(client, locationId),
|
|
5163
|
+
[client, locationId]
|
|
5164
|
+
);
|
|
5165
|
+
const cached = collectionsCache.get(cacheKey);
|
|
5166
|
+
const [collections, setCollections] = react.useState(cached?.collections ?? []);
|
|
5167
|
+
const [isLoading, setIsLoading] = react.useState(enabled && !cached);
|
|
5168
|
+
const [error, setError] = react.useState(null);
|
|
5169
|
+
react.useEffect(() => {
|
|
5170
|
+
if (previousLocationIdRef.current !== locationId) {
|
|
5171
|
+
collectionsCache.clear();
|
|
5172
|
+
collectionsInflight.clear();
|
|
5173
|
+
previousLocationIdRef.current = locationId;
|
|
5174
|
+
}
|
|
5175
|
+
}, [locationId]);
|
|
5176
|
+
const load = react.useCallback(
|
|
5177
|
+
async (force = false) => {
|
|
5178
|
+
if (!enabled) {
|
|
5179
|
+
setIsLoading(false);
|
|
5180
|
+
return;
|
|
5181
|
+
}
|
|
5182
|
+
const nextRequestId = ++requestIdRef.current;
|
|
5183
|
+
setError(null);
|
|
5184
|
+
if (!force) {
|
|
5185
|
+
const cacheEntry = collectionsCache.get(cacheKey);
|
|
5186
|
+
if (cacheEntry) {
|
|
5187
|
+
setCollections(cacheEntry.collections);
|
|
5188
|
+
setIsLoading(false);
|
|
5189
|
+
return;
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
setIsLoading(true);
|
|
5193
|
+
try {
|
|
5194
|
+
const existing = collectionsInflight.get(cacheKey);
|
|
5195
|
+
const promise = existing ?? (async () => {
|
|
5196
|
+
const result = await client.catalogue.getCollections();
|
|
5197
|
+
if (!result.ok) {
|
|
5198
|
+
throw result.error;
|
|
5199
|
+
}
|
|
5200
|
+
return result.value;
|
|
5201
|
+
})();
|
|
5202
|
+
if (!existing) {
|
|
5203
|
+
collectionsInflight.set(cacheKey, promise);
|
|
5204
|
+
promise.finally(() => {
|
|
5205
|
+
collectionsInflight.delete(cacheKey);
|
|
5206
|
+
}).catch(() => void 0);
|
|
5207
|
+
}
|
|
5208
|
+
const value = await promise;
|
|
5209
|
+
collectionsCache.set(cacheKey, { collections: value });
|
|
5210
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5211
|
+
setCollections(value);
|
|
5212
|
+
setError(null);
|
|
5213
|
+
}
|
|
5214
|
+
} catch (loadError) {
|
|
5215
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5216
|
+
setError(loadError);
|
|
5217
|
+
}
|
|
5218
|
+
} finally {
|
|
5219
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5220
|
+
setIsLoading(false);
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
},
|
|
5224
|
+
[cacheKey, client, enabled]
|
|
5225
|
+
);
|
|
5226
|
+
react.useEffect(() => {
|
|
5227
|
+
void load(false);
|
|
5228
|
+
}, [load]);
|
|
5229
|
+
const refetch = react.useCallback(async () => {
|
|
5230
|
+
collectionsCache.delete(cacheKey);
|
|
5231
|
+
await load(true);
|
|
5232
|
+
}, [cacheKey, load]);
|
|
5233
|
+
return { collections, isLoading, error, refetch };
|
|
5234
|
+
}
|
|
5235
|
+
var collectionCache = /* @__PURE__ */ new Map();
|
|
5236
|
+
var collectionInflight = /* @__PURE__ */ new Map();
|
|
5237
|
+
function isLikelySlug2(value) {
|
|
5238
|
+
return /^[a-z0-9-]+$/.test(value);
|
|
5239
|
+
}
|
|
5240
|
+
function buildCollectionCacheKey(client, locationId, idOrSlug) {
|
|
5241
|
+
return JSON.stringify({
|
|
5242
|
+
key: client.getPublicKey(),
|
|
5243
|
+
location_id: locationId || "__none__",
|
|
5244
|
+
collection: idOrSlug
|
|
5245
|
+
});
|
|
5246
|
+
}
|
|
5247
|
+
function useCollection(idOrSlug, options = {}) {
|
|
5248
|
+
const context = useOptionalCimplify();
|
|
5249
|
+
const client = options.client ?? context?.client;
|
|
5250
|
+
if (!client) {
|
|
5251
|
+
throw new Error("useCollection must be used within CimplifyProvider or passed { client }.");
|
|
5252
|
+
}
|
|
5253
|
+
const enabled = options.enabled ?? true;
|
|
5254
|
+
const locationId = client.getLocationId();
|
|
5255
|
+
const previousLocationIdRef = react.useRef(locationId);
|
|
5256
|
+
const requestIdRef = react.useRef(0);
|
|
5257
|
+
const normalizedIdOrSlug = react.useMemo(() => (idOrSlug || "").trim(), [idOrSlug]);
|
|
5258
|
+
const cacheKey = react.useMemo(
|
|
5259
|
+
() => buildCollectionCacheKey(client, locationId, normalizedIdOrSlug),
|
|
5260
|
+
[client, locationId, normalizedIdOrSlug]
|
|
5261
|
+
);
|
|
5262
|
+
const cached = collectionCache.get(cacheKey);
|
|
5263
|
+
const [collection, setCollection] = react.useState(cached?.collection ?? null);
|
|
5264
|
+
const [products, setProducts] = react.useState(cached?.products ?? []);
|
|
5265
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
5266
|
+
enabled && normalizedIdOrSlug.length > 0 && !cached
|
|
5267
|
+
);
|
|
5268
|
+
const [error, setError] = react.useState(null);
|
|
5269
|
+
react.useEffect(() => {
|
|
5270
|
+
if (previousLocationIdRef.current !== locationId) {
|
|
5271
|
+
collectionCache.clear();
|
|
5272
|
+
collectionInflight.clear();
|
|
5273
|
+
previousLocationIdRef.current = locationId;
|
|
5274
|
+
}
|
|
5275
|
+
}, [locationId]);
|
|
5276
|
+
const load = react.useCallback(
|
|
5277
|
+
async (force = false) => {
|
|
5278
|
+
if (!enabled || normalizedIdOrSlug.length === 0) {
|
|
5279
|
+
setCollection(null);
|
|
5280
|
+
setProducts([]);
|
|
5281
|
+
setIsLoading(false);
|
|
5282
|
+
return;
|
|
5283
|
+
}
|
|
5284
|
+
const nextRequestId = ++requestIdRef.current;
|
|
5285
|
+
setError(null);
|
|
5286
|
+
if (!force) {
|
|
5287
|
+
const cacheEntry = collectionCache.get(cacheKey);
|
|
5288
|
+
if (cacheEntry) {
|
|
5289
|
+
setCollection(cacheEntry.collection);
|
|
5290
|
+
setProducts(cacheEntry.products);
|
|
5291
|
+
setIsLoading(false);
|
|
5292
|
+
return;
|
|
5293
|
+
}
|
|
5294
|
+
}
|
|
5295
|
+
setIsLoading(true);
|
|
5296
|
+
try {
|
|
5297
|
+
const existing = collectionInflight.get(cacheKey);
|
|
5298
|
+
const promise = existing ?? (async () => {
|
|
5299
|
+
const collectionResult = isLikelySlug2(normalizedIdOrSlug) ? await client.catalogue.getCollectionBySlug(normalizedIdOrSlug) : await client.catalogue.getCollection(normalizedIdOrSlug);
|
|
5300
|
+
if (!collectionResult.ok) {
|
|
5301
|
+
throw collectionResult.error;
|
|
5302
|
+
}
|
|
5303
|
+
const productsResult = await client.catalogue.getCollectionProducts(
|
|
5304
|
+
collectionResult.value.id
|
|
5305
|
+
);
|
|
5306
|
+
if (!productsResult.ok) {
|
|
5307
|
+
throw productsResult.error;
|
|
5308
|
+
}
|
|
5309
|
+
return {
|
|
5310
|
+
collection: collectionResult.value,
|
|
5311
|
+
products: productsResult.value
|
|
5312
|
+
};
|
|
5313
|
+
})();
|
|
5314
|
+
if (!existing) {
|
|
5315
|
+
collectionInflight.set(cacheKey, promise);
|
|
5316
|
+
promise.finally(() => {
|
|
5317
|
+
collectionInflight.delete(cacheKey);
|
|
5318
|
+
}).catch(() => void 0);
|
|
5319
|
+
}
|
|
5320
|
+
const value = await promise;
|
|
5321
|
+
collectionCache.set(cacheKey, value);
|
|
5322
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5323
|
+
setCollection(value.collection);
|
|
5324
|
+
setProducts(value.products);
|
|
5325
|
+
setError(null);
|
|
5326
|
+
}
|
|
5327
|
+
} catch (loadError) {
|
|
5328
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5329
|
+
setError(loadError);
|
|
5330
|
+
}
|
|
5331
|
+
} finally {
|
|
5332
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5333
|
+
setIsLoading(false);
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
},
|
|
5337
|
+
[cacheKey, client, enabled, normalizedIdOrSlug]
|
|
5338
|
+
);
|
|
5339
|
+
react.useEffect(() => {
|
|
5340
|
+
void load(false);
|
|
5341
|
+
}, [load]);
|
|
5342
|
+
const refetch = react.useCallback(async () => {
|
|
5343
|
+
collectionCache.delete(cacheKey);
|
|
5344
|
+
await load(true);
|
|
5345
|
+
}, [cacheKey, load]);
|
|
5346
|
+
return { collection, products, isLoading, error, refetch };
|
|
5347
|
+
}
|
|
5348
|
+
var bundleCache = /* @__PURE__ */ new Map();
|
|
5349
|
+
var bundleInflight = /* @__PURE__ */ new Map();
|
|
5350
|
+
function isLikelySlug3(value) {
|
|
5351
|
+
return /^[a-z0-9-]+$/.test(value);
|
|
5352
|
+
}
|
|
5353
|
+
function buildBundleCacheKey(client, locationId, idOrSlug) {
|
|
5354
|
+
return JSON.stringify({
|
|
5355
|
+
key: client.getPublicKey(),
|
|
5356
|
+
location_id: locationId || "__none__",
|
|
5357
|
+
bundle: idOrSlug
|
|
5358
|
+
});
|
|
5359
|
+
}
|
|
5360
|
+
function useBundle(idOrSlug, options = {}) {
|
|
5361
|
+
const context = useOptionalCimplify();
|
|
5362
|
+
const client = options.client ?? context?.client;
|
|
5363
|
+
if (!client) {
|
|
5364
|
+
throw new Error("useBundle must be used within CimplifyProvider or passed { client }.");
|
|
5365
|
+
}
|
|
5366
|
+
const enabled = options.enabled ?? true;
|
|
5367
|
+
const locationId = client.getLocationId();
|
|
5368
|
+
const previousLocationIdRef = react.useRef(locationId);
|
|
5369
|
+
const requestIdRef = react.useRef(0);
|
|
5370
|
+
const normalizedIdOrSlug = react.useMemo(() => (idOrSlug || "").trim(), [idOrSlug]);
|
|
5371
|
+
const cacheKey = react.useMemo(
|
|
5372
|
+
() => buildBundleCacheKey(client, locationId, normalizedIdOrSlug),
|
|
5373
|
+
[client, locationId, normalizedIdOrSlug]
|
|
5374
|
+
);
|
|
5375
|
+
const cached = bundleCache.get(cacheKey);
|
|
5376
|
+
const [bundle, setBundle] = react.useState(cached?.bundle ?? null);
|
|
5377
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
5378
|
+
enabled && normalizedIdOrSlug.length > 0 && !cached
|
|
5379
|
+
);
|
|
5380
|
+
const [error, setError] = react.useState(null);
|
|
5381
|
+
react.useEffect(() => {
|
|
5382
|
+
if (previousLocationIdRef.current !== locationId) {
|
|
5383
|
+
bundleCache.clear();
|
|
5384
|
+
bundleInflight.clear();
|
|
5385
|
+
previousLocationIdRef.current = locationId;
|
|
5386
|
+
}
|
|
5387
|
+
}, [locationId]);
|
|
5388
|
+
const load = react.useCallback(
|
|
5389
|
+
async (force = false) => {
|
|
5390
|
+
if (!enabled || normalizedIdOrSlug.length === 0) {
|
|
5391
|
+
setBundle(null);
|
|
5392
|
+
setIsLoading(false);
|
|
5393
|
+
return;
|
|
5394
|
+
}
|
|
5395
|
+
const nextRequestId = ++requestIdRef.current;
|
|
5396
|
+
setError(null);
|
|
5397
|
+
if (!force) {
|
|
5398
|
+
const cacheEntry = bundleCache.get(cacheKey);
|
|
5399
|
+
if (cacheEntry) {
|
|
5400
|
+
setBundle(cacheEntry.bundle);
|
|
5401
|
+
setIsLoading(false);
|
|
5402
|
+
return;
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
setIsLoading(true);
|
|
5406
|
+
try {
|
|
5407
|
+
const existing = bundleInflight.get(cacheKey);
|
|
5408
|
+
const promise = existing ?? (async () => {
|
|
5409
|
+
const result = isLikelySlug3(normalizedIdOrSlug) ? await client.catalogue.getBundleBySlug(normalizedIdOrSlug) : await client.catalogue.getBundle(normalizedIdOrSlug);
|
|
5410
|
+
if (!result.ok) {
|
|
5411
|
+
throw result.error;
|
|
5412
|
+
}
|
|
5413
|
+
return result.value;
|
|
5414
|
+
})();
|
|
5415
|
+
if (!existing) {
|
|
5416
|
+
bundleInflight.set(cacheKey, promise);
|
|
5417
|
+
promise.finally(() => {
|
|
5418
|
+
bundleInflight.delete(cacheKey);
|
|
5419
|
+
}).catch(() => void 0);
|
|
5420
|
+
}
|
|
5421
|
+
const value = await promise;
|
|
5422
|
+
bundleCache.set(cacheKey, { bundle: value });
|
|
5423
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5424
|
+
setBundle(value);
|
|
5425
|
+
setError(null);
|
|
5426
|
+
}
|
|
5427
|
+
} catch (loadError) {
|
|
5428
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5429
|
+
setError(loadError);
|
|
5430
|
+
}
|
|
5431
|
+
} finally {
|
|
5432
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5433
|
+
setIsLoading(false);
|
|
5434
|
+
}
|
|
5435
|
+
}
|
|
5436
|
+
},
|
|
5437
|
+
[cacheKey, client, enabled, normalizedIdOrSlug]
|
|
5438
|
+
);
|
|
5439
|
+
react.useEffect(() => {
|
|
5440
|
+
void load(false);
|
|
5441
|
+
}, [load]);
|
|
5442
|
+
const refetch = react.useCallback(async () => {
|
|
5443
|
+
bundleCache.delete(cacheKey);
|
|
5444
|
+
await load(true);
|
|
5445
|
+
}, [cacheKey, load]);
|
|
5446
|
+
return { bundle, isLoading, error, refetch };
|
|
5447
|
+
}
|
|
5448
|
+
var compositeCache = /* @__PURE__ */ new Map();
|
|
5449
|
+
var compositeInflight = /* @__PURE__ */ new Map();
|
|
5450
|
+
function shouldFetchByProductId(idOrProductId, byProductId) {
|
|
5451
|
+
if (typeof byProductId === "boolean") {
|
|
5452
|
+
return byProductId;
|
|
5453
|
+
}
|
|
5454
|
+
return idOrProductId.startsWith("prod_");
|
|
5455
|
+
}
|
|
5456
|
+
function buildCompositeCacheKey(client, locationId, idOrProductId, byProductId) {
|
|
5457
|
+
return JSON.stringify({
|
|
5458
|
+
key: client.getPublicKey(),
|
|
5459
|
+
location_id: locationId || "__none__",
|
|
5460
|
+
composite: idOrProductId,
|
|
5461
|
+
by_product_id: byProductId
|
|
5462
|
+
});
|
|
5463
|
+
}
|
|
5464
|
+
function useComposite(idOrProductId, options = {}) {
|
|
5465
|
+
const context = useOptionalCimplify();
|
|
5466
|
+
const client = options.client ?? context?.client;
|
|
5467
|
+
if (!client) {
|
|
5468
|
+
throw new Error("useComposite must be used within CimplifyProvider or passed { client }.");
|
|
5469
|
+
}
|
|
5470
|
+
const enabled = options.enabled ?? true;
|
|
5471
|
+
const locationId = client.getLocationId();
|
|
5472
|
+
const previousLocationIdRef = react.useRef(locationId);
|
|
5473
|
+
const requestIdRef = react.useRef(0);
|
|
5474
|
+
const priceRequestIdRef = react.useRef(0);
|
|
5475
|
+
const normalizedIdOrProductId = react.useMemo(
|
|
5476
|
+
() => (idOrProductId || "").trim(),
|
|
5477
|
+
[idOrProductId]
|
|
5478
|
+
);
|
|
5479
|
+
const byProductId = react.useMemo(
|
|
5480
|
+
() => shouldFetchByProductId(normalizedIdOrProductId, options.byProductId),
|
|
5481
|
+
[normalizedIdOrProductId, options.byProductId]
|
|
5482
|
+
);
|
|
5483
|
+
const cacheKey = react.useMemo(
|
|
5484
|
+
() => buildCompositeCacheKey(client, locationId, normalizedIdOrProductId, byProductId),
|
|
5485
|
+
[byProductId, client, locationId, normalizedIdOrProductId]
|
|
5486
|
+
);
|
|
5487
|
+
const cached = compositeCache.get(cacheKey);
|
|
5488
|
+
const [composite, setComposite] = react.useState(cached?.composite ?? null);
|
|
5489
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
5490
|
+
enabled && normalizedIdOrProductId.length > 0 && !cached
|
|
5491
|
+
);
|
|
5492
|
+
const [error, setError] = react.useState(null);
|
|
5493
|
+
const [priceResult, setPriceResult] = react.useState(null);
|
|
5494
|
+
const [isPriceLoading, setIsPriceLoading] = react.useState(false);
|
|
5495
|
+
react.useEffect(() => {
|
|
5496
|
+
if (previousLocationIdRef.current !== locationId) {
|
|
5497
|
+
compositeCache.clear();
|
|
5498
|
+
compositeInflight.clear();
|
|
5499
|
+
previousLocationIdRef.current = locationId;
|
|
5500
|
+
}
|
|
5501
|
+
}, [locationId]);
|
|
5502
|
+
const load = react.useCallback(
|
|
5503
|
+
async (force = false) => {
|
|
5504
|
+
if (!enabled || normalizedIdOrProductId.length === 0) {
|
|
5505
|
+
setComposite(null);
|
|
5506
|
+
setPriceResult(null);
|
|
5507
|
+
setIsLoading(false);
|
|
5508
|
+
return;
|
|
5509
|
+
}
|
|
5510
|
+
const nextRequestId = ++requestIdRef.current;
|
|
5511
|
+
setError(null);
|
|
5512
|
+
if (!force) {
|
|
5513
|
+
const cacheEntry = compositeCache.get(cacheKey);
|
|
5514
|
+
if (cacheEntry) {
|
|
5515
|
+
setComposite(cacheEntry.composite);
|
|
5516
|
+
setIsLoading(false);
|
|
5517
|
+
return;
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
setIsLoading(true);
|
|
5521
|
+
try {
|
|
5522
|
+
const existing = compositeInflight.get(cacheKey);
|
|
5523
|
+
const promise = existing ?? (async () => {
|
|
5524
|
+
const result = byProductId ? await client.catalogue.getCompositeByProductId(normalizedIdOrProductId) : await client.catalogue.getComposite(normalizedIdOrProductId);
|
|
5525
|
+
if (!result.ok) {
|
|
5526
|
+
throw result.error;
|
|
5527
|
+
}
|
|
5528
|
+
return result.value;
|
|
5529
|
+
})();
|
|
5530
|
+
if (!existing) {
|
|
5531
|
+
compositeInflight.set(cacheKey, promise);
|
|
5532
|
+
promise.finally(() => {
|
|
5533
|
+
compositeInflight.delete(cacheKey);
|
|
5534
|
+
}).catch(() => void 0);
|
|
5535
|
+
}
|
|
5536
|
+
const value = await promise;
|
|
5537
|
+
compositeCache.set(cacheKey, { composite: value });
|
|
5538
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5539
|
+
setComposite(value);
|
|
5540
|
+
setPriceResult(null);
|
|
5541
|
+
setError(null);
|
|
5542
|
+
}
|
|
5543
|
+
} catch (loadError) {
|
|
5544
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5545
|
+
setError(loadError);
|
|
5546
|
+
}
|
|
5547
|
+
} finally {
|
|
5548
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5549
|
+
setIsLoading(false);
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
},
|
|
5553
|
+
[byProductId, cacheKey, client, enabled, normalizedIdOrProductId]
|
|
5554
|
+
);
|
|
5555
|
+
react.useEffect(() => {
|
|
5556
|
+
void load(false);
|
|
5557
|
+
}, [load]);
|
|
5558
|
+
const calculatePrice = react.useCallback(
|
|
5559
|
+
async (selections, overrideLocationId) => {
|
|
5560
|
+
if (!composite) {
|
|
5561
|
+
return null;
|
|
5562
|
+
}
|
|
5563
|
+
const nextRequestId = ++priceRequestIdRef.current;
|
|
5564
|
+
setIsPriceLoading(true);
|
|
5565
|
+
try {
|
|
5566
|
+
const result = await client.catalogue.calculateCompositePrice(
|
|
5567
|
+
composite.id,
|
|
5568
|
+
selections,
|
|
5569
|
+
overrideLocationId
|
|
5570
|
+
);
|
|
5571
|
+
if (!result.ok) {
|
|
5572
|
+
throw result.error;
|
|
5573
|
+
}
|
|
5574
|
+
if (nextRequestId === priceRequestIdRef.current) {
|
|
5575
|
+
setPriceResult(result.value);
|
|
5576
|
+
setError(null);
|
|
5577
|
+
}
|
|
5578
|
+
return result.value;
|
|
5579
|
+
} catch (loadError) {
|
|
5580
|
+
if (nextRequestId === priceRequestIdRef.current) {
|
|
5581
|
+
setError(loadError);
|
|
5582
|
+
}
|
|
5583
|
+
return null;
|
|
5584
|
+
} finally {
|
|
5585
|
+
if (nextRequestId === priceRequestIdRef.current) {
|
|
5586
|
+
setIsPriceLoading(false);
|
|
5587
|
+
}
|
|
5588
|
+
}
|
|
5589
|
+
},
|
|
5590
|
+
[client, composite]
|
|
5591
|
+
);
|
|
5592
|
+
const refetch = react.useCallback(async () => {
|
|
5593
|
+
compositeCache.delete(cacheKey);
|
|
5594
|
+
await load(true);
|
|
5595
|
+
}, [cacheKey, load]);
|
|
5596
|
+
return { composite, isLoading, error, refetch, calculatePrice, priceResult, isPriceLoading };
|
|
5597
|
+
}
|
|
5598
|
+
function useSearch(options = {}) {
|
|
5599
|
+
const context = useOptionalCimplify();
|
|
5600
|
+
const client = options.client ?? context?.client;
|
|
5601
|
+
if (!client) {
|
|
5602
|
+
throw new Error("useSearch must be used within CimplifyProvider or passed { client }.");
|
|
5603
|
+
}
|
|
5604
|
+
const minLength = Math.max(0, options.minLength ?? 2);
|
|
5605
|
+
const debounceMs = Math.max(0, options.debounceMs ?? 300);
|
|
5606
|
+
const limit = Math.max(1, options.limit ?? 20);
|
|
5607
|
+
const [query, setQueryState] = react.useState("");
|
|
5608
|
+
const [results, setResults] = react.useState([]);
|
|
5609
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
5610
|
+
const [error, setError] = react.useState(null);
|
|
5611
|
+
const requestIdRef = react.useRef(0);
|
|
5612
|
+
const timerRef = react.useRef(null);
|
|
5613
|
+
react.useEffect(() => {
|
|
5614
|
+
if (timerRef.current) {
|
|
5615
|
+
clearTimeout(timerRef.current);
|
|
5616
|
+
timerRef.current = null;
|
|
5617
|
+
}
|
|
5618
|
+
const trimmedQuery = query.trim();
|
|
5619
|
+
if (trimmedQuery.length < minLength) {
|
|
5620
|
+
setResults([]);
|
|
5621
|
+
setError(null);
|
|
5622
|
+
setIsLoading(false);
|
|
5623
|
+
return;
|
|
5624
|
+
}
|
|
5625
|
+
const nextRequestId = ++requestIdRef.current;
|
|
5626
|
+
setError(null);
|
|
5627
|
+
setIsLoading(true);
|
|
5628
|
+
timerRef.current = setTimeout(() => {
|
|
5629
|
+
void (async () => {
|
|
5630
|
+
try {
|
|
5631
|
+
const result = await client.catalogue.searchProducts(trimmedQuery, {
|
|
5632
|
+
limit,
|
|
5633
|
+
category: options.category
|
|
5634
|
+
});
|
|
5635
|
+
if (!result.ok) {
|
|
5636
|
+
throw result.error;
|
|
5637
|
+
}
|
|
5638
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5639
|
+
setResults(result.value);
|
|
5640
|
+
setError(null);
|
|
5641
|
+
}
|
|
5642
|
+
} catch (loadError) {
|
|
5643
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5644
|
+
setError(loadError);
|
|
5645
|
+
}
|
|
5646
|
+
} finally {
|
|
5647
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5648
|
+
setIsLoading(false);
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
})();
|
|
5652
|
+
}, debounceMs);
|
|
5653
|
+
return () => {
|
|
5654
|
+
if (timerRef.current) {
|
|
5655
|
+
clearTimeout(timerRef.current);
|
|
5656
|
+
timerRef.current = null;
|
|
5657
|
+
}
|
|
5658
|
+
};
|
|
5659
|
+
}, [client, debounceMs, limit, minLength, options.category, query]);
|
|
5660
|
+
const setQuery = react.useCallback((nextQuery) => {
|
|
5661
|
+
setQueryState(nextQuery);
|
|
5662
|
+
}, []);
|
|
5663
|
+
const clear = react.useCallback(() => {
|
|
5664
|
+
requestIdRef.current += 1;
|
|
5665
|
+
if (timerRef.current) {
|
|
5666
|
+
clearTimeout(timerRef.current);
|
|
5667
|
+
timerRef.current = null;
|
|
5668
|
+
}
|
|
5669
|
+
setQueryState("");
|
|
5670
|
+
setResults([]);
|
|
5671
|
+
setError(null);
|
|
5672
|
+
setIsLoading(false);
|
|
5673
|
+
}, []);
|
|
5674
|
+
return { results, isLoading, error, query, setQuery, clear };
|
|
5675
|
+
}
|
|
5676
|
+
var quoteCache = /* @__PURE__ */ new Map();
|
|
5677
|
+
var quoteInflight = /* @__PURE__ */ new Map();
|
|
5678
|
+
function buildQuoteCacheKey(client, locationId, inputSignature) {
|
|
5679
|
+
return JSON.stringify({
|
|
5680
|
+
key: client.getPublicKey(),
|
|
5681
|
+
location_id: locationId || "__none__",
|
|
5682
|
+
input: inputSignature
|
|
5683
|
+
});
|
|
5684
|
+
}
|
|
5685
|
+
function isQuoteExpired(quote) {
|
|
5686
|
+
if (!quote?.expires_at) {
|
|
5687
|
+
return false;
|
|
5688
|
+
}
|
|
5689
|
+
const expiresAt = Date.parse(quote.expires_at);
|
|
5690
|
+
if (!Number.isFinite(expiresAt)) {
|
|
5691
|
+
return false;
|
|
5692
|
+
}
|
|
5693
|
+
return expiresAt <= Date.now();
|
|
5694
|
+
}
|
|
5695
|
+
function normalizeInput(input, fallbackLocationId) {
|
|
5696
|
+
const productId = input.productId.trim();
|
|
5697
|
+
const variantId = input.variantId?.trim();
|
|
5698
|
+
const locationId = input.locationId?.trim() || fallbackLocationId || void 0;
|
|
5699
|
+
return {
|
|
5700
|
+
product_id: productId,
|
|
5701
|
+
variant_id: variantId && variantId.length > 0 ? variantId : void 0,
|
|
5702
|
+
location_id: locationId && locationId.length > 0 ? locationId : void 0,
|
|
5703
|
+
quantity: input.quantity,
|
|
5704
|
+
add_on_option_ids: input.addOnOptionIds,
|
|
5705
|
+
bundle_selections: input.bundleSelections,
|
|
5706
|
+
composite_selections: input.compositeSelections
|
|
5707
|
+
};
|
|
5708
|
+
}
|
|
5709
|
+
function useQuote(input, options = {}) {
|
|
5710
|
+
const context = useOptionalCimplify();
|
|
5711
|
+
const client = options.client ?? context?.client;
|
|
5712
|
+
if (!client) {
|
|
5713
|
+
throw new Error("useQuote must be used within CimplifyProvider or passed { client }.");
|
|
5714
|
+
}
|
|
5715
|
+
const enabled = options.enabled ?? true;
|
|
5716
|
+
const autoRefresh = options.autoRefresh ?? true;
|
|
5717
|
+
const refreshBeforeExpiryMs = Math.max(0, options.refreshBeforeExpiryMs ?? 3e4);
|
|
5718
|
+
const locationId = client.getLocationId();
|
|
5719
|
+
const requestIdRef = react.useRef(0);
|
|
5720
|
+
const refreshTimerRef = react.useRef(null);
|
|
5721
|
+
const expiryTimerRef = react.useRef(null);
|
|
5722
|
+
const inputSignature = react.useMemo(() => JSON.stringify(input ?? null), [input]);
|
|
5723
|
+
const normalizedInput = react.useMemo(() => {
|
|
5724
|
+
if (!input) {
|
|
5725
|
+
return null;
|
|
5726
|
+
}
|
|
5727
|
+
const normalized = normalizeInput(input, locationId);
|
|
5728
|
+
return normalized.product_id.length > 0 ? normalized : null;
|
|
5729
|
+
}, [inputSignature, locationId]);
|
|
5730
|
+
const cacheKey = react.useMemo(
|
|
5731
|
+
() => buildQuoteCacheKey(client, locationId, inputSignature),
|
|
5732
|
+
[client, inputSignature, locationId]
|
|
5733
|
+
);
|
|
5734
|
+
const cached = quoteCache.get(cacheKey);
|
|
5735
|
+
const [quote, setQuote] = react.useState(cached?.quote ?? null);
|
|
5736
|
+
const [isLoading, setIsLoading] = react.useState(enabled && normalizedInput !== null && !cached);
|
|
5737
|
+
const [error, setError] = react.useState(null);
|
|
5738
|
+
const [isExpired, setIsExpired] = react.useState(isQuoteExpired(cached?.quote ?? null));
|
|
5739
|
+
const [messages, setMessages] = react.useState(cached?.quote?.ui_messages ?? []);
|
|
5740
|
+
const load = react.useCallback(
|
|
5741
|
+
async (force = false) => {
|
|
5742
|
+
if (!enabled || !normalizedInput) {
|
|
5743
|
+
setQuote(null);
|
|
5744
|
+
setMessages([]);
|
|
5745
|
+
setIsExpired(false);
|
|
5746
|
+
setError(null);
|
|
5747
|
+
setIsLoading(false);
|
|
5748
|
+
return;
|
|
5749
|
+
}
|
|
5750
|
+
const nextRequestId = ++requestIdRef.current;
|
|
5751
|
+
setError(null);
|
|
5752
|
+
if (!force) {
|
|
5753
|
+
const cacheEntry = quoteCache.get(cacheKey);
|
|
5754
|
+
if (cacheEntry) {
|
|
5755
|
+
setQuote(cacheEntry.quote);
|
|
5756
|
+
setMessages(cacheEntry.quote?.ui_messages ?? []);
|
|
5757
|
+
setIsExpired(isQuoteExpired(cacheEntry.quote));
|
|
5758
|
+
setIsLoading(false);
|
|
5759
|
+
return;
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
setIsLoading(true);
|
|
5763
|
+
try {
|
|
5764
|
+
const existing = quoteInflight.get(cacheKey);
|
|
5765
|
+
const promise = existing ?? (async () => {
|
|
5766
|
+
const result = await client.catalogue.fetchQuote(normalizedInput);
|
|
5767
|
+
if (!result.ok) {
|
|
5768
|
+
throw result.error;
|
|
5769
|
+
}
|
|
5770
|
+
return result.value;
|
|
5771
|
+
})();
|
|
5772
|
+
if (!existing) {
|
|
5773
|
+
quoteInflight.set(cacheKey, promise);
|
|
5774
|
+
promise.finally(() => {
|
|
5775
|
+
quoteInflight.delete(cacheKey);
|
|
5776
|
+
}).catch(() => void 0);
|
|
5777
|
+
}
|
|
5778
|
+
const value = await promise;
|
|
5779
|
+
quoteCache.set(cacheKey, { quote: value });
|
|
5780
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5781
|
+
setQuote(value);
|
|
5782
|
+
setMessages(value.ui_messages ?? []);
|
|
5783
|
+
setIsExpired(isQuoteExpired(value));
|
|
5784
|
+
setError(null);
|
|
5785
|
+
}
|
|
5786
|
+
} catch (loadError) {
|
|
5787
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5788
|
+
setError(loadError);
|
|
5789
|
+
}
|
|
5790
|
+
} finally {
|
|
5791
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5792
|
+
setIsLoading(false);
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
},
|
|
5796
|
+
[cacheKey, client, enabled, normalizedInput]
|
|
5797
|
+
);
|
|
5798
|
+
react.useEffect(() => {
|
|
5799
|
+
void load(false);
|
|
5800
|
+
}, [load]);
|
|
5801
|
+
const refresh = react.useCallback(async () => {
|
|
5802
|
+
if (!enabled || !normalizedInput) {
|
|
5803
|
+
return;
|
|
5804
|
+
}
|
|
5805
|
+
if (!quote?.quote_id) {
|
|
5806
|
+
await load(true);
|
|
5807
|
+
return;
|
|
5808
|
+
}
|
|
5809
|
+
const nextRequestId = ++requestIdRef.current;
|
|
5810
|
+
setError(null);
|
|
5811
|
+
setIsLoading(true);
|
|
5812
|
+
try {
|
|
5813
|
+
const result = await client.catalogue.refreshQuote({
|
|
5814
|
+
quote_id: quote.quote_id,
|
|
5815
|
+
...normalizedInput
|
|
5816
|
+
});
|
|
5817
|
+
if (!result.ok) {
|
|
5818
|
+
throw result.error;
|
|
5819
|
+
}
|
|
5820
|
+
const refreshed = result.value.quote;
|
|
5821
|
+
quoteCache.set(cacheKey, { quote: refreshed });
|
|
5822
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5823
|
+
setQuote(refreshed);
|
|
5824
|
+
setMessages(refreshed.ui_messages ?? []);
|
|
5825
|
+
setIsExpired(isQuoteExpired(refreshed));
|
|
5826
|
+
setError(null);
|
|
5827
|
+
}
|
|
5828
|
+
} catch (refreshError) {
|
|
5829
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5830
|
+
setError(refreshError);
|
|
5831
|
+
}
|
|
5832
|
+
} finally {
|
|
5833
|
+
if (nextRequestId === requestIdRef.current) {
|
|
5834
|
+
setIsLoading(false);
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
}, [cacheKey, client, enabled, load, normalizedInput, quote]);
|
|
5838
|
+
react.useEffect(() => {
|
|
5839
|
+
if (expiryTimerRef.current) {
|
|
5840
|
+
clearTimeout(expiryTimerRef.current);
|
|
5841
|
+
expiryTimerRef.current = null;
|
|
5842
|
+
}
|
|
5843
|
+
const expiresAt = quote?.expires_at ? Date.parse(quote.expires_at) : NaN;
|
|
5844
|
+
if (!Number.isFinite(expiresAt)) {
|
|
5845
|
+
setIsExpired(false);
|
|
5846
|
+
return;
|
|
5847
|
+
}
|
|
5848
|
+
const expired = expiresAt <= Date.now();
|
|
5849
|
+
setIsExpired(expired);
|
|
5850
|
+
if (!expired) {
|
|
5851
|
+
expiryTimerRef.current = setTimeout(() => {
|
|
5852
|
+
setIsExpired(true);
|
|
5853
|
+
}, Math.max(0, expiresAt - Date.now()));
|
|
5854
|
+
}
|
|
5855
|
+
return () => {
|
|
5856
|
+
if (expiryTimerRef.current) {
|
|
5857
|
+
clearTimeout(expiryTimerRef.current);
|
|
5858
|
+
expiryTimerRef.current = null;
|
|
5859
|
+
}
|
|
5860
|
+
};
|
|
5861
|
+
}, [quote?.expires_at, quote?.quote_id]);
|
|
5862
|
+
react.useEffect(() => {
|
|
5863
|
+
if (refreshTimerRef.current) {
|
|
5864
|
+
clearTimeout(refreshTimerRef.current);
|
|
5865
|
+
refreshTimerRef.current = null;
|
|
5866
|
+
}
|
|
5867
|
+
if (!autoRefresh || !enabled || !quote?.expires_at) {
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5870
|
+
const expiresAt = Date.parse(quote.expires_at);
|
|
5871
|
+
if (!Number.isFinite(expiresAt)) {
|
|
5872
|
+
return;
|
|
5873
|
+
}
|
|
5874
|
+
const delay = Math.max(0, expiresAt - Date.now() - refreshBeforeExpiryMs);
|
|
5875
|
+
refreshTimerRef.current = setTimeout(() => {
|
|
5876
|
+
void refresh();
|
|
5877
|
+
}, delay);
|
|
5878
|
+
return () => {
|
|
5879
|
+
if (refreshTimerRef.current) {
|
|
5880
|
+
clearTimeout(refreshTimerRef.current);
|
|
5881
|
+
refreshTimerRef.current = null;
|
|
5882
|
+
}
|
|
5883
|
+
};
|
|
5884
|
+
}, [autoRefresh, enabled, quote?.expires_at, quote?.quote_id, refresh, refreshBeforeExpiryMs]);
|
|
5885
|
+
return { quote, isLoading, error, refresh, isExpired, messages };
|
|
5886
|
+
}
|
|
3866
5887
|
var ElementsContext = react.createContext({
|
|
3867
5888
|
elements: null,
|
|
3868
5889
|
isReady: false
|
|
@@ -4032,7 +6053,20 @@ exports.CimplifyProvider = CimplifyProvider;
|
|
|
4032
6053
|
exports.ElementsProvider = ElementsProvider;
|
|
4033
6054
|
exports.PaymentElement = PaymentElement;
|
|
4034
6055
|
exports.useAds = useAds;
|
|
6056
|
+
exports.useBundle = useBundle;
|
|
6057
|
+
exports.useCart = useCart;
|
|
6058
|
+
exports.useCategories = useCategories;
|
|
4035
6059
|
exports.useCheckout = useCheckout;
|
|
4036
6060
|
exports.useCimplify = useCimplify;
|
|
6061
|
+
exports.useCollection = useCollection;
|
|
6062
|
+
exports.useCollections = useCollections;
|
|
6063
|
+
exports.useComposite = useComposite;
|
|
4037
6064
|
exports.useElements = useElements;
|
|
4038
6065
|
exports.useElementsReady = useElementsReady;
|
|
6066
|
+
exports.useLocations = useLocations;
|
|
6067
|
+
exports.useOptionalCimplify = useOptionalCimplify;
|
|
6068
|
+
exports.useOrder = useOrder;
|
|
6069
|
+
exports.useProduct = useProduct;
|
|
6070
|
+
exports.useProducts = useProducts;
|
|
6071
|
+
exports.useQuote = useQuote;
|
|
6072
|
+
exports.useSearch = useSearch;
|