@cimplify/sdk 0.16.3 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.mjs CHANGED
@@ -1,4 +1,8 @@
1
1
  "use client";
2
+ import { getVariantDisplayName, createCimplifyClient } from './chunk-G6KNFYJD.mjs';
3
+ export { getVariantDisplayName } from './chunk-G6KNFYJD.mjs';
4
+ import { ELEMENT_TYPES, EVENT_TYPES } from './chunk-GFKBH7R2.mjs';
5
+ import { isSupportedCurrency, formatPrice, parsePrice, isOnSale, getDiscountPercentage, getBasePrice } from './chunk-OVM5YWM5.mjs';
2
6
  import React3, { createContext, useContext, useState, useEffect, useRef, useMemo, useSyncExternalStore, useCallback, useId } from 'react';
3
7
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
8
  import { NumberField } from '@base-ui/react/number-field';
@@ -13,8 +17,6 @@ import { Field } from '@base-ui/react/field';
13
17
  import { Input } from '@base-ui/react/input';
14
18
  import { Tabs } from '@base-ui/react/tabs';
15
19
 
16
- // src/ads/index.tsx
17
-
18
20
  // src/ads/identity.ts
19
21
  var COOKIE_NAME = "_cimplify_uid";
20
22
  var COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
@@ -277,3760 +279,99 @@ function Ad({
277
279
  }
278
280
  onImpression?.(ad.id);
279
281
  }
