@cimplify/sdk 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ads-t3FBTU8p.d.mts +20 -0
- package/dist/ads-t3FBTU8p.d.ts +20 -0
- package/dist/advanced.d.mts +25 -0
- package/dist/advanced.d.ts +25 -0
- package/dist/advanced.js +2735 -0
- package/dist/advanced.mjs +2713 -0
- package/dist/{ads-CmO7VVPP.d.mts → client-B4etj3AD.d.mts} +25 -193
- package/dist/{ads-CmO7VVPP.d.ts → client-CYVVuP5J.d.ts} +25 -193
- package/dist/index-BOYF-efj.d.ts +325 -0
- package/dist/index-DzNb32O3.d.mts +325 -0
- package/dist/index.d.mts +7 -346
- package/dist/index.d.ts +7 -346
- package/dist/index.js +249 -48
- package/dist/index.mjs +247 -49
- package/dist/payment-pjpfIKX8.d.mts +181 -0
- package/dist/payment-pjpfIKX8.d.ts +181 -0
- package/dist/react.d.mts +155 -7
- package/dist/react.d.ts +155 -7
- package/dist/react.js +4650 -44
- package/dist/react.mjs +4643 -46
- package/dist/utils.d.mts +2 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +457 -0
- package/dist/utils.mjs +433 -0
- package/package.json +11 -1
package/dist/react.js
CHANGED
|
@@ -11,6 +11,27 @@ var ELEMENT_TYPES = {
|
|
|
11
11
|
ADDRESS: "address",
|
|
12
12
|
PAYMENT: "payment"
|
|
13
13
|
};
|
|
14
|
+
var MESSAGE_TYPES = {
|
|
15
|
+
// Parent → Iframe
|
|
16
|
+
INIT: "init",
|
|
17
|
+
SET_TOKEN: "set_token",
|
|
18
|
+
GET_DATA: "get_data",
|
|
19
|
+
PROCESS_CHECKOUT: "process_checkout",
|
|
20
|
+
ABORT_CHECKOUT: "abort_checkout",
|
|
21
|
+
// Iframe → Parent
|
|
22
|
+
READY: "ready",
|
|
23
|
+
HEIGHT_CHANGE: "height_change",
|
|
24
|
+
AUTHENTICATED: "authenticated",
|
|
25
|
+
REQUIRES_OTP: "requires_otp",
|
|
26
|
+
ERROR: "error",
|
|
27
|
+
ADDRESS_CHANGED: "address_changed",
|
|
28
|
+
ADDRESS_SELECTED: "address_selected",
|
|
29
|
+
PAYMENT_METHOD_SELECTED: "payment_method_selected",
|
|
30
|
+
TOKEN_REFRESHED: "token_refreshed",
|
|
31
|
+
LOGOUT_COMPLETE: "logout_complete",
|
|
32
|
+
CHECKOUT_STATUS: "checkout_status",
|
|
33
|
+
CHECKOUT_COMPLETE: "checkout_complete"
|
|
34
|
+
};
|
|
14
35
|
var EVENT_TYPES = {
|
|
15
36
|
READY: "ready",
|
|
16
37
|
AUTHENTICATED: "authenticated",
|
|
@@ -364,6 +385,7 @@ function CimplifyCheckout({
|
|
|
364
385
|
onError,
|
|
365
386
|
onStatusChange,
|
|
366
387
|
appearance,
|
|
388
|
+
demoMode,
|
|
367
389
|
className
|
|
368
390
|
}) {
|
|
369
391
|
const resolvedOrderTypes = react.useMemo(
|
|
@@ -372,20 +394,131 @@ function CimplifyCheckout({
|
|
|
372
394
|
);
|
|
373
395
|
const [orderType, setOrderType] = react.useState(resolvedOrderTypes[0] || "pickup");
|
|
374
396
|
const [status, setStatus] = react.useState(null);
|
|
397
|
+
const [statusText, setStatusText] = react.useState("");
|
|
375
398
|
const [isSubmitting, setIsSubmitting] = react.useState(false);
|
|
399
|
+
const [isInitializing, setIsInitializing] = react.useState(false);
|
|
376
400
|
const [errorMessage, setErrorMessage] = react.useState(null);
|
|
401
|
+
const [resolvedBusinessId, setResolvedBusinessId] = react.useState(businessId ?? null);
|
|
402
|
+
const [resolvedCartId, setResolvedCartId] = react.useState(cartId ?? null);
|
|
377
403
|
const authMountRef = react.useRef(null);
|
|
378
404
|
const addressMountRef = react.useRef(null);
|
|
379
405
|
const paymentMountRef = react.useRef(null);
|
|
380
406
|
const elementsRef = react.useRef(null);
|
|
381
407
|
const activeCheckoutRef = react.useRef(null);
|
|
408
|
+
const initialAppearanceRef = react.useRef(appearance);
|
|
409
|
+
const hasWarnedInlineAppearanceRef = react.useRef(false);
|
|
410
|
+
const isMountedRef = react.useRef(true);
|
|
411
|
+
const demoRunRef = react.useRef(0);
|
|
412
|
+
const isDemoCheckout = demoMode ?? client.getPublicKey().trim().length === 0;
|
|
413
|
+
const isTestMode = client.isTestMode();
|
|
414
|
+
const emitStatus = react.useCallback(
|
|
415
|
+
(nextStatus, context = {}) => {
|
|
416
|
+
setStatus(nextStatus);
|
|
417
|
+
setStatusText(context.display_text || "");
|
|
418
|
+
onStatusChange?.(nextStatus, context);
|
|
419
|
+
},
|
|
420
|
+
[onStatusChange]
|
|
421
|
+
);
|
|
382
422
|
react.useEffect(() => {
|
|
383
423
|
if (!resolvedOrderTypes.includes(orderType)) {
|
|
384
424
|
setOrderType(resolvedOrderTypes[0] || "pickup");
|
|
385
425
|
}
|
|
386
426
|
}, [resolvedOrderTypes, orderType]);
|
|
387
427
|
react.useEffect(() => {
|
|
388
|
-
|
|
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]);
|
|
435
|
+
react.useEffect(() => {
|
|
436
|
+
let cancelled = false;
|
|
437
|
+
async function bootstrap() {
|
|
438
|
+
if (isDemoCheckout) {
|
|
439
|
+
if (!cancelled) {
|
|
440
|
+
setResolvedBusinessId(businessId ?? null);
|
|
441
|
+
setResolvedCartId(cartId ?? "cart_demo");
|
|
442
|
+
setIsInitializing(false);
|
|
443
|
+
setErrorMessage(null);
|
|
444
|
+
}
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const needsBusinessResolve = !businessId;
|
|
448
|
+
const needsCartResolve = !cartId;
|
|
449
|
+
if (!needsBusinessResolve && !needsCartResolve) {
|
|
450
|
+
if (!cancelled) {
|
|
451
|
+
setResolvedBusinessId(businessId || null);
|
|
452
|
+
setResolvedCartId(cartId || null);
|
|
453
|
+
setIsInitializing(false);
|
|
454
|
+
setErrorMessage(null);
|
|
455
|
+
}
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (!cancelled) {
|
|
459
|
+
setIsInitializing(true);
|
|
460
|
+
setErrorMessage(null);
|
|
461
|
+
}
|
|
462
|
+
let nextBusinessId = businessId ?? null;
|
|
463
|
+
if (!nextBusinessId) {
|
|
464
|
+
try {
|
|
465
|
+
nextBusinessId = await client.resolveBusinessId();
|
|
466
|
+
} catch {
|
|
467
|
+
if (!cancelled) {
|
|
468
|
+
const message = "Unable to initialize checkout business context.";
|
|
469
|
+
setResolvedBusinessId(null);
|
|
470
|
+
setResolvedCartId(null);
|
|
471
|
+
setErrorMessage(message);
|
|
472
|
+
setIsInitializing(false);
|
|
473
|
+
onError?.({ code: "BUSINESS_ID_REQUIRED", message });
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
let nextCartId = cartId ?? null;
|
|
479
|
+
if (!nextCartId) {
|
|
480
|
+
const cartResult = await client.cart.get();
|
|
481
|
+
if (!cartResult.ok || !cartResult.value?.id || cartResult.value.items.length === 0) {
|
|
482
|
+
if (!cancelled) {
|
|
483
|
+
const message = "Your cart is empty. Add items before checkout.";
|
|
484
|
+
setResolvedBusinessId(nextBusinessId);
|
|
485
|
+
setResolvedCartId(null);
|
|
486
|
+
setErrorMessage(message);
|
|
487
|
+
setIsInitializing(false);
|
|
488
|
+
onError?.({ code: "CART_EMPTY", message });
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
nextCartId = cartResult.value.id;
|
|
493
|
+
}
|
|
494
|
+
if (!cancelled) {
|
|
495
|
+
setResolvedBusinessId(nextBusinessId);
|
|
496
|
+
setResolvedCartId(nextCartId);
|
|
497
|
+
setIsInitializing(false);
|
|
498
|
+
setErrorMessage(null);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
void bootstrap();
|
|
502
|
+
return () => {
|
|
503
|
+
cancelled = true;
|
|
504
|
+
};
|
|
505
|
+
}, [businessId, cartId, client, isDemoCheckout, onError]);
|
|
506
|
+
react.useEffect(() => {
|
|
507
|
+
return () => {
|
|
508
|
+
isMountedRef.current = false;
|
|
509
|
+
demoRunRef.current += 1;
|
|
510
|
+
activeCheckoutRef.current?.abort();
|
|
511
|
+
activeCheckoutRef.current = null;
|
|
512
|
+
};
|
|
513
|
+
}, []);
|
|
514
|
+
react.useEffect(() => {
|
|
515
|
+
if (isDemoCheckout || !resolvedBusinessId) {
|
|
516
|
+
elementsRef.current = null;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const elements = client.elements(resolvedBusinessId, {
|
|
520
|
+
appearance: initialAppearanceRef.current
|
|
521
|
+
});
|
|
389
522
|
elementsRef.current = elements;
|
|
390
523
|
const auth = elements.create("auth");
|
|
391
524
|
const address = elements.create("address", { mode: "shipping" });
|
|
@@ -405,27 +538,68 @@ function CimplifyCheckout({
|
|
|
405
538
|
elements.destroy();
|
|
406
539
|
elementsRef.current = null;
|
|
407
540
|
};
|
|
408
|
-
}, [client,
|
|
409
|
-
const handleStatusChange = react.useCallback(
|
|
410
|
-
(nextStatus, context) => {
|
|
411
|
-
setStatus(nextStatus);
|
|
412
|
-
onStatusChange?.(nextStatus, context);
|
|
413
|
-
},
|
|
414
|
-
[onStatusChange]
|
|
415
|
-
);
|
|
541
|
+
}, [client, resolvedBusinessId, isDemoCheckout]);
|
|
416
542
|
const handleSubmit = react.useCallback(async () => {
|
|
417
|
-
if (
|
|
543
|
+
if (isSubmitting || isInitializing || !resolvedCartId) {
|
|
544
|
+
if (!resolvedCartId && !isInitializing) {
|
|
545
|
+
const message = "Your cart is empty. Add items before checkout.";
|
|
546
|
+
setErrorMessage(message);
|
|
547
|
+
onError?.({ code: "CART_EMPTY", message });
|
|
548
|
+
}
|
|
418
549
|
return;
|
|
419
550
|
}
|
|
420
551
|
setErrorMessage(null);
|
|
421
552
|
setIsSubmitting(true);
|
|
422
|
-
|
|
553
|
+
emitStatus("preparing", { display_text: statusToLabel("preparing") });
|
|
554
|
+
if (isDemoCheckout) {
|
|
555
|
+
const runId = demoRunRef.current + 1;
|
|
556
|
+
demoRunRef.current = runId;
|
|
557
|
+
const wait = async (ms) => {
|
|
558
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
559
|
+
return isMountedRef.current && runId === demoRunRef.current;
|
|
560
|
+
};
|
|
561
|
+
try {
|
|
562
|
+
if (!await wait(400)) return;
|
|
563
|
+
emitStatus("processing", { display_text: statusToLabel("processing") });
|
|
564
|
+
if (!await wait(900)) return;
|
|
565
|
+
emitStatus("polling", { display_text: statusToLabel("polling") });
|
|
566
|
+
if (!await wait(1200)) return;
|
|
567
|
+
const result = {
|
|
568
|
+
success: true,
|
|
569
|
+
order: {
|
|
570
|
+
id: `ord_demo_${Date.now()}`,
|
|
571
|
+
order_number: `DEMO-${Math.random().toString(36).slice(2, 8).toUpperCase()}`,
|
|
572
|
+
status: "confirmed",
|
|
573
|
+
total: "0.00",
|
|
574
|
+
currency: "USD"
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
emitStatus("success", {
|
|
578
|
+
order_id: result.order?.id,
|
|
579
|
+
order_number: result.order?.order_number,
|
|
580
|
+
display_text: statusToLabel("success")
|
|
581
|
+
});
|
|
582
|
+
onComplete(result);
|
|
583
|
+
} finally {
|
|
584
|
+
if (isMountedRef.current && runId === demoRunRef.current) {
|
|
585
|
+
setIsSubmitting(false);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (!elementsRef.current) {
|
|
591
|
+
const message = "Checkout is still initializing. Please try again.";
|
|
592
|
+
setErrorMessage(message);
|
|
593
|
+
onError?.({ code: "CHECKOUT_NOT_READY", message });
|
|
594
|
+
setIsSubmitting(false);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
423
597
|
const checkout = elementsRef.current.processCheckout({
|
|
424
|
-
cart_id:
|
|
425
|
-
location_id: locationId,
|
|
598
|
+
cart_id: resolvedCartId,
|
|
599
|
+
location_id: locationId ?? client.getLocationId() ?? void 0,
|
|
426
600
|
order_type: orderType,
|
|
427
601
|
enroll_in_link: enrollInLink,
|
|
428
|
-
on_status_change:
|
|
602
|
+
on_status_change: emitStatus
|
|
429
603
|
});
|
|
430
604
|
activeCheckoutRef.current = checkout;
|
|
431
605
|
try {
|
|
@@ -439,21 +613,45 @@ function CimplifyCheckout({
|
|
|
439
613
|
setErrorMessage(message);
|
|
440
614
|
onError?.({ code, message });
|
|
441
615
|
} finally {
|
|
442
|
-
|
|
443
|
-
|
|
616
|
+
if (isMountedRef.current) {
|
|
617
|
+
activeCheckoutRef.current = null;
|
|
618
|
+
setIsSubmitting(false);
|
|
619
|
+
}
|
|
444
620
|
}
|
|
445
621
|
}, [
|
|
446
|
-
|
|
622
|
+
resolvedCartId,
|
|
623
|
+
client,
|
|
447
624
|
enrollInLink,
|
|
448
|
-
|
|
625
|
+
emitStatus,
|
|
626
|
+
isDemoCheckout,
|
|
627
|
+
isInitializing,
|
|
449
628
|
isSubmitting,
|
|
450
629
|
locationId,
|
|
451
630
|
onComplete,
|
|
452
631
|
onError,
|
|
453
632
|
orderType
|
|
454
633
|
]);
|
|
634
|
+
if (isInitializing) {
|
|
635
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, "data-cimplify-checkout": "", children: /* @__PURE__ */ jsxRuntime.jsx("p", { "data-cimplify-status": "", style: { fontSize: "14px", color: "#52525b" }, children: "Preparing checkout..." }) });
|
|
636
|
+
}
|
|
637
|
+
if (!isDemoCheckout && (!resolvedBusinessId || !resolvedCartId)) {
|
|
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." }) });
|
|
639
|
+
}
|
|
455
640
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, "data-cimplify-checkout": "", children: [
|
|
456
|
-
|
|
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
|
+
),
|
|
654
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-cimplify-section": "auth", children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: isDemoCheckout ? void 0 : authMountRef }) }),
|
|
457
655
|
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-cimplify-section": "order-type", style: { marginTop: "12px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
458
656
|
"div",
|
|
459
657
|
{
|
|
@@ -487,10 +685,10 @@ function CimplifyCheckout({
|
|
|
487
685
|
{
|
|
488
686
|
"data-cimplify-section": "address",
|
|
489
687
|
style: { marginTop: "12px", display: orderType === "delivery" ? "block" : "none" },
|
|
490
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: addressMountRef })
|
|
688
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: isDemoCheckout ? void 0 : addressMountRef })
|
|
491
689
|
}
|
|
492
690
|
),
|
|
493
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-cimplify-section": "payment", style: { marginTop: "12px" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: paymentMountRef }) }),
|
|
691
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "data-cimplify-section": "payment", style: { marginTop: "12px" }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: isDemoCheckout ? void 0 : paymentMountRef }) }),
|
|
494
692
|
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: "12px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
495
693
|
"button",
|
|
496
694
|
{
|
|
@@ -509,34 +707,4433 @@ function CimplifyCheckout({
|
|
|
509
707
|
children: isSubmitting ? "Processing..." : "Complete Order"
|
|
510
708
|
}
|
|
511
709
|
) }),
|
|
512
|
-
status && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-cimplify-status": "", style: { marginTop: "10px", fontSize: "14px", color: "#52525b" }, children: statusToLabel(status) }),
|
|
710
|
+
status && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-cimplify-status": "", style: { marginTop: "10px", fontSize: "14px", color: "#52525b" }, children: statusText || statusToLabel(status) }),
|
|
513
711
|
errorMessage && /* @__PURE__ */ jsxRuntime.jsx("p", { "data-cimplify-error": "", style: { marginTop: "8px", fontSize: "14px", color: "#b91c1c" }, children: errorMessage })
|
|
514
712
|
] });
|
|
515
713
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
714
|
+
|
|
715
|
+
// src/types/common.ts
|
|
716
|
+
var ErrorCode = {
|
|
717
|
+
// General
|
|
718
|
+
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
719
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
720
|
+
TIMEOUT: "TIMEOUT",
|
|
721
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
722
|
+
NOT_FOUND: "NOT_FOUND"};
|
|
723
|
+
var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
|
|
724
|
+
function docsUrlForCode(code) {
|
|
725
|
+
return `${DOCS_ERROR_BASE_URL}#${code.toLowerCase().replace(/_/g, "-")}`;
|
|
522
726
|
}
|
|
523
|
-
|
|
524
|
-
|
|
727
|
+
var ERROR_SUGGESTIONS = {
|
|
728
|
+
UNKNOWN_ERROR: "An unexpected error occurred. Capture the request/response payload and retry with exponential backoff.",
|
|
729
|
+
NETWORK_ERROR: "Check the shopper's connection and retry. If this persists, inspect CORS, DNS, and API reachability.",
|
|
730
|
+
TIMEOUT: "The request exceeded the timeout. Retry once, then poll order status before charging again.",
|
|
731
|
+
UNAUTHORIZED: "Authentication is missing or expired. Ensure a valid access token is set and refresh the session if needed.",
|
|
732
|
+
FORBIDDEN: "The key/session lacks permission for this resource. Verify business ownership and API key scope.",
|
|
733
|
+
NOT_FOUND: "The requested resource does not exist or is not visible in this environment.",
|
|
734
|
+
VALIDATION_ERROR: "One or more fields are invalid. Validate required fields and enum values before retrying.",
|
|
735
|
+
CART_EMPTY: "The cart has no items. Redirect back to menu/catalogue and require at least one line item.",
|
|
736
|
+
CART_EXPIRED: "This cart is no longer active. Recreate a new cart and re-add shopper selections.",
|
|
737
|
+
CART_NOT_FOUND: "Cart could not be located. It may have expired or belongs to a different key/location.",
|
|
738
|
+
ITEM_UNAVAILABLE: "The selected item is unavailable at this location/time. Prompt the shopper to pick an alternative.",
|
|
739
|
+
VARIANT_NOT_FOUND: "The requested variant no longer exists. Refresh product data and require re-selection.",
|
|
740
|
+
VARIANT_OUT_OF_STOCK: "The selected variant is out of stock. Show in-stock variants and block checkout for this line.",
|
|
741
|
+
ADDON_REQUIRED: "A required add-on is missing. Ensure required modifier groups are completed before add-to-cart.",
|
|
742
|
+
ADDON_MAX_EXCEEDED: "Too many add-ons were selected. Enforce max selections client-side before submission.",
|
|
743
|
+
CHECKOUT_VALIDATION_FAILED: "Checkout payload failed validation. Verify customer, order type, and address fields are complete.",
|
|
744
|
+
DELIVERY_ADDRESS_REQUIRED: "Delivery orders require an address. Collect and pass address info before processing checkout.",
|
|
745
|
+
CUSTOMER_INFO_REQUIRED: "Customer details are required. Ensure name/email/phone are available before checkout.",
|
|
746
|
+
PAYMENT_FAILED: "Payment provider rejected or failed processing. Show retry/change-method options to the shopper.",
|
|
747
|
+
PAYMENT_CANCELLED: "Payment was cancelled by the shopper or provider flow. Allow a safe retry path.",
|
|
748
|
+
INSUFFICIENT_FUNDS: "Payment method has insufficient funds. Prompt shopper to use another method.",
|
|
749
|
+
CARD_DECLINED: "Card was declined. Ask shopper to retry or switch payment method.",
|
|
750
|
+
INVALID_OTP: "Authorization code is invalid. Let shopper re-enter OTP/PIN and retry.",
|
|
751
|
+
OTP_EXPIRED: "Authorization code expired. Request a new OTP and re-submit authorization.",
|
|
752
|
+
AUTHORIZATION_FAILED: "Additional payment authorization failed. Retry authorization or change payment method.",
|
|
753
|
+
PAYMENT_ACTION_NOT_COMPLETED: "Required payment action was not completed. Resume provider flow and poll for status.",
|
|
754
|
+
SLOT_UNAVAILABLE: "Selected schedule slot is unavailable. Refresh available slots and ask shopper to reselect.",
|
|
755
|
+
BOOKING_CONFLICT: "The requested booking conflicts with an existing reservation. Pick another slot/resource.",
|
|
756
|
+
SERVICE_NOT_FOUND: "Requested service no longer exists. Refresh service catalogue and retry selection.",
|
|
757
|
+
OUT_OF_STOCK: "Inventory is depleted for this item. Remove it or reduce quantity before checkout.",
|
|
758
|
+
INSUFFICIENT_QUANTITY: "Requested quantity exceeds available inventory. Reduce quantity and retry.",
|
|
759
|
+
BUSINESS_ID_REQUIRED: "Business context could not be resolved. Verify the public key and business bootstrap call.",
|
|
760
|
+
INVALID_CART: "Cart is invalid for checkout. Sync cart state, ensure items exist, then retry.",
|
|
761
|
+
ORDER_TYPE_REQUIRED: "Order type is required. Provide one of delivery, pickup, or dine_in before checkout.",
|
|
762
|
+
NO_PAYMENT_ELEMENT: "PaymentElement is required for processCheckout(). Mount it before triggering checkout.",
|
|
763
|
+
PAYMENT_NOT_MOUNTED: "PaymentElement iframe is not mounted. Mount it in the DOM before processCheckout().",
|
|
764
|
+
AUTH_INCOMPLETE: "AuthElement has not completed authentication. Wait for AUTHENTICATED before checkout.",
|
|
765
|
+
AUTH_LOST: "Session was cleared during checkout. Re-authenticate and restart checkout safely.",
|
|
766
|
+
ALREADY_PROCESSING: "Checkout is already in progress. Disable duplicate submits until completion.",
|
|
767
|
+
CHECKOUT_NOT_READY: "Checkout elements are still initializing. Wait for readiness before submit.",
|
|
768
|
+
CANCELLED: "Checkout was cancelled. Preserve cart state and allow shopper to retry.",
|
|
769
|
+
REQUEST_TIMEOUT: "Provider call timed out. Poll payment/order status before issuing another charge attempt.",
|
|
770
|
+
POPUP_BLOCKED: "Browser blocked provider popup. Ask shopper to enable popups and retry.",
|
|
771
|
+
FX_QUOTE_FAILED: "Failed to lock FX quote. Retry currency quote or fallback to base currency."
|
|
772
|
+
};
|
|
773
|
+
var ERROR_HINTS = Object.fromEntries(
|
|
774
|
+
Object.entries(ERROR_SUGGESTIONS).map(([code, suggestion]) => [
|
|
775
|
+
code,
|
|
776
|
+
{
|
|
777
|
+
docs_url: docsUrlForCode(code),
|
|
778
|
+
suggestion
|
|
779
|
+
}
|
|
780
|
+
])
|
|
781
|
+
);
|
|
782
|
+
var CimplifyError = class extends Error {
|
|
783
|
+
constructor(code, message, retryable = false, docs_url, suggestion) {
|
|
784
|
+
super(message);
|
|
785
|
+
this.code = code;
|
|
786
|
+
this.retryable = retryable;
|
|
787
|
+
this.docs_url = docs_url;
|
|
788
|
+
this.suggestion = suggestion;
|
|
789
|
+
this.name = "CimplifyError";
|
|
790
|
+
}
|
|
791
|
+
/** User-friendly message safe to display */
|
|
792
|
+
get userMessage() {
|
|
793
|
+
return this.message;
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
function getErrorHint(code) {
|
|
797
|
+
return ERROR_HINTS[code];
|
|
525
798
|
}
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
799
|
+
function enrichError(error, options = {}) {
|
|
800
|
+
const hint = getErrorHint(error.code);
|
|
801
|
+
if (hint) {
|
|
802
|
+
if (!error.docs_url) {
|
|
803
|
+
error.docs_url = hint.docs_url;
|
|
804
|
+
}
|
|
805
|
+
if (!error.suggestion) {
|
|
806
|
+
error.suggestion = hint.suggestion;
|
|
807
|
+
}
|
|
808
|
+
} else if (!error.docs_url) {
|
|
809
|
+
error.docs_url = docsUrlForCode(error.code || ErrorCode.UNKNOWN_ERROR);
|
|
810
|
+
}
|
|
811
|
+
if (options.isTestMode && !error.message.includes("pk_test_")) {
|
|
812
|
+
error.message = `${error.message}
|
|
813
|
+
|
|
814
|
+
\u2139 Your API key is a test-mode key (pk_test_...). Verify test data/session before retrying.`;
|
|
815
|
+
}
|
|
816
|
+
return error;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// src/types/result.ts
|
|
820
|
+
function ok(value) {
|
|
821
|
+
return { ok: true, value };
|
|
822
|
+
}
|
|
823
|
+
function err(error) {
|
|
824
|
+
return { ok: false, error };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/catalogue.ts
|
|
828
|
+
function toCimplifyError(error) {
|
|
829
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
830
|
+
if (error instanceof Error) {
|
|
831
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
832
|
+
}
|
|
833
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
834
|
+
}
|
|
835
|
+
async function safe(promise) {
|
|
836
|
+
try {
|
|
837
|
+
return ok(await promise);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
return err(toCimplifyError(error));
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function isRecord(value) {
|
|
843
|
+
return typeof value === "object" && value !== null;
|
|
844
|
+
}
|
|
845
|
+
function readFinalPrice(value) {
|
|
846
|
+
if (!isRecord(value)) return void 0;
|
|
847
|
+
const finalPrice = value.final_price;
|
|
848
|
+
if (typeof finalPrice === "string" || typeof finalPrice === "number") {
|
|
849
|
+
return finalPrice;
|
|
850
|
+
}
|
|
851
|
+
return void 0;
|
|
852
|
+
}
|
|
853
|
+
function normalizeCatalogueProductPayload(product) {
|
|
854
|
+
const normalized = { ...product };
|
|
855
|
+
const defaultPrice = normalized["default_price"];
|
|
856
|
+
if (defaultPrice === void 0 || defaultPrice === null || defaultPrice === "") {
|
|
857
|
+
const derivedDefaultPrice = readFinalPrice(normalized["default_price_info"]) || readFinalPrice(normalized["price_info"]) || (typeof normalized["final_price"] === "string" || typeof normalized["final_price"] === "number" ? normalized["final_price"] : void 0);
|
|
858
|
+
normalized["default_price"] = derivedDefaultPrice ?? "0";
|
|
859
|
+
}
|
|
860
|
+
const variants = normalized["variants"];
|
|
861
|
+
if (Array.isArray(variants)) {
|
|
862
|
+
normalized["variants"] = variants.map((variant) => {
|
|
863
|
+
if (!isRecord(variant)) return variant;
|
|
864
|
+
const normalizedVariant = { ...variant };
|
|
865
|
+
const variantAdjustment = normalizedVariant["price_adjustment"];
|
|
866
|
+
if (variantAdjustment === void 0 || variantAdjustment === null || variantAdjustment === "") {
|
|
867
|
+
normalizedVariant["price_adjustment"] = readFinalPrice(normalizedVariant["price_info"]) ?? "0";
|
|
868
|
+
}
|
|
869
|
+
return normalizedVariant;
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const addOns = normalized["add_ons"];
|
|
873
|
+
if (Array.isArray(addOns)) {
|
|
874
|
+
normalized["add_ons"] = addOns.map((addOn) => {
|
|
875
|
+
if (!isRecord(addOn)) return addOn;
|
|
876
|
+
const normalizedAddOn = { ...addOn };
|
|
877
|
+
const options = normalizedAddOn["options"];
|
|
878
|
+
if (!Array.isArray(options)) return normalizedAddOn;
|
|
879
|
+
normalizedAddOn["options"] = options.map((option) => {
|
|
880
|
+
if (!isRecord(option)) return option;
|
|
881
|
+
const normalizedOption = { ...option };
|
|
882
|
+
const optionPrice = normalizedOption["default_price"];
|
|
883
|
+
if (optionPrice === void 0 || optionPrice === null || optionPrice === "") {
|
|
884
|
+
normalizedOption["default_price"] = readFinalPrice(normalizedOption["default_price_info"]) || readFinalPrice(normalizedOption["price_info"]) || "0";
|
|
885
|
+
}
|
|
886
|
+
return normalizedOption;
|
|
887
|
+
});
|
|
888
|
+
return normalizedAddOn;
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
return normalized;
|
|
892
|
+
}
|
|
893
|
+
function findProductBySlug(products, slug) {
|
|
894
|
+
return products.find((product) => {
|
|
895
|
+
const value = product["slug"];
|
|
896
|
+
return typeof value === "string" && value === slug;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
var CatalogueQueries = class {
|
|
900
|
+
constructor(client) {
|
|
901
|
+
this.client = client;
|
|
902
|
+
}
|
|
903
|
+
async getProducts(options) {
|
|
904
|
+
let query = "products";
|
|
905
|
+
const filters = [];
|
|
906
|
+
if (options?.category) {
|
|
907
|
+
filters.push(`@.category_id=='${options.category}'`);
|
|
908
|
+
}
|
|
909
|
+
if (options?.featured !== void 0) {
|
|
910
|
+
filters.push(`@.featured==${options.featured}`);
|
|
911
|
+
}
|
|
912
|
+
if (options?.in_stock !== void 0) {
|
|
913
|
+
filters.push(`@.in_stock==${options.in_stock}`);
|
|
914
|
+
}
|
|
915
|
+
if (options?.search) {
|
|
916
|
+
filters.push(`@.name contains '${options.search}'`);
|
|
917
|
+
}
|
|
918
|
+
if (options?.min_price !== void 0) {
|
|
919
|
+
filters.push(`@.price>=${options.min_price}`);
|
|
920
|
+
}
|
|
921
|
+
if (options?.max_price !== void 0) {
|
|
922
|
+
filters.push(`@.price<=${options.max_price}`);
|
|
923
|
+
}
|
|
924
|
+
if (filters.length > 0) {
|
|
925
|
+
query += `[?(${filters.join(" && ")})]`;
|
|
926
|
+
}
|
|
927
|
+
if (options?.sort_by) {
|
|
928
|
+
query += `#sort(${options.sort_by},${options.sort_order || "asc"})`;
|
|
929
|
+
}
|
|
930
|
+
if (options?.limit) {
|
|
931
|
+
query += `#limit(${options.limit})`;
|
|
932
|
+
}
|
|
933
|
+
if (options?.offset) {
|
|
934
|
+
query += `#offset(${options.offset})`;
|
|
935
|
+
}
|
|
936
|
+
const result = await safe(this.client.query(query));
|
|
937
|
+
if (!result.ok) return result;
|
|
938
|
+
return ok(result.value.map((product) => normalizeCatalogueProductPayload(product)));
|
|
939
|
+
}
|
|
940
|
+
async getProduct(id) {
|
|
941
|
+
const result = await safe(this.client.query(`products.${id}`));
|
|
942
|
+
if (!result.ok) return result;
|
|
943
|
+
return ok(normalizeCatalogueProductPayload(result.value));
|
|
944
|
+
}
|
|
945
|
+
async getProductBySlug(slug) {
|
|
946
|
+
const filteredResult = await safe(
|
|
947
|
+
this.client.query(`products[?(@.slug=='${slug}')]`)
|
|
948
|
+
);
|
|
949
|
+
if (!filteredResult.ok) return filteredResult;
|
|
950
|
+
const exactMatch = findProductBySlug(filteredResult.value, slug);
|
|
951
|
+
if (exactMatch) {
|
|
952
|
+
return ok(normalizeCatalogueProductPayload(exactMatch));
|
|
953
|
+
}
|
|
954
|
+
if (filteredResult.value.length === 1) {
|
|
955
|
+
return ok(normalizeCatalogueProductPayload(filteredResult.value[0]));
|
|
956
|
+
}
|
|
957
|
+
const unfilteredResult = await safe(this.client.query("products"));
|
|
958
|
+
if (!unfilteredResult.ok) return unfilteredResult;
|
|
959
|
+
const fallbackMatch = findProductBySlug(unfilteredResult.value, slug);
|
|
960
|
+
if (!fallbackMatch) {
|
|
961
|
+
return err(new CimplifyError("NOT_FOUND", `Product not found: ${slug}`, false));
|
|
962
|
+
}
|
|
963
|
+
return ok(normalizeCatalogueProductPayload(fallbackMatch));
|
|
964
|
+
}
|
|
965
|
+
async getVariants(productId) {
|
|
966
|
+
return safe(this.client.query(`products.${productId}.variants`));
|
|
967
|
+
}
|
|
968
|
+
async getVariantAxes(productId) {
|
|
969
|
+
return safe(this.client.query(`products.${productId}.variant_axes`));
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Find a variant by axis selections (e.g., { "Size": "Large", "Color": "Red" })
|
|
973
|
+
* Returns the matching variant or null if no match found.
|
|
974
|
+
*/
|
|
975
|
+
async getVariantByAxisSelections(productId, selections) {
|
|
976
|
+
return safe(
|
|
977
|
+
this.client.query(`products.${productId}.variant`, {
|
|
978
|
+
axis_selections: selections
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Get a specific variant by its ID
|
|
984
|
+
*/
|
|
985
|
+
async getVariantById(productId, variantId) {
|
|
986
|
+
return safe(this.client.query(`products.${productId}.variant.${variantId}`));
|
|
987
|
+
}
|
|
988
|
+
async getAddOns(productId) {
|
|
989
|
+
return safe(this.client.query(`products.${productId}.add_ons`));
|
|
990
|
+
}
|
|
991
|
+
async getCategories() {
|
|
992
|
+
return safe(this.client.query("categories"));
|
|
993
|
+
}
|
|
994
|
+
async getCategory(id) {
|
|
995
|
+
return safe(this.client.query(`categories.${id}`));
|
|
996
|
+
}
|
|
997
|
+
async getCategoryBySlug(slug) {
|
|
998
|
+
const result = await safe(
|
|
999
|
+
this.client.query(`categories[?(@.slug=='${slug}')]`)
|
|
1000
|
+
);
|
|
1001
|
+
if (!result.ok) return result;
|
|
1002
|
+
if (!result.value.length) {
|
|
1003
|
+
return err(new CimplifyError("NOT_FOUND", `Category not found: ${slug}`, false));
|
|
1004
|
+
}
|
|
1005
|
+
return ok(result.value[0]);
|
|
1006
|
+
}
|
|
1007
|
+
async getCategoryProducts(categoryId) {
|
|
1008
|
+
return safe(this.client.query(`products[?(@.category_id=='${categoryId}')]`));
|
|
1009
|
+
}
|
|
1010
|
+
async getCollections() {
|
|
1011
|
+
return safe(this.client.query("collections"));
|
|
1012
|
+
}
|
|
1013
|
+
async getCollection(id) {
|
|
1014
|
+
return safe(this.client.query(`collections.${id}`));
|
|
1015
|
+
}
|
|
1016
|
+
async getCollectionBySlug(slug) {
|
|
1017
|
+
const result = await safe(
|
|
1018
|
+
this.client.query(`collections[?(@.slug=='${slug}')]`)
|
|
1019
|
+
);
|
|
1020
|
+
if (!result.ok) return result;
|
|
1021
|
+
if (!result.value.length) {
|
|
1022
|
+
return err(new CimplifyError("NOT_FOUND", `Collection not found: ${slug}`, false));
|
|
1023
|
+
}
|
|
1024
|
+
return ok(result.value[0]);
|
|
1025
|
+
}
|
|
1026
|
+
async getCollectionProducts(collectionId) {
|
|
1027
|
+
return safe(this.client.query(`collections.${collectionId}.products`));
|
|
1028
|
+
}
|
|
1029
|
+
async searchCollections(query, limit = 20) {
|
|
1030
|
+
return safe(
|
|
1031
|
+
this.client.query(`collections[?(@.name contains '${query}')]#limit(${limit})`)
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
async getBundles() {
|
|
1035
|
+
return safe(this.client.query("bundles"));
|
|
1036
|
+
}
|
|
1037
|
+
async getBundle(id) {
|
|
1038
|
+
return safe(this.client.query(`bundles.${id}`));
|
|
1039
|
+
}
|
|
1040
|
+
async getBundleBySlug(slug) {
|
|
1041
|
+
const result = await safe(
|
|
1042
|
+
this.client.query(`bundles[?(@.slug=='${slug}')]`)
|
|
1043
|
+
);
|
|
1044
|
+
if (!result.ok) return result;
|
|
1045
|
+
if (!result.value.length) {
|
|
1046
|
+
return err(new CimplifyError("NOT_FOUND", `Bundle not found: ${slug}`, false));
|
|
1047
|
+
}
|
|
1048
|
+
return ok(result.value[0]);
|
|
1049
|
+
}
|
|
1050
|
+
async searchBundles(query, limit = 20) {
|
|
1051
|
+
return safe(
|
|
1052
|
+
this.client.query(`bundles[?(@.name contains '${query}')]#limit(${limit})`)
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
async getComposites(options) {
|
|
1056
|
+
let query = "composites";
|
|
1057
|
+
if (options?.limit) {
|
|
1058
|
+
query += `#limit(${options.limit})`;
|
|
1059
|
+
}
|
|
1060
|
+
return safe(this.client.query(query));
|
|
1061
|
+
}
|
|
1062
|
+
async getComposite(id) {
|
|
1063
|
+
return safe(this.client.query(`composites.${id}`));
|
|
1064
|
+
}
|
|
1065
|
+
async getCompositeByProductId(productId) {
|
|
1066
|
+
return safe(this.client.query(`composites.by_product.${productId}`));
|
|
1067
|
+
}
|
|
1068
|
+
async calculateCompositePrice(compositeId, selections, locationId) {
|
|
1069
|
+
return safe(
|
|
1070
|
+
this.client.call("composite.calculatePrice", {
|
|
1071
|
+
composite_id: compositeId,
|
|
1072
|
+
selections,
|
|
1073
|
+
location_id: locationId
|
|
1074
|
+
})
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
async fetchQuote(input) {
|
|
1078
|
+
return safe(this.client.call("catalogue.createQuote", input));
|
|
1079
|
+
}
|
|
1080
|
+
async getQuote(quoteId) {
|
|
1081
|
+
return safe(
|
|
1082
|
+
this.client.call("catalogue.getQuote", {
|
|
1083
|
+
quote_id: quoteId
|
|
1084
|
+
})
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
async refreshQuote(input) {
|
|
1088
|
+
return safe(this.client.call("catalogue.refreshQuote", input));
|
|
1089
|
+
}
|
|
1090
|
+
async search(query, options) {
|
|
1091
|
+
const limit = options?.limit ?? 20;
|
|
1092
|
+
let searchQuery = `products[?(@.name contains '${query}')]`;
|
|
1093
|
+
if (options?.category) {
|
|
1094
|
+
searchQuery = `products[?(@.name contains '${query}' && @.category_id=='${options.category}')]`;
|
|
1095
|
+
}
|
|
1096
|
+
searchQuery += `#limit(${limit})`;
|
|
1097
|
+
return safe(this.client.query(searchQuery));
|
|
1098
|
+
}
|
|
1099
|
+
async searchProducts(query, options) {
|
|
1100
|
+
return safe(
|
|
1101
|
+
this.client.call("catalogue.search", {
|
|
1102
|
+
query,
|
|
1103
|
+
limit: options?.limit ?? 20,
|
|
1104
|
+
category: options?.category
|
|
1105
|
+
})
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
async getMenu(options) {
|
|
1109
|
+
let query = "menu";
|
|
1110
|
+
if (options?.category) {
|
|
1111
|
+
query = `menu[?(@.category=='${options.category}')]`;
|
|
1112
|
+
}
|
|
1113
|
+
if (options?.limit) {
|
|
1114
|
+
query += `#limit(${options.limit})`;
|
|
1115
|
+
}
|
|
1116
|
+
return safe(this.client.query(query));
|
|
1117
|
+
}
|
|
1118
|
+
async getMenuCategory(categoryId) {
|
|
1119
|
+
return safe(this.client.query(`menu.category.${categoryId}`));
|
|
1120
|
+
}
|
|
1121
|
+
async getMenuItem(itemId) {
|
|
1122
|
+
return safe(this.client.query(`menu.${itemId}`));
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
// src/cart.ts
|
|
1127
|
+
function toCimplifyError2(error) {
|
|
1128
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
1129
|
+
if (error instanceof Error) {
|
|
1130
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
1131
|
+
}
|
|
1132
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
1133
|
+
}
|
|
1134
|
+
async function safe2(promise) {
|
|
1135
|
+
try {
|
|
1136
|
+
return ok(await promise);
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
return err(toCimplifyError2(error));
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function isUICartResponse(value) {
|
|
1142
|
+
return "cart" in value;
|
|
1143
|
+
}
|
|
1144
|
+
function unwrapEnrichedCart(value) {
|
|
1145
|
+
return isUICartResponse(value) ? value.cart : value;
|
|
1146
|
+
}
|
|
1147
|
+
var CartOperations = class {
|
|
1148
|
+
constructor(client) {
|
|
1149
|
+
this.client = client;
|
|
1150
|
+
}
|
|
1151
|
+
async get() {
|
|
1152
|
+
const result = await safe2(this.client.query("cart#enriched"));
|
|
1153
|
+
if (!result.ok) return result;
|
|
1154
|
+
return ok(unwrapEnrichedCart(result.value));
|
|
1155
|
+
}
|
|
1156
|
+
async getRaw() {
|
|
1157
|
+
return safe2(this.client.query("cart"));
|
|
1158
|
+
}
|
|
1159
|
+
async getItems() {
|
|
1160
|
+
return safe2(this.client.query("cart_items"));
|
|
1161
|
+
}
|
|
1162
|
+
async getCount() {
|
|
1163
|
+
return safe2(this.client.query("cart#count"));
|
|
1164
|
+
}
|
|
1165
|
+
async getTotal() {
|
|
1166
|
+
return safe2(this.client.query("cart#total"));
|
|
1167
|
+
}
|
|
1168
|
+
async getSummary() {
|
|
1169
|
+
const cartResult = await this.get();
|
|
1170
|
+
if (!cartResult.ok) return cartResult;
|
|
1171
|
+
const cart = cartResult.value;
|
|
1172
|
+
return ok({
|
|
1173
|
+
item_count: cart.items.length,
|
|
1174
|
+
total_items: cart.items.reduce((sum, item) => sum + item.quantity, 0),
|
|
1175
|
+
subtotal: cart.pricing.subtotal,
|
|
1176
|
+
discount_amount: cart.pricing.total_discounts,
|
|
1177
|
+
tax_amount: cart.pricing.tax_amount,
|
|
1178
|
+
total: cart.pricing.total_price,
|
|
1179
|
+
currency: cart.pricing.currency
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
async addItem(input) {
|
|
1183
|
+
return safe2(this.client.call("cart.addItem", input));
|
|
1184
|
+
}
|
|
1185
|
+
async updateItem(cartItemId, updates) {
|
|
1186
|
+
return safe2(
|
|
1187
|
+
this.client.call("cart.updateItem", {
|
|
1188
|
+
cart_item_id: cartItemId,
|
|
1189
|
+
...updates
|
|
1190
|
+
})
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
async updateQuantity(cartItemId, quantity) {
|
|
1194
|
+
return safe2(
|
|
1195
|
+
this.client.call("cart.updateItemQuantity", {
|
|
1196
|
+
cart_item_id: cartItemId,
|
|
1197
|
+
quantity
|
|
1198
|
+
})
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
async removeItem(cartItemId) {
|
|
1202
|
+
return safe2(
|
|
1203
|
+
this.client.call("cart.removeItem", {
|
|
1204
|
+
cart_item_id: cartItemId
|
|
1205
|
+
})
|
|
1206
|
+
);
|
|
1207
|
+
}
|
|
1208
|
+
async clear() {
|
|
1209
|
+
return safe2(this.client.call("cart.clearCart"));
|
|
1210
|
+
}
|
|
1211
|
+
async applyCoupon(code) {
|
|
1212
|
+
return safe2(
|
|
1213
|
+
this.client.call("cart.applyCoupon", {
|
|
1214
|
+
coupon_code: code
|
|
1215
|
+
})
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
async removeCoupon() {
|
|
1219
|
+
return safe2(this.client.call("cart.removeCoupon"));
|
|
1220
|
+
}
|
|
1221
|
+
async isEmpty() {
|
|
1222
|
+
const countResult = await this.getCount();
|
|
1223
|
+
if (!countResult.ok) return countResult;
|
|
1224
|
+
return ok(countResult.value === 0);
|
|
1225
|
+
}
|
|
1226
|
+
async hasItem(productId, variantId) {
|
|
1227
|
+
const itemsResult = await this.getItems();
|
|
1228
|
+
if (!itemsResult.ok) return itemsResult;
|
|
1229
|
+
const found = itemsResult.value.some((item) => {
|
|
1230
|
+
const matchesProduct = item.item_id === productId;
|
|
1231
|
+
if (!variantId) return matchesProduct;
|
|
1232
|
+
const config = item.configuration;
|
|
1233
|
+
if ("variant" in config && config.variant) {
|
|
1234
|
+
return matchesProduct && config.variant.variant_id === variantId;
|
|
1235
|
+
}
|
|
1236
|
+
return matchesProduct;
|
|
1237
|
+
});
|
|
1238
|
+
return ok(found);
|
|
1239
|
+
}
|
|
1240
|
+
async findItem(productId, variantId) {
|
|
1241
|
+
const itemsResult = await this.getItems();
|
|
1242
|
+
if (!itemsResult.ok) return itemsResult;
|
|
1243
|
+
const found = itemsResult.value.find((item) => {
|
|
1244
|
+
const matchesProduct = item.item_id === productId;
|
|
1245
|
+
if (!variantId) return matchesProduct;
|
|
1246
|
+
const config = item.configuration;
|
|
1247
|
+
if ("variant" in config && config.variant) {
|
|
1248
|
+
return matchesProduct && config.variant.variant_id === variantId;
|
|
1249
|
+
}
|
|
1250
|
+
return matchesProduct;
|
|
1251
|
+
});
|
|
1252
|
+
return ok(found);
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
// src/constants.ts
|
|
1257
|
+
var LINK_QUERY = {
|
|
1258
|
+
DATA: "link.data",
|
|
1259
|
+
ADDRESSES: "link.addresses",
|
|
1260
|
+
MOBILE_MONEY: "link.mobile_money",
|
|
1261
|
+
PREFERENCES: "link.preferences"};
|
|
1262
|
+
var LINK_MUTATION = {
|
|
1263
|
+
CHECK_STATUS: "link.check_status",
|
|
1264
|
+
ENROLL: "link.enroll",
|
|
1265
|
+
ENROLL_AND_LINK_ORDER: "link.enroll_and_link_order",
|
|
1266
|
+
UPDATE_PREFERENCES: "link.update_preferences",
|
|
1267
|
+
CREATE_ADDRESS: "link.create_address",
|
|
1268
|
+
UPDATE_ADDRESS: "link.update_address",
|
|
1269
|
+
DELETE_ADDRESS: "link.delete_address",
|
|
1270
|
+
SET_DEFAULT_ADDRESS: "link.set_default_address",
|
|
1271
|
+
TRACK_ADDRESS_USAGE: "link.track_address_usage",
|
|
1272
|
+
CREATE_MOBILE_MONEY: "link.create_mobile_money",
|
|
1273
|
+
DELETE_MOBILE_MONEY: "link.delete_mobile_money",
|
|
1274
|
+
SET_DEFAULT_MOBILE_MONEY: "link.set_default_mobile_money",
|
|
1275
|
+
TRACK_MOBILE_MONEY_USAGE: "link.track_mobile_money_usage",
|
|
1276
|
+
VERIFY_MOBILE_MONEY: "link.verify_mobile_money"};
|
|
1277
|
+
var AUTH_MUTATION = {
|
|
1278
|
+
REQUEST_OTP: "auth.request_otp",
|
|
1279
|
+
VERIFY_OTP: "auth.verify_otp"
|
|
1280
|
+
};
|
|
1281
|
+
var CHECKOUT_MUTATION = {
|
|
1282
|
+
PROCESS: "checkout.process"
|
|
1283
|
+
};
|
|
1284
|
+
var PAYMENT_MUTATION = {
|
|
1285
|
+
SUBMIT_AUTHORIZATION: "payment.submit_authorization",
|
|
1286
|
+
CHECK_STATUS: "order.poll_payment_status"
|
|
1287
|
+
};
|
|
1288
|
+
var ORDER_MUTATION = {
|
|
1289
|
+
UPDATE_CUSTOMER: "order.update_order_customer"
|
|
1290
|
+
};
|
|
1291
|
+
|
|
1292
|
+
// src/utils/price.ts
|
|
1293
|
+
function parsePrice(value) {
|
|
1294
|
+
if (value === void 0 || value === null) {
|
|
1295
|
+
return 0;
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof value === "number") {
|
|
1298
|
+
return isNaN(value) ? 0 : value;
|
|
1299
|
+
}
|
|
1300
|
+
const cleaned = value.replace(/[^\d.-]/g, "");
|
|
1301
|
+
const parsed = parseFloat(cleaned);
|
|
1302
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/utils/payment.ts
|
|
1306
|
+
var PAYMENT_SUCCESS_STATUSES = /* @__PURE__ */ new Set([
|
|
1307
|
+
"success",
|
|
1308
|
+
"succeeded",
|
|
1309
|
+
"paid",
|
|
1310
|
+
"captured",
|
|
1311
|
+
"completed",
|
|
1312
|
+
"authorized"
|
|
1313
|
+
]);
|
|
1314
|
+
var PAYMENT_FAILURE_STATUSES = /* @__PURE__ */ new Set([
|
|
1315
|
+
"failed",
|
|
1316
|
+
"declined",
|
|
1317
|
+
"cancelled",
|
|
1318
|
+
"voided",
|
|
1319
|
+
"error"
|
|
1320
|
+
]);
|
|
1321
|
+
var PAYMENT_REQUIRES_ACTION_STATUSES = /* @__PURE__ */ new Set([
|
|
1322
|
+
"requires_action",
|
|
1323
|
+
"requires_payment_method",
|
|
1324
|
+
"requires_capture"
|
|
1325
|
+
]);
|
|
1326
|
+
var PAYMENT_STATUS_ALIAS_MAP = {
|
|
1327
|
+
ok: "success",
|
|
1328
|
+
done: "success",
|
|
1329
|
+
paid: "paid",
|
|
1330
|
+
paid_in_full: "paid",
|
|
1331
|
+
paid_successfully: "paid",
|
|
1332
|
+
succeeded: "success",
|
|
1333
|
+
captured: "captured",
|
|
1334
|
+
completed: "completed",
|
|
1335
|
+
pending_confirmation: "pending_confirmation",
|
|
1336
|
+
requires_authorization: "requires_action",
|
|
1337
|
+
requires_action: "requires_action",
|
|
1338
|
+
requires_payment_method: "requires_payment_method",
|
|
1339
|
+
requires_capture: "requires_capture",
|
|
1340
|
+
partially_paid: "partially_paid",
|
|
1341
|
+
partially_refunded: "partially_refunded",
|
|
1342
|
+
card_declined: "declined",
|
|
1343
|
+
canceled: "cancelled",
|
|
1344
|
+
authorized: "authorized",
|
|
1345
|
+
cancelled: "cancelled",
|
|
1346
|
+
unresolved: "pending"
|
|
1347
|
+
};
|
|
1348
|
+
var KNOWN_PAYMENT_STATUSES = /* @__PURE__ */ new Set([
|
|
1349
|
+
"pending",
|
|
1350
|
+
"processing",
|
|
1351
|
+
"created",
|
|
1352
|
+
"pending_confirmation",
|
|
1353
|
+
"success",
|
|
1354
|
+
"succeeded",
|
|
1355
|
+
"failed",
|
|
1356
|
+
"declined",
|
|
1357
|
+
"authorized",
|
|
1358
|
+
"refunded",
|
|
1359
|
+
"partially_refunded",
|
|
1360
|
+
"partially_paid",
|
|
1361
|
+
"paid",
|
|
1362
|
+
"unpaid",
|
|
1363
|
+
"requires_action",
|
|
1364
|
+
"requires_payment_method",
|
|
1365
|
+
"requires_capture",
|
|
1366
|
+
"captured",
|
|
1367
|
+
"cancelled",
|
|
1368
|
+
"completed",
|
|
1369
|
+
"voided",
|
|
1370
|
+
"error",
|
|
1371
|
+
"unknown"
|
|
1372
|
+
]);
|
|
1373
|
+
function normalizeStatusToken(status) {
|
|
1374
|
+
return status?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
|
|
1375
|
+
}
|
|
1376
|
+
function normalizePaymentStatusValue(status) {
|
|
1377
|
+
const normalized = normalizeStatusToken(status);
|
|
1378
|
+
if (Object.prototype.hasOwnProperty.call(PAYMENT_STATUS_ALIAS_MAP, normalized)) {
|
|
1379
|
+
return PAYMENT_STATUS_ALIAS_MAP[normalized];
|
|
1380
|
+
}
|
|
1381
|
+
return KNOWN_PAYMENT_STATUSES.has(normalized) ? normalized : "unknown";
|
|
1382
|
+
}
|
|
1383
|
+
function isPaymentStatusSuccess(status) {
|
|
1384
|
+
const normalizedStatus = normalizePaymentStatusValue(status);
|
|
1385
|
+
return PAYMENT_SUCCESS_STATUSES.has(normalizedStatus);
|
|
1386
|
+
}
|
|
1387
|
+
function isPaymentStatusFailure(status) {
|
|
1388
|
+
const normalizedStatus = normalizePaymentStatusValue(status);
|
|
1389
|
+
return PAYMENT_FAILURE_STATUSES.has(normalizedStatus);
|
|
1390
|
+
}
|
|
1391
|
+
function isPaymentStatusRequiresAction(status) {
|
|
1392
|
+
const normalizedStatus = normalizePaymentStatusValue(status);
|
|
1393
|
+
return PAYMENT_REQUIRES_ACTION_STATUSES.has(normalizedStatus);
|
|
1394
|
+
}
|
|
1395
|
+
function normalizeStatusResponse(response) {
|
|
1396
|
+
if (!response || typeof response !== "object") {
|
|
1397
|
+
return {
|
|
1398
|
+
status: "pending",
|
|
1399
|
+
paid: false,
|
|
1400
|
+
message: "No status available"
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
const res = response;
|
|
1404
|
+
const normalizedStatus = normalizePaymentStatusValue(res.status ?? void 0);
|
|
1405
|
+
const paidValue = res.paid === true;
|
|
1406
|
+
const derivedPaid = paidValue || [
|
|
1407
|
+
"success",
|
|
1408
|
+
"succeeded",
|
|
1409
|
+
"paid",
|
|
1410
|
+
"captured",
|
|
1411
|
+
"authorized",
|
|
1412
|
+
"completed"
|
|
1413
|
+
].includes(normalizedStatus);
|
|
1414
|
+
return {
|
|
1415
|
+
status: normalizedStatus,
|
|
1416
|
+
paid: derivedPaid,
|
|
1417
|
+
amount: res.amount,
|
|
1418
|
+
currency: res.currency,
|
|
1419
|
+
reference: res.reference,
|
|
1420
|
+
message: res.message || ""
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/utils/paystack.ts
|
|
1425
|
+
async function openPaystackPopup(options, signal) {
|
|
1426
|
+
if (typeof window === "undefined") {
|
|
1427
|
+
return { success: false, error: "PROVIDER_UNAVAILABLE" };
|
|
1428
|
+
}
|
|
1429
|
+
let PaystackPop;
|
|
1430
|
+
try {
|
|
1431
|
+
const imported = await import('@paystack/inline-js');
|
|
1432
|
+
PaystackPop = imported.default;
|
|
1433
|
+
} catch {
|
|
1434
|
+
return { success: false, error: "PROVIDER_UNAVAILABLE" };
|
|
1435
|
+
}
|
|
1436
|
+
return new Promise((resolve) => {
|
|
1437
|
+
let settled = false;
|
|
1438
|
+
const resolveOnce = (result) => {
|
|
1439
|
+
if (settled) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
settled = true;
|
|
1443
|
+
if (signal) {
|
|
1444
|
+
signal.removeEventListener("abort", onAbort);
|
|
1445
|
+
}
|
|
1446
|
+
resolve(result);
|
|
1447
|
+
};
|
|
1448
|
+
const onAbort = () => {
|
|
1449
|
+
resolveOnce({ success: false, error: "CANCELLED" });
|
|
1450
|
+
};
|
|
1451
|
+
if (signal?.aborted) {
|
|
1452
|
+
resolveOnce({ success: false, error: "CANCELLED" });
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (signal) {
|
|
1456
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1457
|
+
}
|
|
1458
|
+
try {
|
|
1459
|
+
const popup = new PaystackPop();
|
|
1460
|
+
popup.newTransaction({
|
|
1461
|
+
key: options.key,
|
|
1462
|
+
email: options.email,
|
|
1463
|
+
amount: options.amount,
|
|
1464
|
+
currency: options.currency,
|
|
1465
|
+
reference: options.reference,
|
|
1466
|
+
accessCode: options.accessCode,
|
|
1467
|
+
onSuccess: (transaction) => {
|
|
1468
|
+
resolveOnce({
|
|
1469
|
+
success: true,
|
|
1470
|
+
reference: transaction.reference ?? options.reference
|
|
1471
|
+
});
|
|
1472
|
+
},
|
|
1473
|
+
onCancel: () => {
|
|
1474
|
+
resolveOnce({ success: false, error: "PAYMENT_CANCELLED" });
|
|
1475
|
+
},
|
|
1476
|
+
onError: (error) => {
|
|
1477
|
+
resolveOnce({
|
|
1478
|
+
success: false,
|
|
1479
|
+
error: error?.message || "PAYMENT_FAILED"
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
} catch {
|
|
1484
|
+
resolveOnce({ success: false, error: "POPUP_BLOCKED" });
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// src/utils/checkout-resolver.ts
|
|
1490
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
1491
|
+
var DEFAULT_MAX_POLL_ATTEMPTS = 60;
|
|
1492
|
+
var MAX_CONSECUTIVE_NETWORK_ERRORS = 5;
|
|
1493
|
+
var CARD_PROVIDER_PAYSTACK = "paystack";
|
|
1494
|
+
function normalizeCardPopupError(error) {
|
|
1495
|
+
if (!error || error === "PAYMENT_CANCELLED" || error === "CANCELLED") {
|
|
1496
|
+
return "PAYMENT_CANCELLED";
|
|
1497
|
+
}
|
|
1498
|
+
if (error === "PROVIDER_UNAVAILABLE") {
|
|
1499
|
+
return "PROVIDER_UNAVAILABLE";
|
|
1500
|
+
}
|
|
1501
|
+
if (error === "POPUP_BLOCKED") {
|
|
1502
|
+
return "POPUP_BLOCKED";
|
|
1503
|
+
}
|
|
1504
|
+
return "PAYMENT_FAILED";
|
|
1505
|
+
}
|
|
1506
|
+
function normalizeCardProvider(value) {
|
|
1507
|
+
const normalized = value?.trim().toLowerCase();
|
|
1508
|
+
return normalized && normalized.length > 0 ? normalized : CARD_PROVIDER_PAYSTACK;
|
|
1509
|
+
}
|
|
1510
|
+
async function openCardPopup(provider, checkoutResult, email, currency, signal) {
|
|
1511
|
+
if (typeof window === "undefined") {
|
|
1512
|
+
return { success: false, error: "PROVIDER_UNAVAILABLE" };
|
|
1513
|
+
}
|
|
1514
|
+
if (provider === CARD_PROVIDER_PAYSTACK) {
|
|
1515
|
+
return openPaystackPopup(
|
|
1516
|
+
{
|
|
1517
|
+
key: checkoutResult.public_key || "",
|
|
1518
|
+
email,
|
|
1519
|
+
accessCode: checkoutResult.client_secret,
|
|
1520
|
+
reference: checkoutResult.payment_reference || checkoutResult.client_secret,
|
|
1521
|
+
currency
|
|
1522
|
+
},
|
|
1523
|
+
signal
|
|
1524
|
+
);
|
|
1525
|
+
}
|
|
1526
|
+
return { success: false, error: "PROVIDER_UNAVAILABLE" };
|
|
1527
|
+
}
|
|
1528
|
+
function normalizeAuthorizationType(value) {
|
|
1529
|
+
return value === "otp" || value === "pin" ? value : void 0;
|
|
1530
|
+
}
|
|
1531
|
+
function formatMoney2(value) {
|
|
1532
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1533
|
+
return value.toFixed(2);
|
|
1534
|
+
}
|
|
1535
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1536
|
+
return value;
|
|
1537
|
+
}
|
|
1538
|
+
return "0.00";
|
|
1539
|
+
}
|
|
1540
|
+
function createAbortError() {
|
|
1541
|
+
const error = new Error("CANCELLED");
|
|
1542
|
+
error.name = "AbortError";
|
|
1543
|
+
return error;
|
|
1544
|
+
}
|
|
1545
|
+
function isAbortError(error) {
|
|
1546
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
1547
|
+
return true;
|
|
1548
|
+
}
|
|
1549
|
+
return error instanceof Error && (error.name === "AbortError" || error.message === "CANCELLED");
|
|
1550
|
+
}
|
|
1551
|
+
var CheckoutResolver = class {
|
|
1552
|
+
constructor(options) {
|
|
1553
|
+
this.client = options.client;
|
|
1554
|
+
this.checkoutData = options.checkoutData;
|
|
1555
|
+
this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
1556
|
+
this.maxPollAttempts = options.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS;
|
|
1557
|
+
this.onStatusChange = options.onStatusChange;
|
|
1558
|
+
this.onAuthorizationRequired = options.onAuthorizationRequired;
|
|
1559
|
+
this.signal = options.signal;
|
|
1560
|
+
this.returnUrl = options.returnUrl;
|
|
1561
|
+
this.enrollInLink = options.enrollInLink !== false;
|
|
1562
|
+
this.businessId = options.businessId;
|
|
1563
|
+
this.addressData = options.addressData;
|
|
1564
|
+
this.paymentData = options.paymentData;
|
|
1565
|
+
this.orderType = options.orderType;
|
|
1566
|
+
this.cartTotal = options.cartTotal;
|
|
1567
|
+
this.cartCurrency = options.cartCurrency;
|
|
1568
|
+
this.allowPopups = options.allowPopups ?? typeof window !== "undefined";
|
|
1569
|
+
}
|
|
1570
|
+
async resolve(checkoutResult) {
|
|
1571
|
+
try {
|
|
1572
|
+
this.ensureNotAborted();
|
|
1573
|
+
if (!checkoutResult.order_id) {
|
|
1574
|
+
return this.fail("CHECKOUT_FAILED", "Checkout did not return an order ID.", false);
|
|
1575
|
+
}
|
|
1576
|
+
let latestCheckoutResult = checkoutResult;
|
|
1577
|
+
let authorizationType = normalizeAuthorizationType(checkoutResult.authorization_type);
|
|
1578
|
+
let paymentReference = checkoutResult.payment_reference;
|
|
1579
|
+
if (this.isSuccessfulStatus(checkoutResult.payment_status)) {
|
|
1580
|
+
return this.finalizeSuccess(checkoutResult);
|
|
1581
|
+
}
|
|
1582
|
+
if (checkoutResult.client_secret && checkoutResult.public_key) {
|
|
1583
|
+
const provider = normalizeCardProvider(checkoutResult.provider);
|
|
1584
|
+
this.emit("awaiting_authorization", {
|
|
1585
|
+
display_text: "Complete payment in the card authorization popup.",
|
|
1586
|
+
order_id: checkoutResult.order_id,
|
|
1587
|
+
order_number: checkoutResult.order_number
|
|
1588
|
+
});
|
|
1589
|
+
if (!this.allowPopups) {
|
|
1590
|
+
return this.fail(
|
|
1591
|
+
"PROVIDER_UNAVAILABLE",
|
|
1592
|
+
"Card payment popup is unavailable in this environment.",
|
|
1593
|
+
false
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
const popupResult = await openCardPopup(
|
|
1597
|
+
provider,
|
|
1598
|
+
checkoutResult,
|
|
1599
|
+
this.checkoutData.customer.email || "customer@cimplify.io",
|
|
1600
|
+
this.getOrderCurrency(checkoutResult),
|
|
1601
|
+
this.signal
|
|
1602
|
+
);
|
|
1603
|
+
if (!popupResult.success) {
|
|
1604
|
+
const popupError = normalizeCardPopupError(popupResult.error);
|
|
1605
|
+
if (popupError === "POPUP_BLOCKED") {
|
|
1606
|
+
return this.fail(
|
|
1607
|
+
"POPUP_BLOCKED",
|
|
1608
|
+
"Unable to open card payment popup. Please allow popups and try again.",
|
|
1609
|
+
true
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
if (popupError === "PROVIDER_UNAVAILABLE") {
|
|
1613
|
+
return this.fail(
|
|
1614
|
+
"PROVIDER_UNAVAILABLE",
|
|
1615
|
+
"Card payment provider is unavailable.",
|
|
1616
|
+
false
|
|
1617
|
+
);
|
|
1618
|
+
}
|
|
1619
|
+
return this.fail(
|
|
1620
|
+
"PAYMENT_CANCELLED",
|
|
1621
|
+
"Card payment was cancelled before completion.",
|
|
1622
|
+
true
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
paymentReference = popupResult.reference || paymentReference;
|
|
1626
|
+
}
|
|
1627
|
+
if (checkoutResult.authorization_url) {
|
|
1628
|
+
if (typeof window !== "undefined" && this.returnUrl) {
|
|
1629
|
+
window.location.assign(checkoutResult.authorization_url);
|
|
1630
|
+
return this.fail(
|
|
1631
|
+
"REDIRECT_REQUIRED",
|
|
1632
|
+
"Redirecting to complete payment authorization.",
|
|
1633
|
+
true
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
if (typeof window !== "undefined") {
|
|
1637
|
+
const popup = window.open(
|
|
1638
|
+
checkoutResult.authorization_url,
|
|
1639
|
+
"cimplify-auth",
|
|
1640
|
+
"width=520,height=760,noopener,noreferrer"
|
|
1641
|
+
);
|
|
1642
|
+
if (!popup) {
|
|
1643
|
+
return this.fail(
|
|
1644
|
+
"POPUP_BLOCKED",
|
|
1645
|
+
"Authorization popup was blocked. Please allow popups and retry.",
|
|
1646
|
+
true
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
if (checkoutResult.requires_authorization || isPaymentStatusRequiresAction(checkoutResult.payment_status)) {
|
|
1652
|
+
const authorization = await this.handleAuthorization({
|
|
1653
|
+
authorizationType,
|
|
1654
|
+
paymentReference,
|
|
1655
|
+
displayText: checkoutResult.display_text,
|
|
1656
|
+
provider: checkoutResult.provider
|
|
1657
|
+
});
|
|
1658
|
+
if (!authorization.ok) {
|
|
1659
|
+
return authorization.result;
|
|
1660
|
+
}
|
|
1661
|
+
if (authorization.value) {
|
|
1662
|
+
latestCheckoutResult = authorization.value;
|
|
1663
|
+
paymentReference = authorization.value.payment_reference || paymentReference;
|
|
1664
|
+
authorizationType = normalizeAuthorizationType(authorization.value.authorization_type) || authorizationType;
|
|
1665
|
+
if (this.isSuccessfulStatus(authorization.value.payment_status)) {
|
|
1666
|
+
return this.finalizeSuccess(authorization.value);
|
|
1667
|
+
}
|
|
1668
|
+
if (this.isFailureStatus(authorization.value.payment_status)) {
|
|
1669
|
+
return this.fail(
|
|
1670
|
+
"AUTHORIZATION_FAILED",
|
|
1671
|
+
authorization.value.display_text || "Payment authorization failed.",
|
|
1672
|
+
false
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return this.pollUntilTerminal({
|
|
1678
|
+
orderId: checkoutResult.order_id,
|
|
1679
|
+
latestCheckoutResult,
|
|
1680
|
+
paymentReference,
|
|
1681
|
+
authorizationType
|
|
1682
|
+
});
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
if (isAbortError(error)) {
|
|
1685
|
+
return this.fail("CANCELLED", "Checkout was cancelled.", true);
|
|
1686
|
+
}
|
|
1687
|
+
const message = error instanceof Error ? error.message : "Checkout failed unexpectedly.";
|
|
1688
|
+
return this.fail("CHECKOUT_FAILED", message, true);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
async pollUntilTerminal(input) {
|
|
1692
|
+
let consecutiveErrors = 0;
|
|
1693
|
+
let latestCheckoutResult = input.latestCheckoutResult;
|
|
1694
|
+
let paymentReference = input.paymentReference;
|
|
1695
|
+
let authorizationType = input.authorizationType;
|
|
1696
|
+
for (let attempt = 0; attempt < this.maxPollAttempts; attempt++) {
|
|
1697
|
+
this.ensureNotAborted();
|
|
1698
|
+
this.emit("polling", {
|
|
1699
|
+
poll_attempt: attempt,
|
|
1700
|
+
max_poll_attempts: this.maxPollAttempts,
|
|
1701
|
+
order_id: input.orderId,
|
|
1702
|
+
order_number: latestCheckoutResult.order_number
|
|
1703
|
+
});
|
|
1704
|
+
const statusResult = await this.client.checkout.pollPaymentStatus(input.orderId);
|
|
1705
|
+
if (!statusResult.ok) {
|
|
1706
|
+
consecutiveErrors += 1;
|
|
1707
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_NETWORK_ERRORS) {
|
|
1708
|
+
return this.fail(
|
|
1709
|
+
"NETWORK_ERROR",
|
|
1710
|
+
"Unable to confirm payment due to repeated network errors.",
|
|
1711
|
+
true
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
await this.wait(this.pollIntervalMs);
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
consecutiveErrors = 0;
|
|
1718
|
+
if (statusResult.value.reference) {
|
|
1719
|
+
paymentReference = statusResult.value.reference;
|
|
1720
|
+
}
|
|
1721
|
+
const normalized = normalizeStatusResponse(statusResult.value);
|
|
1722
|
+
if (normalized.paid || isPaymentStatusSuccess(normalized.status)) {
|
|
1723
|
+
return this.finalizeSuccess(latestCheckoutResult);
|
|
1724
|
+
}
|
|
1725
|
+
if (isPaymentStatusFailure(normalized.status)) {
|
|
1726
|
+
return this.fail(
|
|
1727
|
+
normalized.status ? normalized.status.toUpperCase() : "PAYMENT_FAILED",
|
|
1728
|
+
normalized.message || "Payment failed during confirmation.",
|
|
1729
|
+
false
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
if (isPaymentStatusRequiresAction(normalized.status)) {
|
|
1733
|
+
const authorization = await this.handleAuthorization({
|
|
1734
|
+
authorizationType,
|
|
1735
|
+
paymentReference,
|
|
1736
|
+
displayText: normalized.message || latestCheckoutResult.display_text,
|
|
1737
|
+
provider: latestCheckoutResult.provider
|
|
1738
|
+
});
|
|
1739
|
+
if (!authorization.ok) {
|
|
1740
|
+
return authorization.result;
|
|
1741
|
+
}
|
|
1742
|
+
if (authorization.value) {
|
|
1743
|
+
latestCheckoutResult = authorization.value;
|
|
1744
|
+
paymentReference = authorization.value.payment_reference || paymentReference;
|
|
1745
|
+
authorizationType = normalizeAuthorizationType(authorization.value.authorization_type) || authorizationType;
|
|
1746
|
+
if (this.isSuccessfulStatus(authorization.value.payment_status)) {
|
|
1747
|
+
return this.finalizeSuccess(authorization.value);
|
|
1748
|
+
}
|
|
1749
|
+
if (this.isFailureStatus(authorization.value.payment_status)) {
|
|
1750
|
+
return this.fail(
|
|
1751
|
+
"AUTHORIZATION_FAILED",
|
|
1752
|
+
authorization.value.display_text || "Payment authorization failed.",
|
|
1753
|
+
false
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
await this.wait(this.pollIntervalMs);
|
|
1759
|
+
}
|
|
1760
|
+
return this.fail(
|
|
1761
|
+
"PAYMENT_TIMEOUT",
|
|
1762
|
+
"Payment confirmation timed out. Please retry checkout.",
|
|
1763
|
+
true
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
async handleAuthorization(input) {
|
|
1767
|
+
const authorizationType = input.authorizationType;
|
|
1768
|
+
const paymentReference = input.paymentReference;
|
|
1769
|
+
if (!authorizationType || !paymentReference) {
|
|
1770
|
+
return { ok: true };
|
|
1771
|
+
}
|
|
1772
|
+
this.emit("awaiting_authorization", {
|
|
1773
|
+
authorization_type: authorizationType,
|
|
1774
|
+
display_text: input.displayText,
|
|
1775
|
+
provider: input.provider
|
|
1776
|
+
});
|
|
1777
|
+
let authorizationResult;
|
|
1778
|
+
const submit = async (code) => {
|
|
1779
|
+
this.ensureNotAborted();
|
|
1780
|
+
const trimmedCode = code.trim();
|
|
1781
|
+
if (!trimmedCode) {
|
|
1782
|
+
throw new Error("Authorization code is required.");
|
|
1783
|
+
}
|
|
1784
|
+
const submitResult = await this.client.checkout.submitAuthorization({
|
|
1785
|
+
reference: paymentReference,
|
|
1786
|
+
auth_type: authorizationType,
|
|
1787
|
+
value: trimmedCode
|
|
1788
|
+
});
|
|
1789
|
+
if (!submitResult.ok) {
|
|
1790
|
+
throw new Error(submitResult.error.message || "Authorization failed.");
|
|
1791
|
+
}
|
|
1792
|
+
authorizationResult = submitResult.value;
|
|
1793
|
+
};
|
|
1794
|
+
try {
|
|
1795
|
+
if (this.onAuthorizationRequired) {
|
|
1796
|
+
await this.onAuthorizationRequired(authorizationType, submit);
|
|
1797
|
+
} else {
|
|
1798
|
+
return {
|
|
1799
|
+
ok: false,
|
|
1800
|
+
result: this.fail(
|
|
1801
|
+
"AUTHORIZATION_REQUIRED",
|
|
1802
|
+
"Authorization callback is required in headless checkout mode.",
|
|
1803
|
+
false
|
|
1804
|
+
)
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
} catch (error) {
|
|
1808
|
+
if (isAbortError(error)) {
|
|
1809
|
+
return {
|
|
1810
|
+
ok: false,
|
|
1811
|
+
result: this.fail("CANCELLED", "Checkout was cancelled.", true)
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
const message = error instanceof Error ? error.message : "Payment authorization failed.";
|
|
1815
|
+
return {
|
|
1816
|
+
ok: false,
|
|
1817
|
+
result: this.fail("AUTHORIZATION_FAILED", message, true)
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
return {
|
|
1821
|
+
ok: true,
|
|
1822
|
+
value: authorizationResult
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
async finalizeSuccess(checkoutResult) {
|
|
1826
|
+
this.emit("finalizing", {
|
|
1827
|
+
order_id: checkoutResult.order_id,
|
|
1828
|
+
order_number: checkoutResult.order_number
|
|
1829
|
+
});
|
|
1830
|
+
const enrolledInLink = await this.maybeEnrollInLink(checkoutResult.order_id);
|
|
1831
|
+
this.emit("success", {
|
|
1832
|
+
order_id: checkoutResult.order_id,
|
|
1833
|
+
order_number: checkoutResult.order_number
|
|
1834
|
+
});
|
|
1835
|
+
return {
|
|
1836
|
+
success: true,
|
|
1837
|
+
order: {
|
|
1838
|
+
id: checkoutResult.order_id,
|
|
1839
|
+
order_number: checkoutResult.order_number || checkoutResult.order_id,
|
|
1840
|
+
status: checkoutResult.payment_status || "paid",
|
|
1841
|
+
total: this.getOrderTotal(checkoutResult),
|
|
1842
|
+
currency: this.getOrderCurrency(checkoutResult)
|
|
1843
|
+
},
|
|
1844
|
+
enrolled_in_link: enrolledInLink
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
async maybeEnrollInLink(orderId) {
|
|
1848
|
+
if (!this.enrollInLink || !orderId) {
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
let businessId = this.businessId;
|
|
1852
|
+
if (!businessId) {
|
|
1853
|
+
const businessResult = await this.client.business.getInfo();
|
|
1854
|
+
if (!businessResult.ok || !businessResult.value?.id) {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
businessId = businessResult.value.id;
|
|
1858
|
+
}
|
|
1859
|
+
const address = this.getEnrollmentAddress();
|
|
1860
|
+
const mobileMoney = this.getEnrollmentMobileMoney();
|
|
1861
|
+
try {
|
|
1862
|
+
const enrollment = await this.client.link.enrollAndLinkOrder({
|
|
1863
|
+
order_id: orderId,
|
|
1864
|
+
business_id: businessId,
|
|
1865
|
+
order_type: this.orderType || this.checkoutData.order_type,
|
|
1866
|
+
address,
|
|
1867
|
+
mobile_money: mobileMoney
|
|
1868
|
+
});
|
|
1869
|
+
return enrollment.ok && enrollment.value.success;
|
|
1870
|
+
} catch {
|
|
1871
|
+
return false;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
getEnrollmentAddress() {
|
|
1875
|
+
const source = this.addressData ?? this.checkoutData.address_info;
|
|
1876
|
+
if (!source?.street_address) {
|
|
1877
|
+
return void 0;
|
|
1878
|
+
}
|
|
1879
|
+
return {
|
|
1880
|
+
label: "Default",
|
|
1881
|
+
street_address: source.street_address,
|
|
1882
|
+
apartment: source.apartment || void 0,
|
|
1883
|
+
city: source.city || "",
|
|
1884
|
+
region: source.region || "",
|
|
1885
|
+
delivery_instructions: source.delivery_instructions || void 0,
|
|
1886
|
+
phone_for_delivery: source.phone_for_delivery || void 0
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
getEnrollmentMobileMoney() {
|
|
1890
|
+
if (this.paymentData?.type === "mobile_money" && this.paymentData.phone_number && this.paymentData.provider) {
|
|
1891
|
+
return {
|
|
1892
|
+
phone_number: this.paymentData.phone_number,
|
|
1893
|
+
provider: this.paymentData.provider,
|
|
1894
|
+
label: this.paymentData.label || "Mobile Money"
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
if (this.checkoutData.mobile_money_details?.phone_number) {
|
|
1898
|
+
return {
|
|
1899
|
+
phone_number: this.checkoutData.mobile_money_details.phone_number,
|
|
1900
|
+
provider: this.checkoutData.mobile_money_details.provider,
|
|
1901
|
+
label: "Mobile Money"
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
return void 0;
|
|
1905
|
+
}
|
|
1906
|
+
getOrderTotal(checkoutResult) {
|
|
1907
|
+
if (checkoutResult.fx?.pay_amount !== void 0) {
|
|
1908
|
+
return formatMoney2(checkoutResult.fx.pay_amount);
|
|
1909
|
+
}
|
|
1910
|
+
if (this.cartTotal !== void 0) {
|
|
1911
|
+
return formatMoney2(this.cartTotal);
|
|
1912
|
+
}
|
|
1913
|
+
return "0.00";
|
|
1914
|
+
}
|
|
1915
|
+
getOrderCurrency(checkoutResult) {
|
|
1916
|
+
if (checkoutResult.fx?.pay_currency) {
|
|
1917
|
+
return checkoutResult.fx.pay_currency;
|
|
1918
|
+
}
|
|
1919
|
+
if (this.cartCurrency) {
|
|
1920
|
+
return this.cartCurrency;
|
|
1921
|
+
}
|
|
1922
|
+
if (this.checkoutData.pay_currency) {
|
|
1923
|
+
return this.checkoutData.pay_currency;
|
|
1924
|
+
}
|
|
1925
|
+
return "GHS";
|
|
1926
|
+
}
|
|
1927
|
+
emit(status, context = {}) {
|
|
1928
|
+
if (!this.onStatusChange) {
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
try {
|
|
1932
|
+
this.onStatusChange(status, context);
|
|
1933
|
+
} catch {
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
fail(code, message, recoverable) {
|
|
1937
|
+
this.emit("failed", {
|
|
1938
|
+
display_text: message
|
|
1939
|
+
});
|
|
1940
|
+
return {
|
|
1941
|
+
success: false,
|
|
1942
|
+
error: {
|
|
1943
|
+
code,
|
|
1944
|
+
message,
|
|
1945
|
+
recoverable
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
isSuccessfulStatus(status) {
|
|
1950
|
+
const normalized = normalizeStatusResponse({ status, paid: false });
|
|
1951
|
+
return normalized.paid || isPaymentStatusSuccess(normalized.status);
|
|
1952
|
+
}
|
|
1953
|
+
isFailureStatus(status) {
|
|
1954
|
+
const normalized = normalizeStatusResponse({ status, paid: false });
|
|
1955
|
+
return isPaymentStatusFailure(normalized.status);
|
|
1956
|
+
}
|
|
1957
|
+
ensureNotAborted() {
|
|
1958
|
+
if (this.signal?.aborted) {
|
|
1959
|
+
throw createAbortError();
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
wait(ms) {
|
|
1963
|
+
this.ensureNotAborted();
|
|
1964
|
+
return new Promise((resolve, reject) => {
|
|
1965
|
+
const timer = setTimeout(() => {
|
|
1966
|
+
if (this.signal) {
|
|
1967
|
+
this.signal.removeEventListener("abort", onAbort);
|
|
1968
|
+
}
|
|
1969
|
+
resolve();
|
|
1970
|
+
}, ms);
|
|
1971
|
+
const onAbort = () => {
|
|
1972
|
+
clearTimeout(timer);
|
|
1973
|
+
reject(createAbortError());
|
|
1974
|
+
};
|
|
1975
|
+
if (this.signal) {
|
|
1976
|
+
this.signal.addEventListener("abort", onAbort, { once: true });
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
|
|
1982
|
+
// src/checkout.ts
|
|
1983
|
+
function toCimplifyError3(error) {
|
|
1984
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
1985
|
+
if (error instanceof Error) {
|
|
1986
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
1987
|
+
}
|
|
1988
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
1989
|
+
}
|
|
1990
|
+
async function safe3(promise) {
|
|
1991
|
+
try {
|
|
1992
|
+
return ok(await promise);
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
return err(toCimplifyError3(error));
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function toTerminalFailure(code, message, recoverable) {
|
|
1998
|
+
return {
|
|
1999
|
+
success: false,
|
|
2000
|
+
error: {
|
|
2001
|
+
code,
|
|
2002
|
+
message,
|
|
2003
|
+
recoverable
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
function generateIdempotencyKey() {
|
|
2008
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
2009
|
+
return `idem_${crypto.randomUUID()}`;
|
|
2010
|
+
}
|
|
2011
|
+
const timestamp = Date.now().toString(36);
|
|
2012
|
+
const random = Math.random().toString(36).substring(2, 15);
|
|
2013
|
+
return `idem_${timestamp}_${random}`;
|
|
2014
|
+
}
|
|
2015
|
+
var CheckoutService = class {
|
|
2016
|
+
constructor(client) {
|
|
2017
|
+
this.client = client;
|
|
2018
|
+
}
|
|
2019
|
+
async process(data) {
|
|
2020
|
+
const checkoutData = {
|
|
2021
|
+
...data,
|
|
2022
|
+
idempotency_key: data.idempotency_key || generateIdempotencyKey()
|
|
2023
|
+
};
|
|
2024
|
+
return safe3(
|
|
2025
|
+
this.client.call(CHECKOUT_MUTATION.PROCESS, {
|
|
2026
|
+
checkout_data: checkoutData
|
|
2027
|
+
})
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
async initializePayment(orderId, method) {
|
|
2031
|
+
return safe3(
|
|
2032
|
+
this.client.call("order.initializePayment", {
|
|
2033
|
+
order_id: orderId,
|
|
2034
|
+
payment_method: method
|
|
2035
|
+
})
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
async submitAuthorization(input) {
|
|
2039
|
+
return safe3(
|
|
2040
|
+
this.client.call(PAYMENT_MUTATION.SUBMIT_AUTHORIZATION, input)
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
async pollPaymentStatus(orderId) {
|
|
2044
|
+
return safe3(
|
|
2045
|
+
this.client.call(PAYMENT_MUTATION.CHECK_STATUS, orderId)
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
async updateOrderCustomer(orderId, customer) {
|
|
2049
|
+
return safe3(
|
|
2050
|
+
this.client.call(ORDER_MUTATION.UPDATE_CUSTOMER, {
|
|
2051
|
+
order_id: orderId,
|
|
2052
|
+
...customer
|
|
2053
|
+
})
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
async verifyPayment(orderId) {
|
|
2057
|
+
return safe3(
|
|
2058
|
+
this.client.call("order.verifyPayment", {
|
|
2059
|
+
order_id: orderId
|
|
2060
|
+
})
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
async processAndResolve(data) {
|
|
2064
|
+
data.on_status_change?.("preparing", {});
|
|
2065
|
+
if (!data.cart_id) {
|
|
2066
|
+
return ok(
|
|
2067
|
+
toTerminalFailure(
|
|
2068
|
+
"INVALID_CART",
|
|
2069
|
+
"A valid cart is required before checkout can start.",
|
|
2070
|
+
false
|
|
2071
|
+
)
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
const cartResult = await this.client.cart.get();
|
|
2075
|
+
if (!cartResult.ok || !cartResult.value?.id) {
|
|
2076
|
+
return ok(
|
|
2077
|
+
toTerminalFailure(
|
|
2078
|
+
"INVALID_CART",
|
|
2079
|
+
"Unable to load cart for checkout.",
|
|
2080
|
+
false
|
|
2081
|
+
)
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
const cart = cartResult.value;
|
|
2085
|
+
if (cart.id !== data.cart_id || cart.items.length === 0) {
|
|
2086
|
+
return ok(
|
|
2087
|
+
toTerminalFailure(
|
|
2088
|
+
"INVALID_CART",
|
|
2089
|
+
"Cart is empty or no longer valid. Please refresh and try again.",
|
|
2090
|
+
false
|
|
2091
|
+
)
|
|
2092
|
+
);
|
|
2093
|
+
}
|
|
2094
|
+
const checkoutData = {
|
|
2095
|
+
cart_id: data.cart_id,
|
|
2096
|
+
location_id: data.location_id,
|
|
2097
|
+
customer: data.customer,
|
|
2098
|
+
order_type: data.order_type,
|
|
2099
|
+
address_info: data.address_info,
|
|
2100
|
+
payment_method: data.payment_method,
|
|
2101
|
+
mobile_money_details: data.mobile_money_details,
|
|
2102
|
+
special_instructions: data.special_instructions,
|
|
2103
|
+
link_address_id: data.link_address_id,
|
|
2104
|
+
link_payment_method_id: data.link_payment_method_id,
|
|
2105
|
+
idempotency_key: data.idempotency_key || generateIdempotencyKey(),
|
|
2106
|
+
metadata: data.metadata,
|
|
2107
|
+
pay_currency: data.pay_currency,
|
|
2108
|
+
fx_quote_id: data.fx_quote_id
|
|
2109
|
+
};
|
|
2110
|
+
const baseCurrency = (cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase();
|
|
2111
|
+
const payCurrency = data.pay_currency?.trim().toUpperCase();
|
|
2112
|
+
const cartTotalAmount = Number.parseFloat(cart.pricing.total_price || "0");
|
|
2113
|
+
if (payCurrency && payCurrency !== baseCurrency && !checkoutData.fx_quote_id && Number.isFinite(cartTotalAmount) && cartTotalAmount > 0) {
|
|
2114
|
+
const fxQuoteResult = await this.client.fx.lockQuote({
|
|
2115
|
+
from: baseCurrency,
|
|
2116
|
+
to: payCurrency,
|
|
2117
|
+
amount: cartTotalAmount
|
|
2118
|
+
});
|
|
2119
|
+
if (!fxQuoteResult.ok) {
|
|
2120
|
+
return ok(
|
|
2121
|
+
toTerminalFailure(
|
|
2122
|
+
"FX_QUOTE_FAILED",
|
|
2123
|
+
fxQuoteResult.error.message || "Unable to lock FX quote for checkout.",
|
|
2124
|
+
true
|
|
2125
|
+
)
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
checkoutData.pay_currency = payCurrency;
|
|
2129
|
+
checkoutData.fx_quote_id = fxQuoteResult.value.id;
|
|
2130
|
+
}
|
|
2131
|
+
data.on_status_change?.("processing", {});
|
|
2132
|
+
const processResult = await this.process(checkoutData);
|
|
2133
|
+
if (!processResult.ok) {
|
|
2134
|
+
return ok(
|
|
2135
|
+
toTerminalFailure(
|
|
2136
|
+
processResult.error.code || "CHECKOUT_FAILED",
|
|
2137
|
+
processResult.error.message || "Unable to process checkout.",
|
|
2138
|
+
processResult.error.retryable ?? true
|
|
2139
|
+
)
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
const resolver = new CheckoutResolver({
|
|
2143
|
+
client: this.client,
|
|
2144
|
+
checkoutData,
|
|
2145
|
+
pollIntervalMs: data.poll_interval_ms,
|
|
2146
|
+
maxPollAttempts: data.max_poll_attempts,
|
|
2147
|
+
onStatusChange: data.on_status_change,
|
|
2148
|
+
onAuthorizationRequired: data.on_authorization_required,
|
|
2149
|
+
signal: data.signal,
|
|
2150
|
+
returnUrl: data.return_url,
|
|
2151
|
+
enrollInLink: data.enroll_in_link,
|
|
2152
|
+
cartTotal: cart.pricing.total_price,
|
|
2153
|
+
cartCurrency: checkoutData.pay_currency || cart.pricing.currency || "GHS",
|
|
2154
|
+
orderType: checkoutData.order_type,
|
|
2155
|
+
allowPopups: data.allow_popups
|
|
2156
|
+
});
|
|
2157
|
+
const resolved = await resolver.resolve(processResult.value);
|
|
2158
|
+
return ok(resolved);
|
|
2159
|
+
}
|
|
2160
|
+
};
|
|
2161
|
+
|
|
2162
|
+
// src/orders.ts
|
|
2163
|
+
function toCimplifyError4(error) {
|
|
2164
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
2165
|
+
if (error instanceof Error) {
|
|
2166
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
2167
|
+
}
|
|
2168
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
2169
|
+
}
|
|
2170
|
+
async function safe4(promise) {
|
|
2171
|
+
try {
|
|
2172
|
+
return ok(await promise);
|
|
2173
|
+
} catch (error) {
|
|
2174
|
+
return err(toCimplifyError4(error));
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
var OrderQueries = class {
|
|
2178
|
+
constructor(client) {
|
|
2179
|
+
this.client = client;
|
|
2180
|
+
}
|
|
2181
|
+
async list(options) {
|
|
2182
|
+
let query = "orders";
|
|
2183
|
+
if (options?.status) {
|
|
2184
|
+
query += `[?(@.status=='${options.status}')]`;
|
|
2185
|
+
}
|
|
2186
|
+
query += "#sort(created_at,desc)";
|
|
2187
|
+
if (options?.limit) {
|
|
2188
|
+
query += `#limit(${options.limit})`;
|
|
2189
|
+
}
|
|
2190
|
+
if (options?.offset) {
|
|
2191
|
+
query += `#offset(${options.offset})`;
|
|
2192
|
+
}
|
|
2193
|
+
return safe4(this.client.query(query));
|
|
2194
|
+
}
|
|
2195
|
+
async get(orderId) {
|
|
2196
|
+
return safe4(this.client.query(`orders.${orderId}`));
|
|
2197
|
+
}
|
|
2198
|
+
async getRecent(limit = 5) {
|
|
2199
|
+
return safe4(this.client.query(`orders#sort(created_at,desc)#limit(${limit})`));
|
|
2200
|
+
}
|
|
2201
|
+
async getByStatus(status) {
|
|
2202
|
+
return safe4(this.client.query(`orders[?(@.status=='${status}')]`));
|
|
2203
|
+
}
|
|
2204
|
+
async cancel(orderId, reason) {
|
|
2205
|
+
return safe4(
|
|
2206
|
+
this.client.call("order.cancelOrder", {
|
|
2207
|
+
order_id: orderId,
|
|
2208
|
+
reason
|
|
2209
|
+
})
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
// src/link.ts
|
|
2215
|
+
function toCimplifyError5(error) {
|
|
2216
|
+
if (error instanceof CimplifyError) return error;
|
|
2217
|
+
if (error instanceof Error) {
|
|
2218
|
+
return new CimplifyError("UNKNOWN_ERROR", error.message, false);
|
|
2219
|
+
}
|
|
2220
|
+
return new CimplifyError("UNKNOWN_ERROR", String(error), false);
|
|
2221
|
+
}
|
|
2222
|
+
async function safe5(promise) {
|
|
2223
|
+
try {
|
|
2224
|
+
return ok(await promise);
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
return err(toCimplifyError5(error));
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
var LinkService = class {
|
|
2230
|
+
constructor(client) {
|
|
2231
|
+
this.client = client;
|
|
2232
|
+
}
|
|
2233
|
+
async requestOtp(input) {
|
|
2234
|
+
return safe5(this.client.linkPost("/v1/link/auth/request-otp", input));
|
|
2235
|
+
}
|
|
2236
|
+
async verifyOtp(input) {
|
|
2237
|
+
const result = await safe5(
|
|
2238
|
+
this.client.linkPost("/v1/link/auth/verify-otp", input)
|
|
2239
|
+
);
|
|
2240
|
+
if (result.ok && result.value.session_token) {
|
|
2241
|
+
this.client.setSessionToken(result.value.session_token);
|
|
2242
|
+
}
|
|
2243
|
+
return result;
|
|
2244
|
+
}
|
|
2245
|
+
async logout() {
|
|
2246
|
+
const result = await safe5(this.client.linkPost("/v1/link/auth/logout"));
|
|
2247
|
+
if (result.ok) {
|
|
2248
|
+
this.client.clearSession();
|
|
2249
|
+
}
|
|
2250
|
+
return result;
|
|
2251
|
+
}
|
|
2252
|
+
async checkStatus(contact) {
|
|
2253
|
+
return safe5(
|
|
2254
|
+
this.client.call(LINK_MUTATION.CHECK_STATUS, {
|
|
2255
|
+
contact
|
|
2256
|
+
})
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
async getLinkData() {
|
|
2260
|
+
return safe5(this.client.query(LINK_QUERY.DATA));
|
|
2261
|
+
}
|
|
2262
|
+
async getAddresses() {
|
|
2263
|
+
return safe5(this.client.query(LINK_QUERY.ADDRESSES));
|
|
2264
|
+
}
|
|
2265
|
+
async getMobileMoney() {
|
|
2266
|
+
return safe5(this.client.query(LINK_QUERY.MOBILE_MONEY));
|
|
2267
|
+
}
|
|
2268
|
+
async getPreferences() {
|
|
2269
|
+
return safe5(this.client.query(LINK_QUERY.PREFERENCES));
|
|
2270
|
+
}
|
|
2271
|
+
async enroll(data) {
|
|
2272
|
+
return safe5(this.client.call(LINK_MUTATION.ENROLL, data));
|
|
2273
|
+
}
|
|
2274
|
+
async enrollAndLinkOrder(data) {
|
|
2275
|
+
return safe5(
|
|
2276
|
+
this.client.call(LINK_MUTATION.ENROLL_AND_LINK_ORDER, data)
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
async updatePreferences(preferences) {
|
|
2280
|
+
return safe5(this.client.call(LINK_MUTATION.UPDATE_PREFERENCES, preferences));
|
|
2281
|
+
}
|
|
2282
|
+
async createAddress(input) {
|
|
2283
|
+
return safe5(this.client.call(LINK_MUTATION.CREATE_ADDRESS, input));
|
|
2284
|
+
}
|
|
2285
|
+
async updateAddress(input) {
|
|
2286
|
+
return safe5(this.client.call(LINK_MUTATION.UPDATE_ADDRESS, input));
|
|
2287
|
+
}
|
|
2288
|
+
async deleteAddress(addressId) {
|
|
2289
|
+
return safe5(this.client.call(LINK_MUTATION.DELETE_ADDRESS, addressId));
|
|
2290
|
+
}
|
|
2291
|
+
async setDefaultAddress(addressId) {
|
|
2292
|
+
return safe5(this.client.call(LINK_MUTATION.SET_DEFAULT_ADDRESS, addressId));
|
|
2293
|
+
}
|
|
2294
|
+
async trackAddressUsage(addressId) {
|
|
2295
|
+
return safe5(
|
|
2296
|
+
this.client.call(LINK_MUTATION.TRACK_ADDRESS_USAGE, {
|
|
2297
|
+
address_id: addressId
|
|
2298
|
+
})
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
async createMobileMoney(input) {
|
|
2302
|
+
return safe5(this.client.call(LINK_MUTATION.CREATE_MOBILE_MONEY, input));
|
|
2303
|
+
}
|
|
2304
|
+
async deleteMobileMoney(mobileMoneyId) {
|
|
2305
|
+
return safe5(this.client.call(LINK_MUTATION.DELETE_MOBILE_MONEY, mobileMoneyId));
|
|
2306
|
+
}
|
|
2307
|
+
async setDefaultMobileMoney(mobileMoneyId) {
|
|
2308
|
+
return safe5(
|
|
2309
|
+
this.client.call(LINK_MUTATION.SET_DEFAULT_MOBILE_MONEY, mobileMoneyId)
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
async trackMobileMoneyUsage(mobileMoneyId) {
|
|
2313
|
+
return safe5(
|
|
2314
|
+
this.client.call(LINK_MUTATION.TRACK_MOBILE_MONEY_USAGE, {
|
|
2315
|
+
mobile_money_id: mobileMoneyId
|
|
2316
|
+
})
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
async verifyMobileMoney(mobileMoneyId) {
|
|
2320
|
+
return safe5(
|
|
2321
|
+
this.client.call(LINK_MUTATION.VERIFY_MOBILE_MONEY, mobileMoneyId)
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
async getSessions() {
|
|
2325
|
+
return safe5(this.client.linkGet("/v1/link/sessions"));
|
|
2326
|
+
}
|
|
2327
|
+
async revokeSession(sessionId) {
|
|
2328
|
+
return safe5(this.client.linkDelete(`/v1/link/sessions/${sessionId}`));
|
|
2329
|
+
}
|
|
2330
|
+
async revokeAllSessions() {
|
|
2331
|
+
return safe5(this.client.linkDelete("/v1/link/sessions"));
|
|
2332
|
+
}
|
|
2333
|
+
async getAddressesRest() {
|
|
2334
|
+
return safe5(this.client.linkGet("/v1/link/addresses"));
|
|
2335
|
+
}
|
|
2336
|
+
async createAddressRest(input) {
|
|
2337
|
+
return safe5(this.client.linkPost("/v1/link/addresses", input));
|
|
2338
|
+
}
|
|
2339
|
+
async deleteAddressRest(addressId) {
|
|
2340
|
+
return safe5(this.client.linkDelete(`/v1/link/addresses/${addressId}`));
|
|
2341
|
+
}
|
|
2342
|
+
async setDefaultAddressRest(addressId) {
|
|
2343
|
+
return safe5(this.client.linkPost(`/v1/link/addresses/${addressId}/default`));
|
|
2344
|
+
}
|
|
2345
|
+
async getMobileMoneyRest() {
|
|
2346
|
+
return safe5(this.client.linkGet("/v1/link/mobile-money"));
|
|
2347
|
+
}
|
|
2348
|
+
async createMobileMoneyRest(input) {
|
|
2349
|
+
return safe5(this.client.linkPost("/v1/link/mobile-money", input));
|
|
2350
|
+
}
|
|
2351
|
+
async deleteMobileMoneyRest(mobileMoneyId) {
|
|
2352
|
+
return safe5(this.client.linkDelete(`/v1/link/mobile-money/${mobileMoneyId}`));
|
|
2353
|
+
}
|
|
2354
|
+
async setDefaultMobileMoneyRest(mobileMoneyId) {
|
|
2355
|
+
return safe5(
|
|
2356
|
+
this.client.linkPost(`/v1/link/mobile-money/${mobileMoneyId}/default`)
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
|
|
2361
|
+
// src/auth.ts
|
|
2362
|
+
function toCimplifyError6(error) {
|
|
2363
|
+
if (error instanceof CimplifyError) return error;
|
|
2364
|
+
if (error instanceof Error) {
|
|
2365
|
+
return new CimplifyError("UNKNOWN_ERROR", error.message, false);
|
|
2366
|
+
}
|
|
2367
|
+
return new CimplifyError("UNKNOWN_ERROR", String(error), false);
|
|
2368
|
+
}
|
|
2369
|
+
async function safe6(promise) {
|
|
2370
|
+
try {
|
|
2371
|
+
return ok(await promise);
|
|
2372
|
+
} catch (error) {
|
|
2373
|
+
return err(toCimplifyError6(error));
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
var AuthService = class {
|
|
2377
|
+
constructor(client) {
|
|
2378
|
+
this.client = client;
|
|
2379
|
+
}
|
|
2380
|
+
async getStatus() {
|
|
2381
|
+
return safe6(this.client.query("auth"));
|
|
2382
|
+
}
|
|
2383
|
+
async getCurrentUser() {
|
|
2384
|
+
const result = await this.getStatus();
|
|
2385
|
+
if (!result.ok) return result;
|
|
2386
|
+
return ok(result.value.customer || null);
|
|
2387
|
+
}
|
|
2388
|
+
async isAuthenticated() {
|
|
2389
|
+
const result = await this.getStatus();
|
|
2390
|
+
if (!result.ok) return result;
|
|
2391
|
+
return ok(result.value.is_authenticated);
|
|
2392
|
+
}
|
|
2393
|
+
async requestOtp(contact, contactType) {
|
|
2394
|
+
return safe6(
|
|
2395
|
+
this.client.call(AUTH_MUTATION.REQUEST_OTP, {
|
|
2396
|
+
contact,
|
|
2397
|
+
contact_type: contactType
|
|
2398
|
+
})
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
async verifyOtp(code, contact) {
|
|
2402
|
+
return safe6(
|
|
2403
|
+
this.client.call(AUTH_MUTATION.VERIFY_OTP, {
|
|
2404
|
+
otp_code: code,
|
|
2405
|
+
contact
|
|
2406
|
+
})
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
async logout() {
|
|
2410
|
+
return safe6(this.client.call("auth.logout"));
|
|
2411
|
+
}
|
|
2412
|
+
async updateProfile(input) {
|
|
2413
|
+
return safe6(this.client.call("auth.update_profile", input));
|
|
2414
|
+
}
|
|
2415
|
+
async changePassword(input) {
|
|
2416
|
+
return safe6(this.client.call("auth.change_password", input));
|
|
2417
|
+
}
|
|
2418
|
+
async resetPassword(email) {
|
|
2419
|
+
return safe6(this.client.call("auth.reset_password", { email }));
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
|
|
2423
|
+
// src/business.ts
|
|
2424
|
+
function toCimplifyError7(error) {
|
|
2425
|
+
if (error instanceof CimplifyError) return enrichError(error);
|
|
2426
|
+
if (error instanceof Error) {
|
|
2427
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
|
|
2428
|
+
}
|
|
2429
|
+
return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
|
|
2430
|
+
}
|
|
2431
|
+
async function safe7(promise) {
|
|
2432
|
+
try {
|
|
2433
|
+
return ok(await promise);
|
|
2434
|
+
} catch (error) {
|
|
2435
|
+
return err(toCimplifyError7(error));
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
var BusinessService = class {
|
|
2439
|
+
constructor(client) {
|
|
2440
|
+
this.client = client;
|
|
2441
|
+
}
|
|
2442
|
+
async getInfo() {
|
|
2443
|
+
return safe7(this.client.query("business.info"));
|
|
2444
|
+
}
|
|
2445
|
+
async getByHandle(handle) {
|
|
2446
|
+
return safe7(this.client.query(`business.handle.${handle}`));
|
|
2447
|
+
}
|
|
2448
|
+
async getByDomain(domain) {
|
|
2449
|
+
return safe7(this.client.query("business.domain", { domain }));
|
|
2450
|
+
}
|
|
2451
|
+
async getSettings() {
|
|
2452
|
+
return safe7(this.client.query("business.settings"));
|
|
2453
|
+
}
|
|
2454
|
+
async getTheme() {
|
|
2455
|
+
return safe7(this.client.query("business.theme"));
|
|
2456
|
+
}
|
|
2457
|
+
async getLocations() {
|
|
2458
|
+
return safe7(this.client.query("business.locations"));
|
|
2459
|
+
}
|
|
2460
|
+
async getLocation(locationId) {
|
|
2461
|
+
return safe7(this.client.query(`business.locations.${locationId}`));
|
|
2462
|
+
}
|
|
2463
|
+
async getHours() {
|
|
2464
|
+
return safe7(this.client.query("business.hours"));
|
|
2465
|
+
}
|
|
2466
|
+
async getLocationHours(locationId) {
|
|
2467
|
+
return safe7(this.client.query(`business.locations.${locationId}.hours`));
|
|
2468
|
+
}
|
|
2469
|
+
async getBootstrap() {
|
|
2470
|
+
const [businessResult, locationsResult, categoriesResult] = await Promise.all([
|
|
2471
|
+
this.getInfo(),
|
|
2472
|
+
this.getLocations(),
|
|
2473
|
+
safe7(this.client.query("categories#select(id,name,slug)"))
|
|
2474
|
+
]);
|
|
2475
|
+
if (!businessResult.ok) return businessResult;
|
|
2476
|
+
if (!locationsResult.ok) return locationsResult;
|
|
2477
|
+
if (!categoriesResult.ok) return categoriesResult;
|
|
2478
|
+
const business = businessResult.value;
|
|
2479
|
+
const locations = locationsResult.value;
|
|
2480
|
+
const categories = categoriesResult.value;
|
|
2481
|
+
const defaultLocation = locations[0];
|
|
2482
|
+
return ok({
|
|
2483
|
+
business,
|
|
2484
|
+
location: defaultLocation,
|
|
2485
|
+
locations,
|
|
2486
|
+
categories,
|
|
2487
|
+
currency: business.default_currency,
|
|
2488
|
+
is_open: defaultLocation?.accepts_online_orders ?? false,
|
|
2489
|
+
accepts_orders: defaultLocation?.accepts_online_orders ?? false
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2494
|
+
// src/inventory.ts
|
|
2495
|
+
function toCimplifyError8(error) {
|
|
2496
|
+
if (error instanceof CimplifyError) return error;
|
|
2497
|
+
if (error instanceof Error) {
|
|
2498
|
+
return new CimplifyError("UNKNOWN_ERROR", error.message, false);
|
|
2499
|
+
}
|
|
2500
|
+
return new CimplifyError("UNKNOWN_ERROR", String(error), false);
|
|
2501
|
+
}
|
|
2502
|
+
async function safe8(promise) {
|
|
2503
|
+
try {
|
|
2504
|
+
return ok(await promise);
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
return err(toCimplifyError8(error));
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
var InventoryService = class {
|
|
2510
|
+
constructor(client) {
|
|
2511
|
+
this.client = client;
|
|
2512
|
+
}
|
|
2513
|
+
async getStockLevels() {
|
|
2514
|
+
return safe8(this.client.query("inventory.stock_levels"));
|
|
2515
|
+
}
|
|
2516
|
+
async getProductStock(productId, locationId) {
|
|
2517
|
+
if (locationId) {
|
|
2518
|
+
return safe8(
|
|
2519
|
+
this.client.query("inventory.product", {
|
|
2520
|
+
product_id: productId,
|
|
2521
|
+
location_id: locationId
|
|
2522
|
+
})
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
return safe8(
|
|
2526
|
+
this.client.query("inventory.product", {
|
|
2527
|
+
product_id: productId
|
|
2528
|
+
})
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
async getVariantStock(variantId, locationId) {
|
|
2532
|
+
return safe8(
|
|
2533
|
+
this.client.query("inventory.variant", {
|
|
2534
|
+
variant_id: variantId,
|
|
2535
|
+
location_id: locationId
|
|
2536
|
+
})
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
async checkProductAvailability(productId, quantity, locationId) {
|
|
2540
|
+
return safe8(
|
|
2541
|
+
this.client.query("inventory.check_availability", {
|
|
2542
|
+
product_id: productId,
|
|
2543
|
+
quantity,
|
|
2544
|
+
location_id: locationId
|
|
2545
|
+
})
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
async checkVariantAvailability(variantId, quantity, locationId) {
|
|
2549
|
+
return safe8(
|
|
2550
|
+
this.client.query("inventory.check_availability", {
|
|
2551
|
+
variant_id: variantId,
|
|
2552
|
+
quantity,
|
|
2553
|
+
location_id: locationId
|
|
2554
|
+
})
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
async checkMultipleAvailability(items, locationId) {
|
|
2558
|
+
const results = await Promise.all(
|
|
2559
|
+
items.map(
|
|
2560
|
+
(item) => item.variant_id ? this.checkVariantAvailability(item.variant_id, item.quantity, locationId) : this.checkProductAvailability(item.product_id, item.quantity, locationId)
|
|
2561
|
+
)
|
|
2562
|
+
);
|
|
2563
|
+
for (const result of results) {
|
|
2564
|
+
if (!result.ok) return result;
|
|
2565
|
+
}
|
|
2566
|
+
return ok(results.map((r) => r.value));
|
|
2567
|
+
}
|
|
2568
|
+
async getSummary() {
|
|
2569
|
+
return safe8(this.client.query("inventory.summary"));
|
|
2570
|
+
}
|
|
2571
|
+
async isInStock(productId, locationId) {
|
|
2572
|
+
const result = await this.checkProductAvailability(productId, 1, locationId);
|
|
2573
|
+
if (!result.ok) return result;
|
|
2574
|
+
return ok(result.value.is_available);
|
|
2575
|
+
}
|
|
2576
|
+
async getAvailableQuantity(productId, locationId) {
|
|
2577
|
+
const result = await this.getProductStock(productId, locationId);
|
|
2578
|
+
if (!result.ok) return result;
|
|
2579
|
+
return ok(result.value.available_quantity);
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
// src/scheduling.ts
|
|
2584
|
+
function toVariables(input) {
|
|
2585
|
+
return Object.fromEntries(Object.entries(input));
|
|
2586
|
+
}
|
|
2587
|
+
function toCimplifyError9(error) {
|
|
2588
|
+
if (error instanceof CimplifyError) return error;
|
|
2589
|
+
if (error instanceof Error) {
|
|
2590
|
+
return new CimplifyError("UNKNOWN_ERROR", error.message, false);
|
|
2591
|
+
}
|
|
2592
|
+
return new CimplifyError("UNKNOWN_ERROR", String(error), false);
|
|
2593
|
+
}
|
|
2594
|
+
async function safe9(promise) {
|
|
2595
|
+
try {
|
|
2596
|
+
return ok(await promise);
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
return err(toCimplifyError9(error));
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
var SchedulingService = class {
|
|
2602
|
+
constructor(client) {
|
|
2603
|
+
this.client = client;
|
|
2604
|
+
}
|
|
2605
|
+
async getServices() {
|
|
2606
|
+
return safe9(this.client.query("scheduling.services"));
|
|
2607
|
+
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Get a specific service by ID
|
|
2610
|
+
* Note: Filters from all services client-side (no single-service endpoint)
|
|
2611
|
+
*/
|
|
2612
|
+
async getService(serviceId) {
|
|
2613
|
+
const result = await this.getServices();
|
|
2614
|
+
if (!result.ok) return result;
|
|
2615
|
+
return ok(result.value.find((s) => s.id === serviceId) || null);
|
|
2616
|
+
}
|
|
2617
|
+
async getAvailableSlots(input) {
|
|
2618
|
+
return safe9(
|
|
2619
|
+
this.client.query("scheduling.slots", toVariables(input))
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
async checkSlotAvailability(input) {
|
|
2623
|
+
return safe9(
|
|
2624
|
+
this.client.query(
|
|
2625
|
+
"scheduling.check_availability",
|
|
2626
|
+
toVariables(input)
|
|
2627
|
+
)
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
async getServiceAvailability(params) {
|
|
2631
|
+
return safe9(
|
|
2632
|
+
this.client.query(
|
|
2633
|
+
"scheduling.availability",
|
|
2634
|
+
toVariables(params)
|
|
2635
|
+
)
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
async getBooking(bookingId) {
|
|
2639
|
+
return safe9(this.client.query(`scheduling.${bookingId}`));
|
|
2640
|
+
}
|
|
2641
|
+
async getCustomerBookings() {
|
|
2642
|
+
return safe9(this.client.query("scheduling"));
|
|
2643
|
+
}
|
|
2644
|
+
async getUpcomingBookings() {
|
|
2645
|
+
return safe9(
|
|
2646
|
+
this.client.query(
|
|
2647
|
+
"scheduling[?(@.status!='completed' && @.status!='cancelled')]#sort(start_time,asc)"
|
|
2648
|
+
)
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
async getPastBookings(limit = 10) {
|
|
2652
|
+
return safe9(
|
|
2653
|
+
this.client.query(
|
|
2654
|
+
`scheduling[?(@.status=='completed')]#sort(start_time,desc)#limit(${limit})`
|
|
2655
|
+
)
|
|
2656
|
+
);
|
|
2657
|
+
}
|
|
2658
|
+
async cancelBooking(input) {
|
|
2659
|
+
return safe9(this.client.call("scheduling.cancel_booking", input));
|
|
2660
|
+
}
|
|
2661
|
+
async rescheduleBooking(input) {
|
|
2662
|
+
return safe9(this.client.call("scheduling.reschedule_booking", input));
|
|
2663
|
+
}
|
|
2664
|
+
async getNextAvailableSlot(serviceId, fromDate) {
|
|
2665
|
+
const date = fromDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2666
|
+
const result = await this.getAvailableSlots({
|
|
2667
|
+
service_id: serviceId,
|
|
2668
|
+
date
|
|
2669
|
+
});
|
|
2670
|
+
if (!result.ok) return result;
|
|
2671
|
+
return ok(result.value.find((slot) => slot.is_available) || null);
|
|
2672
|
+
}
|
|
2673
|
+
async hasAvailabilityOn(serviceId, date) {
|
|
2674
|
+
const result = await this.getAvailableSlots({
|
|
2675
|
+
service_id: serviceId,
|
|
2676
|
+
date
|
|
2677
|
+
});
|
|
2678
|
+
if (!result.ok) return result;
|
|
2679
|
+
return ok(result.value.some((slot) => slot.is_available));
|
|
2680
|
+
}
|
|
2681
|
+
};
|
|
2682
|
+
|
|
2683
|
+
// src/lite.ts
|
|
2684
|
+
function toCimplifyError10(error) {
|
|
2685
|
+
if (error instanceof CimplifyError) return error;
|
|
2686
|
+
if (error instanceof Error) {
|
|
2687
|
+
return new CimplifyError("UNKNOWN_ERROR", error.message, false);
|
|
2688
|
+
}
|
|
2689
|
+
return new CimplifyError("UNKNOWN_ERROR", String(error), false);
|
|
2690
|
+
}
|
|
2691
|
+
async function safe10(promise) {
|
|
2692
|
+
try {
|
|
2693
|
+
return ok(await promise);
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
return err(toCimplifyError10(error));
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
var LiteService = class {
|
|
2699
|
+
constructor(client) {
|
|
2700
|
+
this.client = client;
|
|
2701
|
+
}
|
|
2702
|
+
async getBootstrap() {
|
|
2703
|
+
return safe10(this.client.query("lite.bootstrap"));
|
|
2704
|
+
}
|
|
2705
|
+
async getTable(tableId) {
|
|
2706
|
+
return safe10(this.client.query(`lite.table.${tableId}`));
|
|
2707
|
+
}
|
|
2708
|
+
async getTableByNumber(tableNumber, locationId) {
|
|
2709
|
+
return safe10(
|
|
2710
|
+
this.client.query("lite.table_by_number", {
|
|
2711
|
+
table_number: tableNumber,
|
|
2712
|
+
location_id: locationId
|
|
2713
|
+
})
|
|
2714
|
+
);
|
|
2715
|
+
}
|
|
2716
|
+
async sendToKitchen(tableId, items) {
|
|
2717
|
+
return safe10(
|
|
2718
|
+
this.client.call("lite.send_to_kitchen", {
|
|
2719
|
+
table_id: tableId,
|
|
2720
|
+
items
|
|
2721
|
+
})
|
|
2722
|
+
);
|
|
2723
|
+
}
|
|
2724
|
+
async callWaiter(tableId, reason) {
|
|
2725
|
+
return safe10(
|
|
2726
|
+
this.client.call("lite.call_waiter", {
|
|
2727
|
+
table_id: tableId,
|
|
2728
|
+
reason
|
|
2729
|
+
})
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
async requestBill(tableId) {
|
|
2733
|
+
return safe10(
|
|
2734
|
+
this.client.call("lite.request_bill", {
|
|
2735
|
+
table_id: tableId
|
|
2736
|
+
})
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
async getMenu() {
|
|
2740
|
+
return safe10(this.client.query("lite.menu"));
|
|
2741
|
+
}
|
|
2742
|
+
async getMenuByCategory(categoryId) {
|
|
2743
|
+
return safe10(this.client.query(`lite.menu.category.${categoryId}`));
|
|
2744
|
+
}
|
|
2745
|
+
};
|
|
2746
|
+
|
|
2747
|
+
// src/fx.ts
|
|
2748
|
+
function toCimplifyError11(error) {
|
|
2749
|
+
if (error instanceof CimplifyError) return error;
|
|
2750
|
+
if (error instanceof Error) {
|
|
2751
|
+
return new CimplifyError("UNKNOWN_ERROR", error.message, false);
|
|
2752
|
+
}
|
|
2753
|
+
return new CimplifyError("UNKNOWN_ERROR", String(error), false);
|
|
2754
|
+
}
|
|
2755
|
+
async function safe11(promise) {
|
|
2756
|
+
try {
|
|
2757
|
+
return ok(await promise);
|
|
2758
|
+
} catch (error) {
|
|
2759
|
+
return err(toCimplifyError11(error));
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
var FxService = class {
|
|
2763
|
+
constructor(client) {
|
|
2764
|
+
this.client = client;
|
|
2765
|
+
}
|
|
2766
|
+
async getRate(from, to) {
|
|
2767
|
+
return safe11(this.client.call("fx.getRate", { from, to }));
|
|
2768
|
+
}
|
|
2769
|
+
async lockQuote(request) {
|
|
2770
|
+
return safe11(this.client.call("fx.lockQuote", request));
|
|
2771
|
+
}
|
|
2772
|
+
};
|
|
2773
|
+
|
|
2774
|
+
// src/elements.ts
|
|
2775
|
+
function toCheckoutError(code, message, recoverable) {
|
|
2776
|
+
const hint = getErrorHint(code);
|
|
2777
|
+
return {
|
|
2778
|
+
success: false,
|
|
2779
|
+
error: {
|
|
2780
|
+
code,
|
|
2781
|
+
message,
|
|
2782
|
+
recoverable,
|
|
2783
|
+
docs_url: hint?.docs_url,
|
|
2784
|
+
suggestion: hint?.suggestion
|
|
2785
|
+
}
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
var DEFAULT_LINK_URL = "https://link.cimplify.io";
|
|
2789
|
+
function isAllowedOrigin(origin) {
|
|
2790
|
+
try {
|
|
2791
|
+
const url = new URL(origin);
|
|
2792
|
+
const hostname = url.hostname;
|
|
2793
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
2794
|
+
return true;
|
|
2795
|
+
}
|
|
2796
|
+
if (url.protocol !== "https:") {
|
|
2797
|
+
return false;
|
|
2798
|
+
}
|
|
2799
|
+
return hostname === "cimplify.io" || hostname.endsWith(".cimplify.io");
|
|
2800
|
+
} catch {
|
|
2801
|
+
return false;
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
function parseIframeMessage(data) {
|
|
2805
|
+
if (!data || typeof data !== "object") {
|
|
2806
|
+
return null;
|
|
2807
|
+
}
|
|
2808
|
+
const maybeType = data.type;
|
|
2809
|
+
if (typeof maybeType !== "string") {
|
|
2810
|
+
return null;
|
|
2811
|
+
}
|
|
2812
|
+
return data;
|
|
2813
|
+
}
|
|
2814
|
+
var CimplifyElements = class {
|
|
2815
|
+
constructor(client, businessId, options = {}) {
|
|
2816
|
+
this.elements = /* @__PURE__ */ new Map();
|
|
2817
|
+
this.accessToken = null;
|
|
2818
|
+
this.accountId = null;
|
|
2819
|
+
this.customerId = null;
|
|
2820
|
+
this.customerData = null;
|
|
2821
|
+
this.addressData = null;
|
|
2822
|
+
this.paymentData = null;
|
|
2823
|
+
this.checkoutInProgress = false;
|
|
2824
|
+
this.activeCheckoutAbort = null;
|
|
2825
|
+
this.hasWarnedMissingAuthElement = false;
|
|
2826
|
+
this.businessIdResolvePromise = null;
|
|
2827
|
+
this.client = client;
|
|
2828
|
+
this.businessId = businessId ?? null;
|
|
2829
|
+
this.linkUrl = options.linkUrl || DEFAULT_LINK_URL;
|
|
2830
|
+
this.options = options;
|
|
2831
|
+
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
2832
|
+
if (typeof window !== "undefined") {
|
|
2833
|
+
window.addEventListener("message", this.boundHandleMessage);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
create(type, options = {}) {
|
|
2837
|
+
const existing = this.elements.get(type);
|
|
2838
|
+
if (existing) return existing;
|
|
2839
|
+
const element = new CimplifyElement(type, this.businessId, this.linkUrl, options, this);
|
|
2840
|
+
this.elements.set(type, element);
|
|
2841
|
+
return element;
|
|
2842
|
+
}
|
|
2843
|
+
getElement(type) {
|
|
2844
|
+
return this.elements.get(type);
|
|
2845
|
+
}
|
|
2846
|
+
destroy() {
|
|
2847
|
+
this.elements.forEach((element) => element.destroy());
|
|
2848
|
+
this.elements.clear();
|
|
2849
|
+
if (typeof window !== "undefined") {
|
|
2850
|
+
window.removeEventListener("message", this.boundHandleMessage);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
async submitCheckout(data) {
|
|
2854
|
+
const result = await this.processCheckout({
|
|
2855
|
+
cart_id: data.cart_id,
|
|
2856
|
+
order_type: data.order_type ?? "delivery",
|
|
2857
|
+
location_id: data.location_id,
|
|
2858
|
+
notes: data.notes,
|
|
2859
|
+
scheduled_time: data.scheduled_time,
|
|
2860
|
+
tip_amount: data.tip_amount,
|
|
2861
|
+
enroll_in_link: true
|
|
2862
|
+
});
|
|
2863
|
+
if (result.success) {
|
|
2864
|
+
return {
|
|
2865
|
+
success: true,
|
|
2866
|
+
order: {
|
|
2867
|
+
id: result.order?.id || "",
|
|
2868
|
+
status: result.order?.status || "unknown",
|
|
2869
|
+
total: result.order?.total || ""
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2873
|
+
return {
|
|
2874
|
+
success: false,
|
|
2875
|
+
error: {
|
|
2876
|
+
code: result.error?.code || "CHECKOUT_FAILED",
|
|
2877
|
+
message: result.error?.message || "Checkout failed"
|
|
2878
|
+
}
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
processCheckout(options) {
|
|
2882
|
+
let abortFn = null;
|
|
2883
|
+
const task = (async () => {
|
|
2884
|
+
if (this.checkoutInProgress) {
|
|
2885
|
+
return toCheckoutError(
|
|
2886
|
+
"ALREADY_PROCESSING",
|
|
2887
|
+
"Checkout is already in progress.",
|
|
2888
|
+
false
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
if (!options.cart_id) {
|
|
2892
|
+
return toCheckoutError(
|
|
2893
|
+
"INVALID_CART",
|
|
2894
|
+
"A valid cart is required before checkout can start.",
|
|
2895
|
+
false
|
|
2896
|
+
);
|
|
2897
|
+
}
|
|
2898
|
+
if (!options.order_type) {
|
|
2899
|
+
return toCheckoutError(
|
|
2900
|
+
"ORDER_TYPE_REQUIRED",
|
|
2901
|
+
"Order type is required before checkout can start.",
|
|
2902
|
+
false
|
|
2903
|
+
);
|
|
2904
|
+
}
|
|
2905
|
+
const paymentElement = this.elements.get(ELEMENT_TYPES.PAYMENT);
|
|
2906
|
+
if (!paymentElement) {
|
|
2907
|
+
return toCheckoutError(
|
|
2908
|
+
"NO_PAYMENT_ELEMENT",
|
|
2909
|
+
"Payment element must be mounted before checkout.",
|
|
2910
|
+
false
|
|
2911
|
+
);
|
|
2912
|
+
}
|
|
2913
|
+
if (!paymentElement.isMounted()) {
|
|
2914
|
+
return toCheckoutError(
|
|
2915
|
+
"PAYMENT_NOT_MOUNTED",
|
|
2916
|
+
"Payment element must be mounted before checkout.",
|
|
2917
|
+
false
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
const authElement = this.elements.get(ELEMENT_TYPES.AUTH);
|
|
2921
|
+
if (!authElement && !this.hasWarnedMissingAuthElement) {
|
|
2922
|
+
this.hasWarnedMissingAuthElement = true;
|
|
2923
|
+
console.warn(
|
|
2924
|
+
"[Cimplify] processCheckout() called without AuthElement mounted. For best conversion and Link enrollment, mount <AuthElement> before checkout."
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
if (authElement && !this.accessToken) {
|
|
2928
|
+
return toCheckoutError(
|
|
2929
|
+
"AUTH_INCOMPLETE",
|
|
2930
|
+
"Authentication must complete before checkout can start.",
|
|
2931
|
+
true
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
const addressElement = this.elements.get(ELEMENT_TYPES.ADDRESS);
|
|
2935
|
+
if (addressElement) {
|
|
2936
|
+
await addressElement.getData();
|
|
2937
|
+
}
|
|
2938
|
+
await this.hydrateCustomerData();
|
|
2939
|
+
const processMessage = {
|
|
2940
|
+
type: MESSAGE_TYPES.PROCESS_CHECKOUT,
|
|
2941
|
+
cart_id: options.cart_id,
|
|
2942
|
+
order_type: options.order_type,
|
|
2943
|
+
location_id: options.location_id,
|
|
2944
|
+
notes: options.notes,
|
|
2945
|
+
scheduled_time: options.scheduled_time,
|
|
2946
|
+
tip_amount: options.tip_amount,
|
|
2947
|
+
pay_currency: options.pay_currency,
|
|
2948
|
+
enroll_in_link: options.enroll_in_link ?? true,
|
|
2949
|
+
address: this.addressData ?? void 0,
|
|
2950
|
+
customer: this.customerData ? {
|
|
2951
|
+
name: this.customerData.name,
|
|
2952
|
+
email: this.customerData.email,
|
|
2953
|
+
phone: this.customerData.phone
|
|
2954
|
+
} : null,
|
|
2955
|
+
account_id: this.accountId ?? void 0,
|
|
2956
|
+
customer_id: this.customerId ?? void 0
|
|
2957
|
+
};
|
|
2958
|
+
const timeoutMs = options.timeout_ms ?? 18e4;
|
|
2959
|
+
const paymentWindow = paymentElement.getContentWindow();
|
|
2960
|
+
this.checkoutInProgress = true;
|
|
2961
|
+
return new Promise((resolve) => {
|
|
2962
|
+
let settled = false;
|
|
2963
|
+
const cleanup = () => {
|
|
2964
|
+
if (typeof window !== "undefined") {
|
|
2965
|
+
window.removeEventListener("message", handleCheckoutMessage);
|
|
2966
|
+
}
|
|
2967
|
+
clearTimeout(timeout);
|
|
2968
|
+
this.checkoutInProgress = false;
|
|
2969
|
+
this.activeCheckoutAbort = null;
|
|
2970
|
+
};
|
|
2971
|
+
const settle = (result) => {
|
|
2972
|
+
if (settled) {
|
|
2973
|
+
return;
|
|
2974
|
+
}
|
|
2975
|
+
settled = true;
|
|
2976
|
+
cleanup();
|
|
2977
|
+
resolve(result);
|
|
2978
|
+
};
|
|
2979
|
+
const timeout = setTimeout(() => {
|
|
2980
|
+
settle(
|
|
2981
|
+
toCheckoutError(
|
|
2982
|
+
"TIMEOUT",
|
|
2983
|
+
"Checkout timed out before receiving a terminal response.",
|
|
2984
|
+
true
|
|
2985
|
+
)
|
|
2986
|
+
);
|
|
2987
|
+
}, timeoutMs);
|
|
2988
|
+
const handleCheckoutMessage = (event) => {
|
|
2989
|
+
if (!isAllowedOrigin(event.origin)) {
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
const message = parseIframeMessage(event.data);
|
|
2993
|
+
if (!message) {
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
if (message.type === MESSAGE_TYPES.LOGOUT_COMPLETE) {
|
|
2997
|
+
paymentElement.sendMessage({ type: MESSAGE_TYPES.ABORT_CHECKOUT });
|
|
2998
|
+
settle(
|
|
2999
|
+
toCheckoutError(
|
|
3000
|
+
"AUTH_LOST",
|
|
3001
|
+
"Authentication was cleared during checkout.",
|
|
3002
|
+
true
|
|
3003
|
+
)
|
|
3004
|
+
);
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
if (paymentWindow && event.source && event.source !== paymentWindow) {
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
if (message.type === MESSAGE_TYPES.CHECKOUT_STATUS) {
|
|
3011
|
+
options.on_status_change?.(message.status, message.context);
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
if (message.type === MESSAGE_TYPES.CHECKOUT_COMPLETE) {
|
|
3015
|
+
settle({
|
|
3016
|
+
success: message.success,
|
|
3017
|
+
order: message.order,
|
|
3018
|
+
error: message.error,
|
|
3019
|
+
enrolled_in_link: message.enrolled_in_link
|
|
3020
|
+
});
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
3023
|
+
if (typeof window !== "undefined") {
|
|
3024
|
+
window.addEventListener("message", handleCheckoutMessage);
|
|
3025
|
+
}
|
|
3026
|
+
abortFn = () => {
|
|
3027
|
+
paymentElement.sendMessage({ type: MESSAGE_TYPES.ABORT_CHECKOUT });
|
|
3028
|
+
settle(
|
|
3029
|
+
toCheckoutError(
|
|
3030
|
+
"CANCELLED",
|
|
3031
|
+
"Checkout was cancelled.",
|
|
3032
|
+
true
|
|
3033
|
+
)
|
|
3034
|
+
);
|
|
3035
|
+
};
|
|
3036
|
+
this.activeCheckoutAbort = abortFn;
|
|
3037
|
+
paymentElement.sendMessage(processMessage);
|
|
3038
|
+
});
|
|
3039
|
+
})();
|
|
3040
|
+
const abortable = task;
|
|
3041
|
+
abortable.abort = () => {
|
|
3042
|
+
if (abortFn) {
|
|
3043
|
+
abortFn();
|
|
3044
|
+
}
|
|
3045
|
+
};
|
|
3046
|
+
return abortable;
|
|
3047
|
+
}
|
|
3048
|
+
isAuthenticated() {
|
|
3049
|
+
return this.accessToken !== null;
|
|
3050
|
+
}
|
|
3051
|
+
getAccessToken() {
|
|
3052
|
+
return this.accessToken;
|
|
3053
|
+
}
|
|
3054
|
+
getPublicKey() {
|
|
3055
|
+
return this.client.getPublicKey();
|
|
3056
|
+
}
|
|
3057
|
+
getBusinessId() {
|
|
3058
|
+
return this.businessId;
|
|
3059
|
+
}
|
|
3060
|
+
async resolveBusinessId() {
|
|
3061
|
+
if (this.businessId) {
|
|
3062
|
+
return this.businessId;
|
|
3063
|
+
}
|
|
3064
|
+
if (this.businessIdResolvePromise) {
|
|
3065
|
+
return this.businessIdResolvePromise;
|
|
3066
|
+
}
|
|
3067
|
+
this.businessIdResolvePromise = this.client.resolveBusinessId().then((resolvedBusinessId) => {
|
|
3068
|
+
this.businessId = resolvedBusinessId;
|
|
3069
|
+
return resolvedBusinessId;
|
|
3070
|
+
}).catch(() => null).finally(() => {
|
|
3071
|
+
this.businessIdResolvePromise = null;
|
|
3072
|
+
});
|
|
3073
|
+
return this.businessIdResolvePromise;
|
|
3074
|
+
}
|
|
3075
|
+
getAppearance() {
|
|
3076
|
+
return this.options.appearance;
|
|
3077
|
+
}
|
|
3078
|
+
async hydrateCustomerData() {
|
|
3079
|
+
if (!this.accessToken || this.customerData) {
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
if (!this.client.getAccessToken()) {
|
|
3083
|
+
this.client.setAccessToken(this.accessToken);
|
|
3084
|
+
}
|
|
3085
|
+
const linkDataResult = await this.client.link.getLinkData();
|
|
3086
|
+
if (!linkDataResult.ok || !linkDataResult.value?.customer) {
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3089
|
+
const customer = linkDataResult.value.customer;
|
|
3090
|
+
this.customerData = {
|
|
3091
|
+
name: customer.name || "",
|
|
3092
|
+
email: customer.email || null,
|
|
3093
|
+
phone: customer.phone || null
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
handleMessage(event) {
|
|
3097
|
+
if (!isAllowedOrigin(event.origin)) {
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
const message = parseIframeMessage(event.data);
|
|
3101
|
+
if (!message) return;
|
|
3102
|
+
switch (message.type) {
|
|
3103
|
+
case MESSAGE_TYPES.AUTHENTICATED:
|
|
3104
|
+
const customer = message.customer ?? {
|
|
3105
|
+
name: "",
|
|
3106
|
+
email: null,
|
|
3107
|
+
phone: null
|
|
3108
|
+
};
|
|
3109
|
+
this.accessToken = message.token;
|
|
3110
|
+
this.accountId = message.accountId;
|
|
3111
|
+
this.customerId = message.customerId;
|
|
3112
|
+
this.customerData = customer;
|
|
3113
|
+
this.client.setAccessToken(message.token);
|
|
3114
|
+
this.elements.forEach((element, type) => {
|
|
3115
|
+
if (type !== ELEMENT_TYPES.AUTH) {
|
|
3116
|
+
element.sendMessage({ type: MESSAGE_TYPES.SET_TOKEN, token: message.token });
|
|
3117
|
+
}
|
|
3118
|
+
});
|
|
3119
|
+
break;
|
|
3120
|
+
case MESSAGE_TYPES.TOKEN_REFRESHED:
|
|
3121
|
+
this.accessToken = message.token;
|
|
3122
|
+
break;
|
|
3123
|
+
case MESSAGE_TYPES.ADDRESS_CHANGED:
|
|
3124
|
+
case MESSAGE_TYPES.ADDRESS_SELECTED:
|
|
3125
|
+
this.addressData = message.address;
|
|
3126
|
+
break;
|
|
3127
|
+
case MESSAGE_TYPES.PAYMENT_METHOD_SELECTED:
|
|
3128
|
+
this.paymentData = message.method;
|
|
3129
|
+
break;
|
|
3130
|
+
case MESSAGE_TYPES.LOGOUT_COMPLETE:
|
|
3131
|
+
if (this.checkoutInProgress && this.activeCheckoutAbort) {
|
|
3132
|
+
this.activeCheckoutAbort();
|
|
3133
|
+
}
|
|
3134
|
+
this.accessToken = null;
|
|
3135
|
+
this.accountId = null;
|
|
3136
|
+
this.customerId = null;
|
|
3137
|
+
this.customerData = null;
|
|
3138
|
+
this.addressData = null;
|
|
3139
|
+
this.paymentData = null;
|
|
3140
|
+
this.client.clearSession();
|
|
3141
|
+
break;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
_setAddressData(data) {
|
|
3145
|
+
this.addressData = data;
|
|
3146
|
+
}
|
|
3147
|
+
_setPaymentData(data) {
|
|
3148
|
+
this.paymentData = data;
|
|
3149
|
+
}
|
|
3150
|
+
};
|
|
3151
|
+
var CimplifyElement = class {
|
|
3152
|
+
constructor(type, businessId, linkUrl, options, parent) {
|
|
3153
|
+
this.iframe = null;
|
|
3154
|
+
this.container = null;
|
|
3155
|
+
this.mounted = false;
|
|
3156
|
+
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
3157
|
+
this.resolvers = /* @__PURE__ */ new Map();
|
|
3158
|
+
this.type = type;
|
|
3159
|
+
this.businessId = businessId;
|
|
3160
|
+
this.linkUrl = linkUrl;
|
|
3161
|
+
this.options = options;
|
|
3162
|
+
this.parent = parent;
|
|
3163
|
+
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
3164
|
+
if (typeof window !== "undefined") {
|
|
3165
|
+
window.addEventListener("message", this.boundHandleMessage);
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
mount(container) {
|
|
3169
|
+
if (this.mounted) {
|
|
3170
|
+
console.warn(`Element ${this.type} is already mounted`);
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
const target = typeof container === "string" ? document.querySelector(container) : container;
|
|
3174
|
+
if (!target) {
|
|
3175
|
+
console.error(`Container not found: ${container}`);
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
this.container = target;
|
|
3179
|
+
this.mounted = true;
|
|
3180
|
+
void this.createIframe();
|
|
3181
|
+
}
|
|
3182
|
+
destroy() {
|
|
3183
|
+
if (this.iframe) {
|
|
3184
|
+
this.iframe.remove();
|
|
3185
|
+
this.iframe = null;
|
|
3186
|
+
}
|
|
3187
|
+
this.container = null;
|
|
3188
|
+
this.mounted = false;
|
|
3189
|
+
this.eventHandlers.clear();
|
|
3190
|
+
this.resolvers.forEach((entry) => clearTimeout(entry.timeoutId));
|
|
3191
|
+
this.resolvers.clear();
|
|
3192
|
+
if (typeof window !== "undefined") {
|
|
3193
|
+
window.removeEventListener("message", this.boundHandleMessage);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
on(event, handler) {
|
|
3197
|
+
if (!this.eventHandlers.has(event)) {
|
|
3198
|
+
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
3199
|
+
}
|
|
3200
|
+
this.eventHandlers.get(event).add(handler);
|
|
3201
|
+
}
|
|
3202
|
+
off(event, handler) {
|
|
3203
|
+
this.eventHandlers.get(event)?.delete(handler);
|
|
3204
|
+
}
|
|
3205
|
+
async getData() {
|
|
3206
|
+
if (!this.isMounted()) {
|
|
3207
|
+
return null;
|
|
3208
|
+
}
|
|
3209
|
+
return new Promise((resolve) => {
|
|
3210
|
+
const id = Math.random().toString(36).slice(2);
|
|
3211
|
+
const timeoutId = setTimeout(() => {
|
|
3212
|
+
const entry = this.resolvers.get(id);
|
|
3213
|
+
if (!entry) {
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
this.resolvers.delete(id);
|
|
3217
|
+
entry.resolve(null);
|
|
3218
|
+
}, 5e3);
|
|
3219
|
+
this.resolvers.set(id, { resolve, timeoutId });
|
|
3220
|
+
this.sendMessage({ type: MESSAGE_TYPES.GET_DATA });
|
|
3221
|
+
});
|
|
3222
|
+
}
|
|
3223
|
+
sendMessage(message) {
|
|
3224
|
+
if (this.iframe?.contentWindow) {
|
|
3225
|
+
this.iframe.contentWindow.postMessage(message, this.linkUrl);
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
getContentWindow() {
|
|
3229
|
+
return this.iframe?.contentWindow ?? null;
|
|
3230
|
+
}
|
|
3231
|
+
isMounted() {
|
|
3232
|
+
return this.mounted && Boolean(this.iframe) && this.iframe?.isConnected === true;
|
|
3233
|
+
}
|
|
3234
|
+
async createIframe() {
|
|
3235
|
+
if (!this.container) return;
|
|
3236
|
+
const resolvedBusinessId = this.businessId ?? await this.parent.resolveBusinessId();
|
|
3237
|
+
if (!resolvedBusinessId || !this.container || !this.mounted) {
|
|
3238
|
+
console.error("Unable to mount element without a resolved business ID");
|
|
3239
|
+
this.emit(EVENT_TYPES.ERROR, {
|
|
3240
|
+
code: "BUSINESS_ID_REQUIRED",
|
|
3241
|
+
message: "Unable to initialize checkout without a business ID."
|
|
3242
|
+
});
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
this.businessId = resolvedBusinessId;
|
|
3246
|
+
const iframe = document.createElement("iframe");
|
|
3247
|
+
const url = new URL(`${this.linkUrl}/elements/${this.type}`);
|
|
3248
|
+
url.searchParams.set("businessId", resolvedBusinessId);
|
|
3249
|
+
if (this.options.prefillEmail) url.searchParams.set("email", this.options.prefillEmail);
|
|
3250
|
+
if (this.options.mode) url.searchParams.set("mode", this.options.mode);
|
|
3251
|
+
iframe.src = url.toString();
|
|
3252
|
+
iframe.style.border = "none";
|
|
3253
|
+
iframe.style.width = "100%";
|
|
3254
|
+
iframe.style.display = "block";
|
|
3255
|
+
iframe.style.overflow = "hidden";
|
|
3256
|
+
iframe.setAttribute("allowtransparency", "true");
|
|
3257
|
+
iframe.setAttribute("frameborder", "0");
|
|
3258
|
+
iframe.setAttribute(
|
|
3259
|
+
"sandbox",
|
|
3260
|
+
"allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
3261
|
+
);
|
|
3262
|
+
this.iframe = iframe;
|
|
3263
|
+
this.container.appendChild(iframe);
|
|
3264
|
+
iframe.onload = () => {
|
|
3265
|
+
const publicKey = this.parent.getPublicKey();
|
|
3266
|
+
this.sendMessage({
|
|
3267
|
+
type: MESSAGE_TYPES.INIT,
|
|
3268
|
+
businessId: resolvedBusinessId,
|
|
3269
|
+
publicKey,
|
|
3270
|
+
demoMode: publicKey.length === 0,
|
|
3271
|
+
prefillEmail: this.options.prefillEmail,
|
|
3272
|
+
appearance: this.parent.getAppearance()
|
|
3273
|
+
});
|
|
3274
|
+
const token = this.parent.getAccessToken();
|
|
3275
|
+
if (token && this.type !== ELEMENT_TYPES.AUTH) {
|
|
3276
|
+
this.sendMessage({ type: MESSAGE_TYPES.SET_TOKEN, token });
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
handleMessage(event) {
|
|
3281
|
+
if (!isAllowedOrigin(event.origin)) {
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
const message = parseIframeMessage(event.data);
|
|
3285
|
+
if (!message) return;
|
|
3286
|
+
switch (message.type) {
|
|
3287
|
+
case MESSAGE_TYPES.READY:
|
|
3288
|
+
this.emit(EVENT_TYPES.READY, { height: message.height });
|
|
3289
|
+
break;
|
|
3290
|
+
case MESSAGE_TYPES.HEIGHT_CHANGE:
|
|
3291
|
+
if (this.iframe) this.iframe.style.height = `${message.height}px`;
|
|
3292
|
+
break;
|
|
3293
|
+
case MESSAGE_TYPES.AUTHENTICATED:
|
|
3294
|
+
const customer = message.customer ?? {
|
|
3295
|
+
name: "",
|
|
3296
|
+
email: null,
|
|
3297
|
+
phone: null
|
|
3298
|
+
};
|
|
3299
|
+
this.emit(EVENT_TYPES.AUTHENTICATED, {
|
|
3300
|
+
accountId: message.accountId,
|
|
3301
|
+
customerId: message.customerId,
|
|
3302
|
+
token: message.token,
|
|
3303
|
+
customer
|
|
3304
|
+
});
|
|
3305
|
+
break;
|
|
3306
|
+
case MESSAGE_TYPES.REQUIRES_OTP:
|
|
3307
|
+
this.emit(EVENT_TYPES.REQUIRES_OTP, { contactMasked: message.contactMasked });
|
|
3308
|
+
break;
|
|
3309
|
+
case MESSAGE_TYPES.ERROR:
|
|
3310
|
+
this.emit(EVENT_TYPES.ERROR, { code: message.code, message: message.message });
|
|
3311
|
+
break;
|
|
3312
|
+
case MESSAGE_TYPES.ADDRESS_CHANGED:
|
|
3313
|
+
case MESSAGE_TYPES.ADDRESS_SELECTED:
|
|
3314
|
+
this.parent._setAddressData(message.address);
|
|
3315
|
+
this.emit(EVENT_TYPES.CHANGE, { address: message.address });
|
|
3316
|
+
this.resolveData(message.address);
|
|
3317
|
+
break;
|
|
3318
|
+
case MESSAGE_TYPES.PAYMENT_METHOD_SELECTED:
|
|
3319
|
+
this.parent._setPaymentData(message.method);
|
|
3320
|
+
this.emit(EVENT_TYPES.CHANGE, { paymentMethod: message.method });
|
|
3321
|
+
this.resolveData(message.method);
|
|
3322
|
+
break;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
emit(event, data) {
|
|
3326
|
+
this.eventHandlers.get(event)?.forEach((handler) => handler(data));
|
|
3327
|
+
}
|
|
3328
|
+
resolveData(data) {
|
|
3329
|
+
this.resolvers.forEach((entry) => {
|
|
3330
|
+
clearTimeout(entry.timeoutId);
|
|
3331
|
+
entry.resolve(data);
|
|
3332
|
+
});
|
|
3333
|
+
this.resolvers.clear();
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
function createElements(client, businessId, options) {
|
|
3337
|
+
return new CimplifyElements(client, businessId, options);
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
// src/client.ts
|
|
3341
|
+
var ACCESS_TOKEN_STORAGE_KEY = "cimplify_access_token";
|
|
3342
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
3343
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
3344
|
+
var DEFAULT_RETRY_DELAY_MS = 1e3;
|
|
3345
|
+
function sleep(ms) {
|
|
3346
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3347
|
+
}
|
|
3348
|
+
function isRetryable(error) {
|
|
3349
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
3350
|
+
return true;
|
|
3351
|
+
}
|
|
3352
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
3353
|
+
return true;
|
|
3354
|
+
}
|
|
3355
|
+
if (error instanceof CimplifyError) {
|
|
3356
|
+
return error.retryable;
|
|
3357
|
+
}
|
|
3358
|
+
if (error instanceof CimplifyError && error.code === "SERVER_ERROR") {
|
|
3359
|
+
return true;
|
|
3360
|
+
}
|
|
3361
|
+
return false;
|
|
3362
|
+
}
|
|
3363
|
+
function toNetworkError(error, isTestMode) {
|
|
3364
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
3365
|
+
return enrichError(
|
|
3366
|
+
new CimplifyError(
|
|
3367
|
+
ErrorCode.TIMEOUT,
|
|
3368
|
+
"Request timed out. Please check your connection and try again.",
|
|
3369
|
+
true
|
|
3370
|
+
),
|
|
3371
|
+
{ isTestMode }
|
|
3372
|
+
);
|
|
3373
|
+
}
|
|
3374
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
3375
|
+
return enrichError(
|
|
3376
|
+
new CimplifyError(
|
|
3377
|
+
ErrorCode.NETWORK_ERROR,
|
|
3378
|
+
"Network error. Please check your internet connection.",
|
|
3379
|
+
true
|
|
3380
|
+
),
|
|
3381
|
+
{ isTestMode }
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
if (error instanceof CimplifyError) {
|
|
3385
|
+
return enrichError(error, { isTestMode });
|
|
3386
|
+
}
|
|
3387
|
+
return enrichError(
|
|
3388
|
+
new CimplifyError(
|
|
3389
|
+
ErrorCode.UNKNOWN_ERROR,
|
|
3390
|
+
error instanceof Error ? error.message : "An unknown error occurred",
|
|
3391
|
+
false
|
|
3392
|
+
),
|
|
3393
|
+
{ isTestMode }
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
function deriveUrls() {
|
|
3397
|
+
const hostname = typeof window !== "undefined" ? window.location.hostname : "";
|
|
3398
|
+
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
3399
|
+
const protocol = window.location.protocol;
|
|
3400
|
+
return {
|
|
3401
|
+
baseUrl: `${protocol}//${hostname}:8082`,
|
|
3402
|
+
linkApiUrl: `${protocol}//${hostname}:8080`
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
return {
|
|
3406
|
+
baseUrl: "https://storefronts.cimplify.io",
|
|
3407
|
+
linkApiUrl: "https://api.cimplify.io"
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
function getEnvPublicKey() {
|
|
3411
|
+
try {
|
|
3412
|
+
const env = globalThis.process;
|
|
3413
|
+
return env?.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY || void 0;
|
|
3414
|
+
} catch {
|
|
3415
|
+
return void 0;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
var CimplifyClient = class {
|
|
3419
|
+
constructor(config = {}) {
|
|
3420
|
+
this.accessToken = null;
|
|
3421
|
+
this.context = {};
|
|
3422
|
+
this.businessId = null;
|
|
3423
|
+
this.businessIdResolvePromise = null;
|
|
3424
|
+
this.inflightRequests = /* @__PURE__ */ new Map();
|
|
3425
|
+
this.publicKey = config.publicKey || getEnvPublicKey() || "";
|
|
3426
|
+
const urls = deriveUrls();
|
|
3427
|
+
this.baseUrl = urls.baseUrl;
|
|
3428
|
+
this.linkApiUrl = urls.linkApiUrl;
|
|
3429
|
+
this.credentials = config.credentials || "include";
|
|
3430
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
3431
|
+
this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
3432
|
+
this.retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
|
|
3433
|
+
this.hooks = config.hooks ?? {};
|
|
3434
|
+
this.accessToken = this.loadAccessToken();
|
|
3435
|
+
if (!this.publicKey) {
|
|
3436
|
+
console.warn(
|
|
3437
|
+
'[Cimplify] No public key found. Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in your environment, or pass { publicKey: "pk_..." } to createCimplifyClient().'
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
/** @deprecated Use getAccessToken() instead */
|
|
3442
|
+
getSessionToken() {
|
|
3443
|
+
return this.accessToken;
|
|
3444
|
+
}
|
|
3445
|
+
/** @deprecated Use setAccessToken() instead */
|
|
3446
|
+
setSessionToken(token) {
|
|
3447
|
+
this.setAccessToken(token);
|
|
3448
|
+
}
|
|
3449
|
+
getAccessToken() {
|
|
3450
|
+
return this.accessToken;
|
|
3451
|
+
}
|
|
3452
|
+
getPublicKey() {
|
|
3453
|
+
return this.publicKey;
|
|
3454
|
+
}
|
|
3455
|
+
isTestMode() {
|
|
3456
|
+
return this.publicKey.trim().startsWith("pk_test_");
|
|
3457
|
+
}
|
|
3458
|
+
setAccessToken(token) {
|
|
3459
|
+
const previous = this.accessToken;
|
|
3460
|
+
this.accessToken = token;
|
|
3461
|
+
this.saveAccessToken(token);
|
|
3462
|
+
this.hooks.onSessionChange?.({
|
|
3463
|
+
previousToken: previous,
|
|
3464
|
+
newToken: token,
|
|
3465
|
+
source: "manual"
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
clearSession() {
|
|
3469
|
+
const previous = this.accessToken;
|
|
3470
|
+
this.accessToken = null;
|
|
3471
|
+
this.saveAccessToken(null);
|
|
3472
|
+
this.hooks.onSessionChange?.({
|
|
3473
|
+
previousToken: previous,
|
|
3474
|
+
newToken: null,
|
|
3475
|
+
source: "clear"
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
/** Set the active location/branch for all subsequent requests */
|
|
3479
|
+
setLocationId(locationId) {
|
|
3480
|
+
if (locationId) {
|
|
3481
|
+
this.context.location_id = locationId;
|
|
3482
|
+
} else {
|
|
3483
|
+
delete this.context.location_id;
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
/** Get the currently active location ID */
|
|
3487
|
+
getLocationId() {
|
|
3488
|
+
return this.context.location_id ?? null;
|
|
3489
|
+
}
|
|
3490
|
+
/** Cache a resolved business ID for future element/checkouts initialization. */
|
|
3491
|
+
setBusinessId(businessId) {
|
|
3492
|
+
const normalized = businessId.trim();
|
|
3493
|
+
if (!normalized) {
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
this.businessId = normalized;
|
|
3497
|
+
}
|
|
3498
|
+
/** Get cached business ID if available. */
|
|
3499
|
+
getBusinessId() {
|
|
3500
|
+
return this.businessId;
|
|
3501
|
+
}
|
|
3502
|
+
/**
|
|
3503
|
+
* Resolve business ID from public key once and cache it.
|
|
3504
|
+
* Subsequent calls return the cached value (or shared in-flight promise).
|
|
3505
|
+
*/
|
|
3506
|
+
async resolveBusinessId() {
|
|
3507
|
+
if (this.businessId) {
|
|
3508
|
+
return this.businessId;
|
|
3509
|
+
}
|
|
3510
|
+
if (this.businessIdResolvePromise) {
|
|
3511
|
+
return this.businessIdResolvePromise;
|
|
3512
|
+
}
|
|
3513
|
+
this.businessIdResolvePromise = (async () => {
|
|
3514
|
+
const result = await this.business.getInfo();
|
|
3515
|
+
if (!result.ok || !result.value?.id) {
|
|
3516
|
+
throw new CimplifyError(
|
|
3517
|
+
ErrorCode.NOT_FOUND,
|
|
3518
|
+
"Unable to resolve business ID from the current public key.",
|
|
3519
|
+
true
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
this.businessId = result.value.id;
|
|
3523
|
+
return result.value.id;
|
|
3524
|
+
})();
|
|
3525
|
+
try {
|
|
3526
|
+
return await this.businessIdResolvePromise;
|
|
3527
|
+
} finally {
|
|
3528
|
+
this.businessIdResolvePromise = null;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
loadAccessToken() {
|
|
3532
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
3533
|
+
return localStorage.getItem(ACCESS_TOKEN_STORAGE_KEY);
|
|
3534
|
+
}
|
|
3535
|
+
return null;
|
|
3536
|
+
}
|
|
3537
|
+
saveAccessToken(token) {
|
|
3538
|
+
if (typeof window !== "undefined" && window.localStorage) {
|
|
3539
|
+
if (token) {
|
|
3540
|
+
localStorage.setItem(ACCESS_TOKEN_STORAGE_KEY, token);
|
|
3541
|
+
} else {
|
|
3542
|
+
localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
getHeaders() {
|
|
3547
|
+
const headers = {
|
|
3548
|
+
"Content-Type": "application/json",
|
|
3549
|
+
"X-API-Key": this.publicKey
|
|
3550
|
+
};
|
|
3551
|
+
if (this.accessToken) {
|
|
3552
|
+
headers["Authorization"] = `Bearer ${this.accessToken}`;
|
|
3553
|
+
}
|
|
3554
|
+
return headers;
|
|
3555
|
+
}
|
|
3556
|
+
async resilientFetch(url, options) {
|
|
3557
|
+
const method = options.method || "GET";
|
|
3558
|
+
const path = url.replace(this.baseUrl, "").replace(this.linkApiUrl, "");
|
|
3559
|
+
const startTime = Date.now();
|
|
3560
|
+
let retryCount = 0;
|
|
3561
|
+
const context = {
|
|
3562
|
+
method,
|
|
3563
|
+
path,
|
|
3564
|
+
url,
|
|
3565
|
+
body: options.body ? JSON.parse(options.body) : void 0,
|
|
3566
|
+
startTime
|
|
3567
|
+
};
|
|
3568
|
+
this.hooks.onRequestStart?.(context);
|
|
3569
|
+
let lastError;
|
|
3570
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
3571
|
+
try {
|
|
3572
|
+
const controller = new AbortController();
|
|
3573
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
3574
|
+
const response = await fetch(url, {
|
|
3575
|
+
...options,
|
|
3576
|
+
signal: controller.signal
|
|
3577
|
+
});
|
|
3578
|
+
clearTimeout(timeoutId);
|
|
3579
|
+
if (response.ok || response.status >= 400 && response.status < 500) {
|
|
3580
|
+
this.hooks.onRequestSuccess?.({
|
|
3581
|
+
...context,
|
|
3582
|
+
status: response.status,
|
|
3583
|
+
durationMs: Date.now() - startTime
|
|
3584
|
+
});
|
|
3585
|
+
return response;
|
|
3586
|
+
}
|
|
3587
|
+
if (response.status >= 500 && attempt < this.maxRetries) {
|
|
3588
|
+
retryCount++;
|
|
3589
|
+
const delay = this.retryDelay * Math.pow(2, attempt);
|
|
3590
|
+
this.hooks.onRetry?.({
|
|
3591
|
+
...context,
|
|
3592
|
+
attempt: retryCount,
|
|
3593
|
+
delayMs: delay,
|
|
3594
|
+
error: new Error(`Server error: ${response.status}`)
|
|
3595
|
+
});
|
|
3596
|
+
await sleep(delay);
|
|
3597
|
+
continue;
|
|
3598
|
+
}
|
|
3599
|
+
this.hooks.onRequestSuccess?.({
|
|
3600
|
+
...context,
|
|
3601
|
+
status: response.status,
|
|
3602
|
+
durationMs: Date.now() - startTime
|
|
3603
|
+
});
|
|
3604
|
+
return response;
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
lastError = error;
|
|
3607
|
+
const networkError = toNetworkError(error, this.isTestMode());
|
|
3608
|
+
const errorRetryable = isRetryable(error);
|
|
3609
|
+
if (!errorRetryable || attempt >= this.maxRetries) {
|
|
3610
|
+
this.hooks.onRequestError?.({
|
|
3611
|
+
...context,
|
|
3612
|
+
error: networkError,
|
|
3613
|
+
durationMs: Date.now() - startTime,
|
|
3614
|
+
retryCount,
|
|
3615
|
+
retryable: errorRetryable
|
|
3616
|
+
});
|
|
3617
|
+
throw networkError;
|
|
3618
|
+
}
|
|
3619
|
+
retryCount++;
|
|
3620
|
+
const delay = this.retryDelay * Math.pow(2, attempt);
|
|
3621
|
+
this.hooks.onRetry?.({
|
|
3622
|
+
...context,
|
|
3623
|
+
attempt: retryCount,
|
|
3624
|
+
delayMs: delay,
|
|
3625
|
+
error: networkError
|
|
3626
|
+
});
|
|
3627
|
+
await sleep(delay);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
const finalError = toNetworkError(lastError, this.isTestMode());
|
|
3631
|
+
this.hooks.onRequestError?.({
|
|
3632
|
+
...context,
|
|
3633
|
+
error: finalError,
|
|
3634
|
+
durationMs: Date.now() - startTime,
|
|
3635
|
+
retryCount,
|
|
3636
|
+
retryable: false
|
|
3637
|
+
});
|
|
3638
|
+
throw finalError;
|
|
3639
|
+
}
|
|
3640
|
+
getDedupeKey(type, payload) {
|
|
3641
|
+
return `${type}:${JSON.stringify(payload)}`;
|
|
3642
|
+
}
|
|
3643
|
+
async deduplicatedRequest(key, requestFn) {
|
|
3644
|
+
const existing = this.inflightRequests.get(key);
|
|
3645
|
+
if (existing) {
|
|
3646
|
+
return existing;
|
|
3647
|
+
}
|
|
3648
|
+
const request = requestFn().finally(() => {
|
|
3649
|
+
this.inflightRequests.delete(key);
|
|
3650
|
+
});
|
|
3651
|
+
this.inflightRequests.set(key, request);
|
|
3652
|
+
return request;
|
|
3653
|
+
}
|
|
3654
|
+
async query(query, variables) {
|
|
3655
|
+
const body = { query };
|
|
3656
|
+
if (variables) {
|
|
3657
|
+
body.variables = variables;
|
|
3658
|
+
}
|
|
3659
|
+
if (Object.keys(this.context).length > 0) {
|
|
3660
|
+
body.context = this.context;
|
|
3661
|
+
}
|
|
3662
|
+
const key = this.getDedupeKey("query", body);
|
|
3663
|
+
return this.deduplicatedRequest(key, async () => {
|
|
3664
|
+
const response = await this.resilientFetch(`${this.baseUrl}/api/q`, {
|
|
3665
|
+
method: "POST",
|
|
3666
|
+
credentials: this.credentials,
|
|
3667
|
+
headers: this.getHeaders(),
|
|
3668
|
+
body: JSON.stringify(body)
|
|
3669
|
+
});
|
|
3670
|
+
return this.handleResponse(response);
|
|
3671
|
+
});
|
|
3672
|
+
}
|
|
3673
|
+
async call(method, args) {
|
|
3674
|
+
const body = {
|
|
3675
|
+
method,
|
|
3676
|
+
args: args !== void 0 ? [args] : []
|
|
3677
|
+
};
|
|
3678
|
+
if (Object.keys(this.context).length > 0) {
|
|
3679
|
+
body.context = this.context;
|
|
3680
|
+
}
|
|
3681
|
+
const response = await this.resilientFetch(`${this.baseUrl}/api/m`, {
|
|
3682
|
+
method: "POST",
|
|
3683
|
+
credentials: this.credentials,
|
|
3684
|
+
headers: this.getHeaders(),
|
|
3685
|
+
body: JSON.stringify(body)
|
|
3686
|
+
});
|
|
3687
|
+
return this.handleResponse(response);
|
|
3688
|
+
}
|
|
3689
|
+
async get(path) {
|
|
3690
|
+
const key = this.getDedupeKey("get", path);
|
|
3691
|
+
return this.deduplicatedRequest(key, async () => {
|
|
3692
|
+
const response = await this.resilientFetch(`${this.baseUrl}${path}`, {
|
|
3693
|
+
method: "GET",
|
|
3694
|
+
credentials: this.credentials,
|
|
3695
|
+
headers: this.getHeaders()
|
|
3696
|
+
});
|
|
3697
|
+
return this.handleRestResponse(response);
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
async post(path, body) {
|
|
3701
|
+
const response = await this.resilientFetch(`${this.baseUrl}${path}`, {
|
|
3702
|
+
method: "POST",
|
|
3703
|
+
credentials: this.credentials,
|
|
3704
|
+
headers: this.getHeaders(),
|
|
3705
|
+
body: body ? JSON.stringify(body) : void 0
|
|
3706
|
+
});
|
|
3707
|
+
return this.handleRestResponse(response);
|
|
3708
|
+
}
|
|
3709
|
+
async delete(path) {
|
|
3710
|
+
const response = await this.resilientFetch(`${this.baseUrl}${path}`, {
|
|
3711
|
+
method: "DELETE",
|
|
3712
|
+
credentials: this.credentials,
|
|
3713
|
+
headers: this.getHeaders()
|
|
3714
|
+
});
|
|
3715
|
+
return this.handleRestResponse(response);
|
|
3716
|
+
}
|
|
3717
|
+
async linkGet(path) {
|
|
3718
|
+
const key = this.getDedupeKey("linkGet", path);
|
|
3719
|
+
return this.deduplicatedRequest(key, async () => {
|
|
3720
|
+
const response = await this.resilientFetch(`${this.linkApiUrl}${path}`, {
|
|
3721
|
+
method: "GET",
|
|
3722
|
+
credentials: this.credentials,
|
|
3723
|
+
headers: this.getHeaders()
|
|
3724
|
+
});
|
|
3725
|
+
return this.handleRestResponse(response);
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
async linkPost(path, body) {
|
|
3729
|
+
const response = await this.resilientFetch(`${this.linkApiUrl}${path}`, {
|
|
3730
|
+
method: "POST",
|
|
3731
|
+
credentials: this.credentials,
|
|
3732
|
+
headers: this.getHeaders(),
|
|
3733
|
+
body: body ? JSON.stringify(body) : void 0
|
|
3734
|
+
});
|
|
3735
|
+
return this.handleRestResponse(response);
|
|
3736
|
+
}
|
|
3737
|
+
async linkDelete(path) {
|
|
3738
|
+
const response = await this.resilientFetch(`${this.linkApiUrl}${path}`, {
|
|
3739
|
+
method: "DELETE",
|
|
3740
|
+
credentials: this.credentials,
|
|
3741
|
+
headers: this.getHeaders()
|
|
3742
|
+
});
|
|
3743
|
+
return this.handleRestResponse(response);
|
|
3744
|
+
}
|
|
3745
|
+
async handleRestResponse(response) {
|
|
3746
|
+
const json = await response.json();
|
|
3747
|
+
if (!response.ok) {
|
|
3748
|
+
const error = enrichError(
|
|
3749
|
+
new CimplifyError(
|
|
3750
|
+
json.error?.error_code || "API_ERROR",
|
|
3751
|
+
json.error?.error_message || "An error occurred",
|
|
3752
|
+
false
|
|
3753
|
+
),
|
|
3754
|
+
{ isTestMode: this.isTestMode() }
|
|
3755
|
+
);
|
|
3756
|
+
if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
|
|
3757
|
+
console.warn(
|
|
3758
|
+
"[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
throw error;
|
|
3762
|
+
}
|
|
3763
|
+
return json.data;
|
|
3764
|
+
}
|
|
3765
|
+
async handleResponse(response) {
|
|
3766
|
+
const json = await response.json();
|
|
3767
|
+
if (!json.success || json.error) {
|
|
3768
|
+
const error = enrichError(
|
|
3769
|
+
new CimplifyError(
|
|
3770
|
+
json.error?.code || "UNKNOWN_ERROR",
|
|
3771
|
+
json.error?.message || "An unknown error occurred",
|
|
3772
|
+
json.error?.retryable || false
|
|
3773
|
+
),
|
|
3774
|
+
{ isTestMode: this.isTestMode() }
|
|
3775
|
+
);
|
|
3776
|
+
if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
|
|
3777
|
+
console.warn(
|
|
3778
|
+
"[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
|
|
3779
|
+
);
|
|
3780
|
+
}
|
|
3781
|
+
throw error;
|
|
3782
|
+
}
|
|
3783
|
+
return json.data;
|
|
3784
|
+
}
|
|
3785
|
+
get catalogue() {
|
|
3786
|
+
if (!this._catalogue) {
|
|
3787
|
+
this._catalogue = new CatalogueQueries(this);
|
|
3788
|
+
}
|
|
3789
|
+
return this._catalogue;
|
|
3790
|
+
}
|
|
3791
|
+
get cart() {
|
|
3792
|
+
if (!this._cart) {
|
|
3793
|
+
this._cart = new CartOperations(this);
|
|
3794
|
+
}
|
|
3795
|
+
return this._cart;
|
|
3796
|
+
}
|
|
3797
|
+
get checkout() {
|
|
3798
|
+
if (!this._checkout) {
|
|
3799
|
+
this._checkout = new CheckoutService(this);
|
|
3800
|
+
}
|
|
3801
|
+
return this._checkout;
|
|
3802
|
+
}
|
|
3803
|
+
get orders() {
|
|
3804
|
+
if (!this._orders) {
|
|
3805
|
+
this._orders = new OrderQueries(this);
|
|
3806
|
+
}
|
|
3807
|
+
return this._orders;
|
|
3808
|
+
}
|
|
3809
|
+
get link() {
|
|
3810
|
+
if (!this._link) {
|
|
3811
|
+
this._link = new LinkService(this);
|
|
3812
|
+
}
|
|
3813
|
+
return this._link;
|
|
3814
|
+
}
|
|
3815
|
+
get auth() {
|
|
3816
|
+
if (!this._auth) {
|
|
3817
|
+
this._auth = new AuthService(this);
|
|
3818
|
+
}
|
|
3819
|
+
return this._auth;
|
|
3820
|
+
}
|
|
3821
|
+
get business() {
|
|
3822
|
+
if (!this._business) {
|
|
3823
|
+
this._business = new BusinessService(this);
|
|
3824
|
+
}
|
|
3825
|
+
return this._business;
|
|
3826
|
+
}
|
|
3827
|
+
get inventory() {
|
|
3828
|
+
if (!this._inventory) {
|
|
3829
|
+
this._inventory = new InventoryService(this);
|
|
3830
|
+
}
|
|
3831
|
+
return this._inventory;
|
|
3832
|
+
}
|
|
3833
|
+
get scheduling() {
|
|
3834
|
+
if (!this._scheduling) {
|
|
3835
|
+
this._scheduling = new SchedulingService(this);
|
|
3836
|
+
}
|
|
3837
|
+
return this._scheduling;
|
|
3838
|
+
}
|
|
3839
|
+
get lite() {
|
|
3840
|
+
if (!this._lite) {
|
|
3841
|
+
this._lite = new LiteService(this);
|
|
3842
|
+
}
|
|
3843
|
+
return this._lite;
|
|
3844
|
+
}
|
|
3845
|
+
get fx() {
|
|
3846
|
+
if (!this._fx) {
|
|
3847
|
+
this._fx = new FxService(this);
|
|
3848
|
+
}
|
|
3849
|
+
return this._fx;
|
|
3850
|
+
}
|
|
3851
|
+
/**
|
|
3852
|
+
* Create a CimplifyElements instance for embedding checkout components.
|
|
3853
|
+
* Like Stripe's stripe.elements().
|
|
3854
|
+
*
|
|
3855
|
+
* @param businessId - The business ID for checkout context
|
|
3856
|
+
* @param options - Optional configuration for elements
|
|
3857
|
+
*
|
|
3858
|
+
* @example
|
|
3859
|
+
* ```ts
|
|
3860
|
+
* const elements = client.elements('bus_xxx');
|
|
3861
|
+
* const authElement = elements.create('auth');
|
|
3862
|
+
* authElement.mount('#auth-container');
|
|
3863
|
+
* ```
|
|
3864
|
+
*/
|
|
3865
|
+
elements(businessId, options) {
|
|
3866
|
+
if (businessId) {
|
|
3867
|
+
this.setBusinessId(businessId);
|
|
3868
|
+
}
|
|
3869
|
+
return createElements(this, businessId ?? this.businessId ?? void 0, options);
|
|
3870
|
+
}
|
|
3871
|
+
};
|
|
3872
|
+
function createCimplifyClient(config = {}) {
|
|
3873
|
+
return new CimplifyClient(config);
|
|
3874
|
+
}
|
|
3875
|
+
var LOCATION_STORAGE_KEY = "cimplify_location_id";
|
|
3876
|
+
var DEFAULT_CURRENCY = "USD";
|
|
3877
|
+
var DEFAULT_COUNTRY = "US";
|
|
3878
|
+
function createDefaultClient() {
|
|
3879
|
+
const processRef = globalThis.process;
|
|
3880
|
+
const envPublicKey = processRef?.env?.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY || "";
|
|
3881
|
+
return createCimplifyClient({ publicKey: envPublicKey });
|
|
3882
|
+
}
|
|
3883
|
+
function getStoredLocationId() {
|
|
3884
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
3885
|
+
return null;
|
|
3886
|
+
}
|
|
3887
|
+
const value = window.localStorage.getItem(LOCATION_STORAGE_KEY);
|
|
3888
|
+
if (!value) {
|
|
3889
|
+
return null;
|
|
3890
|
+
}
|
|
3891
|
+
const normalized = value.trim();
|
|
3892
|
+
return normalized.length > 0 ? normalized : null;
|
|
3893
|
+
}
|
|
3894
|
+
function setStoredLocationId(locationId) {
|
|
3895
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
3896
|
+
return;
|
|
3897
|
+
}
|
|
3898
|
+
if (!locationId) {
|
|
3899
|
+
window.localStorage.removeItem(LOCATION_STORAGE_KEY);
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3902
|
+
window.localStorage.setItem(LOCATION_STORAGE_KEY, locationId);
|
|
3903
|
+
}
|
|
3904
|
+
function resolveInitialLocation(locations) {
|
|
3905
|
+
if (locations.length === 0) {
|
|
3906
|
+
return null;
|
|
3907
|
+
}
|
|
3908
|
+
const storedId = getStoredLocationId();
|
|
3909
|
+
if (storedId) {
|
|
3910
|
+
const matched = locations.find((location) => location.id === storedId);
|
|
3911
|
+
if (matched) {
|
|
3912
|
+
return matched;
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
return locations[0];
|
|
3916
|
+
}
|
|
3917
|
+
var CimplifyContext = react.createContext(null);
|
|
3918
|
+
function CimplifyProvider({
|
|
3919
|
+
client,
|
|
3920
|
+
children,
|
|
3921
|
+
onLocationChange
|
|
3922
|
+
}) {
|
|
3923
|
+
const resolvedClient = react.useMemo(() => client ?? createDefaultClient(), [client]);
|
|
3924
|
+
const onLocationChangeRef = react.useRef(onLocationChange);
|
|
3925
|
+
const [business, setBusiness] = react.useState(null);
|
|
3926
|
+
const [locations, setLocations] = react.useState([]);
|
|
3927
|
+
const [currentLocation, setCurrentLocationState] = react.useState(null);
|
|
3928
|
+
const [isReady, setIsReady] = react.useState(false);
|
|
3929
|
+
react.useEffect(() => {
|
|
3930
|
+
onLocationChangeRef.current = onLocationChange;
|
|
3931
|
+
}, [onLocationChange]);
|
|
3932
|
+
const isDemoMode = resolvedClient.getPublicKey().trim().length === 0;
|
|
3933
|
+
const setCurrentLocation = react.useCallback(
|
|
3934
|
+
(location) => {
|
|
3935
|
+
setCurrentLocationState(location);
|
|
3936
|
+
resolvedClient.setLocationId(location.id);
|
|
3937
|
+
setStoredLocationId(location.id);
|
|
3938
|
+
onLocationChangeRef.current?.(location);
|
|
3939
|
+
},
|
|
3940
|
+
[resolvedClient]
|
|
3941
|
+
);
|
|
3942
|
+
react.useEffect(() => {
|
|
3943
|
+
let cancelled = false;
|
|
3944
|
+
async function bootstrap() {
|
|
3945
|
+
setIsReady(false);
|
|
3946
|
+
if (isDemoMode) {
|
|
3947
|
+
if (!cancelled) {
|
|
3948
|
+
setBusiness(null);
|
|
3949
|
+
setLocations([]);
|
|
3950
|
+
setCurrentLocationState(null);
|
|
3951
|
+
resolvedClient.setLocationId(null);
|
|
3952
|
+
setStoredLocationId(null);
|
|
3953
|
+
setIsReady(true);
|
|
3954
|
+
}
|
|
3955
|
+
return;
|
|
3956
|
+
}
|
|
3957
|
+
const [businessResult, locationsResult] = await Promise.all([
|
|
3958
|
+
resolvedClient.business.getInfo(),
|
|
3959
|
+
resolvedClient.business.getLocations()
|
|
3960
|
+
]);
|
|
3961
|
+
if (cancelled) {
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
const nextBusiness = businessResult.ok ? businessResult.value : null;
|
|
3965
|
+
const nextLocations = locationsResult.ok && Array.isArray(locationsResult.value) ? locationsResult.value : [];
|
|
3966
|
+
const initialLocation = resolveInitialLocation(nextLocations);
|
|
3967
|
+
setBusiness(nextBusiness);
|
|
3968
|
+
if (nextBusiness?.id) {
|
|
3969
|
+
resolvedClient.setBusinessId(nextBusiness.id);
|
|
3970
|
+
}
|
|
3971
|
+
setLocations(nextLocations);
|
|
3972
|
+
if (initialLocation) {
|
|
3973
|
+
setCurrentLocationState(initialLocation);
|
|
3974
|
+
resolvedClient.setLocationId(initialLocation.id);
|
|
3975
|
+
setStoredLocationId(initialLocation.id);
|
|
3976
|
+
} else {
|
|
3977
|
+
setCurrentLocationState(null);
|
|
3978
|
+
resolvedClient.setLocationId(null);
|
|
3979
|
+
setStoredLocationId(null);
|
|
3980
|
+
}
|
|
3981
|
+
setIsReady(true);
|
|
3982
|
+
}
|
|
3983
|
+
bootstrap().catch(() => {
|
|
3984
|
+
if (cancelled) {
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
setBusiness(null);
|
|
3988
|
+
setLocations([]);
|
|
3989
|
+
setCurrentLocationState(null);
|
|
3990
|
+
resolvedClient.setLocationId(null);
|
|
3991
|
+
setStoredLocationId(null);
|
|
3992
|
+
setIsReady(true);
|
|
3993
|
+
});
|
|
3994
|
+
return () => {
|
|
3995
|
+
cancelled = true;
|
|
3996
|
+
};
|
|
3997
|
+
}, [resolvedClient, isDemoMode]);
|
|
3998
|
+
const contextValue = react.useMemo(
|
|
3999
|
+
() => ({
|
|
4000
|
+
client: resolvedClient,
|
|
4001
|
+
business,
|
|
4002
|
+
currency: business?.default_currency || DEFAULT_CURRENCY,
|
|
4003
|
+
country: business?.country_code || DEFAULT_COUNTRY,
|
|
4004
|
+
locations,
|
|
4005
|
+
currentLocation,
|
|
4006
|
+
setCurrentLocation,
|
|
4007
|
+
isReady,
|
|
4008
|
+
isDemoMode
|
|
4009
|
+
}),
|
|
4010
|
+
[
|
|
4011
|
+
resolvedClient,
|
|
4012
|
+
business,
|
|
4013
|
+
locations,
|
|
4014
|
+
currentLocation,
|
|
4015
|
+
setCurrentLocation,
|
|
4016
|
+
isReady,
|
|
4017
|
+
isDemoMode
|
|
4018
|
+
]
|
|
4019
|
+
);
|
|
4020
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CimplifyContext.Provider, { value: contextValue, children });
|
|
4021
|
+
}
|
|
4022
|
+
function useCimplify() {
|
|
4023
|
+
const context = react.useContext(CimplifyContext);
|
|
4024
|
+
if (!context) {
|
|
4025
|
+
throw new Error("useCimplify must be used within CimplifyProvider");
|
|
4026
|
+
}
|
|
4027
|
+
return context;
|
|
4028
|
+
}
|
|
4029
|
+
function useOptionalCimplify() {
|
|
4030
|
+
return react.useContext(CimplifyContext);
|
|
4031
|
+
}
|
|
4032
|
+
var productsCache = /* @__PURE__ */ new Map();
|
|
4033
|
+
var productsInflight = /* @__PURE__ */ new Map();
|
|
4034
|
+
function buildProductsCacheKey(client, locationId, options) {
|
|
4035
|
+
return JSON.stringify({
|
|
4036
|
+
key: client.getPublicKey(),
|
|
4037
|
+
location_id: locationId || "__none__",
|
|
4038
|
+
options
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
function useProducts(options = {}) {
|
|
4042
|
+
const context = useOptionalCimplify();
|
|
4043
|
+
const client = options.client ?? context?.client;
|
|
4044
|
+
if (!client) {
|
|
4045
|
+
throw new Error("useProducts must be used within CimplifyProvider or passed { client }.");
|
|
4046
|
+
}
|
|
4047
|
+
const enabled = options.enabled ?? true;
|
|
4048
|
+
const locationId = client.getLocationId();
|
|
4049
|
+
const previousLocationIdRef = react.useRef(locationId);
|
|
4050
|
+
const requestIdRef = react.useRef(0);
|
|
4051
|
+
const queryOptions = react.useMemo(
|
|
4052
|
+
() => ({
|
|
4053
|
+
category: options.category,
|
|
4054
|
+
collection: options.collection,
|
|
4055
|
+
search: options.search,
|
|
4056
|
+
featured: options.featured,
|
|
4057
|
+
limit: options.limit
|
|
4058
|
+
}),
|
|
4059
|
+
[options.category, options.collection, options.featured, options.limit, options.search]
|
|
4060
|
+
);
|
|
4061
|
+
const cacheKey = react.useMemo(
|
|
4062
|
+
() => buildProductsCacheKey(client, locationId, queryOptions),
|
|
4063
|
+
[client, locationId, queryOptions]
|
|
4064
|
+
);
|
|
4065
|
+
const cached = productsCache.get(cacheKey);
|
|
4066
|
+
const [products, setProducts] = react.useState(cached?.products ?? []);
|
|
4067
|
+
const [isLoading, setIsLoading] = react.useState(enabled && !cached);
|
|
4068
|
+
const [error, setError] = react.useState(null);
|
|
4069
|
+
react.useEffect(() => {
|
|
4070
|
+
if (previousLocationIdRef.current !== locationId) {
|
|
4071
|
+
productsCache.clear();
|
|
4072
|
+
productsInflight.clear();
|
|
4073
|
+
previousLocationIdRef.current = locationId;
|
|
4074
|
+
}
|
|
4075
|
+
}, [locationId]);
|
|
4076
|
+
const load = react.useCallback(
|
|
4077
|
+
async (force = false) => {
|
|
4078
|
+
if (!enabled) {
|
|
4079
|
+
setIsLoading(false);
|
|
4080
|
+
return;
|
|
4081
|
+
}
|
|
4082
|
+
const nextRequestId = ++requestIdRef.current;
|
|
4083
|
+
setError(null);
|
|
4084
|
+
if (!force) {
|
|
4085
|
+
const cacheEntry = productsCache.get(cacheKey);
|
|
4086
|
+
if (cacheEntry) {
|
|
4087
|
+
setProducts(cacheEntry.products);
|
|
4088
|
+
setIsLoading(false);
|
|
4089
|
+
return;
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
setIsLoading(true);
|
|
4093
|
+
try {
|
|
4094
|
+
const existing = productsInflight.get(cacheKey);
|
|
4095
|
+
const promise = existing ?? (async () => {
|
|
4096
|
+
const result = await client.catalogue.getProducts(queryOptions);
|
|
4097
|
+
if (!result.ok) {
|
|
4098
|
+
throw result.error;
|
|
4099
|
+
}
|
|
4100
|
+
return result.value;
|
|
4101
|
+
})();
|
|
4102
|
+
if (!existing) {
|
|
4103
|
+
productsInflight.set(
|
|
4104
|
+
cacheKey,
|
|
4105
|
+
promise.finally(() => {
|
|
4106
|
+
productsInflight.delete(cacheKey);
|
|
4107
|
+
})
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
4110
|
+
const value = await promise;
|
|
4111
|
+
productsCache.set(cacheKey, { products: value });
|
|
4112
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4113
|
+
setProducts(value);
|
|
4114
|
+
setError(null);
|
|
4115
|
+
}
|
|
4116
|
+
} catch (loadError) {
|
|
4117
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4118
|
+
setError(loadError);
|
|
4119
|
+
}
|
|
4120
|
+
} finally {
|
|
4121
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4122
|
+
setIsLoading(false);
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
},
|
|
4126
|
+
[cacheKey, client, enabled, queryOptions]
|
|
4127
|
+
);
|
|
4128
|
+
react.useEffect(() => {
|
|
4129
|
+
void load(false);
|
|
4130
|
+
}, [load]);
|
|
4131
|
+
const refetch = react.useCallback(async () => {
|
|
4132
|
+
productsCache.delete(cacheKey);
|
|
4133
|
+
await load(true);
|
|
4134
|
+
}, [cacheKey, load]);
|
|
4135
|
+
return { products, isLoading, error, refetch };
|
|
4136
|
+
}
|
|
4137
|
+
var productCache = /* @__PURE__ */ new Map();
|
|
4138
|
+
var productInflight = /* @__PURE__ */ new Map();
|
|
4139
|
+
function isLikelySlug(value) {
|
|
4140
|
+
return /^[a-z0-9-]+$/.test(value);
|
|
4141
|
+
}
|
|
4142
|
+
function buildProductCacheKey(client, locationId, slugOrId) {
|
|
4143
|
+
return JSON.stringify({
|
|
4144
|
+
key: client.getPublicKey(),
|
|
4145
|
+
location_id: locationId || "__none__",
|
|
4146
|
+
slug_or_id: slugOrId
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
function useProduct(slugOrId, options = {}) {
|
|
4150
|
+
const context = useOptionalCimplify();
|
|
4151
|
+
const client = options.client ?? context?.client;
|
|
4152
|
+
if (!client) {
|
|
4153
|
+
throw new Error("useProduct must be used within CimplifyProvider or passed { client }.");
|
|
4154
|
+
}
|
|
4155
|
+
const enabled = options.enabled ?? true;
|
|
4156
|
+
const locationId = client.getLocationId();
|
|
4157
|
+
const previousLocationIdRef = react.useRef(locationId);
|
|
4158
|
+
const requestIdRef = react.useRef(0);
|
|
4159
|
+
const normalizedSlugOrId = react.useMemo(() => (slugOrId || "").trim(), [slugOrId]);
|
|
4160
|
+
const cacheKey = react.useMemo(
|
|
4161
|
+
() => buildProductCacheKey(client, locationId, normalizedSlugOrId),
|
|
4162
|
+
[client, locationId, normalizedSlugOrId]
|
|
4163
|
+
);
|
|
4164
|
+
const cached = productCache.get(cacheKey);
|
|
4165
|
+
const [product, setProduct] = react.useState(cached?.product ?? null);
|
|
4166
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
4167
|
+
enabled && normalizedSlugOrId.length > 0 && !cached
|
|
4168
|
+
);
|
|
4169
|
+
const [error, setError] = react.useState(null);
|
|
4170
|
+
react.useEffect(() => {
|
|
4171
|
+
if (previousLocationIdRef.current !== locationId) {
|
|
4172
|
+
productCache.clear();
|
|
4173
|
+
productInflight.clear();
|
|
4174
|
+
previousLocationIdRef.current = locationId;
|
|
4175
|
+
}
|
|
4176
|
+
}, [locationId]);
|
|
4177
|
+
const load = react.useCallback(
|
|
4178
|
+
async (force = false) => {
|
|
4179
|
+
if (!enabled || normalizedSlugOrId.length === 0) {
|
|
4180
|
+
setProduct(null);
|
|
4181
|
+
setIsLoading(false);
|
|
4182
|
+
return;
|
|
4183
|
+
}
|
|
4184
|
+
const nextRequestId = ++requestIdRef.current;
|
|
4185
|
+
setError(null);
|
|
4186
|
+
if (!force) {
|
|
4187
|
+
const cacheEntry = productCache.get(cacheKey);
|
|
4188
|
+
if (cacheEntry) {
|
|
4189
|
+
setProduct(cacheEntry.product);
|
|
4190
|
+
setIsLoading(false);
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
setIsLoading(true);
|
|
4195
|
+
try {
|
|
4196
|
+
const existing = productInflight.get(cacheKey);
|
|
4197
|
+
const promise = existing ?? (async () => {
|
|
4198
|
+
const result = isLikelySlug(normalizedSlugOrId) ? await client.catalogue.getProductBySlug(normalizedSlugOrId) : await client.catalogue.getProduct(normalizedSlugOrId);
|
|
4199
|
+
if (!result.ok) {
|
|
4200
|
+
throw result.error;
|
|
4201
|
+
}
|
|
4202
|
+
return result.value;
|
|
4203
|
+
})();
|
|
4204
|
+
if (!existing) {
|
|
4205
|
+
productInflight.set(
|
|
4206
|
+
cacheKey,
|
|
4207
|
+
promise.finally(() => {
|
|
4208
|
+
productInflight.delete(cacheKey);
|
|
4209
|
+
})
|
|
4210
|
+
);
|
|
4211
|
+
}
|
|
4212
|
+
const value = await promise;
|
|
4213
|
+
productCache.set(cacheKey, { product: value });
|
|
4214
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4215
|
+
setProduct(value);
|
|
4216
|
+
setError(null);
|
|
4217
|
+
}
|
|
4218
|
+
} catch (loadError) {
|
|
4219
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4220
|
+
setError(loadError);
|
|
4221
|
+
}
|
|
4222
|
+
} finally {
|
|
4223
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4224
|
+
setIsLoading(false);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
},
|
|
4228
|
+
[cacheKey, client, enabled, normalizedSlugOrId]
|
|
4229
|
+
);
|
|
4230
|
+
react.useEffect(() => {
|
|
4231
|
+
void load(false);
|
|
4232
|
+
}, [load]);
|
|
4233
|
+
const refetch = react.useCallback(async () => {
|
|
4234
|
+
productCache.delete(cacheKey);
|
|
4235
|
+
await load(true);
|
|
4236
|
+
}, [cacheKey, load]);
|
|
4237
|
+
return { product, isLoading, error, refetch };
|
|
4238
|
+
}
|
|
4239
|
+
var categoriesCache = /* @__PURE__ */ new Map();
|
|
4240
|
+
var categoriesInflight = /* @__PURE__ */ new Map();
|
|
4241
|
+
function buildCategoriesCacheKey(client) {
|
|
4242
|
+
return client.getPublicKey() || "__demo__";
|
|
4243
|
+
}
|
|
4244
|
+
function useCategories(options = {}) {
|
|
4245
|
+
const context = useOptionalCimplify();
|
|
4246
|
+
const client = options.client ?? context?.client;
|
|
4247
|
+
if (!client) {
|
|
4248
|
+
throw new Error("useCategories must be used within CimplifyProvider or passed { client }.");
|
|
4249
|
+
}
|
|
4250
|
+
const enabled = options.enabled ?? true;
|
|
4251
|
+
const cacheKey = react.useMemo(() => buildCategoriesCacheKey(client), [client]);
|
|
4252
|
+
const cached = categoriesCache.get(cacheKey);
|
|
4253
|
+
const [categories, setCategories] = react.useState(cached ?? []);
|
|
4254
|
+
const [isLoading, setIsLoading] = react.useState(enabled && !cached);
|
|
4255
|
+
const [error, setError] = react.useState(null);
|
|
4256
|
+
const load = react.useCallback(
|
|
4257
|
+
async (force = false) => {
|
|
4258
|
+
if (!enabled) {
|
|
4259
|
+
setIsLoading(false);
|
|
4260
|
+
return;
|
|
4261
|
+
}
|
|
4262
|
+
setError(null);
|
|
4263
|
+
if (!force) {
|
|
4264
|
+
const cachedCategories = categoriesCache.get(cacheKey);
|
|
4265
|
+
if (cachedCategories) {
|
|
4266
|
+
setCategories(cachedCategories);
|
|
4267
|
+
setIsLoading(false);
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
setIsLoading(true);
|
|
4272
|
+
try {
|
|
4273
|
+
const existing = categoriesInflight.get(cacheKey);
|
|
4274
|
+
const promise = existing ?? (async () => {
|
|
4275
|
+
const result = await client.catalogue.getCategories();
|
|
4276
|
+
if (!result.ok) {
|
|
4277
|
+
throw result.error;
|
|
4278
|
+
}
|
|
4279
|
+
return result.value;
|
|
4280
|
+
})();
|
|
4281
|
+
if (!existing) {
|
|
4282
|
+
categoriesInflight.set(
|
|
4283
|
+
cacheKey,
|
|
4284
|
+
promise.finally(() => {
|
|
4285
|
+
categoriesInflight.delete(cacheKey);
|
|
4286
|
+
})
|
|
4287
|
+
);
|
|
4288
|
+
}
|
|
4289
|
+
const value = await promise;
|
|
4290
|
+
categoriesCache.set(cacheKey, value);
|
|
4291
|
+
setCategories(value);
|
|
4292
|
+
} catch (loadError) {
|
|
4293
|
+
setError(loadError);
|
|
4294
|
+
} finally {
|
|
4295
|
+
setIsLoading(false);
|
|
4296
|
+
}
|
|
4297
|
+
},
|
|
4298
|
+
[cacheKey, client, enabled]
|
|
4299
|
+
);
|
|
4300
|
+
react.useEffect(() => {
|
|
4301
|
+
void load(false);
|
|
4302
|
+
}, [load]);
|
|
4303
|
+
const refetch = react.useCallback(async () => {
|
|
4304
|
+
categoriesCache.delete(cacheKey);
|
|
4305
|
+
await load(true);
|
|
4306
|
+
}, [cacheKey, load]);
|
|
4307
|
+
return { categories, isLoading, error, refetch };
|
|
4308
|
+
}
|
|
4309
|
+
var CART_STORAGE_PREFIX = "cimplify:cart:v2";
|
|
4310
|
+
var DEFAULT_TAX_RATE = 0.08875;
|
|
4311
|
+
var cartStores = /* @__PURE__ */ new Map();
|
|
4312
|
+
function toNumber(value) {
|
|
4313
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
4314
|
+
return value;
|
|
4315
|
+
}
|
|
4316
|
+
if (typeof value === "string") {
|
|
4317
|
+
const parsed = Number.parseFloat(value);
|
|
4318
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
4319
|
+
}
|
|
4320
|
+
return 0;
|
|
4321
|
+
}
|
|
4322
|
+
function roundMoney(value) {
|
|
4323
|
+
return Math.max(0, Number(value.toFixed(2)));
|
|
4324
|
+
}
|
|
4325
|
+
function clampQuantity(quantity) {
|
|
4326
|
+
if (!Number.isFinite(quantity)) {
|
|
4327
|
+
return 1;
|
|
4328
|
+
}
|
|
4329
|
+
return Math.max(1, Math.floor(quantity));
|
|
4330
|
+
}
|
|
4331
|
+
function normalizeOptionIds(value) {
|
|
4332
|
+
if (!value || value.length === 0) {
|
|
4333
|
+
return void 0;
|
|
4334
|
+
}
|
|
4335
|
+
const normalized = Array.from(
|
|
4336
|
+
new Set(
|
|
4337
|
+
value.map((entry) => entry.trim()).filter((entry) => entry.length > 0)
|
|
4338
|
+
)
|
|
4339
|
+
).sort();
|
|
4340
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
4341
|
+
}
|
|
4342
|
+
function buildLineKey(productId, options) {
|
|
4343
|
+
const parts = [productId];
|
|
4344
|
+
if (options.locationId) {
|
|
4345
|
+
parts.push(`l:${options.locationId}`);
|
|
4346
|
+
}
|
|
4347
|
+
if (options.variantId) {
|
|
4348
|
+
parts.push(`v:${options.variantId}`);
|
|
4349
|
+
}
|
|
4350
|
+
const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
|
|
4351
|
+
if (addOnOptionIds && addOnOptionIds.length > 0) {
|
|
4352
|
+
parts.push(`a:${addOnOptionIds.join(",")}`);
|
|
4353
|
+
}
|
|
4354
|
+
if (options.bundleSelections && options.bundleSelections.length > 0) {
|
|
4355
|
+
parts.push(`b:${JSON.stringify(options.bundleSelections)}`);
|
|
4356
|
+
}
|
|
4357
|
+
if (options.compositeSelections && options.compositeSelections.length > 0) {
|
|
4358
|
+
parts.push(`c:${JSON.stringify(options.compositeSelections)}`);
|
|
4359
|
+
}
|
|
4360
|
+
if (options.quoteId) {
|
|
4361
|
+
parts.push(`q:${options.quoteId}`);
|
|
4362
|
+
}
|
|
4363
|
+
return parts.join("|");
|
|
4364
|
+
}
|
|
4365
|
+
function calculateLineSubtotal(item) {
|
|
4366
|
+
let unitPrice = parsePrice(item.product.default_price);
|
|
4367
|
+
if (item.variant?.price_adjustment) {
|
|
4368
|
+
unitPrice += parsePrice(item.variant.price_adjustment);
|
|
4369
|
+
}
|
|
4370
|
+
for (const option of item.addOnOptions || []) {
|
|
4371
|
+
if (option.default_price) {
|
|
4372
|
+
unitPrice += parsePrice(option.default_price);
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
return roundMoney(unitPrice * item.quantity);
|
|
4376
|
+
}
|
|
4377
|
+
function calculateSummary(items) {
|
|
4378
|
+
const subtotal = roundMoney(items.reduce((sum, item) => sum + calculateLineSubtotal(item), 0));
|
|
4379
|
+
const tax = roundMoney(subtotal * DEFAULT_TAX_RATE);
|
|
4380
|
+
return {
|
|
4381
|
+
subtotal,
|
|
4382
|
+
tax,
|
|
4383
|
+
total: roundMoney(subtotal + tax)
|
|
4384
|
+
};
|
|
4385
|
+
}
|
|
4386
|
+
function toProductFromServerItem(item, businessId) {
|
|
4387
|
+
return {
|
|
4388
|
+
id: item.item_id,
|
|
4389
|
+
business_id: businessId,
|
|
4390
|
+
category_id: item.category_id,
|
|
4391
|
+
name: item.name,
|
|
4392
|
+
slug: item.item_id,
|
|
4393
|
+
description: item.description,
|
|
4394
|
+
image_url: item.image_url,
|
|
4395
|
+
default_price: item.base_price,
|
|
4396
|
+
product_type: "product",
|
|
4397
|
+
inventory_type: "none",
|
|
4398
|
+
variant_strategy: "fetch_all",
|
|
4399
|
+
is_active: item.is_available,
|
|
4400
|
+
created_at: item.created_at,
|
|
4401
|
+
updated_at: item.updated_at,
|
|
4402
|
+
metadata: {
|
|
4403
|
+
from_sdk: true
|
|
4404
|
+
}
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
function mapServerCart(serverCart) {
|
|
4408
|
+
const items = serverCart.items.map((item) => {
|
|
4409
|
+
const variant = item.variant_info || item.variant_name ? {
|
|
4410
|
+
id: item.variant_info?.id || item.variant_id || "",
|
|
4411
|
+
name: item.variant_info?.name || item.variant_name || "Variant",
|
|
4412
|
+
price_adjustment: item.variant_info?.price_adjustment
|
|
4413
|
+
} : void 0;
|
|
4414
|
+
const quoteId = typeof item.metadata?.quote_id === "string" ? item.metadata.quote_id : void 0;
|
|
4415
|
+
return {
|
|
4416
|
+
id: item.id,
|
|
4417
|
+
product: toProductFromServerItem(item, serverCart.business_id),
|
|
4418
|
+
quantity: clampQuantity(item.quantity),
|
|
4419
|
+
locationId: serverCart.location_id,
|
|
4420
|
+
variantId: item.variant_id,
|
|
4421
|
+
variant,
|
|
4422
|
+
quoteId,
|
|
4423
|
+
addOnOptionIds: item.add_on_option_ids || void 0,
|
|
4424
|
+
addOnOptions: (item.add_on_options || []).map((option) => ({
|
|
4425
|
+
id: option.id,
|
|
4426
|
+
name: option.name,
|
|
4427
|
+
default_price: option.price
|
|
4428
|
+
})),
|
|
4429
|
+
bundleSelections: item.bundle_selections || void 0,
|
|
4430
|
+
compositeSelections: item.composite_selections || void 0,
|
|
4431
|
+
specialInstructions: item.special_instructions
|
|
4432
|
+
};
|
|
4433
|
+
});
|
|
4434
|
+
return {
|
|
4435
|
+
items,
|
|
4436
|
+
subtotal: roundMoney(toNumber(serverCart.pricing.subtotal)),
|
|
4437
|
+
tax: roundMoney(toNumber(serverCart.pricing.tax_amount)),
|
|
4438
|
+
total: roundMoney(toNumber(serverCart.pricing.total_price)),
|
|
4439
|
+
currency: serverCart.pricing.currency || "USD"
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
function buildStorageKey(storeKey) {
|
|
4443
|
+
return `${CART_STORAGE_PREFIX}:${storeKey}`;
|
|
4444
|
+
}
|
|
4445
|
+
function createEmptySnapshot(currency) {
|
|
4446
|
+
return {
|
|
4447
|
+
items: [],
|
|
4448
|
+
subtotal: 0,
|
|
4449
|
+
tax: 0,
|
|
4450
|
+
total: 0,
|
|
4451
|
+
currency,
|
|
4452
|
+
isLoading: true
|
|
4453
|
+
};
|
|
4454
|
+
}
|
|
4455
|
+
function createCartStore(params) {
|
|
4456
|
+
const { client, storeKey, locationId, isDemoMode, currency } = params;
|
|
4457
|
+
const storageKey = buildStorageKey(storeKey);
|
|
4458
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4459
|
+
let snapshot = createEmptySnapshot(currency);
|
|
4460
|
+
let initialized = false;
|
|
4461
|
+
let initializePromise = null;
|
|
4462
|
+
function emit() {
|
|
4463
|
+
listeners.forEach((listener) => {
|
|
4464
|
+
listener();
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4467
|
+
function setSnapshot(nextSnapshot) {
|
|
4468
|
+
snapshot = nextSnapshot;
|
|
4469
|
+
persistSnapshot();
|
|
4470
|
+
emit();
|
|
4471
|
+
}
|
|
4472
|
+
function updateSnapshot(updater) {
|
|
4473
|
+
setSnapshot(updater(snapshot));
|
|
4474
|
+
}
|
|
4475
|
+
function persistSnapshot() {
|
|
4476
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
4477
|
+
return;
|
|
4478
|
+
}
|
|
4479
|
+
try {
|
|
4480
|
+
window.localStorage.setItem(
|
|
4481
|
+
storageKey,
|
|
4482
|
+
JSON.stringify({
|
|
4483
|
+
items: snapshot.items,
|
|
4484
|
+
subtotal: snapshot.subtotal,
|
|
4485
|
+
tax: snapshot.tax,
|
|
4486
|
+
total: snapshot.total,
|
|
4487
|
+
currency: snapshot.currency
|
|
4488
|
+
})
|
|
4489
|
+
);
|
|
4490
|
+
} catch {
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
function hydrateFromStorage() {
|
|
4494
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
try {
|
|
4498
|
+
const raw = window.localStorage.getItem(storageKey);
|
|
4499
|
+
if (!raw) {
|
|
4500
|
+
return;
|
|
4501
|
+
}
|
|
4502
|
+
const parsed = JSON.parse(raw);
|
|
4503
|
+
if (!parsed || !Array.isArray(parsed.items)) {
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
snapshot = {
|
|
4507
|
+
items: parsed.items,
|
|
4508
|
+
subtotal: toNumber(parsed.subtotal),
|
|
4509
|
+
tax: toNumber(parsed.tax),
|
|
4510
|
+
total: toNumber(parsed.total),
|
|
4511
|
+
currency: typeof parsed.currency === "string" && parsed.currency ? parsed.currency : currency,
|
|
4512
|
+
isLoading: !isDemoMode
|
|
4513
|
+
};
|
|
4514
|
+
emit();
|
|
4515
|
+
} catch {
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
async function sync() {
|
|
4519
|
+
if (isDemoMode) {
|
|
4520
|
+
updateSnapshot((current) => ({
|
|
4521
|
+
...current,
|
|
4522
|
+
isLoading: false
|
|
4523
|
+
}));
|
|
4524
|
+
return;
|
|
4525
|
+
}
|
|
4526
|
+
updateSnapshot((current) => ({
|
|
4527
|
+
...current,
|
|
4528
|
+
isLoading: true
|
|
4529
|
+
}));
|
|
4530
|
+
const result = await client.cart.get();
|
|
4531
|
+
if (!result.ok) {
|
|
4532
|
+
updateSnapshot((current) => ({
|
|
4533
|
+
...current,
|
|
4534
|
+
isLoading: false
|
|
4535
|
+
}));
|
|
4536
|
+
throw result.error;
|
|
4537
|
+
}
|
|
4538
|
+
const next = mapServerCart(result.value);
|
|
4539
|
+
setSnapshot({
|
|
4540
|
+
...next,
|
|
4541
|
+
isLoading: false
|
|
4542
|
+
});
|
|
4543
|
+
}
|
|
4544
|
+
async function maybeResolveQuoteId(product, quantity, options) {
|
|
4545
|
+
if (options.quoteId) {
|
|
4546
|
+
return options.quoteId;
|
|
4547
|
+
}
|
|
4548
|
+
const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
|
|
4549
|
+
const requiresQuote = Boolean(
|
|
4550
|
+
options.variantId || addOnOptionIds && addOnOptionIds.length > 0 || options.bundleSelections && options.bundleSelections.length > 0 || options.compositeSelections && options.compositeSelections.length > 0
|
|
4551
|
+
);
|
|
4552
|
+
if (!requiresQuote || isDemoMode) {
|
|
4553
|
+
return void 0;
|
|
4554
|
+
}
|
|
4555
|
+
const quoteResult = await client.catalogue.fetchQuote({
|
|
4556
|
+
product_id: product.id,
|
|
4557
|
+
quantity,
|
|
4558
|
+
location_id: options.locationId || locationId || void 0,
|
|
4559
|
+
variant_id: options.variantId,
|
|
4560
|
+
add_on_option_ids: addOnOptionIds,
|
|
4561
|
+
bundle_selections: options.bundleSelections,
|
|
4562
|
+
composite_selections: options.compositeSelections
|
|
4563
|
+
});
|
|
4564
|
+
if (!quoteResult.ok) {
|
|
4565
|
+
throw quoteResult.error;
|
|
4566
|
+
}
|
|
4567
|
+
return quoteResult.value.quote_id;
|
|
4568
|
+
}
|
|
4569
|
+
async function addItem(product, quantity = 1, options = {}) {
|
|
4570
|
+
const resolvedQuantity = clampQuantity(quantity);
|
|
4571
|
+
const normalizedOptions = {
|
|
4572
|
+
...options,
|
|
4573
|
+
locationId: options.locationId || locationId || void 0,
|
|
4574
|
+
addOnOptionIds: normalizeOptionIds(options.addOnOptionIds)
|
|
4575
|
+
};
|
|
4576
|
+
const maybeProductWithVariants = product;
|
|
4577
|
+
if (Array.isArray(maybeProductWithVariants.variants) && maybeProductWithVariants.variants.length > 0 && !normalizedOptions.variantId) {
|
|
4578
|
+
console.warn(
|
|
4579
|
+
"[Cimplify] addItem() called without variantId for a product that has variants. Default variant pricing will be used."
|
|
4580
|
+
);
|
|
4581
|
+
}
|
|
4582
|
+
const previousSnapshot = snapshot;
|
|
4583
|
+
const lineKey = buildLineKey(product.id, normalizedOptions);
|
|
4584
|
+
const optimisticItem = {
|
|
4585
|
+
id: `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
4586
|
+
product,
|
|
4587
|
+
quantity: resolvedQuantity,
|
|
4588
|
+
locationId: normalizedOptions.locationId,
|
|
4589
|
+
variantId: normalizedOptions.variantId,
|
|
4590
|
+
variant: normalizedOptions.variant,
|
|
4591
|
+
quoteId: normalizedOptions.quoteId,
|
|
4592
|
+
addOnOptionIds: normalizedOptions.addOnOptionIds,
|
|
4593
|
+
addOnOptions: normalizedOptions.addOnOptions,
|
|
4594
|
+
bundleSelections: normalizedOptions.bundleSelections,
|
|
4595
|
+
compositeSelections: normalizedOptions.compositeSelections,
|
|
4596
|
+
specialInstructions: normalizedOptions.specialInstructions
|
|
4597
|
+
};
|
|
4598
|
+
updateSnapshot((current) => {
|
|
4599
|
+
const existingIndex = current.items.findIndex((item) => {
|
|
4600
|
+
const itemKey = buildLineKey(item.product.id, {
|
|
4601
|
+
locationId: item.locationId,
|
|
4602
|
+
variantId: item.variantId,
|
|
4603
|
+
quoteId: item.quoteId,
|
|
4604
|
+
addOnOptionIds: item.addOnOptionIds,
|
|
4605
|
+
bundleSelections: item.bundleSelections,
|
|
4606
|
+
compositeSelections: item.compositeSelections
|
|
4607
|
+
});
|
|
4608
|
+
return itemKey === lineKey;
|
|
4609
|
+
});
|
|
4610
|
+
const nextItems = [...current.items];
|
|
4611
|
+
if (existingIndex >= 0) {
|
|
4612
|
+
const existing = nextItems[existingIndex];
|
|
4613
|
+
nextItems[existingIndex] = {
|
|
4614
|
+
...existing,
|
|
4615
|
+
quantity: existing.quantity + resolvedQuantity
|
|
4616
|
+
};
|
|
4617
|
+
} else {
|
|
4618
|
+
nextItems.push(optimisticItem);
|
|
4619
|
+
}
|
|
4620
|
+
const summary = calculateSummary(nextItems);
|
|
4621
|
+
return {
|
|
4622
|
+
...current,
|
|
4623
|
+
items: nextItems,
|
|
4624
|
+
subtotal: summary.subtotal,
|
|
4625
|
+
tax: summary.tax,
|
|
4626
|
+
total: summary.total
|
|
4627
|
+
};
|
|
4628
|
+
});
|
|
4629
|
+
if (isDemoMode) {
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
try {
|
|
4633
|
+
const quoteId = await maybeResolveQuoteId(product, resolvedQuantity, normalizedOptions);
|
|
4634
|
+
const result = await client.cart.addItem({
|
|
4635
|
+
item_id: product.id,
|
|
4636
|
+
quantity: resolvedQuantity,
|
|
4637
|
+
variant_id: normalizedOptions.variantId,
|
|
4638
|
+
quote_id: quoteId,
|
|
4639
|
+
add_on_options: normalizedOptions.addOnOptionIds,
|
|
4640
|
+
special_instructions: normalizedOptions.specialInstructions,
|
|
4641
|
+
bundle_selections: normalizedOptions.bundleSelections,
|
|
4642
|
+
composite_selections: normalizedOptions.compositeSelections
|
|
4643
|
+
});
|
|
4644
|
+
if (!result.ok) {
|
|
4645
|
+
throw result.error;
|
|
4646
|
+
}
|
|
4647
|
+
await sync();
|
|
4648
|
+
} catch (error) {
|
|
4649
|
+
snapshot = previousSnapshot;
|
|
4650
|
+
persistSnapshot();
|
|
4651
|
+
emit();
|
|
4652
|
+
throw error;
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
async function removeItem(itemId) {
|
|
4656
|
+
const previousSnapshot = snapshot;
|
|
4657
|
+
updateSnapshot((current) => {
|
|
4658
|
+
const nextItems = current.items.filter((item) => item.id !== itemId);
|
|
4659
|
+
const summary = calculateSummary(nextItems);
|
|
4660
|
+
return {
|
|
4661
|
+
...current,
|
|
4662
|
+
items: nextItems,
|
|
4663
|
+
subtotal: summary.subtotal,
|
|
4664
|
+
tax: summary.tax,
|
|
4665
|
+
total: summary.total
|
|
4666
|
+
};
|
|
4667
|
+
});
|
|
4668
|
+
if (isDemoMode) {
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4671
|
+
try {
|
|
4672
|
+
const result = await client.cart.removeItem(itemId);
|
|
4673
|
+
if (!result.ok) {
|
|
4674
|
+
throw result.error;
|
|
4675
|
+
}
|
|
4676
|
+
await sync();
|
|
4677
|
+
} catch (error) {
|
|
4678
|
+
snapshot = previousSnapshot;
|
|
4679
|
+
persistSnapshot();
|
|
4680
|
+
emit();
|
|
4681
|
+
throw error;
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
async function updateQuantity(itemId, quantity) {
|
|
4685
|
+
if (quantity <= 0) {
|
|
4686
|
+
await removeItem(itemId);
|
|
4687
|
+
return;
|
|
4688
|
+
}
|
|
4689
|
+
const resolvedQuantity = clampQuantity(quantity);
|
|
4690
|
+
const previousSnapshot = snapshot;
|
|
4691
|
+
updateSnapshot((current) => {
|
|
4692
|
+
const nextItems = current.items.map(
|
|
4693
|
+
(item) => item.id === itemId ? {
|
|
4694
|
+
...item,
|
|
4695
|
+
quantity: resolvedQuantity
|
|
4696
|
+
} : item
|
|
4697
|
+
);
|
|
4698
|
+
const summary = calculateSummary(nextItems);
|
|
4699
|
+
return {
|
|
4700
|
+
...current,
|
|
4701
|
+
items: nextItems,
|
|
4702
|
+
subtotal: summary.subtotal,
|
|
4703
|
+
tax: summary.tax,
|
|
4704
|
+
total: summary.total
|
|
4705
|
+
};
|
|
4706
|
+
});
|
|
4707
|
+
if (isDemoMode) {
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
try {
|
|
4711
|
+
const result = await client.cart.updateQuantity(itemId, resolvedQuantity);
|
|
4712
|
+
if (!result.ok) {
|
|
4713
|
+
throw result.error;
|
|
4714
|
+
}
|
|
4715
|
+
await sync();
|
|
4716
|
+
} catch (error) {
|
|
4717
|
+
snapshot = previousSnapshot;
|
|
4718
|
+
persistSnapshot();
|
|
4719
|
+
emit();
|
|
4720
|
+
throw error;
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
async function clearCart() {
|
|
4724
|
+
const previousSnapshot = snapshot;
|
|
4725
|
+
updateSnapshot((current) => ({
|
|
4726
|
+
...current,
|
|
4727
|
+
items: [],
|
|
4728
|
+
subtotal: 0,
|
|
4729
|
+
tax: 0,
|
|
4730
|
+
total: 0
|
|
4731
|
+
}));
|
|
4732
|
+
if (isDemoMode) {
|
|
4733
|
+
return;
|
|
4734
|
+
}
|
|
4735
|
+
try {
|
|
4736
|
+
const result = await client.cart.clear();
|
|
4737
|
+
if (!result.ok) {
|
|
4738
|
+
throw result.error;
|
|
4739
|
+
}
|
|
4740
|
+
await sync();
|
|
4741
|
+
} catch (error) {
|
|
4742
|
+
snapshot = previousSnapshot;
|
|
4743
|
+
persistSnapshot();
|
|
4744
|
+
emit();
|
|
4745
|
+
throw error;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
async function initialize() {
|
|
4749
|
+
if (initialized) {
|
|
4750
|
+
if (initializePromise) {
|
|
4751
|
+
await initializePromise;
|
|
4752
|
+
}
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
initialized = true;
|
|
4756
|
+
hydrateFromStorage();
|
|
4757
|
+
if (isDemoMode) {
|
|
4758
|
+
updateSnapshot((current) => ({
|
|
4759
|
+
...current,
|
|
4760
|
+
isLoading: false
|
|
4761
|
+
}));
|
|
4762
|
+
return;
|
|
4763
|
+
}
|
|
4764
|
+
initializePromise = sync().catch(() => {
|
|
4765
|
+
updateSnapshot((current) => ({
|
|
4766
|
+
...current,
|
|
4767
|
+
isLoading: false
|
|
4768
|
+
}));
|
|
4769
|
+
});
|
|
4770
|
+
await initializePromise;
|
|
4771
|
+
initializePromise = null;
|
|
4772
|
+
}
|
|
4773
|
+
return {
|
|
4774
|
+
subscribe(listener) {
|
|
4775
|
+
listeners.add(listener);
|
|
4776
|
+
return () => {
|
|
4777
|
+
listeners.delete(listener);
|
|
4778
|
+
};
|
|
4779
|
+
},
|
|
4780
|
+
getSnapshot() {
|
|
4781
|
+
return snapshot;
|
|
4782
|
+
},
|
|
4783
|
+
initialize,
|
|
4784
|
+
addItem,
|
|
4785
|
+
removeItem,
|
|
4786
|
+
updateQuantity,
|
|
4787
|
+
clearCart,
|
|
4788
|
+
sync
|
|
4789
|
+
};
|
|
4790
|
+
}
|
|
4791
|
+
function getStoreKey(client, locationId, isDemoMode) {
|
|
4792
|
+
return [
|
|
4793
|
+
client.getPublicKey() || "__demo__",
|
|
4794
|
+
locationId || "__no_location__",
|
|
4795
|
+
isDemoMode ? "demo" : "live"
|
|
4796
|
+
].join(":");
|
|
4797
|
+
}
|
|
4798
|
+
function getOrCreateStore(params) {
|
|
4799
|
+
const { client, locationId, isDemoMode, currency } = params;
|
|
4800
|
+
const storeKey = getStoreKey(client, locationId, isDemoMode);
|
|
4801
|
+
const existing = cartStores.get(storeKey);
|
|
4802
|
+
if (existing) {
|
|
4803
|
+
return existing;
|
|
4804
|
+
}
|
|
4805
|
+
const created = createCartStore({
|
|
4806
|
+
client,
|
|
4807
|
+
storeKey,
|
|
4808
|
+
locationId,
|
|
4809
|
+
isDemoMode,
|
|
4810
|
+
currency
|
|
4811
|
+
});
|
|
4812
|
+
cartStores.set(storeKey, created);
|
|
4813
|
+
return created;
|
|
4814
|
+
}
|
|
4815
|
+
function useCart(options = {}) {
|
|
4816
|
+
const context = useOptionalCimplify();
|
|
4817
|
+
const client = options.client ?? context?.client;
|
|
4818
|
+
if (!client) {
|
|
4819
|
+
throw new Error("useCart must be used within CimplifyProvider or passed { client }.");
|
|
4820
|
+
}
|
|
4821
|
+
const locationId = options.locationId ?? client.getLocationId();
|
|
4822
|
+
const isDemoMode = options.demoMode ?? context?.isDemoMode ?? client.getPublicKey().trim().length === 0;
|
|
4823
|
+
const currency = options.currency ?? context?.currency ?? "USD";
|
|
4824
|
+
const store = react.useMemo(
|
|
4825
|
+
() => getOrCreateStore({
|
|
4826
|
+
client,
|
|
4827
|
+
locationId,
|
|
4828
|
+
isDemoMode,
|
|
4829
|
+
currency
|
|
4830
|
+
}),
|
|
4831
|
+
[client, currency, isDemoMode, locationId]
|
|
4832
|
+
);
|
|
4833
|
+
const snapshot = react.useSyncExternalStore(
|
|
4834
|
+
store.subscribe,
|
|
4835
|
+
store.getSnapshot,
|
|
4836
|
+
store.getSnapshot
|
|
4837
|
+
);
|
|
4838
|
+
react.useEffect(() => {
|
|
4839
|
+
void store.initialize();
|
|
4840
|
+
}, [store]);
|
|
4841
|
+
const addItem = react.useCallback(
|
|
4842
|
+
async (product, quantity, addOptions) => {
|
|
4843
|
+
await store.addItem(product, quantity, addOptions);
|
|
4844
|
+
},
|
|
4845
|
+
[store]
|
|
4846
|
+
);
|
|
4847
|
+
const removeItem = react.useCallback(
|
|
4848
|
+
async (itemId) => {
|
|
4849
|
+
await store.removeItem(itemId);
|
|
4850
|
+
},
|
|
4851
|
+
[store]
|
|
4852
|
+
);
|
|
4853
|
+
const updateQuantity = react.useCallback(
|
|
4854
|
+
async (itemId, quantity) => {
|
|
4855
|
+
await store.updateQuantity(itemId, quantity);
|
|
4856
|
+
},
|
|
4857
|
+
[store]
|
|
4858
|
+
);
|
|
4859
|
+
const clearCart = react.useCallback(async () => {
|
|
4860
|
+
await store.clearCart();
|
|
4861
|
+
}, [store]);
|
|
4862
|
+
const sync = react.useCallback(async () => {
|
|
4863
|
+
try {
|
|
4864
|
+
await store.sync();
|
|
4865
|
+
} catch (syncError) {
|
|
4866
|
+
throw syncError;
|
|
4867
|
+
}
|
|
4868
|
+
}, [store]);
|
|
4869
|
+
const itemCount = react.useMemo(
|
|
4870
|
+
() => snapshot.items.reduce((sum, item) => sum + item.quantity, 0),
|
|
4871
|
+
[snapshot.items]
|
|
4872
|
+
);
|
|
4873
|
+
return {
|
|
4874
|
+
items: snapshot.items,
|
|
4875
|
+
itemCount,
|
|
4876
|
+
subtotal: snapshot.subtotal,
|
|
4877
|
+
tax: snapshot.tax,
|
|
4878
|
+
total: snapshot.total,
|
|
4879
|
+
currency: snapshot.currency,
|
|
4880
|
+
isEmpty: itemCount === 0,
|
|
4881
|
+
isLoading: snapshot.isLoading,
|
|
4882
|
+
addItem,
|
|
4883
|
+
removeItem,
|
|
4884
|
+
updateQuantity,
|
|
4885
|
+
clearCart,
|
|
4886
|
+
sync
|
|
4887
|
+
};
|
|
4888
|
+
}
|
|
4889
|
+
var orderCache = /* @__PURE__ */ new Map();
|
|
4890
|
+
var orderInflight = /* @__PURE__ */ new Map();
|
|
4891
|
+
function buildOrderCacheKey(client, orderId) {
|
|
4892
|
+
return `${client.getPublicKey() || "__demo__"}:${orderId}`;
|
|
4893
|
+
}
|
|
4894
|
+
function useOrder(orderId, options = {}) {
|
|
4895
|
+
const context = useOptionalCimplify();
|
|
4896
|
+
const client = options.client ?? context?.client;
|
|
4897
|
+
if (!client) {
|
|
4898
|
+
throw new Error("useOrder must be used within CimplifyProvider or passed { client }.");
|
|
4899
|
+
}
|
|
4900
|
+
const normalizedOrderId = react.useMemo(() => (orderId || "").trim(), [orderId]);
|
|
4901
|
+
const enabled = options.enabled ?? true;
|
|
4902
|
+
const poll = options.poll ?? false;
|
|
4903
|
+
const pollInterval = options.pollInterval ?? 5e3;
|
|
4904
|
+
const requestIdRef = react.useRef(0);
|
|
4905
|
+
const cacheKey = react.useMemo(
|
|
4906
|
+
() => buildOrderCacheKey(client, normalizedOrderId),
|
|
4907
|
+
[client, normalizedOrderId]
|
|
4908
|
+
);
|
|
4909
|
+
const cached = orderCache.get(cacheKey);
|
|
4910
|
+
const [order, setOrder] = react.useState(cached?.order ?? null);
|
|
4911
|
+
const [isLoading, setIsLoading] = react.useState(
|
|
4912
|
+
enabled && normalizedOrderId.length > 0 && !cached
|
|
4913
|
+
);
|
|
4914
|
+
const [error, setError] = react.useState(null);
|
|
4915
|
+
const load = react.useCallback(
|
|
4916
|
+
async (force = false) => {
|
|
4917
|
+
if (!enabled || normalizedOrderId.length === 0) {
|
|
4918
|
+
setOrder(null);
|
|
4919
|
+
setIsLoading(false);
|
|
4920
|
+
return;
|
|
4921
|
+
}
|
|
4922
|
+
const nextRequestId = ++requestIdRef.current;
|
|
4923
|
+
setError(null);
|
|
4924
|
+
if (!force) {
|
|
4925
|
+
const cacheEntry = orderCache.get(cacheKey);
|
|
4926
|
+
if (cacheEntry) {
|
|
4927
|
+
setOrder(cacheEntry.order);
|
|
4928
|
+
setIsLoading(false);
|
|
4929
|
+
return;
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
setIsLoading(true);
|
|
4933
|
+
try {
|
|
4934
|
+
const existing = orderInflight.get(cacheKey);
|
|
4935
|
+
const promise = existing ?? (async () => {
|
|
4936
|
+
const result = await client.orders.get(normalizedOrderId);
|
|
4937
|
+
if (!result.ok) {
|
|
4938
|
+
throw result.error;
|
|
4939
|
+
}
|
|
4940
|
+
return result.value;
|
|
4941
|
+
})();
|
|
4942
|
+
if (!existing) {
|
|
4943
|
+
orderInflight.set(
|
|
4944
|
+
cacheKey,
|
|
4945
|
+
promise.finally(() => {
|
|
4946
|
+
orderInflight.delete(cacheKey);
|
|
4947
|
+
})
|
|
4948
|
+
);
|
|
4949
|
+
}
|
|
4950
|
+
const value = await promise;
|
|
4951
|
+
orderCache.set(cacheKey, { order: value });
|
|
4952
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4953
|
+
setOrder(value);
|
|
4954
|
+
setError(null);
|
|
4955
|
+
}
|
|
4956
|
+
} catch (loadError) {
|
|
4957
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4958
|
+
setError(loadError);
|
|
4959
|
+
}
|
|
4960
|
+
} finally {
|
|
4961
|
+
if (nextRequestId === requestIdRef.current) {
|
|
4962
|
+
setIsLoading(false);
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
},
|
|
4966
|
+
[cacheKey, client, enabled, normalizedOrderId]
|
|
4967
|
+
);
|
|
4968
|
+
react.useEffect(() => {
|
|
4969
|
+
void load(false);
|
|
4970
|
+
}, [load]);
|
|
4971
|
+
react.useEffect(() => {
|
|
4972
|
+
if (!poll || !enabled || normalizedOrderId.length === 0) {
|
|
4973
|
+
return;
|
|
4974
|
+
}
|
|
4975
|
+
const timer = window.setInterval(() => {
|
|
4976
|
+
void load(true);
|
|
4977
|
+
}, Math.max(1e3, pollInterval));
|
|
4978
|
+
return () => {
|
|
4979
|
+
window.clearInterval(timer);
|
|
4980
|
+
};
|
|
4981
|
+
}, [enabled, load, normalizedOrderId.length, poll, pollInterval]);
|
|
4982
|
+
const refetch = react.useCallback(async () => {
|
|
4983
|
+
orderCache.delete(cacheKey);
|
|
4984
|
+
await load(true);
|
|
4985
|
+
}, [cacheKey, load]);
|
|
4986
|
+
return { order, isLoading, error, refetch };
|
|
4987
|
+
}
|
|
4988
|
+
var LOCATION_STORAGE_KEY2 = "cimplify_location_id";
|
|
4989
|
+
function readStoredLocationId() {
|
|
4990
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
4991
|
+
return null;
|
|
4992
|
+
}
|
|
4993
|
+
const value = window.localStorage.getItem(LOCATION_STORAGE_KEY2);
|
|
4994
|
+
if (!value) {
|
|
4995
|
+
return null;
|
|
4996
|
+
}
|
|
4997
|
+
const normalized = value.trim();
|
|
4998
|
+
return normalized.length > 0 ? normalized : null;
|
|
4999
|
+
}
|
|
5000
|
+
function writeStoredLocationId(locationId) {
|
|
5001
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
5002
|
+
return;
|
|
5003
|
+
}
|
|
5004
|
+
if (!locationId) {
|
|
5005
|
+
window.localStorage.removeItem(LOCATION_STORAGE_KEY2);
|
|
5006
|
+
return;
|
|
5007
|
+
}
|
|
5008
|
+
window.localStorage.setItem(LOCATION_STORAGE_KEY2, locationId);
|
|
5009
|
+
}
|
|
5010
|
+
function resolveLocation(locations, preferredId) {
|
|
5011
|
+
if (locations.length === 0) {
|
|
5012
|
+
return null;
|
|
5013
|
+
}
|
|
5014
|
+
if (preferredId) {
|
|
5015
|
+
const found = locations.find((location) => location.id === preferredId);
|
|
5016
|
+
if (found) {
|
|
5017
|
+
return found;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
5020
|
+
return locations[0];
|
|
5021
|
+
}
|
|
5022
|
+
function useLocations(options = {}) {
|
|
5023
|
+
const context = useOptionalCimplify();
|
|
5024
|
+
if (context && (!options.client || context.client === options.client)) {
|
|
5025
|
+
return {
|
|
5026
|
+
locations: context.locations,
|
|
5027
|
+
currentLocation: context.currentLocation,
|
|
5028
|
+
setCurrentLocation: context.setCurrentLocation,
|
|
5029
|
+
isLoading: !context.isReady
|
|
5030
|
+
};
|
|
5031
|
+
}
|
|
5032
|
+
const client = options.client;
|
|
5033
|
+
const [locations, setLocations] = react.useState([]);
|
|
5034
|
+
const [currentLocation, setCurrentLocationState] = react.useState(null);
|
|
5035
|
+
const [isLoading, setIsLoading] = react.useState(true);
|
|
5036
|
+
const setCurrentLocation = react.useCallback(
|
|
5037
|
+
(location) => {
|
|
5038
|
+
setCurrentLocationState(location);
|
|
5039
|
+
if (client) {
|
|
5040
|
+
client.setLocationId(location.id);
|
|
5041
|
+
}
|
|
5042
|
+
writeStoredLocationId(location.id);
|
|
5043
|
+
},
|
|
5044
|
+
[client]
|
|
5045
|
+
);
|
|
5046
|
+
react.useEffect(() => {
|
|
5047
|
+
if (!client) {
|
|
5048
|
+
setLocations([]);
|
|
5049
|
+
setCurrentLocationState(null);
|
|
5050
|
+
setIsLoading(false);
|
|
5051
|
+
return;
|
|
5052
|
+
}
|
|
5053
|
+
const activeClient = client;
|
|
5054
|
+
let cancelled = false;
|
|
5055
|
+
async function loadLocations() {
|
|
5056
|
+
setIsLoading(true);
|
|
5057
|
+
const result = await activeClient.business.getLocations();
|
|
5058
|
+
if (cancelled) {
|
|
5059
|
+
return;
|
|
5060
|
+
}
|
|
5061
|
+
if (!result.ok) {
|
|
5062
|
+
setLocations([]);
|
|
5063
|
+
setCurrentLocationState(null);
|
|
5064
|
+
setIsLoading(false);
|
|
5065
|
+
return;
|
|
5066
|
+
}
|
|
5067
|
+
const nextLocations = result.value;
|
|
5068
|
+
const preferredId = activeClient.getLocationId() ?? readStoredLocationId();
|
|
5069
|
+
const resolved = resolveLocation(nextLocations, preferredId);
|
|
5070
|
+
setLocations(nextLocations);
|
|
5071
|
+
setCurrentLocationState(resolved);
|
|
5072
|
+
activeClient.setLocationId(resolved?.id || null);
|
|
5073
|
+
writeStoredLocationId(resolved?.id || null);
|
|
5074
|
+
setIsLoading(false);
|
|
5075
|
+
}
|
|
5076
|
+
void loadLocations();
|
|
5077
|
+
return () => {
|
|
5078
|
+
cancelled = true;
|
|
5079
|
+
};
|
|
5080
|
+
}, [client]);
|
|
5081
|
+
return {
|
|
5082
|
+
locations,
|
|
5083
|
+
currentLocation,
|
|
5084
|
+
setCurrentLocation,
|
|
5085
|
+
isLoading
|
|
5086
|
+
};
|
|
5087
|
+
}
|
|
5088
|
+
var ElementsContext = react.createContext({
|
|
5089
|
+
elements: null,
|
|
5090
|
+
isReady: false
|
|
5091
|
+
});
|
|
5092
|
+
function useElements() {
|
|
5093
|
+
return react.useContext(ElementsContext).elements;
|
|
5094
|
+
}
|
|
5095
|
+
function useElementsReady() {
|
|
5096
|
+
return react.useContext(ElementsContext).isReady;
|
|
5097
|
+
}
|
|
5098
|
+
function ElementsProvider({
|
|
5099
|
+
client,
|
|
5100
|
+
businessId,
|
|
5101
|
+
options,
|
|
5102
|
+
children
|
|
5103
|
+
}) {
|
|
5104
|
+
const [elements, setElements] = react.useState(null);
|
|
5105
|
+
const [isReady, setIsReady] = react.useState(false);
|
|
5106
|
+
const initialOptionsRef = react.useRef(options);
|
|
5107
|
+
react.useEffect(() => {
|
|
5108
|
+
let cancelled = false;
|
|
5109
|
+
let instance = null;
|
|
5110
|
+
setIsReady(false);
|
|
5111
|
+
setElements(null);
|
|
5112
|
+
async function bootstrap() {
|
|
5113
|
+
const resolvedBusinessId = businessId ?? await client.resolveBusinessId().catch(() => null);
|
|
5114
|
+
if (!resolvedBusinessId) {
|
|
5115
|
+
if (!cancelled) {
|
|
5116
|
+
setIsReady(false);
|
|
5117
|
+
setElements(null);
|
|
5118
|
+
}
|
|
5119
|
+
return;
|
|
5120
|
+
}
|
|
5121
|
+
instance = client.elements(resolvedBusinessId, initialOptionsRef.current);
|
|
5122
|
+
if (cancelled) {
|
|
5123
|
+
instance.destroy();
|
|
5124
|
+
return;
|
|
5125
|
+
}
|
|
5126
|
+
setElements(instance);
|
|
5127
|
+
setIsReady(true);
|
|
5128
|
+
}
|
|
5129
|
+
void bootstrap();
|
|
5130
|
+
return () => {
|
|
5131
|
+
cancelled = true;
|
|
5132
|
+
if (instance) {
|
|
5133
|
+
instance.destroy();
|
|
5134
|
+
}
|
|
5135
|
+
};
|
|
5136
|
+
}, [client, businessId]);
|
|
540
5137
|
return /* @__PURE__ */ jsxRuntime.jsx(ElementsContext.Provider, { value: { elements, isReady }, children });
|
|
541
5138
|
}
|
|
542
5139
|
function AuthElement({
|
|
@@ -653,9 +5250,18 @@ exports.AdProvider = AdProvider;
|
|
|
653
5250
|
exports.AddressElement = AddressElement;
|
|
654
5251
|
exports.AuthElement = AuthElement;
|
|
655
5252
|
exports.CimplifyCheckout = CimplifyCheckout;
|
|
5253
|
+
exports.CimplifyProvider = CimplifyProvider;
|
|
656
5254
|
exports.ElementsProvider = ElementsProvider;
|
|
657
5255
|
exports.PaymentElement = PaymentElement;
|
|
658
5256
|
exports.useAds = useAds;
|
|
5257
|
+
exports.useCart = useCart;
|
|
5258
|
+
exports.useCategories = useCategories;
|
|
659
5259
|
exports.useCheckout = useCheckout;
|
|
5260
|
+
exports.useCimplify = useCimplify;
|
|
660
5261
|
exports.useElements = useElements;
|
|
661
5262
|
exports.useElementsReady = useElementsReady;
|
|
5263
|
+
exports.useLocations = useLocations;
|
|
5264
|
+
exports.useOptionalCimplify = useOptionalCimplify;
|
|
5265
|
+
exports.useOrder = useOrder;
|
|
5266
|
+
exports.useProduct = useProduct;
|
|
5267
|
+
exports.useProducts = useProducts;
|