280
- },
281
- { threshold: 0.5 }
282
- );
283
- if (containerRef.current) {
284
- observer.observe(containerRef.current);
285
- }
286
- return () => observer.disconnect();
287
- }, [ad, siteId, slot, onImpression, identity, apiBase]);
288
- if (isLoading) {
289
- return /* @__PURE__ */ jsx(
290
- "div",
291
- {
292
- className: `cimplify-ad cimplify-ad--loading ${className || ""}`,
293
- style,
294
- "aria-hidden": "true"
295
- }
296
- );
297
- }
298
- if (!config?.enabled) return null;
299
- if (error) return fallback ? /* @__PURE__ */ jsx(Fragment, { children: fallback }) : null;
300
- if (!ad) return null;
301
- const handleClick = () => {
302
- onClick?.(ad.id);
303
- };
304
- const positionStyles = position === "fixed" ? { position: "fixed", bottom: 0, left: 0, right: 0, zIndex: 9999 } : position === "sticky" ? { position: "sticky", top: 20 } : {};
305
- return /* @__PURE__ */ jsx(
306
- "div",
307
- {
308
- ref: containerRef,
309
- id: `cimplify-ad-${ad.id}`,
310
- className: `cimplify-ad cimplify-ad--${slot} cimplify-ad--${position} cimplify-ad--${config.theme} ${className || ""}`,
311
- style: { ...positionStyles, ...style },
312
- onClick: handleClick,
313
- dangerouslySetInnerHTML: { __html: ad.html }
314
- }
315
- );
316
- }
317
-
318
- // src/utils/cart-transform.ts
319
- function getSelections(item) {
320
- if (item.bundle_resolved?.selections?.length) {
321
- return item.bundle_resolved.selections.map((s) => ({
322
- name: s.product_name || s.component_id,
323
- quantity: s.quantity,
324
- variant_name: s.variant_name
325
- }));
326
- }
327
- if (item.composite_resolved?.selections?.length) {
328
- return item.composite_resolved.selections.map((s) => ({
329
- name: s.component_name || s.component_id,
330
- quantity: s.quantity
331
- }));
332
- }
333
- return void 0;
334
- }
335
- function mapItem(item, fx) {
336
- const amt = (value) => fx ? String(fx.convertPrice(value)) : String(value);
337
- const result = {
338
- name: item.name,
339
- quantity: item.quantity,
340
- unit_price: amt(item.base_price),
341
- total_price: amt(item.total_price),
342
- line_type: item.line_type
343
- };
344
- if (item.image_url) result.image_url = item.image_url;
345
- const variantName = item.variant_info?.name || item.variant_name || void 0;
346
- if (variantName) result.variant_name = variantName;
347
- if (item.scheduled_start) result.scheduled_start = item.scheduled_start;
348
- if (item.scheduled_end) result.scheduled_end = item.scheduled_end;
349
- const selections = getSelections(item);
350
- if (selections) result.selections = selections;
351
- if (item.add_on_options?.length) {
352
- result.add_ons = item.add_on_options.map((opt) => ({
353
- name: opt.name,
354
- price: amt(opt.price ?? "0")
355
- }));
356
- }
357
- if (item.special_instructions) {
358
- result.special_instructions = item.special_instructions;
359
- }
360
- return result;
361
- }
362
- function transformToCheckoutCart(cart, fx) {
363
- const amt = (value) => fx ? String(fx.convertPrice(value)) : String(value);
364
- return {
365
- items: cart.items.map((item) => mapItem(item, fx)),
366
- subtotal: amt(cart.pricing.subtotal),
367
- tax_amount: amt(cart.pricing.tax_amount),
368
- total_discounts: amt(cart.pricing.total_discounts),
369
- service_charge: amt(cart.pricing.service_charge),
370
- total: amt(cart.pricing.total_price),
371
- currency: fx?.displayCurrency ?? cart.pricing.currency
372
- };
373
- }
374
-
375
- // src/types/common.ts
376
- function money(value) {
377
- return typeof value === "string" ? parseFloat(value) || 0 : value;
378
- }
379
- function moneyFromNumber(value) {
380
- return value;
381
- }
382
- var SUPPORTED_CURRENCY_CODES = /* @__PURE__ */ new Set([
383
- "USD",
384
- "EUR",
385
- "GBP",
386
- "JPY",
387
- "CNY",
388
- "CHF",
389
- "CAD",
390
- "AUD",
391
- "GHS",
392
- "NGN",
393
- "KES",
394
- "ZAR",
395
- "XOF",
396
- "XAF",
397
- "EGP",
398
- "TZS",
399
- "UGX",
400
- "RWF",
401
- "ETB",
402
- "ZMW",
403
- "BWP",
404
- "MUR",
405
- "NAD",
406
- "MWK",
407
- "AOA",
408
- "CDF",
409
- "GMD",
410
- "GNF",
411
- "LRD",
412
- "SLL",
413
- "MZN",
414
- "BIF",
415
- "INR",
416
- "BRL",
417
- "MXN",
418
- "KRW",
419
- "TRY",
420
- "THB",
421
- "MYR",
422
- "PHP",
423
- "IDR",
424
- "VND",
425
- "SGD",
426
- "HKD",
427
- "TWD",
428
- "AED",
429
- "SAR",
430
- "ILS"
431
- ]);
432
- function isSupportedCurrency(code) {
433
- return SUPPORTED_CURRENCY_CODES.has(code);
434
- }
435
- function currencyCode(value) {
436
- return value;
437
- }
438
- var ErrorCode = {
439
- UNKNOWN_ERROR: "UNKNOWN_ERROR",
440
- NETWORK_ERROR: "NETWORK_ERROR",
441
- TIMEOUT: "TIMEOUT",
442
- UNAUTHORIZED: "UNAUTHORIZED",
443
- NOT_FOUND: "NOT_FOUND"};
444
- var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
445
- function docsUrlForCode(code) {
446
- return `${DOCS_ERROR_BASE_URL}#${code.toLowerCase().replace(/_/g, "-")}`;
447
- }
448
- var ERROR_SUGGESTIONS = {
449
- UNKNOWN_ERROR: "An unexpected error occurred. Capture the request/response payload and retry with exponential backoff.",
450
- NETWORK_ERROR: "Check the shopper's connection and retry. If this persists, inspect CORS, DNS, and API reachability.",
451
- TIMEOUT: "The request exceeded the timeout. Retry once, then poll order status before charging again.",
452
- UNAUTHORIZED: "Authentication is missing or expired. Ensure a valid access token is set and refresh the session if needed.",
453
- FORBIDDEN: "The key/session lacks permission for this resource. Verify business ownership and API key scope.",
454
- NOT_FOUND: "The requested resource does not exist or is not visible in this environment.",
455
- VALIDATION_ERROR: "One or more fields are invalid. Validate required fields and enum values before retrying.",
456
- CART_EMPTY: "The cart has no items. Redirect back to menu/catalogue and require at least one line item.",
457
- CART_EXPIRED: "This cart is no longer active. Recreate a new cart and re-add shopper selections.",
458
- CART_NOT_FOUND: "Cart could not be located. It may have expired or belongs to a different key/location.",
459
- ITEM_UNAVAILABLE: "The selected item is unavailable at this location/time. Prompt the shopper to pick an alternative.",
460
- VARIANT_NOT_FOUND: "The requested variant no longer exists. Refresh product data and require re-selection.",
461
- VARIANT_OUT_OF_STOCK: "The selected variant is out of stock. Show in-stock variants and block checkout for this line.",
462
- ADDON_REQUIRED: "A required add-on is missing. Ensure required modifier groups are completed before add-to-cart.",
463
- ADDON_MAX_EXCEEDED: "Too many add-ons were selected. Enforce max selections client-side before submission.",
464
- CHECKOUT_VALIDATION_FAILED: "Checkout payload failed validation. Verify customer, order type, and address fields are complete.",
465
- DELIVERY_ADDRESS_REQUIRED: "Delivery orders require an address. Collect and pass address info before processing checkout.",
466
- CUSTOMER_INFO_REQUIRED: "Customer details are required. Ensure name/email/phone are available before checkout.",
467
- QUOTE_NOT_FOUND: "Quote could not be found. Refresh pricing and create a new quote before checkout.",
468
- QUOTE_EXPIRED: "Quote has expired. Re-fetch pricing to generate a new quote with a valid expiry window.",
469
- QUOTE_CONSUMED: "Quote has already been used. Request a fresh quote to prevent duplicate checkout attempts.",
470
- QUOTE_STORAGE_UNAVAILABLE: "Quote storage is temporarily unavailable. Retry shortly and avoid charging until quote fetch succeeds.",
471
- PAYMENT_FAILED: "Payment provider rejected or failed processing. Show retry/change-method options to the shopper.",
472
- PAYMENT_CANCELLED: "Payment was cancelled by the shopper or provider flow. Allow a safe retry path.",
473
- INSUFFICIENT_FUNDS: "Payment method has insufficient funds. Prompt shopper to use another method.",
474
- CARD_DECLINED: "Card was declined. Ask shopper to retry or switch payment method.",
475
- INVALID_OTP: "Authorization code is invalid. Let shopper re-enter OTP/PIN and retry.",
476
- OTP_EXPIRED: "Authorization code expired. Request a new OTP and re-submit authorization.",
477
- AUTHORIZATION_FAILED: "Additional payment authorization failed. Retry authorization or change payment method.",
478
- PAYMENT_ACTION_NOT_COMPLETED: "Required payment action was not completed. Resume provider flow and poll for status.",
479
- SLOT_UNAVAILABLE: "Selected schedule slot is unavailable. Refresh available slots and ask shopper to reselect.",
480
- BOOKING_CONFLICT: "The requested booking conflicts with an existing reservation. Pick another slot/resource.",
481
- SERVICE_NOT_FOUND: "Requested service no longer exists. Refresh service catalogue and retry selection.",
482
- OUT_OF_STOCK: "Inventory is depleted for this item. Remove it or reduce quantity before checkout.",
483
- INSUFFICIENT_QUANTITY: "Requested quantity exceeds available inventory. Reduce quantity and retry.",
484
- BUSINESS_ID_REQUIRED: "Business context could not be resolved. Verify the public key and business bootstrap call.",
485
- INVALID_CART: "Cart is invalid for checkout. Sync cart state, ensure items exist, then retry.",
486
- ORDER_TYPE_REQUIRED: "Order type is required. Provide one of delivery, pickup, or dine_in before checkout.",
487
- NO_PAYMENT_ELEMENT: "PaymentElement is required for processCheckout(). Mount it before triggering checkout.",
488
- PAYMENT_NOT_MOUNTED: "PaymentElement iframe is not mounted. Mount it in the DOM before processCheckout().",
489
- AUTH_INCOMPLETE: "AuthElement has not completed authentication. Wait for AUTHENTICATED before checkout.",
490
- AUTH_LOST: "Session was cleared during checkout. Re-authenticate and restart checkout safely.",
491
- ALREADY_PROCESSING: "Checkout is already in progress. Disable duplicate submits until completion.",
492
- CHECKOUT_NOT_READY: "Checkout elements are still initializing. Wait for readiness before submit.",
493
- CANCELLED: "Checkout was cancelled. Preserve cart state and allow shopper to retry.",
494
- REQUEST_TIMEOUT: "Provider call timed out. Poll payment/order status before issuing another charge attempt.",
495
- POPUP_BLOCKED: "Browser blocked provider popup. Ask shopper to enable popups and retry.",
496
- FX_QUOTE_FAILED: "Failed to lock FX quote. Retry currency quote or fallback to base currency."
497
- };
498
- var ERROR_HINTS = Object.fromEntries(
499
- Object.entries(ERROR_SUGGESTIONS).map(([code, suggestion]) => [
500
- code,
501
- {
502
- docs_url: docsUrlForCode(code),
503
- suggestion
504
- }
505
- ])
506
- );
507
- var CimplifyError = class extends Error {
508
- constructor(code, message, retryable = false, docs_url, suggestion) {
509
- super(message);
510
- this.code = code;
511
- this.retryable = retryable;
512
- this.docs_url = docs_url;
513
- this.suggestion = suggestion;
514
- this.name = "CimplifyError";
515
- }
516
- get userMessage() {
517
- return this.message;
518
- }
519
- };
520
- function getErrorHint(code) {
521
- return ERROR_HINTS[code];
522
- }
523
- function enrichError(error, options = {}) {
524
- const hint = getErrorHint(error.code);
525
- if (hint) {
526
- if (!error.docs_url) {
527
- error.docs_url = hint.docs_url;
528
- }
529
- if (!error.suggestion) {
530
- error.suggestion = hint.suggestion;
531
- }
532
- } else if (!error.docs_url) {
533
- error.docs_url = docsUrlForCode(error.code || ErrorCode.UNKNOWN_ERROR);
534
- }
535
- if (options.isTestMode && !error.message.includes("pk_test_")) {
536
- error.message = `${error.message}
537
-
538
- \u2139 Your API key is a test-mode key (pk_test_...). Verify test data/session before retrying.`;
539
- }
540
- return error;
541
- }
542
-
543
- // src/types/result.ts
544
- function ok(value) {
545
- return { ok: true, value };
546
- }
547
- function err(error) {
548
- return { ok: false, error };
549
- }
550
-
551
- // src/internal/safe.ts
552
- function toCimplifyError(error) {
553
- if (error instanceof CimplifyError) return enrichError(error);
554
- if (error instanceof Error) {
555
- return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
556
- }
557
- return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
558
- }
559
- async function safe(promise) {
560
- try {
561
- return ok(await promise);
562
- } catch (error) {
563
- return err(toCimplifyError(error));
564
- }
565
- }
566
-
567
- // src/catalogue.ts
568
- function withQuery(path, params) {
569
- const query = new URLSearchParams();
570
- for (const [key, value] of Object.entries(params)) {
571
- if (value === void 0) continue;
572
- query.set(key, String(value));
573
- }
574
- const queryString = query.toString();
575
- return queryString ? `${path}?${queryString}` : path;
576
- }
577
- function isRecord(value) {
578
- return typeof value === "object" && value !== null;
579
- }
580
- function readFinalPrice(value) {
581
- if (!isRecord(value)) return void 0;
582
- const finalPrice = value.final_price;
583
- if (typeof finalPrice === "string" || typeof finalPrice === "number") {
584
- return finalPrice;
585
- }
586
- return void 0;
587
- }
588
- function normalizeAddOnPayload(addOn) {
589
- if (!isRecord(addOn)) return addOn;
590
- const normalizedAddOn = { ...addOn };
591
- const options = normalizedAddOn["options"];
592
- if (!Array.isArray(options)) return normalizedAddOn;
593
- normalizedAddOn["options"] = options.map((option) => {
594
- if (!isRecord(option)) return option;
595
- const normalizedOption = { ...option };
596
- const optionPrice = normalizedOption["default_price"];
597
- if (optionPrice === void 0 || optionPrice === null || optionPrice === "") {
598
- normalizedOption["default_price"] = readFinalPrice(normalizedOption["default_price_info"]) || readFinalPrice(normalizedOption["price_info"]) || "0";
599
- }
600
- return normalizedOption;
601
- });
602
- return normalizedAddOn;
603
- }
604
- function normalizeCatalogueProductPayload(product) {
605
- const normalized = { ...product };
606
- const defaultPrice = normalized["default_price"];
607
- if (defaultPrice === void 0 || defaultPrice === null || defaultPrice === "") {
608
- 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);
609
- normalized["default_price"] = derivedDefaultPrice ?? "0";
610
- }
611
- const addOns = normalized["add_ons"];
612
- if (Array.isArray(addOns)) {
613
- normalized["add_ons"] = addOns.map((addOn) => normalizeAddOnPayload(addOn));
614
- }
615
- return normalized;
616
- }
617
- function toFiniteNumber(value) {
618
- if (typeof value === "number" && Number.isFinite(value)) {
619
- return value;
620
- }
621
- if (typeof value === "string" && value.trim().length > 0) {
622
- const parsed = Number(value);
623
- if (Number.isFinite(parsed)) {
624
- return parsed;
625
- }
626
- }
627
- return void 0;
628
- }
629
- function normalizePagination(value) {
630
- if (!isRecord(value)) {
631
- return void 0;
632
- }
633
- const totalCount = toFiniteNumber(value.total_count);
634
- const currentPage = toFiniteNumber(value.current_page);
635
- const pageSize = toFiniteNumber(value.page_size);
636
- const totalPages = toFiniteNumber(value.total_pages);
637
- if (totalCount === void 0 || currentPage === void 0 || pageSize === void 0 || totalPages === void 0) {
638
- return void 0;
639
- }
640
- return {
641
- total_count: totalCount,
642
- current_page: currentPage,
643
- page_size: pageSize,
644
- total_pages: totalPages,
645
- has_more: value.has_more === true,
646
- next_cursor: typeof value.next_cursor === "string" ? value.next_cursor : void 0
647
- };
648
- }
649
- function normalizeCatalogueResult(payload) {
650
- if (Array.isArray(payload)) {
651
- return {
652
- items: payload.map((product) => normalizeCatalogueProductPayload(product)),
653
- is_complete: true
654
- };
655
- }
656
- if (!isRecord(payload)) {
657
- return {
658
- items: [],
659
- is_complete: true
660
- };
661
- }
662
- const rawItems = Array.isArray(payload.products) ? payload.products : Array.isArray(payload.items) ? payload.items : [];
663
- return {
664
- items: rawItems.map((product) => normalizeCatalogueProductPayload(product)),
665
- is_complete: typeof payload.is_complete === "boolean" ? payload.is_complete : true,
666
- total_available: toFiniteNumber(payload.total_available),
667
- pagination: normalizePagination(payload.pagination)
668
- };
669
- }
670
- function normalizeCatalogueSnapshot(payload) {
671
- if (Array.isArray(payload)) {
672
- return {
673
- categories: [],
674
- products: payload.map((product) => normalizeCatalogueProductPayload(product)),
675
- add_ons: [],
676
- is_complete: true
677
- };
678
- }
679
- if (!isRecord(payload)) {
680
- return {
681
- categories: [],
682
- products: [],
683
- add_ons: [],
684
- is_complete: true
685
- };
686
- }
687
- const rawProducts = Array.isArray(payload.products) ? payload.products : Array.isArray(payload.items) ? payload.items : [];
688
- const rawCategories = Array.isArray(payload.categories) ? payload.categories : [];
689
- const rawAddOns = Array.isArray(payload.add_ons) ? payload.add_ons : [];
690
- return {
691
- categories: rawCategories,
692
- products: rawProducts.map((product) => normalizeCatalogueProductPayload(product)),
693
- add_ons: rawAddOns.map((addOn) => normalizeAddOnPayload(addOn)),
694
- is_complete: typeof payload.is_complete === "boolean" ? payload.is_complete : true,
695
- total_available: toFiniteNumber(payload.total_available),
696
- pagination: normalizePagination(payload.pagination)
697
- };
698
- }
699
- var CatalogueQueries = class {
700
- constructor(client) {
701
- this.client = client;
702
- }
703
- async getCatalogue() {
704
- const result = await safe(this.client.get("/api/v1/catalogue"));
705
- if (!result.ok) return result;
706
- return ok(normalizeCatalogueSnapshot(result.value));
707
- }
708
- async getProducts(options) {
709
- const path = withQuery("/api/v1/catalogue/products", {
710
- category_id: options?.category,
711
- taxonomy_id: options?.taxonomy,
712
- search: options?.search,
713
- page: options?.page,
714
- tags: options?.tags?.join(","),
715
- featured: options?.featured,
716
- in_stock: options?.in_stock,
717
- min_price: options?.min_price,
718
- max_price: options?.max_price,
719
- sort_by: options?.sort_by,
720
- sort_order: options?.sort_order,
721
- limit: options?.limit,
722
- offset: options?.offset,
723
- cursor: options?.cursor
724
- });
725
- const result = await safe(this.client.get(path));
726
- if (!result.ok) return result;
727
- return ok(normalizeCatalogueResult(result.value));
728
- }
729
- async getProduct(id) {
730
- const encodedId = encodeURIComponent(id);
731
- const result = await safe(this.client.get(`/api/v1/catalogue/products/${encodedId}`));
732
- if (!result.ok) return result;
733
- return ok(normalizeCatalogueProductPayload(result.value));
734
- }
735
- async getProductBySlug(slug) {
736
- const encodedSlug = encodeURIComponent(slug);
737
- const result = await safe(this.client.get(`/api/v1/catalogue/products/slug/${encodedSlug}`));
738
- if (!result.ok) return result;
739
- return ok(normalizeCatalogueProductPayload(result.value));
740
- }
741
- async getVariants(productId) {
742
- const encodedId = encodeURIComponent(productId);
743
- return safe(this.client.get(`/api/v1/catalogue/products/${encodedId}/variants`));
744
- }
745
- async getVariantAxes(productId) {
746
- const encodedId = encodeURIComponent(productId);
747
- return safe(this.client.get(`/api/v1/catalogue/products/${encodedId}/variant-axes`));
748
- }
749
- async getVariantByAxisSelections(productId, selections) {
750
- const encodedId = encodeURIComponent(productId);
751
- return safe(
752
- this.client.post(
753
- `/api/v1/catalogue/products/${encodedId}/variants/find`,
754
- { axis_selections: selections }
755
- )
756
- );
757
- }
758
- async getVariantById(productId, variantId) {
759
- const encodedProductId = encodeURIComponent(productId);
760
- const encodedVariantId = encodeURIComponent(variantId);
761
- return safe(
762
- this.client.get(
763
- `/api/v1/catalogue/products/${encodedProductId}/variants/${encodedVariantId}`
764
- )
765
- );
766
- }
767
- async getAddOns(productId) {
768
- const encodedId = encodeURIComponent(productId);
769
- return safe(this.client.get(`/api/v1/catalogue/products/${encodedId}/add-ons`));
770
- }
771
- async getCategories() {
772
- return safe(this.client.get("/api/v1/catalogue/categories"));
773
- }
774
- async getCategory(id) {
775
- const encodedId = encodeURIComponent(id);
776
- return safe(this.client.get(`/api/v1/catalogue/categories/${encodedId}`));
777
- }
778
- async getCategoryBySlug(slug) {
779
- const encodedSlug = encodeURIComponent(slug);
780
- return safe(this.client.get(`/api/v1/catalogue/categories/slug/${encodedSlug}`));
781
- }
782
- async getCategoryProducts(categoryId) {
783
- const encodedId = encodeURIComponent(categoryId);
784
- return safe(this.client.get(`/api/v1/catalogue/categories/${encodedId}/products`));
785
- }
786
- async getCollections() {
787
- return safe(this.client.get("/api/v1/catalogue/collections"));
788
- }
789
- async getCollection(id) {
790
- const encodedId = encodeURIComponent(id);
791
- return safe(this.client.get(`/api/v1/catalogue/collections/${encodedId}`));
792
- }
793
- async getCollectionBySlug(slug) {
794
- const encodedSlug = encodeURIComponent(slug);
795
- return safe(this.client.get(`/api/v1/catalogue/collections/slug/${encodedSlug}`));
796
- }
797
- async getCollectionProducts(collectionId) {
798
- const encodedId = encodeURIComponent(collectionId);
799
- return safe(this.client.get(`/api/v1/catalogue/collections/${encodedId}/products`));
800
- }
801
- async searchCollections(query, limit = 20) {
802
- const path = withQuery("/api/v1/catalogue/collections", { search: query, limit });
803
- return safe(this.client.get(path));
804
- }
805
- async getBundles() {
806
- return safe(this.client.get("/api/v1/catalogue/bundles"));
807
- }
808
- async searchBundles(query, limit = 20) {
809
- const path = withQuery("/api/v1/catalogue/bundles", { search: query, limit });
810
- return safe(this.client.get(path));
811
- }
812
- async getComposites(options) {
813
- const path = withQuery("/api/v1/catalogue/composites", { limit: options?.limit });
814
- return safe(this.client.get(path));
815
- }
816
- async calculateCompositePrice(compositeId, selections, locationId) {
817
- const encodedId = encodeURIComponent(compositeId);
818
- return safe(
819
- this.client.post(
820
- `/api/v1/catalogue/composites/${encodedId}/calculate-price`,
821
- {
822
- selections,
823
- location_id: locationId
824
- }
825
- )
826
- );
827
- }
828
- async fetchQuote(input) {
829
- return safe(this.client.post("/api/v1/catalogue/quotes", input));
830
- }
831
- async getQuote(quoteId) {
832
- const encodedQuoteId = encodeURIComponent(quoteId);
833
- return safe(this.client.get(`/api/v1/catalogue/quotes/${encodedQuoteId}`));
834
- }
835
- async refreshQuote(input) {
836
- const encodedQuoteId = encodeURIComponent(input.quote_id);
837
- return safe(
838
- this.client.post(
839
- `/api/v1/catalogue/quotes/${encodedQuoteId}/refresh`,
840
- input
841
- )
842
- );
843
- }
844
- async getTaxonomies(parentId) {
845
- const path = withQuery("/api/v1/catalogue/taxonomies", { parent_id: parentId });
846
- return safe(this.client.get(path));
847
- }
848
- async getTaxonomy(id) {
849
- const encodedId = encodeURIComponent(id);
850
- return safe(this.client.get(`/api/v1/catalogue/taxonomies/${encodedId}`));
851
- }
852
- async getTaxonomyPath(id) {
853
- const encodedId = encodeURIComponent(id);
854
- return safe(this.client.get(`/api/v1/catalogue/taxonomies/${encodedId}/path`));
855
- }
856
- async searchTaxonomies(query, limit) {
857
- const path = withQuery("/api/v1/catalogue/taxonomies/search", { q: query, limit });
858
- return safe(this.client.get(path));
859
- }
860
- async getProductSchedules(productId) {
861
- const encodedId = encodeURIComponent(productId);
862
- return safe(this.client.get(`/api/v1/catalogue/products/${encodedId}/schedules`));
863
- }
864
- async checkProductAvailableNow(productId, locationId) {
865
- const encodedId = encodeURIComponent(productId);
866
- const path = withQuery(`/api/v1/catalogue/products/${encodedId}/available-now`, { location_id: locationId });
867
- return safe(this.client.get(path));
868
- }
869
- async getDeals(locationId) {
870
- const path = withQuery("/api/v1/catalogue/deals", { location_id: locationId });
871
- return safe(this.client.get(path));
872
- }
873
- async getProductsOnSale() {
874
- return safe(this.client.get("/api/v1/catalogue/deals/on-sale"));
875
- }
876
- async getProductDeals(productId) {
877
- const encodedId = encodeURIComponent(productId);
878
- return safe(this.client.get(`/api/v1/catalogue/products/${encodedId}/deals`));
879
- }
880
- async getCategoryDeals(categoryId) {
881
- const encodedId = encodeURIComponent(categoryId);
882
- return safe(this.client.get(`/api/v1/catalogue/categories/${encodedId}/deals`));
883
- }
884
- async getCollectionDeals(collectionId) {
885
- const encodedId = encodeURIComponent(collectionId);
886
- return safe(this.client.get(`/api/v1/catalogue/collections/${encodedId}/deals`));
887
- }
888
- async validateDiscountCode(code, orderSubtotal, locationId) {
889
- return safe(
890
- this.client.post("/api/v1/catalogue/deals/validate", {
891
- discount_code: code,
892
- order_subtotal: orderSubtotal,
893
- location_id: locationId
894
- })
895
- );
896
- }
897
- async search(query, options) {
898
- const result = await this.getProducts({
899
- search: query,
900
- category: options?.category,
901
- limit: options?.limit ?? 20
902
- });
903
- if (!result.ok) return result;
904
- return ok(result.value.items);
905
- }
906
- async searchProducts(query, options) {
907
- return this.search(query, options);
908
- }
909
- async getMenu(options) {
910
- const path = withQuery("/api/v1/catalogue/menu", {
911
- category_id: options?.category,
912
- limit: options?.limit
913
- });
914
- return safe(this.client.get(path));
915
- }
916
- async getMenuCategory(categoryId) {
917
- const encodedId = encodeURIComponent(categoryId);
918
- return safe(this.client.get(`/api/v1/catalogue/menu/categories/${encodedId}`));
919
- }
920
- async getMenuItem(itemId) {
921
- const encodedId = encodeURIComponent(itemId);
922
- return safe(this.client.get(`/api/v1/catalogue/menu/items/${encodedId}`));
923
- }
924
- async getTags(options) {
925
- const path = withQuery("/api/v1/catalogue/tags", {
926
- tag_group: options?.tag_group,
927
- search: options?.search,
928
- min_usage: options?.min_usage,
929
- limit: options?.limit,
930
- offset: options?.offset
931
- });
932
- return safe(this.client.get(path));
933
- }
934
- };
935
-
936
- // src/cart.ts
937
- function isUICartResponse(value) {
938
- return "cart" in value;
939
- }
940
- function unwrapEnrichedCart(value) {
941
- return isUICartResponse(value) ? value.cart : value;
942
- }
943
- var CartOperations = class {
944
- constructor(client) {
945
- this.client = client;
946
- }
947
- async get() {
948
- const result = await safe(this.client.get("/api/v1/cart"));
949
- if (!result.ok) return result;
950
- return ok(unwrapEnrichedCart(result.value));
951
- }
952
- async getRaw() {
953
- return safe(this.client.get("/api/v1/cart"));
954
- }
955
- async getItems() {
956
- return safe(this.client.get("/api/v1/cart/items"));
957
- }
958
- async getCount() {
959
- return safe(this.client.get("/api/v1/cart/count"));
960
- }
961
- async getTotal() {
962
- return safe(this.client.get("/api/v1/cart/total"));
963
- }
964
- async getSummary() {
965
- const cartResult = await this.get();
966
- if (!cartResult.ok) return cartResult;
967
- const cart = cartResult.value;
968
- return ok({
969
- item_count: cart.items.length,
970
- total_items: cart.items.reduce((sum, item) => sum + item.quantity, 0),
971
- subtotal: cart.pricing.subtotal,
972
- discount_amount: cart.pricing.total_discounts,
973
- tax_amount: cart.pricing.tax_amount,
974
- total: cart.pricing.total_price,
975
- currency: cart.pricing.currency
976
- });
977
- }
978
- async addItem(input) {
979
- return safe(this.client.post("/api/v1/cart/items", input));
980
- }
981
- async updateItem(cartItemId, updates) {
982
- if (typeof updates.quantity === "number") {
983
- return this.updateQuantity(cartItemId, updates.quantity);
984
- }
985
- const encodedId = encodeURIComponent(cartItemId);
986
- return safe(this.client.patch(`/api/v1/cart/items/${encodedId}`, updates));
987
- }
988
- async updateQuantity(cartItemId, quantity) {
989
- const encodedId = encodeURIComponent(cartItemId);
990
- return safe(
991
- this.client.patch(`/api/v1/cart/items/${encodedId}`, {
992
- quantity
993
- })
994
- );
995
- }
996
- async removeItem(cartItemId) {
997
- const encodedId = encodeURIComponent(cartItemId);
998
- return safe(this.client.delete(`/api/v1/cart/items/${encodedId}`));
999
- }
1000
- async clear() {
1001
- return safe(this.client.delete("/api/v1/cart"));
1002
- }
1003
- async applyCoupon(code) {
1004
- return safe(
1005
- this.client.post("/api/v1/cart/coupons", {
1006
- coupon_code: code
1007
- })
1008
- );
1009
- }
1010
- async removeCoupon() {
1011
- return safe(this.client.delete("/api/v1/cart/coupons/current"));
1012
- }
1013
- async isEmpty() {
1014
- const countResult = await this.getCount();
1015
- if (!countResult.ok) return countResult;
1016
- return ok(countResult.value === 0);
1017
- }
1018
- async hasItem(productId, variantId) {
1019
- const itemsResult = await this.getItems();
1020
- if (!itemsResult.ok) return itemsResult;
1021
- const found = itemsResult.value.some((item) => {
1022
- const matchesProduct = item.item_id === productId;
1023
- if (!variantId) return matchesProduct;
1024
- const config = item.configuration;
1025
- if ("variant" in config && config.variant) {
1026
- return matchesProduct && config.variant.variant_id === variantId;
1027
- }
1028
- return matchesProduct;
1029
- });
1030
- return ok(found);
1031
- }
1032
- async findItem(productId, variantId) {
1033
- const itemsResult = await this.getItems();
1034
- if (!itemsResult.ok) return itemsResult;
1035
- const found = itemsResult.value.find((item) => {
1036
- const matchesProduct = item.item_id === productId;
1037
- if (!variantId) return matchesProduct;
1038
- const config = item.configuration;
1039
- if ("variant" in config && config.variant) {
1040
- return matchesProduct && config.variant.variant_id === variantId;
1041
- }
1042
- return matchesProduct;
1043
- });
1044
- return ok(found);
1045
- }
1046
- };
1047
-
1048
- // src/constants.ts
1049
- var MOBILE_MONEY_PROVIDER = {
1050
- MTN: "mtn",
1051
- VODAFONE: "vodafone",
1052
- AIRTEL: "airtel"
1053
- };
1054
-
1055
- // src/utils/price.ts
1056
- var CURRENCY_SYMBOLS = {
1057
- USD: "$",
1058
- EUR: "\u20AC",
1059
- GBP: "\xA3",
1060
- JPY: "\xA5",
1061
- CNY: "\xA5",
1062
- CHF: "CHF",
1063
- CAD: "C$",
1064
- AUD: "A$",
1065
- NZD: "NZ$",
1066
- HKD: "HK$",
1067
- SGD: "S$",
1068
- INR: "\u20B9",
1069
- BRL: "R$",
1070
- MXN: "MX$",
1071
- KRW: "\u20A9",
1072
- RUB: "\u20BD",
1073
- TRY: "\u20BA",
1074
- THB: "\u0E3F",
1075
- PLN: "z\u0142",
1076
- SEK: "kr",
1077
- NOK: "kr",
1078
- DKK: "kr",
1079
- CZK: "K\u010D",
1080
- HUF: "Ft",
1081
- ILS: "\u20AA",
1082
- AED: "\u062F.\u0625",
1083
- SAR: "\uFDFC",
1084
- MYR: "RM",
1085
- PHP: "\u20B1",
1086
- IDR: "Rp",
1087
- VND: "\u20AB",
1088
- TWD: "NT$",
1089
- GHS: "GH\u20B5",
1090
- NGN: "\u20A6",
1091
- KES: "KSh",
1092
- ZAR: "R",
1093
- XOF: "CFA",
1094
- XAF: "FCFA",
1095
- EGP: "E\xA3",
1096
- MAD: "MAD",
1097
- TZS: "TSh",
1098
- UGX: "USh",
1099
- RWF: "FRw",
1100
- ETB: "Br",
1101
- ZMW: "ZK",
1102
- BWP: "P",
1103
- MUR: "\u20A8",
1104
- SCR: "\u20A8",
1105
- NAD: "N$",
1106
- SZL: "E",
1107
- LSL: "L",
1108
- MWK: "MK",
1109
- AOA: "Kz",
1110
- CDF: "FC",
1111
- GMD: "D",
1112
- GNF: "FG",
1113
- LRD: "L$",
1114
- SLL: "Le",
1115
- MZN: "MT",
1116
- SDG: "SDG",
1117
- SSP: "SSP",
1118
- SOS: "Sh.So.",
1119
- DJF: "Fdj",
1120
- ERN: "Nfk",
1121
- CVE: "$",
1122
- STN: "Db",
1123
- KMF: "CF",
1124
- BIF: "FBu"
1125
- };
1126
- function getCurrencySymbol(currencyCode3) {
1127
- return CURRENCY_SYMBOLS[currencyCode3.toUpperCase()] || currencyCode3;
1128
- }
1129
- function formatPrice(amount, currency = "GHS", locale = "en-US") {
1130
- const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
1131
- if (isNaN(numAmount)) {
1132
- return `${getCurrencySymbol(currency)}0.00`;
1133
- }
1134
- try {
1135
- return new Intl.NumberFormat(locale, {
1136
- style: "currency",
1137
- currency: currency.toUpperCase(),
1138
- minimumFractionDigits: 2,
1139
- maximumFractionDigits: 2
1140
- }).format(numAmount);
1141
- } catch {
1142
- return `${getCurrencySymbol(currency)}${numAmount.toFixed(2)}`;
1143
- }
1144
- }
1145
- function parsePrice(value) {
1146
- if (value === void 0 || value === null) {
1147
- return 0;
1148
- }
1149
- if (typeof value === "number") {
1150
- return isNaN(value) ? 0 : value;
1151
- }
1152
- const cleaned = value.replace(/[^\d.-]/g, "");
1153
- const parsed = parseFloat(cleaned);
1154
- return isNaN(parsed) ? 0 : parsed;
1155
- }
1156
- function getDisplayPrice(product) {
1157
- if (product.price_info) {
1158
- return parsePrice(product.price_info.final_price);
1159
- }
1160
- if (product.final_price !== void 0 && product.final_price !== null) {
1161
- return parsePrice(product.final_price);
1162
- }
1163
- if (product.default_price !== void 0 && product.default_price !== null) {
1164
- return parsePrice(product.default_price);
1165
- }
1166
- return 0;
1167
- }
1168
- function getBasePrice(product) {
1169
- if (product.price_info) {
1170
- return parsePrice(product.price_info.base_price);
1171
- }
1172
- if (product.base_price !== void 0 && product.base_price !== null) {
1173
- return parsePrice(product.base_price);
1174
- }
1175
- if (product.default_price !== void 0 && product.default_price !== null) {
1176
- return parsePrice(product.default_price);
1177
- }
1178
- return 0;
1179
- }
1180
- function isOnSale(product) {
1181
- const basePrice = getBasePrice(product);
1182
- const finalPrice = getDisplayPrice(product);
1183
- return basePrice > finalPrice && basePrice > 0;
1184
- }
1185
- function getDiscountPercentage(product) {
1186
- const basePrice = getBasePrice(product);
1187
- const finalPrice = getDisplayPrice(product);
1188
- if (basePrice > finalPrice && basePrice > 0) {
1189
- return Math.round((basePrice - finalPrice) / basePrice * 100);
1190
- }
1191
- return 0;
1192
- }
1193
-
1194
- // src/utils/payment.ts
1195
- var PAYMENT_SUCCESS_STATUSES = /* @__PURE__ */ new Set([
1196
- "success",
1197
- "succeeded",
1198
- "paid",
1199
- "captured",
1200
- "completed",
1201
- "authorized"
1202
- ]);
1203
- var PAYMENT_FAILURE_STATUSES = /* @__PURE__ */ new Set([
1204
- "failed",
1205
- "declined",
1206
- "cancelled",
1207
- "voided",
1208
- "error",
1209
- "abandoned"
1210
- ]);
1211
- var PAYMENT_REQUIRES_ACTION_STATUSES = /* @__PURE__ */ new Set([
1212
- "requires_action",
1213
- "requires_payment_method",
1214
- "requires_capture"
1215
- ]);
1216
- var PAYMENT_STATUS_ALIAS_MAP = {
1217
- ok: "success",
1218
- done: "success",
1219
- paid: "paid",
1220
- paid_in_full: "paid",
1221
- paid_successfully: "paid",
1222
- succeeded: "success",
1223
- captured: "captured",
1224
- completed: "completed",
1225
- pending_confirmation: "pending_confirmation",
1226
- requires_authorization: "requires_action",
1227
- requires_action: "requires_action",
1228
- requires_payment_method: "requires_payment_method",
1229
- requires_capture: "requires_capture",
1230
- partially_paid: "partially_paid",
1231
- partially_refunded: "partially_refunded",
1232
- card_declined: "declined",
1233
- canceled: "cancelled",
1234
- authorized: "authorized",
1235
- cancelled: "cancelled",
1236
- unresolved: "pending"
1237
- };
1238
- var KNOWN_PAYMENT_STATUSES = /* @__PURE__ */ new Set([
1239
- "initialized",
1240
- "pending",
1241
- "processing",
1242
- "authorized",
1243
- "captured",
1244
- "failed",
1245
- "cancelled",
1246
- "refunded",
1247
- "unknown",
1248
- "voided",
1249
- "requires_action",
1250
- "abandoned",
1251
- "reversed",
1252
- "disputed",
1253
- "refund_pending",
1254
- "created",
1255
- "pending_confirmation",
1256
- "success",
1257
- "succeeded",
1258
- "declined",
1259
- "partially_refunded",
1260
- "partially_paid",
1261
- "paid",
1262
- "unpaid",
1263
- "requires_payment_method",
1264
- "requires_capture",
1265
- "completed",
1266
- "error"
1267
- ]);
1268
- function normalizeStatusToken(status) {
1269
- return status?.trim().toLowerCase().replace(/[\s-]+/g, "_") ?? "";
1270
- }
1271
- function normalizePaymentStatusValue(status) {
1272
- const normalized = normalizeStatusToken(status);
1273
- if (Object.prototype.hasOwnProperty.call(PAYMENT_STATUS_ALIAS_MAP, normalized)) {
1274
- return PAYMENT_STATUS_ALIAS_MAP[normalized];
1275
- }
1276
- return KNOWN_PAYMENT_STATUSES.has(normalized) ? normalized : "unknown";
1277
- }
1278
- function isPaymentStatusSuccess(status) {
1279
- const normalizedStatus = normalizePaymentStatusValue(status);
1280
- return PAYMENT_SUCCESS_STATUSES.has(normalizedStatus);
1281
- }
1282
- function isPaymentStatusFailure(status) {
1283
- const normalizedStatus = normalizePaymentStatusValue(status);
1284
- return PAYMENT_FAILURE_STATUSES.has(normalizedStatus);
1285
- }
1286
- function isPaymentStatusRequiresAction(status) {
1287
- const normalizedStatus = normalizePaymentStatusValue(status);
1288
- return PAYMENT_REQUIRES_ACTION_STATUSES.has(normalizedStatus);
1289
- }
1290
- function normalizeStatusResponse(response) {
1291
- if (!response || typeof response !== "object") {
1292
- return {
1293
- status: "pending",
1294
- paid: false,
1295
- amount: moneyFromNumber(0),
1296
- currency: "",
1297
- message: "No status available"
1298
- };
1299
- }
1300
- const res = response;
1301
- const normalizedStatus = normalizePaymentStatusValue(res.status ?? void 0);
1302
- const normalizedAmount = typeof res.amount === "string" ? money(res.amount) : typeof res.amount === "number" && Number.isFinite(res.amount) ? moneyFromNumber(res.amount) : moneyFromNumber(0);
1303
- const normalizedCurrency = typeof res.currency === "string" && res.currency.trim().length > 0 ? res.currency : "";
1304
- const paidValue = res.paid === true;
1305
- const derivedPaid = paidValue || [
1306
- "success",
1307
- "succeeded",
1308
- "paid",
1309
- "captured",
1310
- "authorized",
1311
- "completed"
1312
- ].includes(normalizedStatus);
1313
- return {
1314
- status: normalizedStatus,
1315
- paid: derivedPaid,
1316
- amount: normalizedAmount,
1317
- currency: normalizedCurrency,
1318
- reference: res.reference,
1319
- message: res.message || ""
1320
- };
1321
- }
1322
-
1323
- // src/utils/paystack.ts
1324
- async function openPaystackPopup(options, signal) {
1325
- if (typeof window === "undefined") {
1326
- return { success: false, error: "PROVIDER_UNAVAILABLE" };
1327
- }
1328
- let PaystackPop;
1329
- try {
1330
- const imported = await import('@paystack/inline-js');
1331
- PaystackPop = imported.default;
1332
- } catch {
1333
- return { success: false, error: "PROVIDER_UNAVAILABLE" };
1334
- }
1335
- return new Promise((resolve) => {
1336
- let settled = false;
1337
- const resolveOnce = (result) => {
1338
- if (settled) {
1339
- return;
1340
- }
1341
- settled = true;
1342
- if (signal) {
1343
- signal.removeEventListener("abort", onAbort);
1344
- }
1345
- resolve(result);
1346
- };
1347
- const onAbort = () => {
1348
- resolveOnce({ success: false, error: "CANCELLED" });
1349
- };
1350
- if (signal?.aborted) {
1351
- resolveOnce({ success: false, error: "CANCELLED" });
1352
- return;
1353
- }
1354
- if (signal) {
1355
- signal.addEventListener("abort", onAbort, { once: true });
1356
- }
1357
- try {
1358
- const popup = new PaystackPop();
1359
- popup.newTransaction({
1360
- key: options.key,
1361
- email: options.email,
1362
- amount: options.amount,
1363
- currency: options.currency,
1364
- reference: options.reference,
1365
- accessCode: options.accessCode,
1366
- onSuccess: (transaction) => {
1367
- resolveOnce({
1368
- success: true,
1369
- reference: transaction.reference ?? options.reference
1370
- });
1371
- },
1372
- onCancel: () => {
1373
- resolveOnce({ success: false, error: "PAYMENT_CANCELLED" });
1374
- },
1375
- onError: (error) => {
1376
- resolveOnce({
1377
- success: false,
1378
- error: error?.message || "PAYMENT_FAILED"
1379
- });
1380
- }
1381
- });
1382
- } catch {
1383
- resolveOnce({ success: false, error: "POPUP_BLOCKED" });
1384
- }
1385
- });
1386
- }
1387
-
1388
- // src/utils/checkout-resolver.ts
1389
- var DEFAULT_POLL_INTERVAL_MS = 3e3;
1390
- var DEFAULT_MAX_POLL_ATTEMPTS = 60;
1391
- var MAX_CONSECUTIVE_NETWORK_ERRORS = 5;
1392
- var CARD_PROVIDER_PAYSTACK = "paystack";
1393
- var NEXT_ACTION_NONE = "none";
1394
- var NEXT_ACTION_CARD_POPUP = "card_popup";
1395
- var NEXT_ACTION_REDIRECT = "redirect";
1396
- var NEXT_ACTION_AUTHORIZATION = "authorization";
1397
- function normalizeCardPopupError(error) {
1398
- if (!error || error === "PAYMENT_CANCELLED" || error === "CANCELLED") {
1399
- return "PAYMENT_CANCELLED";
1400
- }
1401
- if (error === "PROVIDER_UNAVAILABLE") {
1402
- return "PROVIDER_UNAVAILABLE";
1403
- }
1404
- if (error === "POPUP_BLOCKED") {
1405
- return "POPUP_BLOCKED";
1406
- }
1407
- return "PAYMENT_FAILED";
1408
- }
1409
- function normalizeCardProvider(value) {
1410
- const normalized = value?.trim().toLowerCase();
1411
- return normalized && normalized.length > 0 ? normalized : CARD_PROVIDER_PAYSTACK;
1412
- }
1413
- async function openCardPopup(provider, checkoutResult, email, currency, signal) {
1414
- if (typeof window === "undefined") {
1415
- return { success: false, error: "PROVIDER_UNAVAILABLE" };
1416
- }
1417
- if (provider === CARD_PROVIDER_PAYSTACK) {
1418
- return openPaystackPopup(
1419
- {
1420
- key: checkoutResult.public_key || "",
1421
- email,
1422
- accessCode: checkoutResult.client_secret,
1423
- reference: checkoutResult.payment_reference || checkoutResult.client_secret,
1424
- currency
1425
- },
1426
- signal
1427
- );
1428
- }
1429
- return { success: false, error: "PROVIDER_UNAVAILABLE" };
1430
- }
1431
- var VALID_MOBILE_MONEY_PROVIDERS = new Set(Object.values(MOBILE_MONEY_PROVIDER));
1432
- function isValidMobileMoneyProvider(value) {
1433
- return VALID_MOBILE_MONEY_PROVIDERS.has(value);
1434
- }
1435
- function normalizeAuthorizationType(value) {
1436
- if (value && typeof value === "object" && "address" in value) {
1437
- return "address";
1438
- }
1439
- if (typeof value !== "string") return void 0;
1440
- const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, "_");
1441
- if (normalized === "otp" || normalized === "pin") return normalized;
1442
- if (normalized === "phone" || normalized === "birthday" || normalized === "address") {
1443
- return normalized;
1444
- }
1445
- return void 0;
1446
- }
1447
- function formatMoney2(value) {
1448
- if (typeof value === "number" && Number.isFinite(value)) {
1449
- return value.toFixed(2);
1450
- }
1451
- if (typeof value === "string" && value.trim().length > 0) {
1452
- return value;
1453
- }
1454
- return "0.00";
1455
- }
1456
- function createAbortError() {
1457
- const error = new Error("CANCELLED");
1458
- error.name = "AbortError";
1459
- return error;
1460
- }
1461
- function isAbortError(error) {
1462
- if (error instanceof DOMException && error.name === "AbortError") {
1463
- return true;
1464
- }
1465
- return error instanceof Error && (error.name === "AbortError" || error.message === "CANCELLED");
1466
- }
1467
- var CheckoutResolver = class {
1468
- constructor(options) {
1469
- this.client = options.client;
1470
- this.checkoutData = options.checkoutData;
1471
- this.pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1472
- this.maxPollAttempts = options.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS;
1473
- this.onStatusChange = options.onStatusChange;
1474
- this.onAuthorizationRequired = options.onAuthorizationRequired;
1475
- this.signal = options.signal;
1476
- this.returnUrl = options.returnUrl;
1477
- this.enrollInLink = options.enrollInLink !== false;
1478
- this.businessId = options.businessId;
1479
- this.addressData = options.addressData;
1480
- this.paymentData = options.paymentData;
1481
- this.orderType = options.orderType;
1482
- this.cartTotal = options.cartTotal;
1483
- this.cartCurrency = options.cartCurrency;
1484
- this.allowPopups = options.allowPopups ?? typeof window !== "undefined";
1485
- }
1486
- async resolve(checkoutResult) {
1487
- try {
1488
- this.ensureNotAborted();
1489
- if (!checkoutResult.order_id) {
1490
- return this.fail("CHECKOUT_FAILED", "Checkout did not return an order ID.", false);
1491
- }
1492
- if (checkoutResult.next_action) {
1493
- return this.resolveNextAction(checkoutResult);
1494
- }
1495
- let latestCheckoutResult = checkoutResult;
1496
- let authorizationType = normalizeAuthorizationType(checkoutResult.authorization_type);
1497
- let paymentReference = checkoutResult.payment_reference;
1498
- if (this.isSuccessfulStatus(checkoutResult.payment_status)) {
1499
- return this.finalizeSuccess(checkoutResult);
1500
- }
1501
- if (checkoutResult.client_secret && checkoutResult.public_key) {
1502
- const provider = normalizeCardProvider(checkoutResult.provider);
1503
- this.emit("awaiting_authorization", {
1504
- display_text: "Complete payment in the card authorization popup.",
1505
- order_id: checkoutResult.order_id,
1506
- order_number: checkoutResult.order_number
1507
- });
1508
- if (!this.allowPopups) {
1509
- return this.fail(
1510
- "PROVIDER_UNAVAILABLE",
1511
- "Card payment popup is unavailable in this environment.",
1512
- false
1513
- );
1514
- }
1515
- const popupResult = await openCardPopup(
1516
- provider,
1517
- checkoutResult,
1518
- this.checkoutData.customer.email || "customer@cimplify.io",
1519
- this.getOrderCurrency(checkoutResult),
1520
- this.signal
1521
- );
1522
- if (!popupResult.success) {
1523
- const popupError = normalizeCardPopupError(popupResult.error);
1524
- if (popupError === "POPUP_BLOCKED") {
1525
- return this.fail(
1526
- "POPUP_BLOCKED",
1527
- "Unable to open card payment popup. Please allow popups and try again.",
1528
- true
1529
- );
1530
- }
1531
- if (popupError === "PROVIDER_UNAVAILABLE") {
1532
- return this.fail(
1533
- "PROVIDER_UNAVAILABLE",
1534
- "Card payment provider is unavailable.",
1535
- false
1536
- );
1537
- }
1538
- return this.fail(
1539
- "PAYMENT_CANCELLED",
1540
- "Card payment was cancelled before completion.",
1541
- true
1542
- );
1543
- }
1544
- paymentReference = popupResult.reference || paymentReference;
1545
- }
1546
- if (checkoutResult.authorization_url) {
1547
- if (typeof window !== "undefined" && this.returnUrl) {
1548
- window.location.assign(checkoutResult.authorization_url);
1549
- return this.fail(
1550
- "REDIRECT_REQUIRED",
1551
- "Redirecting to complete payment authorization.",
1552
- true
1553
- );
1554
- }
1555
- if (typeof window !== "undefined") {
1556
- const popup = window.open(
1557
- checkoutResult.authorization_url,
1558
- "cimplify-auth",
1559
- "width=520,height=760,noopener,noreferrer"
1560
- );
1561
- if (!popup) {
1562
- return this.fail(
1563
- "POPUP_BLOCKED",
1564
- "Authorization popup was blocked. Please allow popups and retry.",
1565
- true
1566
- );
1567
- }
1568
- }
1569
- }
1570
- if (checkoutResult.requires_authorization || isPaymentStatusRequiresAction(checkoutResult.payment_status)) {
1571
- const authorization = await this.handleAuthorization({
1572
- authorizationType,
1573
- paymentReference,
1574
- displayText: checkoutResult.display_text,
1575
- provider: checkoutResult.provider
1576
- });
1577
- if (!authorization.ok) {
1578
- return authorization.result;
1579
- }
1580
- if (authorization.value) {
1581
- latestCheckoutResult = authorization.value;
1582
- paymentReference = authorization.value.payment_reference || paymentReference;
1583
- authorizationType = normalizeAuthorizationType(authorization.value.authorization_type) || authorizationType;
1584
- if (this.isSuccessfulStatus(authorization.value.payment_status)) {
1585
- return this.finalizeSuccess(authorization.value);
1586
- }
1587
- if (this.isFailureStatus(authorization.value.payment_status)) {
1588
- return this.fail(
1589
- "AUTHORIZATION_FAILED",
1590
- authorization.value.display_text || "Payment authorization failed.",
1591
- false
1592
- );
1593
- }
1594
- }
1595
- }
1596
- return this.pollUntilTerminal({
1597
- orderId: checkoutResult.order_id,
1598
- latestCheckoutResult,
1599
- paymentReference,
1600
- authorizationType
1601
- });
1602
- } catch (error) {
1603
- if (isAbortError(error)) {
1604
- return this.fail("CANCELLED", "Checkout was cancelled.", true);
1605
- }
1606
- const message = error instanceof Error ? error.message : "Checkout failed unexpectedly.";
1607
- return this.fail("CHECKOUT_FAILED", message, true);
1608
- }
1609
- }
1610
- async resolveNextAction(checkoutResult) {
1611
- const action = checkoutResult.next_action;
1612
- if (this.isSuccessfulStatus(checkoutResult.payment_status)) {
1613
- return this.finalizeSuccess(checkoutResult);
1614
- }
1615
- let latestCheckoutResult = checkoutResult;
1616
- let paymentReference = checkoutResult.payment_reference;
1617
- switch (action.type) {
1618
- case NEXT_ACTION_NONE:
1619
- return this.finalizeSuccess(checkoutResult);
1620
- case NEXT_ACTION_CARD_POPUP: {
1621
- const provider = normalizeCardProvider(action.provider);
1622
- this.emit("awaiting_authorization", {
1623
- display_text: "Complete payment in the card authorization popup.",
1624
- order_id: checkoutResult.order_id,
1625
- order_number: checkoutResult.order_number
1626
- });
1627
- if (!this.allowPopups) {
1628
- return this.fail(
1629
- "PROVIDER_UNAVAILABLE",
1630
- "Card payment popup is unavailable in this environment.",
1631
- false
1632
- );
1633
- }
1634
- const popupResult = await openCardPopup(
1635
- provider,
1636
- checkoutResult,
1637
- this.checkoutData.customer.email || "customer@cimplify.io",
1638
- this.getOrderCurrency(checkoutResult),
1639
- this.signal
1640
- );
1641
- if (!popupResult.success) {
1642
- const popupError = normalizeCardPopupError(popupResult.error);
1643
- if (popupError === "POPUP_BLOCKED") {
1644
- return this.fail(
1645
- "POPUP_BLOCKED",
1646
- "Unable to open card payment popup. Please allow popups and try again.",
1647
- true
1648
- );
1649
- }
1650
- if (popupError === "PROVIDER_UNAVAILABLE") {
1651
- return this.fail("PROVIDER_UNAVAILABLE", "Card payment provider is unavailable.", false);
1652
- }
1653
- return this.fail("PAYMENT_CANCELLED", "Card payment was cancelled before completion.", true);
1654
- }
1655
- paymentReference = popupResult.reference || paymentReference;
1656
- break;
1657
- }
1658
- case NEXT_ACTION_REDIRECT: {
1659
- if (typeof window !== "undefined" && this.returnUrl) {
1660
- window.location.assign(action.authorization_url);
1661
- return this.fail("REDIRECT_REQUIRED", "Redirecting to complete payment authorization.", true);
1662
- }
1663
- if (typeof window !== "undefined") {
1664
- const popup = window.open(
1665
- action.authorization_url,
1666
- "cimplify-auth",
1667
- "width=520,height=760,noopener,noreferrer"
1668
- );
1669
- if (!popup) {
1670
- return this.fail(
1671
- "POPUP_BLOCKED",
1672
- "Authorization popup was blocked. Please allow popups and retry.",
1673
- true
1674
- );
1675
- }
1676
- }
1677
- break;
1678
- }
1679
- case NEXT_ACTION_AUTHORIZATION: {
1680
- const authType = normalizeAuthorizationType(action.authorization_type);
1681
- const authorization = await this.handleAuthorization({
1682
- authorizationType: authType,
1683
- paymentReference,
1684
- displayText: action.display_text,
1685
- provider: checkoutResult.provider
1686
- });
1687
- if (!authorization.ok) {
1688
- return authorization.result;
1689
- }
1690
- if (authorization.value) {
1691
- latestCheckoutResult = authorization.value;
1692
- paymentReference = authorization.value.payment_reference || paymentReference;
1693
- if (this.isSuccessfulStatus(authorization.value.payment_status)) {
1694
- return this.finalizeSuccess(authorization.value);
1695
- }
1696
- if (this.isFailureStatus(authorization.value.payment_status)) {
1697
- return this.fail(
1698
- "AUTHORIZATION_FAILED",
1699
- authorization.value.display_text || "Payment authorization failed.",
1700
- false
1701
- );
1702
- }
1703
- }
1704
- break;
1705
- }
1706
- }
1707
- return this.pollUntilTerminal({
1708
- orderId: checkoutResult.order_id,
1709
- latestCheckoutResult,
1710
- paymentReference,
1711
- authorizationType: normalizeAuthorizationType(checkoutResult.authorization_type)
1712
- });
1713
- }
1714
- async pollUntilTerminal(input) {
1715
- let consecutiveErrors = 0;
1716
- let latestCheckoutResult = input.latestCheckoutResult;
1717
- let paymentReference = input.paymentReference;
1718
- let authorizationType = input.authorizationType;
1719
- for (let attempt = 0; attempt < this.maxPollAttempts; attempt++) {
1720
- this.ensureNotAborted();
1721
- this.emit("polling", {
1722
- poll_attempt: attempt,
1723
- max_poll_attempts: this.maxPollAttempts,
1724
- order_id: input.orderId,
1725
- order_number: latestCheckoutResult.order_number
1726
- });
1727
- const statusResult = await this.client.checkout.pollPaymentStatus(input.orderId);
1728
- if (!statusResult.ok) {
1729
- consecutiveErrors += 1;
1730
- if (consecutiveErrors >= MAX_CONSECUTIVE_NETWORK_ERRORS) {
1731
- return this.fail(
1732
- "NETWORK_ERROR",
1733
- "Unable to confirm payment due to repeated network errors.",
1734
- true
1735
- );
1736
- }
1737
- await this.wait(this.pollIntervalMs);
1738
- continue;
1739
- }
1740
- consecutiveErrors = 0;
1741
- if (statusResult.value.reference) {
1742
- paymentReference = statusResult.value.reference;
1743
- }
1744
- const normalized = normalizeStatusResponse(statusResult.value);
1745
- if (normalized.paid || isPaymentStatusSuccess(normalized.status)) {
1746
- return this.finalizeSuccess(latestCheckoutResult);
1747
- }
1748
- if (isPaymentStatusFailure(normalized.status)) {
1749
- return this.fail(
1750
- normalized.status ? normalized.status.toUpperCase() : "PAYMENT_FAILED",
1751
- normalized.message || "Payment failed during confirmation.",
1752
- false
1753
- );
1754
- }
1755
- if (isPaymentStatusRequiresAction(normalized.status)) {
1756
- const authorization = await this.handleAuthorization({
1757
- authorizationType,
1758
- paymentReference,
1759
- displayText: normalized.message || latestCheckoutResult.display_text,
1760
- provider: latestCheckoutResult.provider
1761
- });
1762
- if (!authorization.ok) {
1763
- return authorization.result;
1764
- }
1765
- if (authorization.value) {
1766
- latestCheckoutResult = authorization.value;
1767
- paymentReference = authorization.value.payment_reference || paymentReference;
1768
- authorizationType = normalizeAuthorizationType(authorization.value.authorization_type) || authorizationType;
1769
- if (this.isSuccessfulStatus(authorization.value.payment_status)) {
1770
- return this.finalizeSuccess(authorization.value);
1771
- }
1772
- if (this.isFailureStatus(authorization.value.payment_status)) {
1773
- return this.fail(
1774
- "AUTHORIZATION_FAILED",
1775
- authorization.value.display_text || "Payment authorization failed.",
1776
- false
1777
- );
1778
- }
1779
- }
1780
- }
1781
- await this.wait(this.pollIntervalMs);
1782
- }
1783
- try {
1784
- const finalResult = await this.client.checkout.pollPaymentStatus(input.orderId);
1785
- if (finalResult.ok) {
1786
- const normalized = normalizeStatusResponse(finalResult.value);
1787
- if (normalized.paid || isPaymentStatusSuccess(normalized.status)) {
1788
- return this.finalizeSuccess(latestCheckoutResult);
1789
- }
1790
- }
1791
- } catch {
1792
- }
1793
- return this.fail(
1794
- "PAYMENT_TIMEOUT",
1795
- "Payment confirmation timed out. Please retry checkout.",
1796
- true
1797
- );
1798
- }
1799
- async handleAuthorization(input) {
1800
- const authorizationType = input.authorizationType;
1801
- const paymentReference = input.paymentReference;
1802
- if (!authorizationType || !paymentReference) {
1803
- return { ok: true };
1804
- }
1805
- this.emit("awaiting_authorization", {
1806
- authorization_type: authorizationType,
1807
- display_text: input.displayText,
1808
- provider: input.provider
1809
- });
1810
- let authorizationResult;
1811
- const submit = async (code) => {
1812
- this.ensureNotAborted();
1813
- const trimmedCode = code.trim();
1814
- if (!trimmedCode) {
1815
- throw new Error("Authorization code is required.");
1816
- }
1817
- const submitResult = await this.client.checkout.submitAuthorization({
1818
- reference: paymentReference,
1819
- auth_type: authorizationType,
1820
- value: trimmedCode
1821
- });
1822
- if (!submitResult.ok) {
1823
- throw new Error(submitResult.error.message || "Authorization failed.");
1824
- }
1825
- authorizationResult = submitResult.value;
1826
- };
1827
- try {
1828
- if (this.onAuthorizationRequired) {
1829
- await this.onAuthorizationRequired(authorizationType, submit);
1830
- } else {
1831
- return {
1832
- ok: false,
1833
- result: this.fail(
1834
- "AUTHORIZATION_REQUIRED",
1835
- "Authorization callback is required in headless checkout mode.",
1836
- false
1837
- )
1838
- };
1839
- }
1840
- } catch (error) {
1841
- if (isAbortError(error)) {
1842
- return {
1843
- ok: false,
1844
- result: this.fail("CANCELLED", "Checkout was cancelled.", true)
1845
- };
1846
- }
1847
- const message = error instanceof Error ? error.message : "Payment authorization failed.";
1848
- return {
1849
- ok: false,
1850
- result: this.fail("AUTHORIZATION_FAILED", message, true)
1851
- };
1852
- }
1853
- return {
1854
- ok: true,
1855
- value: authorizationResult
1856
- };
1857
- }
1858
- async finalizeSuccess(checkoutResult) {
1859
- this.emit("finalizing", {
1860
- order_id: checkoutResult.order_id,
1861
- order_number: checkoutResult.order_number
1862
- });
1863
- const enrolledInLink = await this.maybeEnrollInLink(checkoutResult.order_id);
1864
- this.emit("success", {
1865
- order_id: checkoutResult.order_id,
1866
- order_number: checkoutResult.order_number
1867
- });
1868
- return {
1869
- success: true,
1870
- order: {
1871
- id: checkoutResult.order_id,
1872
- order_number: checkoutResult.order_number || checkoutResult.order_id,
1873
- status: checkoutResult.payment_status || "paid",
1874
- total: this.getOrderTotal(checkoutResult),
1875
- currency: this.getOrderCurrency(checkoutResult)
1876
- },
1877
- enrolled_in_link: enrolledInLink
1878
- };
1879
- }
1880
- async maybeEnrollInLink(orderId) {
1881
- if (!this.enrollInLink || !orderId) {
1882
- return false;
1883
- }
1884
- let businessId = this.businessId;
1885
- if (!businessId) {
1886
- const businessResult = await this.client.business.getInfo();
1887
- if (!businessResult.ok || !businessResult.value?.id) {
1888
- return false;
1889
- }
1890
- businessId = businessResult.value.id;
1891
- }
1892
- const address = this.getEnrollmentAddress();
1893
- const mobileMoney = this.getEnrollmentMobileMoney();
1894
- try {
1895
- const enrollment = await this.client.link.enrollAndLinkOrder({
1896
- order_id: orderId,
1897
- business_id: businessId,
1898
- order_type: this.orderType || this.checkoutData.order_type,
1899
- address,
1900
- mobile_money: mobileMoney
1901
- });
1902
- return enrollment.ok && enrollment.value.success;
1903
- } catch {
1904
- return false;
1905
- }
1906
- }
1907
- getEnrollmentAddress() {
1908
- const source = this.addressData ?? this.checkoutData.address_info;
1909
- if (!source?.street_address) {
1910
- return void 0;
1911
- }
1912
- return {
1913
- label: "Default",
1914
- street_address: source.street_address,
1915
- apartment: source.apartment || void 0,
1916
- city: source.city || "",
1917
- region: source.region || "",
1918
- delivery_instructions: source.delivery_instructions || void 0,
1919
- phone_for_delivery: source.phone_for_delivery || void 0
1920
- };
1921
- }
1922
- getEnrollmentMobileMoney() {
1923
- if (this.paymentData?.type === "mobile_money" && this.paymentData.phone_number && this.paymentData.provider && isValidMobileMoneyProvider(this.paymentData.provider)) {
1924
- return {
1925
- phone_number: this.paymentData.phone_number,
1926
- provider: this.paymentData.provider,
1927
- label: this.paymentData.label || "Mobile Money"
1928
- };
1929
- }
1930
- if (this.checkoutData.mobile_money_details?.phone_number) {
1931
- return {
1932
- phone_number: this.checkoutData.mobile_money_details.phone_number,
1933
- provider: this.checkoutData.mobile_money_details.provider,
1934
- label: "Mobile Money"
1935
- };
1936
- }
1937
- return void 0;
1938
- }
1939
- getOrderTotal(checkoutResult) {
1940
- if (checkoutResult.fx?.pay_amount !== void 0) {
1941
- return formatMoney2(checkoutResult.fx.pay_amount);
1942
- }
1943
- if (this.cartTotal !== void 0) {
1944
- return formatMoney2(this.cartTotal);
1945
- }
1946
- return "0.00";
1947
- }
1948
- getOrderCurrency(checkoutResult) {
1949
- if (checkoutResult.fx?.pay_currency) {
1950
- return checkoutResult.fx.pay_currency;
1951
- }
1952
- if (this.cartCurrency) {
1953
- return this.cartCurrency;
1954
- }
1955
- if (this.checkoutData.pay_currency) {
1956
- return this.checkoutData.pay_currency;
1957
- }
1958
- return "GHS";
1959
- }
1960
- emit(status, context = {}) {
1961
- if (!this.onStatusChange) {
1962
- return;
1963
- }
1964
- try {
1965
- this.onStatusChange(status, context);
1966
- } catch {
1967
- }
1968
- }
1969
- fail(code, message, recoverable) {
1970
- this.emit("failed", {
1971
- display_text: message
1972
- });
1973
- return {
1974
- success: false,
1975
- error: {
1976
- code,
1977
- message,
1978
- recoverable
1979
- }
1980
- };
1981
- }
1982
- isSuccessfulStatus(status) {
1983
- const normalized = normalizeStatusResponse({ status, paid: false });
1984
- return normalized.paid || isPaymentStatusSuccess(normalized.status);
1985
- }
1986
- isFailureStatus(status) {
1987
- const normalized = normalizeStatusResponse({ status, paid: false });
1988
- return isPaymentStatusFailure(normalized.status);
1989
- }
1990
- ensureNotAborted() {
1991
- if (this.signal?.aborted) {
1992
- throw createAbortError();
1993
- }
1994
- }
1995
- wait(ms) {
1996
- this.ensureNotAborted();
1997
- return new Promise((resolve, reject) => {
1998
- const timer = setTimeout(() => {
1999
- if (this.signal) {
2000
- this.signal.removeEventListener("abort", onAbort);
2001
- }
2002
- resolve();
2003
- }, ms);
2004
- const onAbort = () => {
2005
- clearTimeout(timer);
2006
- reject(createAbortError());
2007
- };
2008
- if (this.signal) {
2009
- this.signal.addEventListener("abort", onAbort, { once: true });
2010
- }
2011
- });
2012
- }
2013
- };
2014
-
2015
- // src/checkout.ts
2016
- function toTerminalFailure(code, message, recoverable) {
2017
- return {
2018
- success: false,
2019
- error: {
2020
- code,
2021
- message,
2022
- recoverable
2023
- }
2024
- };
2025
- }
2026
- function generateIdempotencyKey() {
2027
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
2028
- return `idem_${crypto.randomUUID()}`;
2029
- }
2030
- const timestamp = Date.now().toString(36);
2031
- const random = Math.random().toString(36).substring(2, 15);
2032
- return `idem_${timestamp}_${random}`;
2033
- }
2034
- var CheckoutService = class {
2035
- constructor(client) {
2036
- this.client = client;
2037
- }
2038
- orderTokenParam(orderId) {
2039
- const token = this.client.getOrderToken(orderId);
2040
- return token ? `?token=${encodeURIComponent(token)}` : "";
2041
- }
2042
- async process(data) {
2043
- const checkoutData = {
2044
- ...data,
2045
- idempotency_key: data.idempotency_key || generateIdempotencyKey()
2046
- };
2047
- const result = await safe(
2048
- this.client.post("/api/v1/checkout", {
2049
- checkout_data: checkoutData
2050
- })
2051
- );
2052
- if (result.ok && result.value.bill_token) {
2053
- this.client.setOrderToken(result.value.order_id, result.value.bill_token);
2054
- }
2055
- return result;
2056
- }
2057
- async initializePayment(_orderId, _method) {
2058
- return err(
2059
- new CimplifyError(
2060
- "NOT_IMPLEMENTED",
2061
- "initializePayment is not available in REST mode.",
2062
- false
2063
- )
2064
- );
2065
- }
2066
- async submitAuthorization(input) {
2067
- return safe(this.client.post("/api/v1/payments/authorization", input));
2068
- }
2069
- async pollPaymentStatus(orderId) {
2070
- const encodedId = encodeURIComponent(orderId);
2071
- const tokenParam = this.orderTokenParam(orderId);
2072
- return safe(this.client.get(`/api/v1/orders/${encodedId}/payment-status${tokenParam}`));
2073
- }
2074
- async updateOrderCustomer(orderId, customer) {
2075
- const encodedId = encodeURIComponent(orderId);
2076
- const tokenParam = this.orderTokenParam(orderId);
2077
- return safe(this.client.post(`/api/v1/orders/${encodedId}/customer${tokenParam}`, customer));
2078
- }
2079
- async verifyPayment(orderId) {
2080
- const encodedId = encodeURIComponent(orderId);
2081
- const tokenParam = this.orderTokenParam(orderId);
2082
- return safe(this.client.post(`/api/v1/orders/${encodedId}/verify-payment${tokenParam}`));
2083
- }
2084
- async processAndResolve(data) {
2085
- data.on_status_change?.("preparing", {});
2086
- if (!data.cart_id) {
2087
- return ok(
2088
- toTerminalFailure(
2089
- "INVALID_CART",
2090
- "A valid cart is required before checkout can start.",
2091
- false
2092
- )
2093
- );
2094
- }
2095
- const cartResult = await this.client.cart.get();
2096
- if (!cartResult.ok || !cartResult.value?.id) {
2097
- return ok(
2098
- toTerminalFailure(
2099
- "INVALID_CART",
2100
- "Unable to load cart for checkout.",
2101
- false
2102
- )
2103
- );
2104
- }
2105
- const cart = cartResult.value;
2106
- if (cart.id !== data.cart_id || cart.items.length === 0) {
2107
- return ok(
2108
- toTerminalFailure(
2109
- "INVALID_CART",
2110
- "Cart is empty or no longer valid. Please refresh and try again.",
2111
- false
2112
- )
2113
- );
2114
- }
2115
- const checkoutData = {
2116
- cart_id: data.cart_id,
2117
- location_id: data.location_id,
2118
- customer: data.customer,
2119
- order_type: data.order_type,
2120
- address_info: data.address_info,
2121
- payment_method: data.payment_method,
2122
- mobile_money_details: data.mobile_money_details,
2123
- special_instructions: data.special_instructions,
2124
- link_address_id: data.link_address_id,
2125
- link_payment_method_id: data.link_payment_method_id,
2126
- idempotency_key: data.idempotency_key || generateIdempotencyKey(),
2127
- metadata: data.metadata,
2128
- pay_currency: data.pay_currency,
2129
- fx_quote_id: data.fx_quote_id
2130
- };
2131
- const baseCurrency = currencyCode(
2132
- (cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase()
2133
- );
2134
- const payCurrency = data.pay_currency?.trim().toUpperCase();
2135
- const payCurrencyCode = payCurrency ? currencyCode(payCurrency) : void 0;
2136
- const cartTotalAmount = typeof cart.pricing.total_price === "number" ? cart.pricing.total_price : Number.parseFloat(cart.pricing.total_price || "0");
2137
- if (payCurrencyCode && payCurrencyCode !== baseCurrency && !checkoutData.fx_quote_id && Number.isFinite(cartTotalAmount) && cartTotalAmount > 0) {
2138
- const fxQuoteResult = await this.client.fx.lockQuote({
2139
- from: baseCurrency,
2140
- to: payCurrencyCode,
2141
- amount: cart.pricing.total_price
2142
- });
2143
- if (!fxQuoteResult.ok) {
2144
- return ok(
2145
- toTerminalFailure(
2146
- "FX_QUOTE_FAILED",
2147
- fxQuoteResult.error.message || "Unable to lock FX quote for checkout.",
2148
- true
2149
- )
2150
- );
2151
- }
2152
- checkoutData.pay_currency = payCurrencyCode;
2153
- checkoutData.fx_quote_id = fxQuoteResult.value.id;
2154
- }
2155
- data.on_status_change?.("processing", {});
2156
- const processResult = await this.process(checkoutData);
2157
- if (!processResult.ok) {
2158
- return ok(
2159
- toTerminalFailure(
2160
- processResult.error.code || "CHECKOUT_FAILED",
2161
- processResult.error.message || "Unable to process checkout.",
2162
- processResult.error.retryable ?? true
2163
- )
2164
- );
2165
- }
2166
- const resolver = new CheckoutResolver({
2167
- client: this.client,
2168
- checkoutData,
2169
- pollIntervalMs: data.poll_interval_ms,
2170
- maxPollAttempts: data.max_poll_attempts,
2171
- onStatusChange: data.on_status_change,
2172
- onAuthorizationRequired: data.on_authorization_required,
2173
- signal: data.signal,
2174
- returnUrl: data.return_url,
2175
- enrollInLink: data.enroll_in_link,
2176
- cartTotal: cart.pricing.total_price,
2177
- cartCurrency: checkoutData.pay_currency || cart.pricing.currency || "GHS",
2178
- orderType: checkoutData.order_type,
2179
- allowPopups: data.allow_popups
2180
- });
2181
- const resolved = await resolver.resolve(processResult.value);
2182
- return ok(resolved);
2183
- }
2184
- };
2185
-
2186
- // src/orders.ts
2187
- var OrderQueries = class {
2188
- constructor(client) {
2189
- this.client = client;
2190
- }
2191
- orderTokenParam(orderId) {
2192
- const token = this.client.getOrderToken(orderId);
2193
- return token ? `?token=${encodeURIComponent(token)}` : "";
2194
- }
2195
- async list(options) {
2196
- const params = new URLSearchParams();
2197
- if (options?.status) params.set("status", options.status);
2198
- if (options?.limit) params.set("limit", String(options.limit));
2199
- if (options?.offset) params.set("offset", String(options.offset));
2200
- const path = params.toString() ? `/api/v1/orders?${params.toString()}` : "/api/v1/orders";
2201
- return safe(this.client.get(path));
2202
- }
2203
- async get(orderId) {
2204
- const encodedId = encodeURIComponent(orderId);
2205
- const tokenParam = this.orderTokenParam(orderId);
2206
- return safe(this.client.get(`/api/v1/orders/${encodedId}${tokenParam}`));
2207
- }
2208
- async getRecent(limit = 5) {
2209
- return this.list({ limit });
2210
- }
2211
- async getByStatus(status) {
2212
- return this.list({ status });
2213
- }
2214
- async cancel(orderId, reason) {
2215
- const encodedId = encodeURIComponent(orderId);
2216
- const tokenParam = this.orderTokenParam(orderId);
2217
- return safe(
2218
- this.client.post(`/api/v1/orders/${encodedId}/cancel${tokenParam}`, {
2219
- reason
2220
- })
2221
- );
2222
- }
2223
- };
2224
-
2225
- // src/link.ts
2226
- function encodePathSegment(value) {
2227
- return encodeURIComponent(value);
2228
- }
2229
- function toCreateAddressPayload(input) {
2230
- return {
2231
- label: input.label,
2232
- street_address: input.address_line1,
2233
- apartment: input.address_line2,
2234
- city: input.city,
2235
- region: input.state ?? "",
2236
- postal_code: input.postal_code,
2237
- country: input.country
2238
- };
2239
- }
2240
- function toUpdateAddressPayload(input) {
2241
- return {
2242
- label: input.label,
2243
- street_address: input.address_line1,
2244
- apartment: input.address_line2,
2245
- city: input.city,
2246
- region: input.state,
2247
- postal_code: input.postal_code,
2248
- country: input.country
2249
- };
2250
- }
2251
- function toCreateMobileMoneyPayload(input) {
2252
- return {
2253
- phone_number: input.phone_number,
2254
- provider: input.provider,
2255
- label: input.account_name
2256
- };
2257
- }
2258
- var LinkService = class {
2259
- constructor(client) {
2260
- this.client = client;
2261
- }
2262
- async getProfile() {
2263
- return safe(this.client.linkGet("/v1/link/profile"));
2264
- }
2265
- async requestOtp(input) {
2266
- return safe(this.client.linkPost("/v1/link/auth/request-otp", input));
2267
- }
2268
- async verifyOtp(input) {
2269
- const result = await safe(
2270
- this.client.linkPost("/v1/link/auth/verify-otp", input)
2271
- );
2272
- if (result.ok && result.value.session_token) {
2273
- this.client.setAccessToken(result.value.session_token);
2274
- }
2275
- return result;
2276
- }
2277
- async logout() {
2278
- const result = await safe(this.client.linkPost("/v1/link/auth/logout"));
2279
- if (result.ok) {
2280
- this.client.clearSession();
2281
- }
2282
- return result;
2283
- }
2284
- async checkStatus(contact) {
2285
- return safe(this.client.linkPost("/v1/link/check-status", { contact }));
2286
- }
2287
- async getLinkData() {
2288
- return safe(this.client.linkGet("/v1/link/data"));
2289
- }
2290
- async getAddresses() {
2291
- return safe(this.client.linkGet("/v1/link/addresses"));
2292
- }
2293
- async getMobileMoney() {
2294
- return safe(this.client.linkGet("/v1/link/mobile-money"));
2295
- }
2296
- async getPreferences() {
2297
- return safe(this.client.linkGet("/v1/link/preferences"));
2298
- }
2299
- async enroll(data) {
2300
- return safe(this.client.linkPost("/v1/link/enroll", data));
2301
- }
2302
- async enrollAndLinkOrder(data) {
2303
- return safe(
2304
- this.client.linkPost("/v1/link/enroll-and-link-order", data)
2305
- );
2306
- }
2307
- async updatePreferences(preferences) {
2308
- return safe(this.client.linkPost("/v1/link/preferences", preferences));
2309
- }
2310
- async createAddress(input) {
2311
- return safe(
2312
- this.client.linkPost("/v1/link/addresses", toCreateAddressPayload(input))
2313
- );
2314
- }
2315
- async updateAddress(input) {
2316
- return safe(
2317
- this.client.linkPost(
2318
- `/v1/link/addresses/${encodePathSegment(input.address_id)}`,
2319
- toUpdateAddressPayload(input)
2320
- )
2321
- );
2322
- }
2323
- async deleteAddress(addressId) {
2324
- return safe(
2325
- this.client.linkDelete(`/v1/link/addresses/${encodePathSegment(addressId)}`)
2326
- );
2327
- }
2328
- async setDefaultAddress(addressId) {
2329
- return safe(
2330
- this.client.linkPost(
2331
- `/v1/link/addresses/${encodePathSegment(addressId)}/default`
2332
- )
2333
- );
2334
- }
2335
- async trackAddressUsage(addressId) {
2336
- return safe(
2337
- this.client.linkPost(
2338
- `/v1/link/addresses/${encodePathSegment(addressId)}/track-usage`
2339
- )
2340
- );
2341
- }
2342
- async createMobileMoney(input) {
2343
- return safe(
2344
- this.client.linkPost(
2345
- "/v1/link/mobile-money",
2346
- toCreateMobileMoneyPayload(input)
2347
- )
2348
- );
2349
- }
2350
- async deleteMobileMoney(mobileMoneyId) {
2351
- return safe(
2352
- this.client.linkDelete(
2353
- `/v1/link/mobile-money/${encodePathSegment(mobileMoneyId)}`
2354
- )
2355
- );
2356
- }
2357
- async setDefaultMobileMoney(mobileMoneyId) {
2358
- return safe(
2359
- this.client.linkPost(
2360
- `/v1/link/mobile-money/${encodePathSegment(mobileMoneyId)}/default`
2361
- )
2362
- );
2363
- }
2364
- async trackMobileMoneyUsage(mobileMoneyId) {
2365
- return safe(
2366
- this.client.linkPost(
2367
- `/v1/link/mobile-money/${encodePathSegment(mobileMoneyId)}/track-usage`
2368
- )
2369
- );
2370
- }
2371
- async verifyMobileMoney(mobileMoneyId) {
2372
- return safe(
2373
- this.client.linkPost(
2374
- `/v1/link/mobile-money/${encodePathSegment(mobileMoneyId)}/verify`
2375
- )
2376
- );
2377
- }
2378
- async getSessions() {
2379
- return safe(this.client.linkGet("/v1/link/sessions"));
2380
- }
2381
- async revokeSession(sessionId) {
2382
- return safe(this.client.linkDelete(`/v1/link/sessions/${sessionId}`));
2383
- }
2384
- async revokeAllSessions() {
2385
- return safe(this.client.linkDelete("/v1/link/sessions"));
2386
- }
2387
- };
2388
-
2389
- // src/auth.ts
2390
- var AuthService = class {
2391
- constructor(client) {
2392
- this.client = client;
2393
- }
2394
- async getStatus() {
2395
- return safe(this.client.get("/api/v1/auth/status"));
2396
- }
2397
- async getCurrentUser() {
2398
- const result = await this.getStatus();
2399
- if (!result.ok) return result;
2400
- return ok(result.value.customer || null);
2401
- }
2402
- async isAuthenticated() {
2403
- const result = await this.getStatus();
2404
- if (!result.ok) return result;
2405
- return ok(result.value.is_authenticated);
2406
- }
2407
- async requestOtp(contact, contactType) {
2408
- return safe(
2409
- this.client.post("/api/v1/auth/request-otp", {
2410
- contact,
2411
- contact_type: contactType
2412
- })
2413
- );
2414
- }
2415
- async verifyOtp(code, contact) {
2416
- return safe(
2417
- this.client.post("/api/v1/auth/verify-otp", {
2418
- otp_code: code,
2419
- contact
2420
- })
2421
- );
2422
- }
2423
- async logout() {
2424
- return safe(this.client.post("/api/v1/auth/logout"));
2425
- }
2426
- async updateProfile(input) {
2427
- return safe(this.client.post("/api/v1/auth/profile", input));
2428
- }
2429
- };
2430
-
2431
- // src/business.ts
2432
- var BusinessService = class {
2433
- constructor(client) {
2434
- this.client = client;
2435
- }
2436
- async getInfo() {
2437
- return safe(this.client.get("/api/v1/business"));
2438
- }
2439
- async getByHandle(handle) {
2440
- const encodedHandle = encodeURIComponent(handle);
2441
- return safe(this.client.get(`/api/v1/business/by-handle/${encodedHandle}`));
2442
- }
2443
- async getByDomain(domain) {
2444
- const encodedDomain = encodeURIComponent(domain);
2445
- return safe(this.client.get(`/api/v1/business/by-domain?domain=${encodedDomain}`));
2446
- }
2447
- async getSettings() {
2448
- return safe(this.client.get("/api/v1/business/settings"));
2449
- }
2450
- async getLocations() {
2451
- return safe(this.client.get("/api/v1/business/locations"));
2452
- }
2453
- async getLocation(locationId) {
2454
- const result = await this.getLocations();
2455
- if (!result.ok) return err(result.error);
2456
- const location = result.value.find((item) => item.id === locationId);
2457
- if (!location) {
2458
- return err(new CimplifyError(ErrorCode.NOT_FOUND, `Location not found: ${locationId}`, false));
2459
- }
2460
- return ok(location);
2461
- }
2462
- async getHours() {
2463
- return safe(this.client.get("/api/v1/business/hours"));
2464
- }
2465
- async getLocationHours(locationId) {
2466
- const result = await this.getHours();
2467
- if (!result.ok) return result;
2468
- return ok(result.value.filter((hour) => hour.location_id === locationId));
2469
- }
2470
- async getBootstrap() {
2471
- return safe(this.client.get("/api/v1/bootstrap"));
2472
- }
2473
- };
2474
-
2475
- // src/inventory.ts
2476
- var InventoryService = class {
2477
- constructor(client) {
2478
- this.client = client;
2479
- }
2480
- withQuery(path, params) {
2481
- const searchParams = new URLSearchParams();
2482
- for (const [key, value] of Object.entries(params)) {
2483
- if (value === void 0) continue;
2484
- searchParams.set(key, String(value));
2485
- }
2486
- const query = searchParams.toString();
2487
- return query ? `${path}?${query}` : path;
2488
- }
2489
- async getProductStock(productId, locationId) {
2490
- const encodedId = encodeURIComponent(productId);
2491
- const path = this.withQuery(`/api/v1/inventory/products/${encodedId}/stock`, {
2492
- location_id: locationId
2493
- });
2494
- return safe(this.client.get(path));
2495
- }
2496
- async getVariantStock(variantId, locationId) {
2497
- const encodedId = encodeURIComponent(variantId);
2498
- const path = this.withQuery(`/api/v1/inventory/variants/${encodedId}/stock`, {
2499
- location_id: locationId
2500
- });
2501
- return safe(this.client.get(path));
2502
- }
2503
- async checkProductAvailability(productId, quantity, locationId) {
2504
- const encodedId = encodeURIComponent(productId);
2505
- const path = this.withQuery(`/api/v1/inventory/products/${encodedId}/availability`, {
2506
- quantity,
2507
- location_id: locationId
2508
- });
2509
- return safe(this.client.get(path));
2510
- }
2511
- async checkVariantAvailability(variantId, quantity, locationId) {
2512
- const encodedId = encodeURIComponent(variantId);
2513
- const path = this.withQuery(`/api/v1/inventory/variants/${encodedId}/availability`, {
2514
- quantity,
2515
- location_id: locationId
2516
- });
2517
- return safe(this.client.get(path));
2518
- }
2519
- async checkMultipleAvailability(items, locationId) {
2520
- const results = await Promise.all(
2521
- items.map(
2522
- (item) => item.variant_id ? this.checkVariantAvailability(item.variant_id, item.quantity, locationId) : this.checkProductAvailability(item.product_id, item.quantity, locationId)
2523
- )
2524
- );
2525
- for (const result of results) {
2526
- if (!result.ok) return result;
2527
- }
2528
- return ok(results.map((r) => r.value));
2529
- }
2530
- async isInStock(productId, locationId) {
2531
- const result = await this.checkProductAvailability(productId, 1, locationId);
2532
- if (!result.ok) return result;
2533
- return ok(result.value.is_available);
2534
- }
2535
- async getAvailableQuantity(productId, locationId) {
2536
- const result = await this.getProductStock(productId, locationId);
2537
- if (!result.ok) return result;
2538
- return ok(result.value.available_quantity);
2539
- }
2540
- };
2541
-
2542
- // src/scheduling.ts
2543
- function withQuery2(path, params) {
2544
- const searchParams = new URLSearchParams();
2545
- for (const [key, value] of Object.entries(params)) {
2546
- if (value === void 0) continue;
2547
- searchParams.set(key, String(value));
2548
- }
2549
- const query = searchParams.toString();
2550
- return query ? `${path}?${query}` : path;
2551
- }
2552
- function firstScheduledTime(booking) {
2553
- return booking.service_items.find((item) => typeof item.scheduled_start === "string")?.scheduled_start || booking.created_at;
2554
- }
2555
- var SchedulingService = class {
2556
- constructor(client) {
2557
- this.client = client;
2558
- }
2559
- async getServices() {
2560
- return safe(this.client.get("/api/v1/scheduling/services"));
2561
- }
2562
- async getService(serviceId) {
2563
- const encodedId = encodeURIComponent(serviceId);
2564
- return safe(this.client.get(`/api/v1/scheduling/services/${encodedId}`));
2565
- }
2566
- async getAvailableSlots(input) {
2567
- const path = withQuery2("/api/v1/scheduling/slots", {
2568
- service_id: input.service_id,
2569
- date: input.date,
2570
- participant_count: input.participant_count,
2571
- duration_minutes: input.duration_minutes
2572
- });
2573
- return safe(this.client.get(path));
2574
- }
2575
- async checkSlotAvailability(input) {
2576
- const path = withQuery2("/api/v1/scheduling/slots/check", {
2577
- service_id: input.service_id,
2578
- slot_time: input.slot_time,
2579
- duration_minutes: input.duration_minutes,
2580
- participant_count: input.participant_count
2581
- });
2582
- return safe(this.client.get(path));
2583
- }
2584
- async getServiceAvailability(params) {
2585
- const path = withQuery2("/api/v1/scheduling/availability", {
2586
- service_id: params.service_id,
2587
- start_date: params.start_date,
2588
- end_date: params.end_date,
2589
- location_id: params.location_id,
2590
- participant_count: params.participant_count
2591
- });
2592
- return safe(this.client.get(path));
2593
- }
2594
- async getBooking(bookingId) {
2595
- const encodedId = encodeURIComponent(bookingId);
2596
- return safe(this.client.get(`/api/v1/scheduling/bookings/${encodedId}`));
2597
- }
2598
- async getCustomerBookings(customerId) {
2599
- const path = withQuery2("/api/v1/scheduling/bookings", {
2600
- customer_id: customerId
2601
- });
2602
- return safe(this.client.get(path));
2603
- }
2604
- async getUpcomingBookings() {
2605
- const result = await this.getCustomerBookings();
2606
- if (!result.ok) return result;
2607
- const upcoming = result.value.filter((booking) => {
2608
- const status = String(booking.status).toLowerCase();
2609
- return status !== "completed" && status !== "cancelled";
2610
- }).sort((a, b) => firstScheduledTime(a).localeCompare(firstScheduledTime(b)));
2611
- return ok(upcoming);
2612
- }
2613
- async getPastBookings(limit = 10) {
2614
- const result = await this.getCustomerBookings();
2615
- if (!result.ok) return result;
2616
- const past = result.value.filter((booking) => String(booking.status).toLowerCase() === "completed").sort((a, b) => firstScheduledTime(b).localeCompare(firstScheduledTime(a))).slice(0, Math.max(0, limit));
2617
- return ok(past);
2618
- }
2619
- async cancelBooking(input) {
2620
- const encodedId = encodeURIComponent(input.booking_id);
2621
- return safe(
2622
- this.client.post(`/api/v1/scheduling/bookings/${encodedId}/cancel`, {
2623
- reason: input.reason
2624
- })
2625
- );
2626
- }
2627
- async rescheduleBooking(input) {
2628
- return safe(
2629
- this.client.post("/api/v1/scheduling/bookings/reschedule", input)
2630
- );
2631
- }
2632
- async getNextAvailableSlot(serviceId, fromDate) {
2633
- const date = fromDate || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2634
- const result = await this.getAvailableSlots({
2635
- service_id: serviceId,
2636
- date
2637
- });
2638
- if (!result.ok) return result;
2639
- return ok(result.value.find((slot) => slot.is_available) || null);
2640
- }
2641
- async hasAvailabilityOn(serviceId, date) {
2642
- const result = await this.getAvailableSlots({
2643
- service_id: serviceId,
2644
- date
2645
- });
2646
- if (!result.ok) return result;
2647
- return ok(result.value.some((slot) => slot.is_available));
2648
- }
2649
- };
2650
-
2651
- // src/lite.ts
2652
- var LiteService = class {
2653
- constructor(client) {
2654
- this.client = client;
2655
- }
2656
- async getBootstrap() {
2657
- return safe(this.client.get("/api/v1/lite/bootstrap"));
2658
- }
2659
- async getTable(tableId) {
2660
- const encodedId = encodeURIComponent(tableId);
2661
- return safe(this.client.get(`/api/v1/lite/tables/${encodedId}`));
2662
- }
2663
- async getMenu() {
2664
- return safe(this.client.get("/api/v1/lite/menu"));
2665
- }
2666
- async getMenuByCategory(categoryId) {
2667
- const encodedId = encodeURIComponent(categoryId);
2668
- return safe(this.client.get(`/api/v1/lite/menu/categories/${encodedId}`));
2669
- }
2670
- };
2671
-
2672
- // src/fx.ts
2673
- var FxService = class {
2674
- constructor(client) {
2675
- this.client = client;
2676
- }
2677
- async getRate(from, to) {
2678
- const path = `/api/v1/fx/rate?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`;
2679
- return safe(this.client.get(path));
2680
- }
2681
- async lockQuote(request) {
2682
- return safe(this.client.post("/api/v1/fx/quotes", request));
2683
- }
2684
- };
2685
-
2686
- // src/activity.ts
2687
- var ActivityService = class {
2688
- constructor(client) {
2689
- this.client = client;
2690
- }
2691
- trackProductView(productId, options = {}) {
2692
- const event = {
2693
- type: "product_view",
2694
- product_id: productId,
2695
- product_name: options.productName,
2696
- category_id: options.categoryId,
2697
- price: options.price,
2698
- page_path: options.pagePath ?? currentPath()
2699
- };
2700
- this.sendEvents([event]);
2701
- }
2702
- trackCategoryView(categoryId, options = {}) {
2703
- const event = {
2704
- type: "category_view",
2705
- category_id: categoryId,
2706
- category_name: options.categoryName,
2707
- page_path: options.pagePath ?? currentPath()
2708
- };
2709
- this.sendEvents([event]);
2710
- }
2711
- sendEvents(events) {
2712
- try {
2713
- this.client.post("/api/v1/activity/events", { events }).catch(() => {
2714
- });
2715
- } catch {
2716
- }
2717
- }
2718
- };
2719
- function currentPath() {
2720
- if (typeof window !== "undefined") {
2721
- return window.location.pathname;
2722
- }
2723
- return void 0;
2724
- }
2725
-
2726
- // src/types/elements.ts
2727
- var ELEMENT_TYPES = {
2728
- AUTH: "auth",
2729
- ADDRESS: "address",
2730
- PAYMENT: "payment",
2731
- CHECKOUT: "checkout"
2732
- };
2733
- var MESSAGE_TYPES = {
2734
- INIT: "init",
2735
- SET_TOKEN: "set_token",
2736
- SET_CART: "set_cart",
2737
- GET_DATA: "get_data",
2738
- PROCESS_CHECKOUT: "process_checkout",
2739
- ABORT_CHECKOUT: "abort_checkout",
2740
- READY: "ready",
2741
- HEIGHT_CHANGE: "height_change",
2742
- AUTHENTICATED: "authenticated",
2743
- REQUIRES_OTP: "requires_otp",
2744
- ERROR: "error",
2745
- ADDRESS_CHANGED: "address_changed",
2746
- ADDRESS_SELECTED: "address_selected",
2747
- PAYMENT_METHOD_SELECTED: "payment_method_selected",
2748
- TOKEN_REFRESHED: "token_refreshed",
2749
- LOGOUT_COMPLETE: "logout_complete",
2750
- CONTACT_PROVIDED: "contact_provided",
2751
- CHECKOUT_STATUS: "checkout_status",
2752
- CHECKOUT_COMPLETE: "checkout_complete",
2753
- ORDER_TYPE_CHANGED: "order_type_changed",
2754
- REQUEST_SUBMIT: "request_submit"
2755
- };
2756
- var EVENT_TYPES = {
2757
- READY: "ready",
2758
- AUTHENTICATED: "authenticated",
2759
- REQUIRES_OTP: "requires_otp",
2760
- ERROR: "error",
2761
- CHANGE: "change",
2762
- ORDER_TYPE_CHANGED: "order_type_changed",
2763
- REQUEST_SUBMIT: "request_submit"
2764
- };
2765
-
2766
- // src/elements.ts
2767
- function toCheckoutError(code, message, recoverable) {
2768
- const hint = getErrorHint(code);
2769
- return {
2770
- success: false,
2771
- error: {
2772
- code,
2773
- message,
2774
- recoverable,
2775
- docs_url: hint?.docs_url,
2776
- suggestion: hint?.suggestion
2777
- }
2778
- };
2779
- }
2780
- var DEFAULT_LINK_URL = "https://link.cimplify.io";
2781
- function generateNonce() {
2782
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
2783
- return crypto.randomUUID().replace(/-/g, "").slice(0, 16);
2784
- }
2785
- return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
2786
- }
2787
- function isAllowedOrigin(origin) {
2788
- try {
2789
- const url = new URL(origin);
2790
- const hostname = url.hostname;
2791
- if (hostname === "localhost" || hostname === "127.0.0.1") {
2792
- return true;
2793
- }
2794
- if (url.protocol !== "https:") {
2795
- return false;
2796
- }
2797
- return hostname === "cimplify.io" || hostname.endsWith(".cimplify.io");
2798
- } catch {
2799
- return false;
2800
- }
2801
- }
2802
- function parseIframeMessage(data) {
2803
- if (!data || typeof data !== "object") {
2804
- return null;
2805
- }
2806
- const maybeType = data.type;
2807
- if (typeof maybeType !== "string") {
2808
- return null;
2809
- }
2810
- return data;
2811
- }
2812
- var CimplifyElements = class {
2813
- constructor(client, businessId, options = {}) {
2814
- this.elements = /* @__PURE__ */ new Map();
2815
- this.accessToken = null;
2816
- this.accountId = null;
2817
- this.customerId = null;
2818
- this.customerData = null;
2819
- this.addressData = null;
2820
- this.paymentData = null;
2821
- this.checkoutInProgress = false;
2822
- this.activeCheckoutAbort = null;
2823
- this.businessIdResolvePromise = null;
2824
- this.client = client;
2825
- this.businessId = businessId ?? null;
2826
- this.linkUrl = options.linkUrl || DEFAULT_LINK_URL;
2827
- this.options = options;
2828
- this.boundHandleMessage = this.handleMessage.bind(this);
2829
- if (typeof window !== "undefined") {
2830
- window.addEventListener("message", this.boundHandleMessage);
2831
- }
2832
- }
2833
- debug(...args) {
2834
- if (this.client.isTestMode()) {
2835
- console.debug(...args);
2836
- }
2837
- }
2838
- create(type, options = {}) {
2839
- const existing = this.elements.get(type);
2840
- if (existing) return existing;
2841
- const element = new CimplifyElement(type, this.businessId, this.linkUrl, options, this);
2842
- this.elements.set(type, element);
2843
- return element;
2844
- }
2845
- getElement(type) {
2846
- return this.elements.get(type);
2847
- }
2848
- _removeElement(type, instance) {
2849
- const current = this.elements.get(type);
2850
- if (current === instance) {
2851
- this.elements.delete(type);
2852
- }
2853
- }
2854
- destroy() {
2855
- this.elements.forEach((element) => element.destroy());
2856
- this.elements.clear();
2857
- if (typeof window !== "undefined") {
2858
- window.removeEventListener("message", this.boundHandleMessage);
2859
- }
2860
- }
2861
- async submitCheckout(data) {
2862
- const result = await this.processCheckout({
2863
- cart_id: data.cart_id,
2864
- order_type: data.order_type ?? "delivery",
2865
- location_id: data.location_id,
2866
- notes: data.notes,
2867
- scheduled_time: data.scheduled_time,
2868
- tip_amount: data.tip_amount,
2869
- enroll_in_link: true
2870
- });
2871
- if (result.success) {
2872
- return {
2873
- success: true,
2874
- order: {
2875
- id: result.order?.id || "",
2876
- status: result.order?.status || "unknown",
2877
- total: result.order?.total || ""
2878
- }
2879
- };
2880
- }
2881
- return {
2882
- success: false,
2883
- error: {
2884
- code: result.error?.code || "CHECKOUT_FAILED",
2885
- message: result.error?.message || "Checkout failed"
2886
- }
2887
- };
2888
- }
2889
- processCheckout(options) {
2890
- let abortFn = null;
2891
- const task = (async () => {
2892
- this.debug("[cimplify:checkout] processCheckout called", { cart_id: options.cart_id, order_type: options.order_type, location_id: options.location_id });
2893
- if (this.checkoutInProgress) {
2894
- this.debug("[cimplify:checkout] BLOCKED: already processing");
2895
- return toCheckoutError(
2896
- "ALREADY_PROCESSING",
2897
- "Checkout is already in progress.",
2898
- false
2899
- );
2900
- }
2901
- this.checkoutInProgress = true;
2902
- if (!options.cart_id) {
2903
- this.debug("[cimplify:checkout] BLOCKED: no cart_id");
2904
- this.checkoutInProgress = false;
2905
- return toCheckoutError(
2906
- "INVALID_CART",
2907
- "A valid cart is required before checkout can start.",
2908
- false
2909
- );
2910
- }
2911
- if (!options.order_type) {
2912
- this.debug("[cimplify:checkout] BLOCKED: no order_type");
2913
- this.checkoutInProgress = false;
2914
- return toCheckoutError(
2915
- "ORDER_TYPE_REQUIRED",
2916
- "Order type is required before checkout can start.",
2917
- false
2918
- );
2919
- }
2920
- const checkoutElement = this.elements.get(ELEMENT_TYPES.CHECKOUT) || this.elements.get(ELEMENT_TYPES.PAYMENT);
2921
- if (!checkoutElement) {
2922
- this.debug("[cimplify:checkout] BLOCKED: no checkout element");
2923
- this.checkoutInProgress = false;
2924
- return toCheckoutError(
2925
- "NO_PAYMENT_ELEMENT",
2926
- "Checkout element must be mounted before checkout.",
2927
- false
2928
- );
2929
- }
2930
- if (!checkoutElement.isMounted()) {
2931
- this.debug("[cimplify:checkout] BLOCKED: checkout element not mounted");
2932
- this.checkoutInProgress = false;
2933
- return toCheckoutError(
2934
- "PAYMENT_NOT_MOUNTED",
2935
- "Checkout element must be mounted before checkout.",
2936
- false
2937
- );
2938
- }
2939
- const isUnifiedCheckout = this.elements.has(ELEMENT_TYPES.CHECKOUT);
2940
- if (!isUnifiedCheckout) {
2941
- const authElement = this.elements.get(ELEMENT_TYPES.AUTH);
2942
- if (authElement && !this.accessToken) {
2943
- this.debug("[cimplify:checkout] BLOCKED: auth incomplete");
2944
- this.checkoutInProgress = false;
2945
- return toCheckoutError(
2946
- "AUTH_INCOMPLETE",
2947
- "Authentication must complete before checkout can start.",
2948
- true
2949
- );
2950
- }
2951
- const addressElement = this.elements.get(ELEMENT_TYPES.ADDRESS);
2952
- if (addressElement) {
2953
- this.debug("[cimplify:checkout] getting address data...");
2954
- await addressElement.getData();
2955
- this.debug("[cimplify:checkout] address data resolved", this.addressData);
2956
- }
2957
- await this.hydrateCustomerData();
2958
- }
2959
- this.debug("[cimplify:checkout] customer data hydrated", { customerData: this.customerData, accountId: this.accountId, isUnifiedCheckout });
2960
- const processMessage = {
2961
- type: MESSAGE_TYPES.PROCESS_CHECKOUT,
2962
- cart_id: options.cart_id,
2963
- order_type: options.order_type,
2964
- location_id: options.location_id,
2965
- notes: options.notes,
2966
- scheduled_time: options.scheduled_time,
2967
- tip_amount: options.tip_amount,
2968
- pay_currency: options.pay_currency,
2969
- enroll_in_link: options.enroll_in_link ?? true,
2970
- address: isUnifiedCheckout ? void 0 : this.addressData ?? void 0,
2971
- customer: isUnifiedCheckout ? void 0 : this.customerData ? {
2972
- name: this.customerData.name,
2973
- email: this.customerData.email,
2974
- phone: this.customerData.phone
2975
- } : null,
2976
- account_id: this.accountId ?? void 0,
2977
- customer_id: this.customerId ?? void 0
2978
- };
2979
- const timeoutMs = options.timeout_ms ?? 18e4;
2980
- const paymentWindow = checkoutElement.getContentWindow();
2981
- return new Promise((resolve) => {
2982
- let settled = false;
2983
- const cleanup = () => {
2984
- if (typeof window !== "undefined") {
2985
- window.removeEventListener("message", handleCheckoutMessage);
2986
- }
2987
- clearTimeout(timeout);
2988
- this.checkoutInProgress = false;
2989
- this.activeCheckoutAbort = null;
2990
- };
2991
- const settle = (result) => {
2992
- if (settled) {
2993
- return;
2994
- }
2995
- settled = true;
2996
- cleanup();
2997
- resolve(result);
2998
- };
2999
- const timeout = setTimeout(() => {
3000
- settle(
3001
- toCheckoutError(
3002
- "TIMEOUT",
3003
- "Checkout timed out before receiving a terminal response.",
3004
- true
3005
- )
3006
- );
3007
- }, timeoutMs);
3008
- const handleCheckoutMessage = (event) => {
3009
- if (!isAllowedOrigin(event.origin)) {
3010
- return;
3011
- }
3012
- const message = parseIframeMessage(event.data);
3013
- if (!message) {
3014
- return;
3015
- }
3016
- if (message.type === MESSAGE_TYPES.LOGOUT_COMPLETE) {
3017
- checkoutElement.sendMessage({ type: MESSAGE_TYPES.ABORT_CHECKOUT });
3018
- settle(
3019
- toCheckoutError(
3020
- "AUTH_LOST",
3021
- "Authentication was cleared during checkout.",
3022
- true
3023
- )
3024
- );
3025
- return;
3026
- }
3027
- if (paymentWindow && event.source && event.source !== paymentWindow) {
3028
- return;
3029
- }
3030
- if (message.type === MESSAGE_TYPES.CHECKOUT_STATUS) {
3031
- options.on_status_change?.(message.status, message.context);
3032
- return;
3033
- }
3034
- if (message.type === MESSAGE_TYPES.CHECKOUT_COMPLETE) {
3035
- this.debug("[cimplify:checkout] received checkout_complete", { success: message.success, error: message.error });
3036
- settle({
3037
- success: message.success,
3038
- order: message.order,
3039
- error: message.error,
3040
- enrolled_in_link: message.enrolled_in_link
3041
- });
3042
- }
3043
- };
3044
- if (typeof window !== "undefined") {
3045
- window.addEventListener("message", handleCheckoutMessage);
3046
- }
3047
- abortFn = () => {
3048
- checkoutElement.sendMessage({ type: MESSAGE_TYPES.ABORT_CHECKOUT });
3049
- settle(
3050
- toCheckoutError(
3051
- "CANCELLED",
3052
- "Checkout was cancelled.",
3053
- true
3054
- )
3055
- );
3056
- };
3057
- this.activeCheckoutAbort = abortFn;
3058
- this.debug("[cimplify:checkout] sending process_checkout to checkout iframe", { cart_id: processMessage.cart_id, order_type: processMessage.order_type, hasCustomer: !!processMessage.customer, hasAddress: !!processMessage.address, isUnifiedCheckout });
3059
- checkoutElement.sendMessage(processMessage);
3060
- this.debug("[cimplify:checkout] postMessage sent, waiting for checkout_complete...");
3061
- });
3062
- })();
3063
- const abortable = task;
3064
- abortable.abort = () => {
3065
- if (abortFn) {
3066
- abortFn();
3067
- }
3068
- };
3069
- return abortable;
3070
- }
3071
- isAuthenticated() {
3072
- return this.accessToken !== null;
3073
- }
3074
- getAccessToken() {
3075
- return this.accessToken;
3076
- }
3077
- getPublicKey() {
3078
- return this.client.getPublicKey();
3079
- }
3080
- getBusinessId() {
3081
- return this.businessId;
3082
- }
3083
- async resolveBusinessId() {
3084
- if (this.businessId) {
3085
- return this.businessId;
3086
- }
3087
- if (this.businessIdResolvePromise) {
3088
- return this.businessIdResolvePromise;
3089
- }
3090
- this.businessIdResolvePromise = this.client.resolveBusinessId().then((resolvedBusinessId) => {
3091
- this.businessId = resolvedBusinessId;
3092
- return resolvedBusinessId;
3093
- }).catch(() => null).finally(() => {
3094
- this.businessIdResolvePromise = null;
3095
- });
3096
- return this.businessIdResolvePromise;
3097
- }
3098
- getAppearance() {
3099
- return this.options.appearance;
3100
- }
3101
- async hydrateCustomerData() {
3102
- if (!this.accessToken || this.customerData) {
3103
- return;
3104
- }
3105
- if (!this.client.getAccessToken()) {
3106
- this.client.setAccessToken(this.accessToken);
3107
- }
3108
- const linkDataResult = await this.client.link.getLinkData();
3109
- if (!linkDataResult.ok || !linkDataResult.value?.customer) {
3110
- return;
3111
- }
3112
- const customer = linkDataResult.value.customer;
3113
- this.customerData = {
3114
- name: customer.name || "",
3115
- email: customer.email || null,
3116
- phone: customer.phone || null
3117
- };
3118
- }
3119
- handleMessage(event) {
3120
- if (!isAllowedOrigin(event.origin)) {
3121
- return;
3122
- }
3123
- const message = parseIframeMessage(event.data);
3124
- if (!message) return;
3125
- switch (message.type) {
3126
- case MESSAGE_TYPES.AUTHENTICATED:
3127
- const customer = message.customer ?? {
3128
- name: "",
3129
- email: null,
3130
- phone: null
3131
- };
3132
- this.accessToken = message.token;
3133
- this.accountId = message.accountId;
3134
- this.customerId = message.customerId;
3135
- this.customerData = customer;
3136
- this.client.setAccessToken(message.token);
3137
- this.elements.forEach((element, type) => {
3138
- if (type !== ELEMENT_TYPES.AUTH && type !== ELEMENT_TYPES.CHECKOUT) {
3139
- element.sendMessage({ type: MESSAGE_TYPES.SET_TOKEN, token: message.token });
3140
- }
3141
- });
3142
- break;
3143
- case MESSAGE_TYPES.TOKEN_REFRESHED:
3144
- this.accessToken = message.token;
3145
- break;
3146
- case MESSAGE_TYPES.ADDRESS_CHANGED:
3147
- case MESSAGE_TYPES.ADDRESS_SELECTED:
3148
- this.addressData = message.address;
3149
- break;
3150
- case MESSAGE_TYPES.PAYMENT_METHOD_SELECTED:
3151
- this.paymentData = message.method;
3152
- break;
3153
- case MESSAGE_TYPES.CONTACT_PROVIDED:
3154
- if (!this.customerData) {
3155
- this.customerData = { name: "", email: null, phone: null };
3156
- }
3157
- if (message.contactType === "email") {
3158
- this.customerData.email = message.contact;
3159
- } else {
3160
- this.customerData.phone = message.contact;
3161
- }
3162
- break;
3163
- case MESSAGE_TYPES.LOGOUT_COMPLETE:
3164
- if (this.checkoutInProgress && this.activeCheckoutAbort) {
3165
- this.activeCheckoutAbort();
3166
- }
3167
- this.accessToken = null;
3168
- this.accountId = null;
3169
- this.customerId = null;
3170
- this.customerData = null;
3171
- this.addressData = null;
3172
- this.paymentData = null;
3173
- this.client.clearSession();
3174
- break;
3175
- }
3176
- }
3177
- _setAddressData(data) {
3178
- this.addressData = data;
3179
- }
3180
- _setPaymentData(data) {
3181
- this.paymentData = data;
3182
- }
3183
- _setGuestContact(contact, contactType) {
3184
- if (!this.customerData) {
3185
- this.customerData = { name: "", email: null, phone: null };
3186
- }
3187
- if (contactType === "email") {
3188
- this.customerData.email = contact;
3189
- } else {
3190
- this.customerData.phone = contact;
3191
- }
3192
- }
3193
- };
3194
- var CimplifyElement = class {
3195
- constructor(type, businessId, linkUrl, options, parent) {
3196
- this.iframe = null;
3197
- this.container = null;
3198
- this.mounted = false;
3199
- this.pendingInit = null;
3200
- this.eventHandlers = /* @__PURE__ */ new Map();
3201
- this.resolvers = /* @__PURE__ */ new Map();
3202
- this.listening = false;
3203
- this.type = type;
3204
- this.businessId = businessId;
3205
- this.linkUrl = linkUrl;
3206
- this.options = options;
3207
- this.parent = parent;
3208
- this.nonce = generateNonce();
3209
- this.boundHandleMessage = this.handleMessage.bind(this);
3210
- if (typeof window !== "undefined") {
3211
- window.addEventListener("message", this.boundHandleMessage);
3212
- this.listening = true;
3213
- }
3214
- }
3215
- mount(container) {
3216
- if (this.mounted) {
3217
- console.warn(`Element ${this.type} is already mounted`);
3218
- return;
3219
- }
3220
- const target = typeof container === "string" ? document.querySelector(container) : container;
3221
- if (!target) {
3222
- console.error(`Container not found: ${container}`);
3223
- return;
3224
- }
3225
- this.container = target;
3226
- this.mounted = true;
3227
- if (!this.listening && typeof window !== "undefined") {
3228
- window.addEventListener("message", this.boundHandleMessage);
3229
- this.listening = true;
3230
- }
3231
- void this.createIframe();
3232
- }
3233
- destroy() {
3234
- if (this.iframe) {
3235
- this.iframe.remove();
3236
- this.iframe = null;
3237
- }
3238
- this.container = null;
3239
- this.mounted = false;
3240
- this.eventHandlers.clear();
3241
- this.resolvers.forEach((entry) => clearTimeout(entry.timeoutId));
3242
- this.resolvers.clear();
3243
- if (this.listening && typeof window !== "undefined") {
3244
- window.removeEventListener("message", this.boundHandleMessage);
3245
- this.listening = false;
3246
- }
3247
- this.parent._removeElement(this.type, this);
3248
- }
3249
- on(event, handler) {
3250
- if (!this.eventHandlers.has(event)) {
3251
- this.eventHandlers.set(event, /* @__PURE__ */ new Set());
3252
- }
3253
- this.eventHandlers.get(event).add(handler);
3254
- }
3255
- off(event, handler) {
3256
- this.eventHandlers.get(event)?.delete(handler);
3257
- }
3258
- async getData() {
3259
- if (!this.isMounted()) {
3260
- return null;
3261
- }
3262
- return new Promise((resolve) => {
3263
- const id = Math.random().toString(36).slice(2);
3264
- const timeoutId = setTimeout(() => {
3265
- const entry = this.resolvers.get(id);
3266
- if (!entry) {
3267
- return;
3268
- }
3269
- this.resolvers.delete(id);
3270
- entry.resolve(null);
3271
- }, 5e3);
3272
- this.resolvers.set(id, { resolve, timeoutId });
3273
- this.sendMessage({ type: MESSAGE_TYPES.GET_DATA });
3274
- });
3275
- }
3276
- setCart(cart) {
3277
- this.sendMessage({ type: MESSAGE_TYPES.SET_CART, cart });
3278
- }
3279
- sendMessage(message) {
3280
- if (this.iframe?.contentWindow) {
3281
- this.iframe.contentWindow.postMessage(message, this.linkUrl);
3282
- } else {
3283
- console.warn("[cimplify:checkout] sendMessage DROPPED \u2014 no contentWindow", { type: message.type, hasIframe: !!this.iframe, isConnected: this.iframe?.isConnected });
3284
- }
3285
- }
3286
- getContentWindow() {
3287
- return this.iframe?.contentWindow ?? null;
3288
- }
3289
- isMounted() {
3290
- return this.mounted && Boolean(this.iframe) && this.iframe?.isConnected === true;
3291
- }
3292
- async createIframe() {
3293
- if (!this.container) return;
3294
- const resolvedBusinessId = this.businessId ?? await this.parent.resolveBusinessId();
3295
- if (!resolvedBusinessId || !this.container || !this.mounted) {
3296
- console.error("Unable to mount element without a resolved business ID");
3297
- this.emit(EVENT_TYPES.ERROR, {
3298
- code: "BUSINESS_ID_REQUIRED",
3299
- message: "Unable to initialize checkout without a business ID."
3300
- });
3301
- return;
3302
- }
3303
- this.businessId = resolvedBusinessId;
3304
- const iframe = document.createElement("iframe");
3305
- const url = new URL(`${this.linkUrl}/elements/${this.type}`);
3306
- url.searchParams.set("businessId", resolvedBusinessId);
3307
- url.searchParams.set("nonce", this.nonce);
3308
- if (this.options.prefillEmail) url.searchParams.set("email", this.options.prefillEmail);
3309
- if (this.options.mode) url.searchParams.set("mode", this.options.mode);
3310
- iframe.src = url.toString();
3311
- iframe.style.border = "none";
3312
- iframe.style.width = "100%";
3313
- iframe.style.minHeight = "200px";
3314
- iframe.style.display = "block";
3315
- iframe.style.overflow = "hidden";
3316
- iframe.style.background = "transparent";
3317
- iframe.setAttribute("allowtransparency", "true");
3318
- iframe.setAttribute("frameborder", "0");
3319
- iframe.setAttribute("allow", "geolocation");
3320
- iframe.setAttribute(
3321
- "sandbox",
3322
- "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
3323
- );
3324
- this.iframe = iframe;
3325
- this.container.appendChild(iframe);
3326
- this.pendingInit = {
3327
- businessId: resolvedBusinessId,
3328
- publicKey: this.parent.getPublicKey()
3329
- };
3330
- }
3331
- handleMessage(event) {
3332
- if (!isAllowedOrigin(event.origin)) {
3333
- return;
3334
- }
3335
- const data = event.data;
3336
- if (data?.nonce) {
3337
- if (data.nonce !== this.nonce) return;
3338
- } else {
3339
- const iframeWindow = this.iframe?.contentWindow;
3340
- if (iframeWindow && event.source && event.source !== iframeWindow) {
3341
- return;
3342
- }
3343
- }
3344
- const message = parseIframeMessage(event.data);
3345
- if (!message) return;
3346
- switch (message.type) {
3347
- case MESSAGE_TYPES.READY:
3348
- if (this.iframe && message.height) this.iframe.style.height = `${message.height}px`;
3349
- if (this.pendingInit) {
3350
- const { publicKey } = this.pendingInit;
3351
- this.sendMessage({
3352
- type: MESSAGE_TYPES.INIT,
3353
- businessId: this.pendingInit.businessId,
3354
- publicKey,
3355
- demoMode: publicKey.length === 0,
3356
- prefillEmail: this.options.prefillEmail,
3357
- appearance: this.parent.getAppearance(),
3358
- orderTypes: this.options.orderTypes,
3359
- defaultOrderType: this.options.defaultOrderType,
3360
- renderSubmitButton: true,
3361
- submitLabel: this.options.submitLabel
3362
- });
3363
- const token = this.parent.getAccessToken();
3364
- if (token && this.type !== ELEMENT_TYPES.AUTH) {
3365
- this.sendMessage({ type: MESSAGE_TYPES.SET_TOKEN, token });
3366
- }
3367
- this.pendingInit = null;
3368
- }
3369
- this.emit(EVENT_TYPES.READY, { height: message.height });
3370
- break;
3371
- case MESSAGE_TYPES.HEIGHT_CHANGE:
3372
- if (this.iframe) this.iframe.style.height = `${message.height}px`;
3373
- break;
3374
- case MESSAGE_TYPES.AUTHENTICATED:
3375
- const customer = message.customer ?? {
3376
- name: "",
3377
- email: null,
3378
- phone: null
3379
- };
3380
- this.emit(EVENT_TYPES.AUTHENTICATED, {
3381
- accountId: message.accountId,
3382
- customerId: message.customerId,
3383
- token: message.token,
3384
- customer
3385
- });
3386
- break;
3387
- case MESSAGE_TYPES.REQUIRES_OTP:
3388
- this.emit(EVENT_TYPES.REQUIRES_OTP, { contactMasked: message.contactMasked });
3389
- break;
3390
- case MESSAGE_TYPES.ERROR:
3391
- this.emit(EVENT_TYPES.ERROR, { code: message.code, message: message.message });
3392
- break;
3393
- case MESSAGE_TYPES.ADDRESS_CHANGED:
3394
- case MESSAGE_TYPES.ADDRESS_SELECTED:
3395
- this.parent._setAddressData(message.address);
3396
- this.emit(EVENT_TYPES.CHANGE, { address: message.address });
3397
- this.resolveData(message.address);
3398
- break;
3399
- case MESSAGE_TYPES.PAYMENT_METHOD_SELECTED:
3400
- this.parent._setPaymentData(message.method);
3401
- this.emit(EVENT_TYPES.CHANGE, { paymentMethod: message.method });
3402
- this.resolveData(message.method);
3403
- break;
3404
- case MESSAGE_TYPES.CONTACT_PROVIDED:
3405
- this.parent._setGuestContact(message.contact, message.contactType);
3406
- break;
3407
- case MESSAGE_TYPES.ORDER_TYPE_CHANGED:
3408
- this.emit(EVENT_TYPES.ORDER_TYPE_CHANGED, { orderType: message.orderType });
3409
- break;
3410
- case MESSAGE_TYPES.REQUEST_SUBMIT:
3411
- this.emit(EVENT_TYPES.REQUEST_SUBMIT, {});
3412
- break;
3413
- }
3414
- }
3415
- emit(event, data) {
3416
- this.eventHandlers.get(event)?.forEach((handler) => handler(data));
3417
- }
3418
- resolveData(data) {
3419
- this.resolvers.forEach((entry) => {
3420
- clearTimeout(entry.timeoutId);
3421
- entry.resolve(data);
3422
- });
3423
- this.resolvers.clear();
3424
- }
3425
- };
3426
- function createElements(client, businessId, options) {
3427
- return new CimplifyElements(client, businessId, options);
3428
- }
3429
-
3430
- // src/client.ts
3431
- var ACCESS_TOKEN_STORAGE_KEY = "cimplify_access_token";
3432
- var SESSION_TOKEN_STORAGE_KEY = "cimplify_session_token";
3433
- var ORDER_TOKEN_PREFIX = "cimplify_ot_";
3434
- var SESSION_TOKEN_HEADER = "x-session-token";
3435
- var ORDER_TOKEN_TTL_MS = 24 * 60 * 60 * 1e3;
3436
- var DEFAULT_TIMEOUT_MS = 3e4;
3437
- var DEFAULT_MAX_RETRIES = 3;
3438
- var DEFAULT_RETRY_DELAY_MS = 1e3;
3439
- function sleep(ms) {
3440
- return new Promise((resolve) => setTimeout(resolve, ms));
3441
- }
3442
- function isRetryable(error) {
3443
- if (error instanceof TypeError && error.message.includes("fetch")) {
3444
- return true;
3445
- }
3446
- if (error instanceof DOMException && error.name === "AbortError") {
3447
- return true;
3448
- }
3449
- if (error instanceof CimplifyError) {
3450
- return error.retryable;
3451
- }
3452
- return false;
3453
- }
3454
- function toNetworkError(error, isTestMode) {
3455
- if (error instanceof DOMException && error.name === "AbortError") {
3456
- return enrichError(
3457
- new CimplifyError(
3458
- ErrorCode.TIMEOUT,
3459
- "Request timed out. Please check your connection and try again.",
3460
- true
3461
- ),
3462
- { isTestMode }
3463
- );
3464
- }
3465
- if (error instanceof TypeError && error.message.includes("fetch")) {
3466
- return enrichError(
3467
- new CimplifyError(
3468
- ErrorCode.NETWORK_ERROR,
3469
- "Network error. Please check your internet connection.",
3470
- true
3471
- ),
3472
- { isTestMode }
3473
- );
3474
- }
3475
- if (error instanceof CimplifyError) {
3476
- return enrichError(error, { isTestMode });
3477
- }
3478
- return enrichError(
3479
- new CimplifyError(
3480
- ErrorCode.UNKNOWN_ERROR,
3481
- error instanceof Error ? error.message : "An unknown error occurred",
3482
- false
3483
- ),
3484
- { isTestMode }
3485
- );
3486
- }
3487
- function deriveUrls() {
3488
- const hostname = typeof window !== "undefined" ? window.location.hostname : "";
3489
- if (hostname === "localhost" || hostname === "127.0.0.1") {
3490
- const protocol = window.location.protocol;
3491
- return {
3492
- baseUrl: `${protocol}//${hostname}:8082`,
3493
- linkApiUrl: `${protocol}//${hostname}:8080`
3494
- };
3495
- }
3496
- return {
3497
- baseUrl: "https://storefronts.cimplify.io",
3498
- linkApiUrl: "https://api.cimplify.io"
3499
- };
3500
- }
3501
- function normalizeUrl(url) {
3502
- const trimmed = url.trim();
3503
- if (!trimmed) return trimmed;
3504
- return trimmed.replace(/\/+$/, "");
3505
- }
3506
- function getEnvPublicKey() {
3507
- try {
3508
- const env = globalThis.process;
3509
- return env?.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY || void 0;
3510
- } catch {
3511
- return void 0;
3512
- }
3513
- }
3514
- var CimplifyClient = class {
3515
- constructor(config = {}) {
3516
- this.accessToken = null;
3517
- this.sessionToken = null;
3518
- this.context = {};
3519
- this.businessId = null;
3520
- this.businessIdResolvePromise = null;
3521
- this.inflightRequests = /* @__PURE__ */ new Map();
3522
- this.publicKey = config.publicKey || getEnvPublicKey() || "";
3523
- const urls = deriveUrls();
3524
- this.baseUrl = config.baseUrl ? normalizeUrl(config.baseUrl) : urls.baseUrl;
3525
- this.linkApiUrl = config.linkApiUrl ? normalizeUrl(config.linkApiUrl) : urls.linkApiUrl;
3526
- this.credentials = config.credentials || "include";
3527
- this.timeout = config.timeout ?? DEFAULT_TIMEOUT_MS;
3528
- this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
3529
- this.retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY_MS;
3530
- this.hooks = config.hooks ?? {};
3531
- this.accessToken = this.loadAccessToken();
3532
- this.sessionToken = this.loadSessionToken();
3533
- if (!this.publicKey && !config.suppressPublicKeyWarning) {
3534
- console.warn(
3535
- '[Cimplify] No public key found. Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in your environment, or pass { publicKey: "pk_..." } to createCimplifyClient().'
3536
- );
3537
- }
3538
- }
3539
- buildUrl(base, path) {
3540
- const url = `${base}${path}`;
3541
- const locationId = this.context.location_id;
3542
- if (!locationId) return url;
3543
- const separator = url.includes("?") ? "&" : "?";
3544
- return `${url}${separator}location_id=${encodeURIComponent(locationId)}`;
3545
- }
3546
- getAccessToken() {
3547
- return this.accessToken;
3548
- }
3549
- getPublicKey() {
3550
- return this.publicKey;
3551
- }
3552
- isTestMode() {
3553
- return this.publicKey.trim().startsWith("pk_test_");
3554
- }
3555
- setAccessToken(token) {
3556
- const previous = this.accessToken;
3557
- this.accessToken = token;
3558
- this.saveAccessToken(token);
3559
- this.hooks.onSessionChange?.({
3560
- previousToken: previous,
3561
- newToken: token,
3562
- source: "manual"
3563
- });
3564
- }
3565
- clearSession() {
3566
- const previous = this.accessToken;
3567
- this.accessToken = null;
3568
- this.sessionToken = null;
3569
- this.saveAccessToken(null);
3570
- if (typeof window !== "undefined" && window.localStorage) {
3571
- localStorage.removeItem(SESSION_TOKEN_STORAGE_KEY);
3572
- }
3573
- this.hooks.onSessionChange?.({
3574
- previousToken: previous,
3575
- newToken: null,
3576
- source: "clear"
3577
- });
3578
- }
3579
- setOrderToken(orderId, token) {
3580
- if (typeof window !== "undefined" && window.localStorage) {
3581
- try {
3582
- const entry = JSON.stringify({ token, storedAt: Date.now() });
3583
- localStorage.setItem(`${ORDER_TOKEN_PREFIX}${orderId}`, entry);
3584
- } catch {
3585
- }
3586
- }
3587
- }
3588
- getOrderToken(orderId) {
3589
- if (typeof window !== "undefined" && window.localStorage) {
3590
- try {
3591
- const raw = localStorage.getItem(`${ORDER_TOKEN_PREFIX}${orderId}`);
3592
- if (!raw) return null;
3593
- const entry = JSON.parse(raw);
3594
- if (Date.now() - entry.storedAt > ORDER_TOKEN_TTL_MS) {
3595
- localStorage.removeItem(`${ORDER_TOKEN_PREFIX}${orderId}`);
3596
- return null;
3597
- }
3598
- return entry.token;
3599
- } catch {
3600
- return null;
3601
- }
3602
- }
3603
- return null;
3604
- }
3605
- clearOrderTokens() {
3606
- if (typeof window !== "undefined" && window.localStorage) {
3607
- const keysToRemove = [];
3608
- for (let i = 0; i < localStorage.length; i++) {
3609
- const key = localStorage.key(i);
3610
- if (key?.startsWith(ORDER_TOKEN_PREFIX)) {
3611
- keysToRemove.push(key);
3612
- }
3613
- }
3614
- keysToRemove.forEach((k) => localStorage.removeItem(k));
3615
- }
3616
- }
3617
- setLocationId(locationId) {
3618
- if (locationId) {
3619
- this.context.location_id = locationId;
3620
- } else {
3621
- delete this.context.location_id;
3622
- }
3623
- }
3624
- getLocationId() {
3625
- return this.context.location_id ?? null;
3626
- }
3627
- setBusinessId(businessId) {
3628
- const normalized = businessId.trim();
3629
- if (!normalized) {
3630
- return;
3631
- }
3632
- this.businessId = normalized;
3633
- }
3634
- getBusinessId() {
3635
- return this.businessId;
3636
- }
3637
- async resolveBusinessId() {
3638
- if (this.businessId) {
3639
- return this.businessId;
3640
- }
3641
- if (this.businessIdResolvePromise) {
3642
- return this.businessIdResolvePromise;
3643
- }
3644
- this.businessIdResolvePromise = (async () => {
3645
- const result = await this.business.getInfo();
3646
- if (!result.ok || !result.value?.id) {
3647
- throw new CimplifyError(
3648
- ErrorCode.NOT_FOUND,
3649
- "Unable to resolve business ID from the current public key.",
3650
- true
3651
- );
3652
- }
3653
- this.businessId = result.value.id;
3654
- return result.value.id;
3655
- })();
3656
- try {
3657
- return await this.businessIdResolvePromise;
3658
- } finally {
3659
- this.businessIdResolvePromise = null;
3660
- }
3661
- }
3662
- loadAccessToken() {
3663
- if (typeof window !== "undefined" && window.localStorage) {
3664
- return localStorage.getItem(ACCESS_TOKEN_STORAGE_KEY);
3665
- }
3666
- return null;
3667
- }
3668
- saveAccessToken(token) {
3669
- if (typeof window !== "undefined" && window.localStorage) {
3670
- try {
3671
- if (token) {
3672
- localStorage.setItem(ACCESS_TOKEN_STORAGE_KEY, token);
3673
- } else {
3674
- localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
3675
- }
3676
- } catch {
3677
- }
3678
- }
3679
- }
3680
- loadSessionToken() {
3681
- if (typeof window !== "undefined" && window.localStorage) {
3682
- return localStorage.getItem(SESSION_TOKEN_STORAGE_KEY);
3683
- }
3684
- return null;
3685
- }
3686
- saveSessionToken(token) {
3687
- if (typeof window !== "undefined" && window.localStorage) {
3688
- try {
3689
- localStorage.setItem(SESSION_TOKEN_STORAGE_KEY, token);
3690
- } catch {
3691
- }
3692
- }
3693
- }
3694
- captureSessionToken(response) {
3695
- const token = response.headers.get(SESSION_TOKEN_HEADER);
3696
- if (token && token !== this.sessionToken) {
3697
- this.sessionToken = token;
3698
- this.saveSessionToken(token);
3699
- }
3700
- }
3701
- getHeaders() {
3702
- const headers = {
3703
- "Content-Type": "application/json"
3704
- };
3705
- const trimmedPublicKey = this.publicKey.trim();
3706
- if (trimmedPublicKey) {
3707
- headers["X-API-Key"] = trimmedPublicKey;
3708
- }
3709
- if (this.accessToken) {
3710
- headers["Authorization"] = `Bearer ${this.accessToken}`;
3711
- }
3712
- if (this.sessionToken) {
3713
- headers[SESSION_TOKEN_HEADER] = this.sessionToken;
3714
- }
3715
- return headers;
3716
- }
3717
- async resilientFetch(url, options) {
3718
- const method = options.method || "GET";
3719
- const path = url.replace(this.baseUrl, "").replace(this.linkApiUrl, "");
3720
- const startTime = Date.now();
3721
- let retryCount = 0;
3722
- const context = {
3723
- method,
3724
- path,
3725
- url,
3726
- body: options.body ? JSON.parse(options.body) : void 0,
3727
- startTime
3728
- };
3729
- this.hooks.onRequestStart?.(context);
3730
- let lastError;
3731
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
3732
- try {
3733
- const controller = new AbortController();
3734
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
3735
- const response = await fetch(url, {
3736
- ...options,
3737
- signal: controller.signal
3738
- });
3739
- clearTimeout(timeoutId);
3740
- this.captureSessionToken(response);
3741
- if (response.ok) {
3742
- this.hooks.onRequestSuccess?.({
3743
- ...context,
3744
- status: response.status,
3745
- durationMs: Date.now() - startTime
3746
- });
3747
- return response;
3748
- }
3749
- if (response.status === 429 && attempt < this.maxRetries) {
3750
- retryCount++;
3751
- const retryAfter = response.headers.get("Retry-After");
3752
- const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3, 3e4) || this.retryDelay * Math.pow(2, attempt) : this.retryDelay * Math.pow(2, attempt);
3753
- this.hooks.onRetry?.({
3754
- ...context,
3755
- attempt: retryCount,
3756
- delayMs: delay,
3757
- error: new Error(`Rate limited: ${response.status}`)
3758
- });
3759
- await sleep(delay);
3760
- continue;
3761
- }
3762
- if (response.status >= 400 && response.status < 500) {
3763
- this.hooks.onRequestError?.({
3764
- ...context,
3765
- error: new CimplifyError(
3766
- `HTTP_${response.status}`,
3767
- `Request failed with status ${response.status}`,
3768
- false
3769
- ),
3770
- status: response.status,
3771
- durationMs: Date.now() - startTime,
3772
- retryCount,
3773
- retryable: false
3774
- });
3775
- return response;
3776
- }
3777
- if (response.status >= 500 && attempt < this.maxRetries) {
3778
- retryCount++;
3779
- const delay = this.retryDelay * Math.pow(2, attempt);
3780
- this.hooks.onRetry?.({
3781
- ...context,
3782
- attempt: retryCount,
3783
- delayMs: delay,
3784
- error: new Error(`Server error: ${response.status}`)
3785
- });
3786
- await sleep(delay);
3787
- continue;
3788
- }
3789
- this.hooks.onRequestError?.({
3790
- ...context,
3791
- error: new CimplifyError(
3792
- "SERVER_ERROR",
3793
- `Server error ${response.status} after ${retryCount} retries`,
3794
- false
3795
- ),
3796
- status: response.status,
3797
- durationMs: Date.now() - startTime,
3798
- retryCount,
3799
- retryable: false
3800
- });
3801
- return response;
3802
- } catch (error) {
3803
- lastError = error;
3804
- const networkError = toNetworkError(error, this.isTestMode());
3805
- const errorRetryable = isRetryable(error);
3806
- if (!errorRetryable || attempt >= this.maxRetries) {
3807
- this.hooks.onRequestError?.({
3808
- ...context,
3809
- error: networkError,
3810
- durationMs: Date.now() - startTime,
3811
- retryCount,
3812
- retryable: errorRetryable
3813
- });
3814
- throw networkError;
3815
- }
3816
- retryCount++;
3817
- const delay = this.retryDelay * Math.pow(2, attempt);
3818
- this.hooks.onRetry?.({
3819
- ...context,
3820
- attempt: retryCount,
3821
- delayMs: delay,
3822
- error: networkError
3823
- });
3824
- await sleep(delay);
3825
- }
3826
- }
3827
- const finalError = toNetworkError(lastError, this.isTestMode());
3828
- this.hooks.onRequestError?.({
3829
- ...context,
3830
- error: finalError,
3831
- durationMs: Date.now() - startTime,
3832
- retryCount,
3833
- retryable: false
3834
- });
3835
- throw finalError;
3836
- }
3837
- getDedupeKey(type, payload) {
3838
- return `${type}:${JSON.stringify(payload)}`;
3839
- }
3840
- async deduplicatedRequest(key, requestFn) {
3841
- const existing = this.inflightRequests.get(key);
3842
- if (existing) {
3843
- return existing;
282
+ },
283
+ { threshold: 0.5 }
284
+ );
285
+ if (containerRef.current) {
286
+ observer.observe(containerRef.current);
3844
287
  }
3845
- const request = requestFn().finally(() => {
3846
- this.inflightRequests.delete(key);
3847
- });
3848
- this.inflightRequests.set(key, request);
3849
- return request;
3850
- }
3851
- async get(path) {
3852
- const key = this.getDedupeKey("get", path);
3853
- return this.deduplicatedRequest(key, async () => {
3854
- const response = await this.resilientFetch(this.buildUrl(this.baseUrl, path), {
3855
- method: "GET",
3856
- credentials: this.credentials,
3857
- headers: this.getHeaders()
3858
- });
3859
- return this.handleRestResponse(response);
3860
- });
3861
- }
3862
- async post(path, body) {
3863
- const response = await this.resilientFetch(this.buildUrl(this.baseUrl, path), {
3864
- method: "POST",
3865
- credentials: this.credentials,
3866
- headers: this.getHeaders(),
3867
- body: body ? JSON.stringify(body) : void 0
3868
- });
3869
- return this.handleRestResponse(response);
3870
- }
3871
- async patch(path, body) {
3872
- const response = await this.resilientFetch(this.buildUrl(this.baseUrl, path), {
3873
- method: "PATCH",
3874
- credentials: this.credentials,
3875
- headers: this.getHeaders(),
3876
- body: body ? JSON.stringify(body) : void 0
3877
- });
3878
- return this.handleRestResponse(response);
3879
- }
3880
- async delete(path) {
3881
- const response = await this.resilientFetch(this.buildUrl(this.baseUrl, path), {
3882
- method: "DELETE",
3883
- credentials: this.credentials,
3884
- headers: this.getHeaders()
3885
- });
3886
- return this.handleRestResponse(response);
3887
- }
3888
- async linkGet(path) {
3889
- const key = this.getDedupeKey("linkGet", path);
3890
- return this.deduplicatedRequest(key, async () => {
3891
- const response = await this.resilientFetch(`${this.linkApiUrl}${path}`, {
3892
- method: "GET",
3893
- credentials: this.credentials,
3894
- headers: this.getHeaders()
3895
- });
3896
- return this.handleRestResponse(response);
3897
- });
3898
- }
3899
- async linkPost(path, body) {
3900
- const response = await this.resilientFetch(`${this.linkApiUrl}${path}`, {
3901
- method: "POST",
3902
- credentials: this.credentials,
3903
- headers: this.getHeaders(),
3904
- body: body ? JSON.stringify(body) : void 0
3905
- });
3906
- return this.handleRestResponse(response);
3907
- }
3908
- async linkDelete(path) {
3909
- const response = await this.resilientFetch(`${this.linkApiUrl}${path}`, {
3910
- method: "DELETE",
3911
- credentials: this.credentials,
3912
- headers: this.getHeaders()
3913
- });
3914
- return this.handleRestResponse(response);
3915
- }
3916
- async handleRestResponse(response) {
3917
- const json = await response.json();
3918
- if (!response.ok) {
3919
- const errorCode = typeof json.error === "object" && json.error?.error_code || typeof json.error === "object" && json.error?.code || "API_ERROR";
3920
- const errorMessage = typeof json.error === "object" && json.error?.error_message || typeof json.error === "object" && json.error?.message || typeof json.error === "string" && json.error || typeof json.message === "string" && json.message || "An error occurred";
3921
- const retryable = typeof json.error === "object" && typeof json.error?.retryable === "boolean" ? json.error.retryable : false;
3922
- const error = enrichError(
3923
- new CimplifyError(
3924
- errorCode,
3925
- errorMessage,
3926
- retryable
3927
- ),
3928
- { isTestMode: this.isTestMode() }
3929
- );
3930
- if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
3931
- console.warn(
3932
- "[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
3933
- );
288
+ return () => observer.disconnect();
289
+ }, [ad, siteId, slot, onImpression, identity, apiBase]);
290
+ if (isLoading) {
291
+ return /* @__PURE__ */ jsx(
292
+ "div",
293
+ {
294
+ className: `cimplify-ad cimplify-ad--loading ${className || ""}`,
295
+ style,
296
+ "aria-hidden": "true"
3934
297
  }
3935
- throw error;
3936
- }
3937
- if (json?.success === false || json?.error?.code || json?.error?.message) {
3938
- const error = enrichError(
3939
- new CimplifyError(
3940
- json.error?.code || "API_ERROR",
3941
- json.error?.message || "An error occurred",
3942
- json.error?.retryable || false
3943
- ),
3944
- { isTestMode: this.isTestMode() }
3945
- );
3946
- throw error;
3947
- }
3948
- if (json?.data !== void 0) {
3949
- return json.data;
3950
- }
3951
- return json;
3952
- }
3953
- get catalogue() {
3954
- if (!this._catalogue) {
3955
- this._catalogue = new CatalogueQueries(this);
3956
- }
3957
- return this._catalogue;
3958
- }
3959
- get cart() {
3960
- if (!this._cart) {
3961
- this._cart = new CartOperations(this);
3962
- }
3963
- return this._cart;
3964
- }
3965
- get checkout() {
3966
- if (!this._checkout) {
3967
- this._checkout = new CheckoutService(this);
3968
- }
3969
- return this._checkout;
3970
- }
3971
- get orders() {
3972
- if (!this._orders) {
3973
- this._orders = new OrderQueries(this);
3974
- }
3975
- return this._orders;
3976
- }
3977
- get link() {
3978
- if (!this._link) {
3979
- this._link = new LinkService(this);
3980
- }
3981
- return this._link;
3982
- }
3983
- get auth() {
3984
- if (!this._auth) {
3985
- this._auth = new AuthService(this);
3986
- }
3987
- return this._auth;
3988
- }
3989
- get business() {
3990
- if (!this._business) {
3991
- this._business = new BusinessService(this);
3992
- }
3993
- return this._business;
3994
- }
3995
- get inventory() {
3996
- if (!this._inventory) {
3997
- this._inventory = new InventoryService(this);
3998
- }
3999
- return this._inventory;
4000
- }
4001
- get scheduling() {
4002
- if (!this._scheduling) {
4003
- this._scheduling = new SchedulingService(this);
4004
- }
4005
- return this._scheduling;
298
+ );
4006
299
  }
4007
- get lite() {
4008
- if (!this._lite) {
4009
- this._lite = new LiteService(this);
300
+ if (!config?.enabled) return null;
301
+ if (error) return fallback ? /* @__PURE__ */ jsx(Fragment, { children: fallback }) : null;
302
+ if (!ad) return null;
303
+ const handleClick = () => {
304
+ onClick?.(ad.id);
305
+ };
306
+ const positionStyles = position === "fixed" ? { position: "fixed", bottom: 0, left: 0, right: 0, zIndex: 9999 } : position === "sticky" ? { position: "sticky", top: 20 } : {};
307
+ return /* @__PURE__ */ jsx(
308
+ "div",
309
+ {
310
+ ref: containerRef,
311
+ id: `cimplify-ad-${ad.id}`,
312
+ className: `cimplify-ad cimplify-ad--${slot} cimplify-ad--${position} cimplify-ad--${config.theme} ${className || ""}`,
313
+ style: { ...positionStyles, ...style },
314
+ onClick: handleClick,
315
+ dangerouslySetInnerHTML: { __html: ad.html }
4010
316
  }
4011
- return this._lite;
317
+ );
318
+ }
319
+
320
+ // src/utils/cart-transform.ts
321
+ function getSelections(item) {
322
+ if (item.bundle_resolved?.selections?.length) {
323
+ return item.bundle_resolved.selections.map((s) => ({
324
+ name: s.product_name || s.component_id,
325
+ quantity: s.quantity,
326
+ variant_name: s.variant_name
327
+ }));
4012
328
  }
4013
- get fx() {
4014
- if (!this._fx) {
4015
- this._fx = new FxService(this);
4016
- }
4017
- return this._fx;
329
+ if (item.composite_resolved?.selections?.length) {
330
+ return item.composite_resolved.selections.map((s) => ({
331
+ name: s.component_name || s.component_id,
332
+ quantity: s.quantity
333
+ }));
4018
334
  }
4019
- get activity() {
4020
- if (!this._activity) {
4021
- this._activity = new ActivityService(this);
4022
- }
4023
- return this._activity;
335
+ return void 0;
336
+ }
337
+ function mapItem(item, fx) {
338
+ const amt = (value) => fx ? String(fx.convertPrice(value)) : String(value);
339
+ const result = {
340
+ name: item.name,
341
+ quantity: item.quantity,
342
+ unit_price: amt(item.base_price),
343
+ total_price: amt(item.total_price),
344
+ line_type: item.line_type
345
+ };
346
+ if (item.image_url) result.image_url = item.image_url;
347
+ const variantName = item.variant_info?.name || item.variant_name || void 0;
348
+ if (variantName) result.variant_name = variantName;
349
+ if (item.scheduled_start) result.scheduled_start = item.scheduled_start;
350
+ if (item.scheduled_end) result.scheduled_end = item.scheduled_end;
351
+ const selections = getSelections(item);
352
+ if (selections) result.selections = selections;
353
+ if (item.add_on_options?.length) {
354
+ result.add_ons = item.add_on_options.map((opt) => ({
355
+ name: opt.name,
356
+ price: amt(opt.price ?? "0")
357
+ }));
4024
358
  }
4025
- elements(businessId, options) {
4026
- if (businessId) {
4027
- this.setBusinessId(businessId);
4028
- }
4029
- return createElements(this, businessId ?? this.businessId ?? void 0, options);
359
+ if (item.special_instructions) {
360
+ result.special_instructions = item.special_instructions;
4030
361
  }
4031
- };
4032
- function createCimplifyClient(config = {}) {
4033
- return new CimplifyClient(config);
362
+ return result;
363
+ }
364
+ function transformToCheckoutCart(cart, fx) {
365
+ const amt = (value) => fx ? String(fx.convertPrice(value)) : String(value);
366
+ return {
367
+ items: cart.items.map((item) => mapItem(item, fx)),
368
+ subtotal: amt(cart.pricing.subtotal),
369
+ tax_amount: amt(cart.pricing.tax_amount),
370
+ total_discounts: amt(cart.pricing.total_discounts),
371
+ service_charge: amt(cart.pricing.service_charge),
372
+ total: amt(cart.pricing.total_price),
373
+ currency: fx?.displayCurrency ?? cart.pricing.currency
374
+ };
4034
375
  }
4035
376
  var CART_STORAGE_PREFIX = "cimplify:cart:v2";
4036
377
  var DEFAULT_TAX_RATE = 0.08875;
@@ -4121,6 +462,28 @@ function calculateSummary(items) {
4121
462
  total: roundMoney(subtotal + tax)
4122
463
  };
4123
464
  }
465
+ function lineKeyFromItem(item) {
466
+ return buildLineKey(item.product.id, {
467
+ locationId: item.locationId,
468
+ variantId: item.variantId,
469
+ quoteId: item.quoteId,
470
+ addOnOptionIds: item.addOnOptionIds,
471
+ bundleSelections: item.bundleSelections,
472
+ compositeSelections: item.compositeSelections,
473
+ scheduledStart: item.scheduledStart,
474
+ staffId: item.staffId
475
+ });
476
+ }
477
+ function attachInternalItemMetadata(item) {
478
+ return {
479
+ ...item,
480
+ _lineKey: lineKeyFromItem(item)
481
+ };
482
+ }
483
+ function stripInternalItemMetadata(item) {
484
+ const { _lineKey, ...rest } = item;
485
+ return rest;
486
+ }
4124
487
  function toProductFromServerItem(item, businessId) {
4125
488
  return {
4126
489
  id: item.item_id,
@@ -4150,7 +513,7 @@ function mapServerCart(serverCart) {
4150
513
  price_adjustment: item.variant_info?.price_adjustment
4151
514
  } : void 0;
4152
515
  const quoteId = typeof item.metadata?.quote_id === "string" ? item.metadata.quote_id : void 0;
4153
- return {
516
+ return attachInternalItemMetadata({
4154
517
  id: item.id,
4155
518
  product: toProductFromServerItem(item, serverCart.business_id),
4156
519
  quantity: clampQuantity(item.quantity),
@@ -4158,7 +521,7 @@ function mapServerCart(serverCart) {
4158
521
  variantId: item.variant_id,
4159
522
  variant,
4160
523
  quoteId,
4161
- addOnOptionIds: item.add_on_option_ids || void 0,
524
+ addOnOptionIds: normalizeOptionIds(item.add_on_option_ids || void 0),
4162
525
  addOnOptions: (item.add_on_options || []).map((option) => ({
4163
526
  id: option.id,
4164
527
  name: option.name,
@@ -4170,7 +533,7 @@ function mapServerCart(serverCart) {
4170
533
  scheduledStart: item.scheduled_start || void 0,
4171
534
  scheduledEnd: item.scheduled_end || void 0,
4172
535
  staffId: item.staff_id || void 0
4173
- };
536
+ });
4174
537
  });
4175
538
  return {
4176
539
  items,
@@ -4200,6 +563,7 @@ function createCartStore(params) {
4200
563
  let snapshot = createEmptySnapshot(currency);
4201
564
  let initialized = false;
4202
565
  let initializePromise = null;
566
+ let persistTimer = null;
4203
567
  function emit() {
4204
568
  listeners.forEach((listener) => {
4205
569
  listener();
@@ -4207,7 +571,7 @@ function createCartStore(params) {
4207
571
  }
4208
572
  function setSnapshot(nextSnapshot) {
4209
573
  snapshot = nextSnapshot;
4210
- persistSnapshot();
574
+ schedulePersist();
4211
575
  emit();
4212
576
  }
4213
577
  function updateSnapshot(updater) {
@@ -4221,7 +585,7 @@ function createCartStore(params) {
4221
585
  window.localStorage.setItem(
4222
586
  storageKey,
4223
587
  JSON.stringify({
4224
- items: snapshot.items,
588
+ items: snapshot.items.map((item) => stripInternalItemMetadata(item)),
4225
589
  subtotal: snapshot.subtotal,
4226
590
  tax: snapshot.tax,
4227
591
  total: snapshot.total,
@@ -4231,6 +595,15 @@ function createCartStore(params) {
4231
595
  } catch {
4232
596
  }
4233
597
  }
598
+ function schedulePersist() {
599
+ if (persistTimer) {
600
+ return;
601
+ }
602
+ persistTimer = setTimeout(() => {
603
+ persistTimer = null;
604
+ persistSnapshot();
605
+ }, 100);
606
+ }
4234
607
  function hydrateFromStorage() {
4235
608
  if (typeof window === "undefined" || !window.localStorage) {
4236
609
  return;
@@ -4245,7 +618,7 @@ function createCartStore(params) {
4245
618
  return;
4246
619
  }
4247
620
  snapshot = {
4248
- items: parsed.items,
621
+ items: parsed.items.map((item) => attachInternalItemMetadata(item)),
4249
622
  subtotal: toNumber(parsed.subtotal),
4250
623
  tax: toNumber(parsed.tax),
4251
624
  total: toNumber(parsed.total),
@@ -4322,7 +695,7 @@ function createCartStore(params) {
4322
695
  }
4323
696
  const previousSnapshot = snapshot;
4324
697
  const lineKey = buildLineKey(product.id, normalizedOptions);
4325
- const optimisticItem = {
698
+ const optimisticItem = attachInternalItemMetadata({
4326
699
  id: `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
4327
700
  product,
4328
701
  quantity: resolvedQuantity,
@@ -4338,21 +711,11 @@ function createCartStore(params) {
4338
711
  scheduledStart: normalizedOptions.scheduledStart,
4339
712
  scheduledEnd: normalizedOptions.scheduledEnd,
4340
713
  staffId: normalizedOptions.staffId
4341
- };
714
+ });
4342
715
  updateSnapshot((current) => {
4343
- const existingIndex = current.items.findIndex((item) => {
4344
- const itemKey = buildLineKey(item.product.id, {
4345
- locationId: item.locationId,
4346
- variantId: item.variantId,
4347
- quoteId: item.quoteId,
4348
- addOnOptionIds: item.addOnOptionIds,
4349
- bundleSelections: item.bundleSelections,
4350
- compositeSelections: item.compositeSelections,
4351
- scheduledStart: item.scheduledStart,
4352
- staffId: item.staffId
4353
- });
4354
- return itemKey === lineKey;
4355
- });
716
+ const existingIndex = current.items.findIndex(
717
+ (item) => (item._lineKey ?? lineKeyFromItem(item)) === lineKey
718
+ );
4356
719
  const nextItems = [...current.items];
4357
720
  if (existingIndex >= 0) {
4358
721
  const existing = nextItems[existingIndex];
@@ -4562,7 +925,7 @@ function getOrCreateStore(params) {
4562
925
  return created;
4563
926
  }
4564
927
  function useCart(options = {}) {
4565
- const context = useOptionalCimplify();
928
+ const context = useOptionalCimplifyClient();
4566
929
  const client = options.client ?? context?.client;
4567
930
  if (!client) {
4568
931
  throw new Error("useCart must be used within CimplifyProvider or passed { client }.");
@@ -4700,7 +1063,8 @@ function resolveInitialLocation(locations) {
4700
1063
  }
4701
1064
  return locations[0];
4702
1065
  }
4703
- var CimplifyContext = createContext(null);
1066
+ var CimplifyClientContext = createContext(null);
1067
+ var CimplifyStateContext = createContext(null);
4704
1068
  function CimplifyProvider({
4705
1069
  client,
4706
1070
  children,
@@ -4839,49 +1203,76 @@ function CimplifyProvider({
4839
1203
  cancelled = true;
4840
1204
  };
4841
1205
  }, [resolvedClient, isDemoMode]);
4842
- const contextValue = useMemo(
1206
+ const clientContextValue = useMemo(
4843
1207
  () => ({
4844
1208
  client: resolvedClient,
4845
1209
  business,
4846
1210
  currency: baseCurrency,
4847
1211
  country: business?.country_code || DEFAULT_COUNTRY,
4848
1212
  locations,
4849
- currentLocation,
4850
- setCurrentLocation,
4851
1213
  isReady,
4852
1214
  isDemoMode,
1215
+ baseCurrency
1216
+ }),
1217
+ [
1218
+ resolvedClient,
1219
+ business,
4853
1220
  baseCurrency,
1221
+ locations,
1222
+ isReady,
1223
+ isDemoMode
1224
+ ]
1225
+ );
1226
+ const stateContextValue = useMemo(
1227
+ () => ({
1228
+ currentLocation,
1229
+ setCurrentLocation,
4854
1230
  displayCurrency,
4855
1231
  setDisplayCurrency,
4856
1232
  convertPrice,
4857
1233
  fxRate
4858
1234
  }),
4859
1235
  [
4860
- resolvedClient,
4861
- business,
4862
- baseCurrency,
4863
- locations,
4864
1236
  currentLocation,
4865
1237
  setCurrentLocation,
4866
- isReady,
4867
- isDemoMode,
4868
1238
  displayCurrency,
4869
1239
  setDisplayCurrency,
4870
1240
  convertPrice,
4871
1241
  fxRate
4872
1242
  ]
4873
1243
  );
4874
- return /* @__PURE__ */ jsx(CimplifyContext.Provider, { value: contextValue, children });
1244
+ return /* @__PURE__ */ jsx(CimplifyClientContext.Provider, { value: clientContextValue, children: /* @__PURE__ */ jsx(CimplifyStateContext.Provider, { value: stateContextValue, children }) });
4875
1245
  }
4876
1246
  function useCimplify() {
4877
- const context = useContext(CimplifyContext);
4878
- if (!context) {
1247
+ const clientContext = useContext(CimplifyClientContext);
1248
+ const stateContext = useContext(CimplifyStateContext);
1249
+ if (!clientContext || !stateContext) {
4879
1250
  throw new Error("useCimplify must be used within CimplifyProvider");
4880
1251
  }
4881
- return context;
1252
+ return useMemo(
1253
+ () => ({ ...clientContext, ...stateContext }),
1254
+ [clientContext, stateContext]
1255
+ );
4882
1256
  }
4883
1257
  function useOptionalCimplify() {
4884
- return useContext(CimplifyContext);
1258
+ const clientContext = useContext(CimplifyClientContext);
1259
+ const stateContext = useContext(CimplifyStateContext);
1260
+ return useMemo(() => {
1261
+ if (!clientContext || !stateContext) {
1262
+ return null;
1263
+ }
1264
+ return { ...clientContext, ...stateContext };
1265
+ }, [clientContext, stateContext]);
1266
+ }
1267
+ function useCimplifyClient() {
1268
+ const context = useContext(CimplifyClientContext);
1269
+ if (!context) {
1270
+ throw new Error("useCimplifyClient must be used within CimplifyProvider");
1271
+ }
1272
+ return context;
1273
+ }
1274
+ function useOptionalCimplifyClient() {
1275
+ return useContext(CimplifyClientContext);
4885
1276
  }
4886
1277
  var SPACE = { sm: 8};
4887
1278
  function shellColors(isDark, primaryColor) {
@@ -5266,14 +1657,26 @@ function Price({ amount, className, prefix }) {
5266
1657
  var productsCache = /* @__PURE__ */ new Map();
5267
1658
  var productsInflight = /* @__PURE__ */ new Map();
5268
1659
  function buildProductsCacheKey(client, locationId, options) {
5269
- return JSON.stringify({
5270
- key: client.getPublicKey(),
5271
- location_id: locationId || "__none__",
5272
- options
5273
- });
1660
+ return [
1661
+ client.getPublicKey(),
1662
+ locationId ?? "",
1663
+ options.category ?? "",
1664
+ options.collection ?? "",
1665
+ options.taxonomy ?? "",
1666
+ options.search ?? "",
1667
+ options.featured ?? "",
1668
+ options.limit ?? "",
1669
+ options.sort_by ?? "",
1670
+ options.sort_order ?? "",
1671
+ options.tags?.join(",") ?? "",
1672
+ options.in_stock ?? "",
1673
+ options.min_price ?? "",
1674
+ options.max_price ?? "",
1675
+ options.page ?? ""
1676
+ ].join("|");
5274
1677
  }
5275
1678
  function useProducts(options = {}) {
5276
- const context = useOptionalCimplify();
1679
+ const context = useOptionalCimplifyClient();
5277
1680
  const client = options.client ?? context?.client;
5278
1681
  if (!client) {
5279
1682
  throw new Error("useProducts must be used within CimplifyProvider or passed { client }.");
@@ -5282,6 +1685,7 @@ function useProducts(options = {}) {
5282
1685
  const locationId = client.getLocationId();
5283
1686
  const previousLocationIdRef = useRef(locationId);
5284
1687
  const requestIdRef = useRef(0);
1688
+ const tagsSignature = options.tags?.join(",") ?? "";
5285
1689
  const queryOptions = useMemo(
5286
1690
  () => ({
5287
1691
  category: options.category,
@@ -5307,7 +1711,7 @@ function useProducts(options = {}) {
5307
1711
  options.limit,
5308
1712
  options.sort_by,
5309
1713
  options.sort_order,
5310
- options.tags,
1714
+ tagsSignature,
5311
1715
  options.in_stock,
5312
1716
  options.min_price,
5313
1717
  options.max_price,
@@ -5425,7 +1829,7 @@ function buildProductCacheKey(client, locationId, slugOrId) {
5425
1829
  });
5426
1830
  }
5427
1831
  function useProduct(slugOrId, options = {}) {
5428
- const context = useOptionalCimplify();
1832
+ const context = useOptionalCimplifyClient();
5429
1833
  const client = options.client ?? context?.client;
5430
1834
  if (!client) {
5431
1835
  throw new Error("useProduct must be used within CimplifyProvider or passed { client }.");
@@ -5529,7 +1933,7 @@ function buildCategoriesCacheKey(client) {
5529
1933
  return client.getPublicKey() || "__demo__";
5530
1934
  }
5531
1935
  function useCategories(options = {}) {
5532
- const context = useOptionalCimplify();
1936
+ const context = useOptionalCimplifyClient();
5533
1937
  const client = options.client ?? context?.client;
5534
1938
  if (!client) {
5535
1939
  throw new Error("useCategories must be used within CimplifyProvider or passed { client }.");
@@ -5599,7 +2003,7 @@ function buildOrderCacheKey(client, orderId) {
5599
2003
  return `${client.getPublicKey() || "__demo__"}:${orderId}`;
5600
2004
  }
5601
2005
  function useOrder(orderId, options = {}) {
5602
- const context = useOptionalCimplify();
2006
+ const context = useOptionalCimplifyClient();
5603
2007
  const client = options.client ?? context?.client;
5604
2008
  if (!client) {
5605
2009
  throw new Error("useOrder must be used within CimplifyProvider or passed { client }.");
@@ -5801,7 +2205,7 @@ function buildCollectionsCacheKey(client, locationId) {
5801
2205
  });
5802
2206
  }
5803
2207
  function useCollections(options = {}) {
5804
- const context = useOptionalCimplify();
2208
+ const context = useOptionalCimplifyClient();
5805
2209
  const client = options.client ?? context?.client;
5806
2210
  if (!client) {
5807
2211
  throw new Error("useCollections must be used within CimplifyProvider or passed { client }.");
@@ -5897,7 +2301,7 @@ function buildCollectionCacheKey(client, locationId, idOrSlug) {
5897
2301
  });
5898
2302
  }
5899
2303
  function useCollection(idOrSlug, options = {}) {
5900
- const context = useOptionalCimplify();
2304
+ const context = useOptionalCimplifyClient();
5901
2305
  const client = options.client ?? context?.client;
5902
2306
  if (!client) {
5903
2307
  throw new Error("useCollection must be used within CimplifyProvider or passed { client }.");
@@ -5998,7 +2402,7 @@ function useCollection(idOrSlug, options = {}) {
5998
2402
  return { collection, products, isLoading, error, refetch };
5999
2403
  }
6000
2404
  function useSearch(options = {}) {
6001
- const context = useOptionalCimplify();
2405
+ const context = useOptionalCimplifyClient();
6002
2406
  const client = options.client ?? context?.client;
6003
2407
  if (!client) {
6004
2408
  throw new Error("useSearch must be used within CimplifyProvider or passed { client }.");
@@ -6078,11 +2482,46 @@ function useSearch(options = {}) {
6078
2482
  var quoteCache = /* @__PURE__ */ new Map();
6079
2483
  var quoteInflight = /* @__PURE__ */ new Map();
6080
2484
  function buildQuoteCacheKey(client, locationId, inputSignature) {
6081
- return JSON.stringify({
6082
- key: client.getPublicKey(),
6083
- location_id: locationId || "__none__",
6084
- input: inputSignature
6085
- });
2485
+ return [client.getPublicKey(), locationId ?? "", inputSignature].join("|");
2486
+ }
2487
+ function serializeQuoteBundleSelections(selections) {
2488
+ if (!selections || selections.length === 0) {
2489
+ return "";
2490
+ }
2491
+ return selections.map((selection) => {
2492
+ return [
2493
+ selection.component_id,
2494
+ selection.quantity,
2495
+ selection.variant_id ?? ""
2496
+ ].join(":");
2497
+ }).join(";");
2498
+ }
2499
+ function serializeQuoteCompositeSelections(selections) {
2500
+ if (!selections || selections.length === 0) {
2501
+ return "";
2502
+ }
2503
+ return selections.map((selection) => {
2504
+ return [
2505
+ selection.component_id,
2506
+ selection.quantity,
2507
+ selection.variant_id ?? "",
2508
+ selection.add_on_option_id ?? ""
2509
+ ].join(":");
2510
+ }).join(";");
2511
+ }
2512
+ function buildQuoteInputSignature(input) {
2513
+ if (!input) {
2514
+ return "";
2515
+ }
2516
+ return [
2517
+ input.productId,
2518
+ input.variantId ?? "",
2519
+ input.locationId ?? "",
2520
+ input.quantity ?? "",
2521
+ input.addOnOptionIds?.join(",") ?? "",
2522
+ serializeQuoteBundleSelections(input.bundleSelections),
2523
+ serializeQuoteCompositeSelections(input.compositeSelections)
2524
+ ].join("|");
6086
2525
  }
6087
2526
  function isQuoteExpired(quote) {
6088
2527
  if (!quote?.expires_at) {
@@ -6109,7 +2548,7 @@ function normalizeInput(input, fallbackLocationId) {
6109
2548
  };
6110
2549
  }
6111
2550
  function useQuote(input, options = {}) {
6112
- const context = useOptionalCimplify();
2551
+ const context = useOptionalCimplifyClient();
6113
2552
  const client = options.client ?? context?.client;
6114
2553
  if (!client) {
6115
2554
  throw new Error("useQuote must be used within CimplifyProvider or passed { client }.");
@@ -6119,10 +2558,8 @@ function useQuote(input, options = {}) {
6119
2558
  const refreshBeforeExpiryMs = Math.max(0, options.refreshBeforeExpiryMs ?? 3e4);
6120
2559
  const locationId = client.getLocationId();
6121
2560
  const requestIdRef = useRef(0);
6122
- const refreshTimerRef = useRef(null);
6123
- const expiryTimerRef = useRef(null);
6124
2561
  const debounceTimerRef = useRef(null);
6125
- const inputSignature = useMemo(() => JSON.stringify(input ?? null), [input]);
2562
+ const inputSignature = buildQuoteInputSignature(input);
6126
2563
  const normalizedInput = useMemo(() => {
6127
2564
  if (!input) {
6128
2565
  return null;
@@ -6140,6 +2577,13 @@ function useQuote(input, options = {}) {
6140
2577
  const [error, setError] = useState(null);
6141
2578
  const [isExpired, setIsExpired] = useState(isQuoteExpired(cached?.quote ?? null));
6142
2579
  const [messages, setMessages] = useState(cached?.quote?.ui_messages ?? []);
2580
+ const quoteExpiryTimestamp = useMemo(() => {
2581
+ if (!quote?.expires_at) {
2582
+ return Number.NaN;
2583
+ }
2584
+ const expiresAt = Date.parse(quote.expires_at);
2585
+ return Number.isFinite(expiresAt) ? expiresAt : Number.NaN;
2586
+ }, [quote?.expires_at]);
6143
2587
  const load = useCallback(
6144
2588
  async (force = false) => {
6145
2589
  if (!enabled || !normalizedInput) {
@@ -6257,52 +2701,34 @@ function useQuote(input, options = {}) {
6257
2701
  }
6258
2702
  }, [cacheKey, client, enabled, load, normalizedInput, quote]);
6259
2703
  useEffect(() => {
6260
- if (expiryTimerRef.current) {
6261
- clearTimeout(expiryTimerRef.current);
6262
- expiryTimerRef.current = null;
6263
- }
6264
- const expiresAt = quote?.expires_at ? Date.parse(quote.expires_at) : NaN;
6265
- if (!Number.isFinite(expiresAt)) {
2704
+ if (!Number.isFinite(quoteExpiryTimestamp)) {
6266
2705
  setIsExpired(false);
6267
2706
  return;
6268
2707
  }
6269
- const expired = expiresAt <= Date.now();
6270
- setIsExpired(expired);
6271
- if (!expired) {
6272
- expiryTimerRef.current = setTimeout(() => {
6273
- setIsExpired(true);
6274
- }, Math.max(0, expiresAt - Date.now()));
6275
- }
6276
- return () => {
6277
- if (expiryTimerRef.current) {
6278
- clearTimeout(expiryTimerRef.current);
6279
- expiryTimerRef.current = null;
6280
- }
6281
- };
6282
- }, [quote?.expires_at, quote?.quote_id]);
6283
- useEffect(() => {
6284
- if (refreshTimerRef.current) {
6285
- clearTimeout(refreshTimerRef.current);
6286
- refreshTimerRef.current = null;
6287
- }
6288
- if (!autoRefresh || !enabled || !quote?.expires_at) {
6289
- return;
2708
+ const now = Date.now();
2709
+ const isCurrentlyExpired = quoteExpiryTimestamp <= now;
2710
+ const timers = [];
2711
+ setIsExpired(isCurrentlyExpired);
2712
+ if (!isCurrentlyExpired) {
2713
+ timers.push(
2714
+ setTimeout(() => {
2715
+ setIsExpired(true);
2716
+ }, Math.max(0, quoteExpiryTimestamp - now))
2717
+ );
6290
2718
  }
6291
- const expiresAt = Date.parse(quote.expires_at);
6292
- if (!Number.isFinite(expiresAt)) {
6293
- return;
2719
+ if (autoRefresh && enabled && !isCurrentlyExpired) {
2720
+ timers.push(
2721
+ setTimeout(() => {
2722
+ void refresh();
2723
+ }, Math.max(0, quoteExpiryTimestamp - now - refreshBeforeExpiryMs))
2724
+ );
6294
2725
  }
6295
- const delay = Math.max(0, expiresAt - Date.now() - refreshBeforeExpiryMs);
6296
- refreshTimerRef.current = setTimeout(() => {
6297
- void refresh();
6298
- }, delay);
6299
2726
  return () => {
6300
- if (refreshTimerRef.current) {
6301
- clearTimeout(refreshTimerRef.current);
6302
- refreshTimerRef.current = null;
2727
+ for (const timer of timers) {
2728
+ clearTimeout(timer);
6303
2729
  }
6304
2730
  };
6305
- }, [autoRefresh, enabled, quote?.expires_at, quote?.quote_id, refresh, refreshBeforeExpiryMs]);
2731
+ }, [autoRefresh, enabled, quoteExpiryTimestamp, refresh, refreshBeforeExpiryMs]);
6306
2732
  return { quote, isLoading, error, refresh, isExpired, messages };
6307
2733
  }
6308
2734
  var taxonomiesCache = /* @__PURE__ */ new Map();
@@ -6314,7 +2740,7 @@ function buildTaxonomiesCacheKey(client, parentId) {
6314
2740
  });
6315
2741
  }
6316
2742
  function useTaxonomies(options = {}) {
6317
- const context = useOptionalCimplify();
2743
+ const context = useOptionalCimplifyClient();
6318
2744
  const client = options.client ?? context?.client;
6319
2745
  if (!client) {
6320
2746
  throw new Error("useTaxonomies must be used within CimplifyProvider or passed { client }.");
@@ -6391,7 +2817,7 @@ function buildTaxonomyCacheKey(client, id) {
6391
2817
  });
6392
2818
  }
6393
2819
  function useTaxonomy(id, options = {}) {
6394
- const context = useOptionalCimplify();
2820
+ const context = useOptionalCimplifyClient();
6395
2821
  const client = options.client ?? context?.client;
6396
2822
  if (!client) {
6397
2823
  throw new Error("useTaxonomy must be used within CimplifyProvider or passed { client }.");
@@ -6471,7 +2897,7 @@ function buildTaxonomyPathCacheKey(client, id) {
6471
2897
  });
6472
2898
  }
6473
2899
  function useTaxonomyPath(id, options = {}) {
6474
- const context = useOptionalCimplify();
2900
+ const context = useOptionalCimplifyClient();
6475
2901
  const client = options.client ?? context?.client;
6476
2902
  if (!client) {
6477
2903
  throw new Error("useTaxonomyPath must be used within CimplifyProvider or passed { client }.");
@@ -6544,7 +2970,7 @@ function buildSchedulesCacheKey(client, productId) {
6544
2970
  });
6545
2971
  }
6546
2972
  function useProductSchedules(productId, options = {}) {
6547
- const context = useOptionalCimplify();
2973
+ const context = useOptionalCimplifyClient();
6548
2974
  const client = options.client ?? context?.client;
6549
2975
  if (!client) {
6550
2976
  throw new Error("useProductSchedules must be used within CimplifyProvider or passed { client }.");
@@ -6618,7 +3044,7 @@ function buildAvailabilityCacheKey(client, productId, locationId) {
6618
3044
  });
6619
3045
  }
6620
3046
  function useProductAvailability(productId, options = {}) {
6621
- const context = useOptionalCimplify();
3047
+ const context = useOptionalCimplifyClient();
6622
3048
  const client = options.client ?? context?.client;
6623
3049
  if (!client) {
6624
3050
  throw new Error("useProductAvailability must be used within CimplifyProvider or passed { client }.");
@@ -6707,7 +3133,7 @@ function buildDealsCacheKey(client, locationId) {
6707
3133
  return JSON.stringify({ key: client.getPublicKey(), location_id: locationId });
6708
3134
  }
6709
3135
  function useDeals(options = {}) {
6710
- const context = useOptionalCimplify();
3136
+ const context = useOptionalCimplifyClient();
6711
3137
  const client = options.client ?? context?.client;
6712
3138
  if (!client) {
6713
3139
  throw new Error("useDeals must be used within CimplifyProvider or passed { client }.");
@@ -6786,7 +3212,7 @@ function buildOnSaleCacheKey(client) {
6786
3212
  return client.getPublicKey() || "__demo__";
6787
3213
  }
6788
3214
  function useProductsOnSale(options = {}) {
6789
- const context = useOptionalCimplify();
3215
+ const context = useOptionalCimplifyClient();
6790
3216
  const client = options.client ?? context?.client;
6791
3217
  if (!client) {
6792
3218
  throw new Error("useProductsOnSale must be used within CimplifyProvider or passed { client }.");
@@ -6856,7 +3282,7 @@ function buildProductDealsCacheKey(client, productId) {
6856
3282
  return JSON.stringify({ key: client.getPublicKey(), product_id: productId });
6857
3283
  }
6858
3284
  function useProductDeals(productId, options = {}) {
6859
- const context = useOptionalCimplify();
3285
+ const context = useOptionalCimplifyClient();
6860
3286
  const client = options.client ?? context?.client;
6861
3287
  if (!client) {
6862
3288
  throw new Error("useProductDeals must be used within CimplifyProvider or passed { client }.");
@@ -6924,7 +3350,7 @@ function useProductDeals(productId, options = {}) {
6924
3350
  return { deals, isLoading, error, refetch };
6925
3351
  }
6926
3352
  function useValidateDiscount(options = {}) {
6927
- const context = useOptionalCimplify();
3353
+ const context = useOptionalCimplifyClient();
6928
3354
  const client = options.client ?? context?.client;
6929
3355
  if (!client) {
6930
3356
  throw new Error("useValidateDiscount must be used within CimplifyProvider or passed { client }.");
@@ -6962,7 +3388,7 @@ function buildBundlesCacheKey(client, locationId) {
6962
3388
  });
6963
3389
  }
6964
3390
  function useBundles(options = {}) {
6965
- const context = useOptionalCimplify();
3391
+ const context = useOptionalCimplifyClient();
6966
3392
  const client = options.client ?? context?.client;
6967
3393
  if (!client) {
6968
3394
  throw new Error("useBundles must be used within CimplifyProvider or passed { client }.");
@@ -7088,7 +3514,7 @@ function buildOrdersCacheKey(client, status, limit) {
7088
3514
  return JSON.stringify({ key: client.getPublicKey(), status, limit });
7089
3515
  }
7090
3516
  function useOrders(options = {}) {
7091
- const context = useOptionalCimplify();
3517
+ const context = useOptionalCimplifyClient();
7092
3518
  const client = options.client ?? context?.client;
7093
3519
  if (!client) {
7094
3520
  throw new Error("useOrders must be used within CimplifyProvider or passed { client }.");
@@ -7173,7 +3599,7 @@ function buildServicesCacheKey(client) {
7173
3599
  return JSON.stringify({ key: client.getPublicKey() });
7174
3600
  }
7175
3601
  function useServices(options = {}) {
7176
- const context = useOptionalCimplify();
3602
+ const context = useOptionalCimplifyClient();
7177
3603
  const client = options.client ?? context?.client;
7178
3604
  if (!client) {
7179
3605
  throw new Error("useServices must be used within CimplifyProvider or passed { client }.");
@@ -7257,7 +3683,7 @@ function buildSlotsCacheKey(client, serviceId, date, participantCount) {
7257
3683
  });
7258
3684
  }
7259
3685
  function useAvailableSlots(serviceId, date, options = {}) {
7260
- const context = useOptionalCimplify();
3686
+ const context = useOptionalCimplifyClient();
7261
3687
  const client = options.client ?? context?.client;
7262
3688
  if (!client) {
7263
3689
  throw new Error("useAvailableSlots must be used within CimplifyProvider or passed { client }.");
@@ -7351,7 +3777,7 @@ function buildAvailabilityCacheKey2(client, serviceId, startDate, endDate, parti
7351
3777
  });
7352
3778
  }
7353
3779
  function useServiceAvailability(serviceId, startDate, endDate, options = {}) {
7354
- const context = useOptionalCimplify();
3780
+ const context = useOptionalCimplifyClient();
7355
3781
  const client = options.client ?? context?.client;
7356
3782
  if (!client) {
7357
3783
  throw new Error(
@@ -7449,7 +3875,7 @@ function buildBookingsCacheKey(client, filter) {
7449
3875
  return JSON.stringify({ key: client.getPublicKey(), filter });
7450
3876
  }
7451
3877
  function useBookings(options = {}) {
7452
- const context = useOptionalCimplify();
3878
+ const context = useOptionalCimplifyClient();
7453
3879
  const client = options.client ?? context?.client;
7454
3880
  if (!client) {
7455
3881
  throw new Error("useBookings must be used within CimplifyProvider or passed { client }.");
@@ -7828,43 +4254,6 @@ function QuantitySelector({
7828
4254
  }
7829
4255
  );
7830
4256
  }
7831
-
7832
- // src/utils/variant.ts
7833
- function stripProductPrefix(variantName, productName) {
7834
- if (!variantName || !productName) return variantName || productName;
7835
- const lower = variantName.toLowerCase().trim();
7836
- if (lower === "default" || lower === "standard") return productName;
7837
- if (lower.endsWith("-default") || lower.endsWith("-standard")) return productName;
7838
- if (variantName.startsWith(productName)) {
7839
- const rest = variantName.slice(productName.length);
7840
- const match = rest.match(/^\s*[-–—]\s*(.*)/);
7841
- if (match && match[1].length > 0) return match[1];
7842
- }
7843
- const lowerProduct = productName.toLowerCase().trim();
7844
- if (lower.startsWith(lowerProduct)) {
7845
- const rest = variantName.slice(productName.length);
7846
- const match = rest.match(/^\s*[-–—]\s*(.*)/);
7847
- if (match && match[1].length > 0) return match[1];
7848
- }
7849
- const lastDash = variantName.lastIndexOf("-");
7850
- if (lastDash > 0 && lastDash < variantName.length - 1) {
7851
- const prefix = variantName.slice(0, lastDash).trim();
7852
- const suffix = variantName.slice(lastDash + 1).trim();
7853
- if (suffix.length > 0 && prefix.length > suffix.length * 2) {
7854
- return suffix;
7855
- }
7856
- }
7857
- return variantName;
7858
- }
7859
- function getVariantDisplayName(variant, productName) {
7860
- if (variant.display_attributes?.length) {
7861
- return variant.display_attributes.map((a) => a.value_name).join(" / ");
7862
- }
7863
- if (variant.name) {
7864
- return productName ? stripProductPrefix(variant.name, productName) : variant.name;
7865
- }
7866
- return productName || (variant.is_default ? "Default" : "Option");
7867
- }
7868
4257
  function VariantSelector({
7869
4258
  variants,
7870
4259
  variantAxes,
@@ -8039,13 +4428,14 @@ function AddOnSelector({
8039
4428
  className,
8040
4429
  classNames
8041
4430
  }) {
4431
+ const selectedSet = useMemo(() => new Set(selectedOptions), [selectedOptions]);
8042
4432
  const isOptionSelected = useCallback(
8043
- (optionId) => selectedOptions.includes(optionId),
8044
- [selectedOptions]
4433
+ (optionId) => selectedSet.has(optionId),
4434
+ [selectedSet]
8045
4435
  );
8046
4436
  const handleCheckedChange = useCallback(
8047
4437
  (addOn, optionId, checked) => {
8048
- const isSelected = selectedOptions.includes(optionId);
4438
+ const isSelected = selectedSet.has(optionId);
8049
4439
  if (addOn.is_mutually_exclusive || !addOn.is_multiple_allowed) {
8050
4440
  const groupOptionIds = new Set(addOn.options.map((o) => o.id));
8051
4441
  const withoutGroup = selectedOptions.filter((id) => !groupOptionIds.has(id));
@@ -8060,9 +4450,12 @@ function AddOnSelector({
8060
4450
  if (isSelected) {
8061
4451
  onOptionsChange(selectedOptions.filter((id) => id !== optionId));
8062
4452
  } else {
8063
- const currentCount = selectedOptions.filter(
8064
- (id) => addOn.options.some((o) => o.id === id)
8065
- ).length;
4453
+ let currentCount = 0;
4454
+ for (const option of addOn.options) {
4455
+ if (selectedSet.has(option.id)) {
4456
+ currentCount += 1;
4457
+ }
4458
+ }
8066
4459
  if (addOn.max_selections && currentCount >= addOn.max_selections) {
8067
4460
  return;
8068
4461
  }
@@ -8070,15 +4463,18 @@ function AddOnSelector({
8070
4463
  }
8071
4464
  }
8072
4465
  },
8073
- [selectedOptions, onOptionsChange]
4466
+ [selectedOptions, selectedSet, onOptionsChange]
8074
4467
  );
8075
4468
  if (!addOns || addOns.length === 0) {
8076
4469
  return null;
8077
4470
  }
8078
4471
  return /* @__PURE__ */ jsx("div", { "data-cimplify-addon-selector": true, className: cn("space-y-6", className, classNames?.root), children: addOns.map((addOn) => {
8079
- const currentSelections = selectedOptions.filter(
8080
- (id) => addOn.options.some((o) => o.id === id)
8081
- ).length;
4472
+ let currentSelections = 0;
4473
+ for (const option of addOn.options) {
4474
+ if (selectedSet.has(option.id)) {
4475
+ currentSelections += 1;
4476
+ }
4477
+ }
8082
4478
  const minMet = !addOn.min_selections || currentSelections >= addOn.min_selections;
8083
4479
  const isSingleSelect = addOn.is_mutually_exclusive || !addOn.is_multiple_allowed;
8084
4480
  return /* @__PURE__ */ jsxs(
@@ -8445,7 +4841,7 @@ function CompositeSelector({
8445
4841
  className,
8446
4842
  classNames
8447
4843
  }) {
8448
- const { client } = useCimplify();
4844
+ const { client } = useCimplifyClient();
8449
4845
  const [groupSelections, setGroupSelections] = useState({});
8450
4846
  const [priceResult, setPriceResult] = useState(null);
8451
4847
  const [isPriceLoading, setIsPriceLoading] = useState(false);
@@ -8475,6 +4871,13 @@ function CompositeSelector({
8475
4871
  }
8476
4872
  return true;
8477
4873
  }, [groups, groupSelections]);
4874
+ const sortedGroups = useMemo(
4875
+ () => [...groups].sort((a, b) => a.display_order - b.display_order).map((group) => ({
4876
+ ...group,
4877
+ _sortedComponents: group.components.filter((component) => component.is_available && !component.is_archived).sort((a, b) => a.display_order - b.display_order)
4878
+ })),
4879
+ [groups]
4880
+ );
8478
4881
  useEffect(() => {
8479
4882
  onReady?.(allGroupsSatisfied);
8480
4883
  }, [allGroupsSatisfied, onReady]);
@@ -8565,7 +4968,7 @@ function CompositeSelector({
8565
4968
  return null;
8566
4969
  }
8567
4970
  return /* @__PURE__ */ jsxs("div", { "data-cimplify-composite-selector": true, className: cn("space-y-6", className, classNames?.root), children: [
8568
- [...groups].sort((a, b) => a.display_order - b.display_order).map((group) => {
4971
+ sortedGroups.map((group) => {
8569
4972
  const groupSels = groupSelections[group.id] || {};
8570
4973
  const totalSelected = Object.values(groupSels).reduce((sum, q) => sum + q, 0);
8571
4974
  const minMet = totalSelected >= group.min_selections;
@@ -8630,7 +5033,7 @@ function CompositeSelector({
8630
5033
  role: isSingleSelect ? "radiogroup" : "group",
8631
5034
  "aria-label": group.name,
8632
5035
  className: cn("divide-y divide-border", classNames?.components),
8633
- children: group.components.filter((c) => c.is_available && !c.is_archived).sort((a, b) => a.display_order - b.display_order).map((component) => {
5036
+ children: group._sortedComponents.map((component) => {
8634
5037
  const qty = groupSels[component.id] || 0;
8635
5038
  const isSelected = qty > 0;
8636
5039
  const displayName = component.display_name || component.id;
@@ -9714,6 +6117,15 @@ function ProductGrid({
9714
6117
  const md = columns?.md ?? 2;
9715
6118
  const lg = columns?.lg ?? 3;
9716
6119
  const xl = columns?.xl ?? 4;
6120
+ const css = React3.useMemo(
6121
+ () => [
6122
+ `#${gridId}{display:grid;grid-template-columns:repeat(${sm},1fr);gap:1rem}`,
6123
+ `@media(min-width:768px){#${gridId}{grid-template-columns:repeat(${md},1fr)}}`,
6124
+ `@media(min-width:1024px){#${gridId}{grid-template-columns:repeat(${lg},1fr)}}`,
6125
+ `@media(min-width:1280px){#${gridId}{grid-template-columns:repeat(${xl},1fr)}}`
6126
+ ].join(""),
6127
+ [gridId, sm, md, lg, xl]
6128
+ );
9717
6129
  if (products.length === 0) {
9718
6130
  return /* @__PURE__ */ jsx(
9719
6131
  "div",
@@ -9725,12 +6137,6 @@ function ProductGrid({
9725
6137
  }
9726
6138
  );
9727
6139
  }
9728
- const css = [
9729
- `#${gridId}{display:grid;grid-template-columns:repeat(${sm},1fr);gap:1rem}`,
9730
- `@media(min-width:768px){#${gridId}{grid-template-columns:repeat(${md},1fr)}}`,
9731
- `@media(min-width:1024px){#${gridId}{grid-template-columns:repeat(${lg},1fr)}}`,
9732
- `@media(min-width:1280px){#${gridId}{grid-template-columns:repeat(${xl},1fr)}}`
9733
- ].join("");
9734
6140
  return /* @__PURE__ */ jsxs(Fragment, { children: [
9735
6141
  /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: css } }),
9736
6142
  /* @__PURE__ */ jsx(
@@ -11335,7 +7741,7 @@ function CheckoutPage({
11335
7741
  className,
11336
7742
  classNames
11337
7743
  }) {
11338
- const { client } = useCimplify();
7744
+ const { client } = useCimplifyClient();
11339
7745
  return /* @__PURE__ */ jsxs("div", { "data-cimplify-checkout-page": true, className: cn(className, classNames?.root), children: [
11340
7746
  /* @__PURE__ */ jsx("div", { "data-cimplify-checkout-header": true, className: classNames?.header, children: /* @__PURE__ */ jsx("h1", { "data-cimplify-checkout-title": true, className: classNames?.title, children: title }) }),
11341
7747
  /* @__PURE__ */ jsx(
@@ -11709,8 +8115,8 @@ function BookingPage({
11709
8115
  staffId: selectedStaffId || void 0
11710
8116
  });
11711
8117
  onBooked?.(selectedSlot, selectedStaffId);
11712
- } catch (err2) {
11713
- setError(err2 instanceof Error ? err2.message : "Failed to add booking to cart");
8118
+ } catch (err) {
8119
+ setError(err instanceof Error ? err.message : "Failed to add booking to cart");
11714
8120
  } finally {
11715
8121
  setIsSubmitting(false);
11716
8122
  }
@@ -11907,4 +8313,4 @@ function BookingsPage({
11907
8313
  ] });
11908
8314
  }
11909
8315
 
11910
- export { Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BookingCard, BookingList, BookingPage, BookingsPage, BundleSelector, CartPage, CartSummary, CataloguePage, CategoryFilter, CategoryGrid, CheckoutPage, CimplifyCheckout, CimplifyProvider, CollectionPage, CompositeSelector, DateSlotPicker, DealBanner, DealsPage, DiscountInput, ElementsProvider, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderSummary, PaymentElement, Price, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductPage, ProductSheet, QuantitySelector, SaleBadge, SearchInput, SearchPage, ServiceCard, ServiceGrid, SlotPicker, StaffPicker, StoreNav, VariantSelector, cn, getVariantDisplayName, useAds, useAvailableSlots, useBookings, useBundles, useCart, useCategories, useCheckout, useCimplify, useCollection, useCollections, useDeals, useElements, useElementsReady, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, useQuote, useSearch, useServiceAvailability, useServices, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount };
8316
+ export { Ad, AdProvider, AddOnSelector, AddressElement, AuthElement, AvailabilityBadge, BookingCard, BookingList, BookingPage, BookingsPage, BundleSelector, CartPage, CartSummary, CataloguePage, CategoryFilter, CategoryGrid, CheckoutPage, CimplifyCheckout, CimplifyProvider, CollectionPage, CompositeSelector, DateSlotPicker, DealBanner, DealsPage, DiscountInput, ElementsProvider, OrderDetailPage, OrderHistory, OrderHistoryPage, OrderSummary, PaymentElement, Price, ProductCard, ProductCustomizer, ProductGrid, ProductImageGallery, ProductPage, ProductSheet, QuantitySelector, SaleBadge, SearchInput, SearchPage, ServiceCard, ServiceGrid, SlotPicker, StaffPicker, StoreNav, VariantSelector, cn, useAds, useAvailableSlots, useBookings, useBundles, useCart, useCategories, useCheckout, useCimplify, useCimplifyClient, useCollection, useCollections, useDeals, useElements, useElementsReady, useLocations, useOptionalCimplify, useOrder, useOrders, useProduct, useProductAvailability, useProductDeals, useProductPrice, useProductSchedules, useProducts, useProductsOnSale, useQuote, useSearch, useServiceAvailability, useServices, useTaxonomies, useTaxonomy, useTaxonomyPath, useValidateDiscount };