@cimplify/sdk 0.8.7 → 0.8.9

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,49 +1,7 @@
1
- import React2, { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react';
1
+ import React3, { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react';
2
2
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
3
 
4
- // src/react/index.tsx
5
-
6
- // src/types/elements.ts
7
- var ELEMENT_TYPES = {
8
- AUTH: "auth",
9
- ADDRESS: "address",
10
- PAYMENT: "payment",
11
- CHECKOUT: "checkout"
12
- };
13
- var MESSAGE_TYPES = {
14
- // Parent → Iframe
15
- INIT: "init",
16
- SET_TOKEN: "set_token",
17
- SET_CART: "set_cart",
18
- GET_DATA: "get_data",
19
- PROCESS_CHECKOUT: "process_checkout",
20
- ABORT_CHECKOUT: "abort_checkout",
21
- // Iframe → Parent
22
- READY: "ready",
23
- HEIGHT_CHANGE: "height_change",
24
- AUTHENTICATED: "authenticated",
25
- REQUIRES_OTP: "requires_otp",
26
- ERROR: "error",
27
- ADDRESS_CHANGED: "address_changed",
28
- ADDRESS_SELECTED: "address_selected",
29
- PAYMENT_METHOD_SELECTED: "payment_method_selected",
30
- TOKEN_REFRESHED: "token_refreshed",
31
- LOGOUT_COMPLETE: "logout_complete",
32
- CONTACT_PROVIDED: "contact_provided",
33
- CHECKOUT_STATUS: "checkout_status",
34
- CHECKOUT_COMPLETE: "checkout_complete",
35
- ORDER_TYPE_CHANGED: "order_type_changed",
36
- REQUEST_SUBMIT: "request_submit"
37
- };
38
- var EVENT_TYPES = {
39
- READY: "ready",
40
- AUTHENTICATED: "authenticated",
41
- REQUIRES_OTP: "requires_otp",
42
- ERROR: "error",
43
- CHANGE: "change",
44
- ORDER_TYPE_CHANGED: "order_type_changed",
45
- REQUEST_SUBMIT: "request_submit"
46
- };
4
+ // src/ads/index.tsx
47
5
 
48
6
  // src/ads/identity.ts
49
7
  var COOKIE_NAME = "_cimplify_uid";
@@ -362,12 +320,13 @@ function getSelections(item) {
362
320
  }
363
321
  return void 0;
364
322
  }
365
- function mapItem(item) {
323
+ function mapItem(item, fx) {
324
+ const amt = (value) => fx ? String(fx.convertPrice(value)) : String(value);
366
325
  const result = {
367
326
  name: item.name,
368
327
  quantity: item.quantity,
369
- unit_price: String(item.base_price),
370
- total_price: String(item.total_price),
328
+ unit_price: amt(item.base_price),
329
+ total_price: amt(item.total_price),
371
330
  line_type: item.line_type
372
331
  };
373
332
  if (item.image_url) result.image_url = item.image_url;
@@ -380,7 +339,7 @@ function mapItem(item) {
380
339
  if (item.add_on_options?.length) {
381
340
  result.add_ons = item.add_on_options.map((opt) => ({
382
341
  name: opt.name,
383
- price: String(opt.price ?? "0")
342
+ price: amt(opt.price ?? "0")
384
343
  }));
385
344
  }
386
345
  if (item.special_instructions) {
@@ -388,623 +347,231 @@ function mapItem(item) {
388
347
  }
389
348
  return result;
390
349
  }
391
- function transformToCheckoutCart(cart) {
350
+ function transformToCheckoutCart(cart, fx) {
351
+ const amt = (value) => fx ? String(fx.convertPrice(value)) : String(value);
392
352
  return {
393
- items: cart.items.map(mapItem),
394
- subtotal: String(cart.pricing.subtotal),
395
- tax_amount: String(cart.pricing.tax_amount),
396
- total_discounts: String(cart.pricing.total_discounts),
397
- service_charge: String(cart.pricing.service_charge),
398
- total: String(cart.pricing.total_price),
399
- currency: cart.pricing.currency
353
+ items: cart.items.map((item) => mapItem(item, fx)),
354
+ subtotal: amt(cart.pricing.subtotal),
355
+ tax_amount: amt(cart.pricing.tax_amount),
356
+ total_discounts: amt(cart.pricing.total_discounts),
357
+ service_charge: amt(cart.pricing.service_charge),
358
+ total: amt(cart.pricing.total_price),
359
+ currency: fx?.displayCurrency ?? cart.pricing.currency
400
360
  };
401
361
  }
402
- var SPACE = { sm: 8};
403
- function shellColors(isDark, primaryColor) {
404
- return {
405
- text: isDark ? "#f4f4f5" : "#1a1a1a",
406
- textSecondary: isDark ? "#a1a1aa" : "#52525b",
407
- textMuted: isDark ? "#71717a" : "#a1a1aa",
408
- border: isDark ? "#27272a" : "#e4e4e7",
409
- surface: isDark ? "#18181b" : "#fafafa",
410
- error: "#dc2626",
411
- primary: primaryColor
412
- };
362
+
363
+ // src/types/common.ts
364
+ function money(value) {
365
+ return value;
413
366
  }
414
- function statusToLabel(status) {
415
- if (!status) {
416
- return "";
367
+ function moneyFromNumber(value) {
368
+ return value.toFixed(2);
369
+ }
370
+ var SUPPORTED_CURRENCY_CODES = /* @__PURE__ */ new Set([
371
+ "USD",
372
+ "EUR",
373
+ "GBP",
374
+ "JPY",
375
+ "CNY",
376
+ "CHF",
377
+ "CAD",
378
+ "AUD",
379
+ "GHS",
380
+ "NGN",
381
+ "KES",
382
+ "ZAR",
383
+ "XOF",
384
+ "XAF",
385
+ "EGP",
386
+ "TZS",
387
+ "UGX",
388
+ "RWF",
389
+ "ETB",
390
+ "ZMW",
391
+ "BWP",
392
+ "MUR",
393
+ "NAD",
394
+ "MWK",
395
+ "AOA",
396
+ "CDF",
397
+ "GMD",
398
+ "GNF",
399
+ "LRD",
400
+ "SLL",
401
+ "MZN",
402
+ "BIF",
403
+ "INR",
404
+ "BRL",
405
+ "MXN",
406
+ "KRW",
407
+ "TRY",
408
+ "THB",
409
+ "MYR",
410
+ "PHP",
411
+ "IDR",
412
+ "VND",
413
+ "SGD",
414
+ "HKD",
415
+ "TWD",
416
+ "AED",
417
+ "SAR",
418
+ "ILS"
419
+ ]);
420
+ function isSupportedCurrency(code) {
421
+ return SUPPORTED_CURRENCY_CODES.has(code);
422
+ }
423
+ function currencyCode(value) {
424
+ return value;
425
+ }
426
+ var ErrorCode = {
427
+ // General
428
+ UNKNOWN_ERROR: "UNKNOWN_ERROR",
429
+ NETWORK_ERROR: "NETWORK_ERROR",
430
+ TIMEOUT: "TIMEOUT",
431
+ UNAUTHORIZED: "UNAUTHORIZED",
432
+ NOT_FOUND: "NOT_FOUND"};
433
+ var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
434
+ function docsUrlForCode(code) {
435
+ return `${DOCS_ERROR_BASE_URL}#${code.toLowerCase().replace(/_/g, "-")}`;
436
+ }
437
+ var ERROR_SUGGESTIONS = {
438
+ UNKNOWN_ERROR: "An unexpected error occurred. Capture the request/response payload and retry with exponential backoff.",
439
+ NETWORK_ERROR: "Check the shopper's connection and retry. If this persists, inspect CORS, DNS, and API reachability.",
440
+ TIMEOUT: "The request exceeded the timeout. Retry once, then poll order status before charging again.",
441
+ UNAUTHORIZED: "Authentication is missing or expired. Ensure a valid access token is set and refresh the session if needed.",
442
+ FORBIDDEN: "The key/session lacks permission for this resource. Verify business ownership and API key scope.",
443
+ NOT_FOUND: "The requested resource does not exist or is not visible in this environment.",
444
+ VALIDATION_ERROR: "One or more fields are invalid. Validate required fields and enum values before retrying.",
445
+ CART_EMPTY: "The cart has no items. Redirect back to menu/catalogue and require at least one line item.",
446
+ CART_EXPIRED: "This cart is no longer active. Recreate a new cart and re-add shopper selections.",
447
+ CART_NOT_FOUND: "Cart could not be located. It may have expired or belongs to a different key/location.",
448
+ ITEM_UNAVAILABLE: "The selected item is unavailable at this location/time. Prompt the shopper to pick an alternative.",
449
+ VARIANT_NOT_FOUND: "The requested variant no longer exists. Refresh product data and require re-selection.",
450
+ VARIANT_OUT_OF_STOCK: "The selected variant is out of stock. Show in-stock variants and block checkout for this line.",
451
+ ADDON_REQUIRED: "A required add-on is missing. Ensure required modifier groups are completed before add-to-cart.",
452
+ ADDON_MAX_EXCEEDED: "Too many add-ons were selected. Enforce max selections client-side before submission.",
453
+ CHECKOUT_VALIDATION_FAILED: "Checkout payload failed validation. Verify customer, order type, and address fields are complete.",
454
+ DELIVERY_ADDRESS_REQUIRED: "Delivery orders require an address. Collect and pass address info before processing checkout.",
455
+ CUSTOMER_INFO_REQUIRED: "Customer details are required. Ensure name/email/phone are available before checkout.",
456
+ QUOTE_NOT_FOUND: "Quote could not be found. Refresh pricing and create a new quote before checkout.",
457
+ QUOTE_EXPIRED: "Quote has expired. Re-fetch pricing to generate a new quote with a valid expiry window.",
458
+ QUOTE_CONSUMED: "Quote has already been used. Request a fresh quote to prevent duplicate checkout attempts.",
459
+ QUOTE_STORAGE_UNAVAILABLE: "Quote storage is temporarily unavailable. Retry shortly and avoid charging until quote fetch succeeds.",
460
+ PAYMENT_FAILED: "Payment provider rejected or failed processing. Show retry/change-method options to the shopper.",
461
+ PAYMENT_CANCELLED: "Payment was cancelled by the shopper or provider flow. Allow a safe retry path.",
462
+ INSUFFICIENT_FUNDS: "Payment method has insufficient funds. Prompt shopper to use another method.",
463
+ CARD_DECLINED: "Card was declined. Ask shopper to retry or switch payment method.",
464
+ INVALID_OTP: "Authorization code is invalid. Let shopper re-enter OTP/PIN and retry.",
465
+ OTP_EXPIRED: "Authorization code expired. Request a new OTP and re-submit authorization.",
466
+ AUTHORIZATION_FAILED: "Additional payment authorization failed. Retry authorization or change payment method.",
467
+ PAYMENT_ACTION_NOT_COMPLETED: "Required payment action was not completed. Resume provider flow and poll for status.",
468
+ SLOT_UNAVAILABLE: "Selected schedule slot is unavailable. Refresh available slots and ask shopper to reselect.",
469
+ BOOKING_CONFLICT: "The requested booking conflicts with an existing reservation. Pick another slot/resource.",
470
+ SERVICE_NOT_FOUND: "Requested service no longer exists. Refresh service catalogue and retry selection.",
471
+ OUT_OF_STOCK: "Inventory is depleted for this item. Remove it or reduce quantity before checkout.",
472
+ INSUFFICIENT_QUANTITY: "Requested quantity exceeds available inventory. Reduce quantity and retry.",
473
+ BUSINESS_ID_REQUIRED: "Business context could not be resolved. Verify the public key and business bootstrap call.",
474
+ INVALID_CART: "Cart is invalid for checkout. Sync cart state, ensure items exist, then retry.",
475
+ ORDER_TYPE_REQUIRED: "Order type is required. Provide one of delivery, pickup, or dine_in before checkout.",
476
+ NO_PAYMENT_ELEMENT: "PaymentElement is required for processCheckout(). Mount it before triggering checkout.",
477
+ PAYMENT_NOT_MOUNTED: "PaymentElement iframe is not mounted. Mount it in the DOM before processCheckout().",
478
+ AUTH_INCOMPLETE: "AuthElement has not completed authentication. Wait for AUTHENTICATED before checkout.",
479
+ AUTH_LOST: "Session was cleared during checkout. Re-authenticate and restart checkout safely.",
480
+ ALREADY_PROCESSING: "Checkout is already in progress. Disable duplicate submits until completion.",
481
+ CHECKOUT_NOT_READY: "Checkout elements are still initializing. Wait for readiness before submit.",
482
+ CANCELLED: "Checkout was cancelled. Preserve cart state and allow shopper to retry.",
483
+ REQUEST_TIMEOUT: "Provider call timed out. Poll payment/order status before issuing another charge attempt.",
484
+ POPUP_BLOCKED: "Browser blocked provider popup. Ask shopper to enable popups and retry.",
485
+ FX_QUOTE_FAILED: "Failed to lock FX quote. Retry currency quote or fallback to base currency."
486
+ };
487
+ var ERROR_HINTS = Object.fromEntries(
488
+ Object.entries(ERROR_SUGGESTIONS).map(([code, suggestion]) => [
489
+ code,
490
+ {
491
+ docs_url: docsUrlForCode(code),
492
+ suggestion
493
+ }
494
+ ])
495
+ );
496
+ var CimplifyError = class extends Error {
497
+ constructor(code, message, retryable = false, docs_url, suggestion) {
498
+ super(message);
499
+ this.code = code;
500
+ this.retryable = retryable;
501
+ this.docs_url = docs_url;
502
+ this.suggestion = suggestion;
503
+ this.name = "CimplifyError";
417
504
  }
418
- if (status === "preparing") {
419
- return "Preparing checkout";
505
+ /** User-friendly message safe to display */
506
+ get userMessage() {
507
+ return this.message;
420
508
  }
421
- if (status === "recovering") {
422
- return "Resuming payment";
509
+ };
510
+ function getErrorHint(code) {
511
+ return ERROR_HINTS[code];
512
+ }
513
+ function enrichError(error, options = {}) {
514
+ const hint = getErrorHint(error.code);
515
+ if (hint) {
516
+ if (!error.docs_url) {
517
+ error.docs_url = hint.docs_url;
518
+ }
519
+ if (!error.suggestion) {
520
+ error.suggestion = hint.suggestion;
521
+ }
522
+ } else if (!error.docs_url) {
523
+ error.docs_url = docsUrlForCode(error.code || ErrorCode.UNKNOWN_ERROR);
423
524
  }
424
- if (status === "processing") {
425
- return "Processing payment";
525
+ if (options.isTestMode && !error.message.includes("pk_test_")) {
526
+ error.message = `${error.message}
527
+
528
+ \u2139 Your API key is a test-mode key (pk_test_...). Verify test data/session before retrying.`;
426
529
  }
427
- if (status === "awaiting_authorization") {
428
- return "Waiting for authorization";
530
+ return error;
531
+ }
532
+
533
+ // src/types/result.ts
534
+ function ok(value) {
535
+ return { ok: true, value };
536
+ }
537
+ function err(error) {
538
+ return { ok: false, error };
539
+ }
540
+
541
+ // src/catalogue.ts
542
+ function toCimplifyError(error) {
543
+ if (error instanceof CimplifyError) return enrichError(error);
544
+ if (error instanceof Error) {
545
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
429
546
  }
430
- if (status === "polling") {
431
- return "Confirming payment";
547
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
548
+ }
549
+ async function safe(promise) {
550
+ try {
551
+ return ok(await promise);
552
+ } catch (error) {
553
+ return err(toCimplifyError(error));
432
554
  }
433
- if (status === "finalizing") {
434
- return "Finalizing order";
555
+ }
556
+ function withQuery(path, params) {
557
+ const query = new URLSearchParams();
558
+ for (const [key, value] of Object.entries(params)) {
559
+ if (value === void 0) continue;
560
+ query.set(key, String(value));
435
561
  }
436
- if (status === "success") {
437
- return "Payment complete";
562
+ const queryString = query.toString();
563
+ return queryString ? `${path}?${queryString}` : path;
564
+ }
565
+ function isRecord(value) {
566
+ return typeof value === "object" && value !== null;
567
+ }
568
+ function readFinalPrice(value) {
569
+ if (!isRecord(value)) return void 0;
570
+ const finalPrice = value.final_price;
571
+ if (typeof finalPrice === "string" || typeof finalPrice === "number") {
572
+ return finalPrice;
438
573
  }
439
- return "Payment failed";
440
- }
441
- function CimplifyCheckout({
442
- client,
443
- businessId,
444
- cartId,
445
- locationId,
446
- linkUrl,
447
- orderTypes,
448
- enrollInLink = true,
449
- onComplete,
450
- onError,
451
- onStatusChange,
452
- appearance,
453
- demoMode,
454
- className
455
- }) {
456
- const resolvedOrderTypes = useMemo(
457
- () => orderTypes && orderTypes.length > 0 ? orderTypes : ["pickup", "delivery"],
458
- [orderTypes]
459
- );
460
- const [orderType, setOrderType] = useState(resolvedOrderTypes[0] || "pickup");
461
- const [status, setStatus] = useState(null);
462
- const [statusText, setStatusText] = useState("");
463
- const [isSubmitting, setIsSubmitting] = useState(false);
464
- const [isInitializing, setIsInitializing] = useState(false);
465
- const [errorMessage, setErrorMessage] = useState(null);
466
- const [resolvedBusinessId, setResolvedBusinessId] = useState(businessId ?? null);
467
- const [resolvedCartId, setResolvedCartId] = useState(cartId ?? null);
468
- const [resolvedCart, setResolvedCart] = useState(null);
469
- const checkoutMountRef = useRef(null);
470
- const elementsRef = useRef(null);
471
- const activeCheckoutRef = useRef(null);
472
- const initialAppearanceRef = useRef(appearance);
473
- const hasWarnedInlineAppearanceRef = useRef(false);
474
- const isMountedRef = useRef(true);
475
- const demoRunRef = useRef(0);
476
- const isDemoCheckout = demoMode ?? client.getPublicKey().trim().length === 0;
477
- const isTestMode = client.isTestMode();
478
- const primaryColor = appearance?.variables?.primaryColor || "#0a2540";
479
- const isDark = appearance?.theme === "dark";
480
- const emitStatus = React2.useEffectEvent(
481
- (nextStatus, context = {}) => {
482
- setStatus(nextStatus);
483
- setStatusText(context.display_text || "");
484
- onStatusChange?.(nextStatus, context);
485
- }
486
- );
487
- const fireError = React2.useEffectEvent(
488
- (error) => {
489
- onError?.(error);
490
- }
491
- );
492
- useEffect(() => {
493
- if (!resolvedOrderTypes.includes(orderType)) {
494
- setOrderType(resolvedOrderTypes[0] || "pickup");
495
- }
496
- }, [resolvedOrderTypes, orderType]);
497
- useEffect(() => {
498
- if (appearance && appearance !== initialAppearanceRef.current && !hasWarnedInlineAppearanceRef.current) {
499
- hasWarnedInlineAppearanceRef.current = true;
500
- console.warn(
501
- "[Cimplify] `appearance` prop reference changed after mount. Elements keep the initial appearance to avoid iframe remount. Memoize appearance with useMemo() to remove this warning."
502
- );
503
- }
504
- }, [appearance]);
505
- useEffect(() => {
506
- let cancelled = false;
507
- async function bootstrap() {
508
- if (isDemoCheckout) {
509
- if (!cancelled) {
510
- setResolvedBusinessId(businessId ?? null);
511
- setResolvedCartId(cartId ?? "cart_demo");
512
- setIsInitializing(false);
513
- setErrorMessage(null);
514
- }
515
- return;
516
- }
517
- const needsBusinessResolve = !businessId;
518
- const needsCartResolve = !cartId;
519
- if (!needsBusinessResolve && !needsCartResolve) {
520
- if (!cancelled) {
521
- setResolvedBusinessId(businessId || null);
522
- setResolvedCartId(cartId || null);
523
- setIsInitializing(false);
524
- setErrorMessage(null);
525
- }
526
- client.cart.get().then((cartResult) => {
527
- if (!cancelled && cartResult.ok && cartResult.value) {
528
- setResolvedCart(cartResult.value);
529
- }
530
- }).catch(() => {
531
- });
532
- return;
533
- }
534
- if (!cancelled) {
535
- setIsInitializing(true);
536
- setErrorMessage(null);
537
- }
538
- let nextBusinessId = businessId ?? null;
539
- if (!nextBusinessId) {
540
- try {
541
- nextBusinessId = await client.resolveBusinessId();
542
- } catch {
543
- if (!cancelled) {
544
- const message = "Unable to initialize checkout business context.";
545
- setResolvedBusinessId(null);
546
- setResolvedCartId(null);
547
- setErrorMessage(message);
548
- setIsInitializing(false);
549
- fireError({ code: "BUSINESS_ID_REQUIRED", message });
550
- }
551
- return;
552
- }
553
- }
554
- let nextCartId = cartId ?? null;
555
- if (!nextCartId) {
556
- const cartResult = await client.cart.get();
557
- if (!cartResult.ok || !cartResult.value?.id || cartResult.value.items.length === 0) {
558
- if (!cancelled) {
559
- const message = "Your cart is empty. Add items before checkout.";
560
- setResolvedBusinessId(nextBusinessId);
561
- setResolvedCartId(null);
562
- setErrorMessage(message);
563
- setIsInitializing(false);
564
- fireError({ code: "CART_EMPTY", message });
565
- }
566
- return;
567
- }
568
- nextCartId = cartResult.value.id;
569
- if (!cancelled) {
570
- setResolvedCart(cartResult.value);
571
- }
572
- }
573
- if (!cancelled) {
574
- setResolvedBusinessId(nextBusinessId);
575
- setResolvedCartId(nextCartId);
576
- setIsInitializing(false);
577
- setErrorMessage(null);
578
- }
579
- }
580
- void bootstrap();
581
- return () => {
582
- cancelled = true;
583
- };
584
- }, [businessId, cartId, client, isDemoCheckout]);
585
- useEffect(() => {
586
- return () => {
587
- isMountedRef.current = false;
588
- demoRunRef.current += 1;
589
- activeCheckoutRef.current?.abort();
590
- activeCheckoutRef.current = null;
591
- };
592
- }, []);
593
- const handleSubmit = React2.useEffectEvent(async () => {
594
- if (isSubmitting || isInitializing || !resolvedCartId) {
595
- if (!resolvedCartId && !isInitializing) {
596
- const message = "Your cart is empty. Add items before checkout.";
597
- setErrorMessage(message);
598
- fireError({ code: "CART_EMPTY", message });
599
- }
600
- return;
601
- }
602
- setErrorMessage(null);
603
- setIsSubmitting(true);
604
- emitStatus("preparing", { display_text: statusToLabel("preparing") });
605
- if (isDemoCheckout) {
606
- const runId = demoRunRef.current + 1;
607
- demoRunRef.current = runId;
608
- const wait = async (ms) => {
609
- await new Promise((resolve) => setTimeout(resolve, ms));
610
- return isMountedRef.current && runId === demoRunRef.current;
611
- };
612
- try {
613
- if (!await wait(400)) return;
614
- emitStatus("processing", { display_text: statusToLabel("processing") });
615
- if (!await wait(900)) return;
616
- emitStatus("polling", { display_text: statusToLabel("polling") });
617
- if (!await wait(1200)) return;
618
- const result = {
619
- success: true,
620
- order: {
621
- id: `ord_demo_${Date.now()}`,
622
- order_number: `DEMO-${Math.random().toString(36).slice(2, 8).toUpperCase()}`,
623
- status: "confirmed",
624
- total: "0.00",
625
- currency: "USD"
626
- }
627
- };
628
- emitStatus("success", {
629
- order_id: result.order?.id,
630
- order_number: result.order?.order_number,
631
- display_text: statusToLabel("success")
632
- });
633
- onComplete(result);
634
- } finally {
635
- if (isMountedRef.current && runId === demoRunRef.current) {
636
- setIsSubmitting(false);
637
- }
638
- }
639
- return;
640
- }
641
- if (!elementsRef.current) {
642
- const message = "Checkout is still initializing. Please try again.";
643
- setErrorMessage(message);
644
- fireError({ code: "CHECKOUT_NOT_READY", message });
645
- setIsSubmitting(false);
646
- return;
647
- }
648
- const checkout = elementsRef.current.processCheckout({
649
- cart_id: resolvedCartId,
650
- location_id: locationId ?? client.getLocationId() ?? void 0,
651
- order_type: orderType,
652
- enroll_in_link: enrollInLink,
653
- on_status_change: emitStatus
654
- });
655
- activeCheckoutRef.current = checkout;
656
- try {
657
- const result = await checkout;
658
- if (result.success) {
659
- onComplete(result);
660
- return;
661
- }
662
- const code = result.error?.code || "CHECKOUT_FAILED";
663
- const message = result.error?.message || "Payment failed.";
664
- setErrorMessage(message);
665
- fireError({ code, message });
666
- } finally {
667
- if (isMountedRef.current) {
668
- activeCheckoutRef.current = null;
669
- setIsSubmitting(false);
670
- }
671
- }
672
- });
673
- useEffect(() => {
674
- if (isDemoCheckout || !resolvedBusinessId) {
675
- elementsRef.current = null;
676
- return;
677
- }
678
- const elements = client.elements(resolvedBusinessId, {
679
- appearance: initialAppearanceRef.current,
680
- linkUrl
681
- });
682
- elementsRef.current = elements;
683
- const checkout = elements.create("checkout", {
684
- orderTypes: resolvedOrderTypes,
685
- defaultOrderType: resolvedOrderTypes[0]
686
- });
687
- if (checkoutMountRef.current) {
688
- checkout.mount(checkoutMountRef.current);
689
- }
690
- checkout.on("ready", () => {
691
- if (resolvedCart) {
692
- checkout.setCart(transformToCheckoutCart(resolvedCart));
693
- }
694
- });
695
- checkout.on("order_type_changed", (data) => {
696
- const typed = data;
697
- if (typed.orderType) {
698
- setOrderType(typed.orderType);
699
- }
700
- });
701
- checkout.on("request_submit", () => {
702
- void handleSubmit();
703
- });
704
- return () => {
705
- activeCheckoutRef.current?.abort();
706
- activeCheckoutRef.current = null;
707
- elements.destroy();
708
- elementsRef.current = null;
709
- };
710
- }, [client, resolvedBusinessId, isDemoCheckout]);
711
- useEffect(() => {
712
- if (!resolvedCart || !elementsRef.current) return;
713
- const checkoutElement = elementsRef.current.getElement("checkout");
714
- if (checkoutElement) {
715
- checkoutElement.setCart(transformToCheckoutCart(resolvedCart));
716
- }
717
- }, [resolvedCart]);
718
- const colors = shellColors(isDark ?? false, primaryColor);
719
- if (isInitializing) {
720
- return /* @__PURE__ */ jsx("div", { className, "data-cimplify-checkout": "", children: /* @__PURE__ */ jsx("p", { "data-cimplify-status": "", style: { fontSize: 13, color: colors.textSecondary }, children: "Preparing checkout..." }) });
721
- }
722
- if (!isDemoCheckout && (!resolvedBusinessId || !resolvedCartId)) {
723
- return /* @__PURE__ */ jsx("div", { className, "data-cimplify-checkout": "", children: /* @__PURE__ */ jsx("p", { "data-cimplify-error": "", style: { fontSize: 13, color: colors.error }, children: errorMessage || "Unable to initialize checkout. Please refresh and try again." }) });
724
- }
725
- return /* @__PURE__ */ jsxs("div", { className, "data-cimplify-checkout": "", children: [
726
- isTestMode && !isDemoCheckout && /* @__PURE__ */ jsx(
727
- "p",
728
- {
729
- "data-cimplify-test-mode": "",
730
- style: {
731
- marginBottom: "10px",
732
- fontSize: "12px",
733
- fontWeight: 600,
734
- color: "#92400e"
735
- },
736
- children: "Test mode - no real charges"
737
- }
738
- ),
739
- /* @__PURE__ */ jsx("div", { "data-cimplify-section": "checkout", children: /* @__PURE__ */ jsx("div", { ref: isDemoCheckout ? void 0 : checkoutMountRef }) }),
740
- status && /* @__PURE__ */ jsx("p", { "data-cimplify-status": "", style: { marginTop: SPACE.sm, fontSize: 13, color: colors.textSecondary }, children: statusText || statusToLabel(status) }),
741
- errorMessage && /* @__PURE__ */ jsx("p", { "data-cimplify-error": "", style: { marginTop: SPACE.sm, fontSize: 13, color: colors.error }, children: errorMessage })
742
- ] });
743
- }
744
-
745
- // src/utils/price.ts
746
- var CURRENCY_SYMBOLS = {
747
- // Major world currencies
748
- USD: "$",
749
- EUR: "\u20AC",
750
- GBP: "\xA3",
751
- JPY: "\xA5",
752
- CNY: "\xA5",
753
- CHF: "CHF",
754
- CAD: "C$",
755
- AUD: "A$",
756
- NZD: "NZ$",
757
- HKD: "HK$",
758
- SGD: "S$",
759
- INR: "\u20B9",
760
- BRL: "R$",
761
- MXN: "MX$",
762
- KRW: "\u20A9",
763
- RUB: "\u20BD",
764
- TRY: "\u20BA",
765
- THB: "\u0E3F",
766
- PLN: "z\u0142",
767
- SEK: "kr",
768
- NOK: "kr",
769
- DKK: "kr",
770
- CZK: "K\u010D",
771
- HUF: "Ft",
772
- ILS: "\u20AA",
773
- AED: "\u062F.\u0625",
774
- SAR: "\uFDFC",
775
- MYR: "RM",
776
- PHP: "\u20B1",
777
- IDR: "Rp",
778
- VND: "\u20AB",
779
- TWD: "NT$",
780
- // African currencies
781
- GHS: "GH\u20B5",
782
- NGN: "\u20A6",
783
- KES: "KSh",
784
- ZAR: "R",
785
- XOF: "CFA",
786
- XAF: "FCFA",
787
- EGP: "E\xA3",
788
- MAD: "MAD",
789
- TZS: "TSh",
790
- UGX: "USh",
791
- RWF: "FRw",
792
- ETB: "Br",
793
- ZMW: "ZK",
794
- BWP: "P",
795
- MUR: "\u20A8",
796
- SCR: "\u20A8",
797
- NAD: "N$",
798
- SZL: "E",
799
- LSL: "L",
800
- MWK: "MK",
801
- AOA: "Kz",
802
- CDF: "FC",
803
- GMD: "D",
804
- GNF: "FG",
805
- LRD: "L$",
806
- SLL: "Le",
807
- MZN: "MT",
808
- SDG: "SDG",
809
- SSP: "SSP",
810
- SOS: "Sh.So.",
811
- DJF: "Fdj",
812
- ERN: "Nfk",
813
- CVE: "$",
814
- STN: "Db",
815
- KMF: "CF",
816
- BIF: "FBu"
817
- };
818
- function getCurrencySymbol(currencyCode2) {
819
- return CURRENCY_SYMBOLS[currencyCode2.toUpperCase()] || currencyCode2;
820
- }
821
- function formatPrice(amount, currency = "GHS", locale = "en-US") {
822
- const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
823
- if (isNaN(numAmount)) {
824
- return `${getCurrencySymbol(currency)}0.00`;
825
- }
826
- try {
827
- return new Intl.NumberFormat(locale, {
828
- style: "currency",
829
- currency: currency.toUpperCase(),
830
- minimumFractionDigits: 2,
831
- maximumFractionDigits: 2
832
- }).format(numAmount);
833
- } catch {
834
- return `${getCurrencySymbol(currency)}${numAmount.toFixed(2)}`;
835
- }
836
- }
837
- function parsePrice(value) {
838
- if (value === void 0 || value === null) {
839
- return 0;
840
- }
841
- if (typeof value === "number") {
842
- return isNaN(value) ? 0 : value;
843
- }
844
- const cleaned = value.replace(/[^\d.-]/g, "");
845
- const parsed = parseFloat(cleaned);
846
- return isNaN(parsed) ? 0 : parsed;
847
- }
848
-
849
- // src/types/common.ts
850
- function money(value) {
851
- return value;
852
- }
853
- function moneyFromNumber(value) {
854
- return value.toFixed(2);
855
- }
856
- function currencyCode(value) {
857
- return value;
858
- }
859
- var ErrorCode = {
860
- // General
861
- UNKNOWN_ERROR: "UNKNOWN_ERROR",
862
- NETWORK_ERROR: "NETWORK_ERROR",
863
- TIMEOUT: "TIMEOUT",
864
- UNAUTHORIZED: "UNAUTHORIZED",
865
- NOT_FOUND: "NOT_FOUND"};
866
- var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
867
- function docsUrlForCode(code) {
868
- return `${DOCS_ERROR_BASE_URL}#${code.toLowerCase().replace(/_/g, "-")}`;
869
- }
870
- var ERROR_SUGGESTIONS = {
871
- UNKNOWN_ERROR: "An unexpected error occurred. Capture the request/response payload and retry with exponential backoff.",
872
- NETWORK_ERROR: "Check the shopper's connection and retry. If this persists, inspect CORS, DNS, and API reachability.",
873
- TIMEOUT: "The request exceeded the timeout. Retry once, then poll order status before charging again.",
874
- UNAUTHORIZED: "Authentication is missing or expired. Ensure a valid access token is set and refresh the session if needed.",
875
- FORBIDDEN: "The key/session lacks permission for this resource. Verify business ownership and API key scope.",
876
- NOT_FOUND: "The requested resource does not exist or is not visible in this environment.",
877
- VALIDATION_ERROR: "One or more fields are invalid. Validate required fields and enum values before retrying.",
878
- CART_EMPTY: "The cart has no items. Redirect back to menu/catalogue and require at least one line item.",
879
- CART_EXPIRED: "This cart is no longer active. Recreate a new cart and re-add shopper selections.",
880
- CART_NOT_FOUND: "Cart could not be located. It may have expired or belongs to a different key/location.",
881
- ITEM_UNAVAILABLE: "The selected item is unavailable at this location/time. Prompt the shopper to pick an alternative.",
882
- VARIANT_NOT_FOUND: "The requested variant no longer exists. Refresh product data and require re-selection.",
883
- VARIANT_OUT_OF_STOCK: "The selected variant is out of stock. Show in-stock variants and block checkout for this line.",
884
- ADDON_REQUIRED: "A required add-on is missing. Ensure required modifier groups are completed before add-to-cart.",
885
- ADDON_MAX_EXCEEDED: "Too many add-ons were selected. Enforce max selections client-side before submission.",
886
- CHECKOUT_VALIDATION_FAILED: "Checkout payload failed validation. Verify customer, order type, and address fields are complete.",
887
- DELIVERY_ADDRESS_REQUIRED: "Delivery orders require an address. Collect and pass address info before processing checkout.",
888
- CUSTOMER_INFO_REQUIRED: "Customer details are required. Ensure name/email/phone are available before checkout.",
889
- QUOTE_NOT_FOUND: "Quote could not be found. Refresh pricing and create a new quote before checkout.",
890
- QUOTE_EXPIRED: "Quote has expired. Re-fetch pricing to generate a new quote with a valid expiry window.",
891
- QUOTE_CONSUMED: "Quote has already been used. Request a fresh quote to prevent duplicate checkout attempts.",
892
- QUOTE_STORAGE_UNAVAILABLE: "Quote storage is temporarily unavailable. Retry shortly and avoid charging until quote fetch succeeds.",
893
- PAYMENT_FAILED: "Payment provider rejected or failed processing. Show retry/change-method options to the shopper.",
894
- PAYMENT_CANCELLED: "Payment was cancelled by the shopper or provider flow. Allow a safe retry path.",
895
- INSUFFICIENT_FUNDS: "Payment method has insufficient funds. Prompt shopper to use another method.",
896
- CARD_DECLINED: "Card was declined. Ask shopper to retry or switch payment method.",
897
- INVALID_OTP: "Authorization code is invalid. Let shopper re-enter OTP/PIN and retry.",
898
- OTP_EXPIRED: "Authorization code expired. Request a new OTP and re-submit authorization.",
899
- AUTHORIZATION_FAILED: "Additional payment authorization failed. Retry authorization or change payment method.",
900
- PAYMENT_ACTION_NOT_COMPLETED: "Required payment action was not completed. Resume provider flow and poll for status.",
901
- SLOT_UNAVAILABLE: "Selected schedule slot is unavailable. Refresh available slots and ask shopper to reselect.",
902
- BOOKING_CONFLICT: "The requested booking conflicts with an existing reservation. Pick another slot/resource.",
903
- SERVICE_NOT_FOUND: "Requested service no longer exists. Refresh service catalogue and retry selection.",
904
- OUT_OF_STOCK: "Inventory is depleted for this item. Remove it or reduce quantity before checkout.",
905
- INSUFFICIENT_QUANTITY: "Requested quantity exceeds available inventory. Reduce quantity and retry.",
906
- BUSINESS_ID_REQUIRED: "Business context could not be resolved. Verify the public key and business bootstrap call.",
907
- INVALID_CART: "Cart is invalid for checkout. Sync cart state, ensure items exist, then retry.",
908
- ORDER_TYPE_REQUIRED: "Order type is required. Provide one of delivery, pickup, or dine_in before checkout.",
909
- NO_PAYMENT_ELEMENT: "PaymentElement is required for processCheckout(). Mount it before triggering checkout.",
910
- PAYMENT_NOT_MOUNTED: "PaymentElement iframe is not mounted. Mount it in the DOM before processCheckout().",
911
- AUTH_INCOMPLETE: "AuthElement has not completed authentication. Wait for AUTHENTICATED before checkout.",
912
- AUTH_LOST: "Session was cleared during checkout. Re-authenticate and restart checkout safely.",
913
- ALREADY_PROCESSING: "Checkout is already in progress. Disable duplicate submits until completion.",
914
- CHECKOUT_NOT_READY: "Checkout elements are still initializing. Wait for readiness before submit.",
915
- CANCELLED: "Checkout was cancelled. Preserve cart state and allow shopper to retry.",
916
- REQUEST_TIMEOUT: "Provider call timed out. Poll payment/order status before issuing another charge attempt.",
917
- POPUP_BLOCKED: "Browser blocked provider popup. Ask shopper to enable popups and retry.",
918
- FX_QUOTE_FAILED: "Failed to lock FX quote. Retry currency quote or fallback to base currency."
919
- };
920
- var ERROR_HINTS = Object.fromEntries(
921
- Object.entries(ERROR_SUGGESTIONS).map(([code, suggestion]) => [
922
- code,
923
- {
924
- docs_url: docsUrlForCode(code),
925
- suggestion
926
- }
927
- ])
928
- );
929
- var CimplifyError = class extends Error {
930
- constructor(code, message, retryable = false, docs_url, suggestion) {
931
- super(message);
932
- this.code = code;
933
- this.retryable = retryable;
934
- this.docs_url = docs_url;
935
- this.suggestion = suggestion;
936
- this.name = "CimplifyError";
937
- }
938
- /** User-friendly message safe to display */
939
- get userMessage() {
940
- return this.message;
941
- }
942
- };
943
- function getErrorHint(code) {
944
- return ERROR_HINTS[code];
945
- }
946
- function enrichError(error, options = {}) {
947
- const hint = getErrorHint(error.code);
948
- if (hint) {
949
- if (!error.docs_url) {
950
- error.docs_url = hint.docs_url;
951
- }
952
- if (!error.suggestion) {
953
- error.suggestion = hint.suggestion;
954
- }
955
- } else if (!error.docs_url) {
956
- error.docs_url = docsUrlForCode(error.code || ErrorCode.UNKNOWN_ERROR);
957
- }
958
- if (options.isTestMode && !error.message.includes("pk_test_")) {
959
- error.message = `${error.message}
960
-
961
- \u2139 Your API key is a test-mode key (pk_test_...). Verify test data/session before retrying.`;
962
- }
963
- return error;
964
- }
965
-
966
- // src/types/result.ts
967
- function ok(value) {
968
- return { ok: true, value };
969
- }
970
- function err(error) {
971
- return { ok: false, error };
972
- }
973
-
974
- // src/catalogue.ts
975
- function toCimplifyError(error) {
976
- if (error instanceof CimplifyError) return enrichError(error);
977
- if (error instanceof Error) {
978
- return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
979
- }
980
- return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
981
- }
982
- async function safe(promise) {
983
- try {
984
- return ok(await promise);
985
- } catch (error) {
986
- return err(toCimplifyError(error));
987
- }
988
- }
989
- function withQuery(path, params) {
990
- const query = new URLSearchParams();
991
- for (const [key, value] of Object.entries(params)) {
992
- if (value === void 0) continue;
993
- query.set(key, String(value));
994
- }
995
- const queryString = query.toString();
996
- return queryString ? `${path}?${queryString}` : path;
997
- }
998
- function isRecord(value) {
999
- return typeof value === "object" && value !== null;
1000
- }
1001
- function readFinalPrice(value) {
1002
- if (!isRecord(value)) return void 0;
1003
- const finalPrice = value.final_price;
1004
- if (typeof finalPrice === "string" || typeof finalPrice === "number") {
1005
- return finalPrice;
1006
- }
1007
- return void 0;
574
+ return void 0;
1008
575
  }
1009
576
  function normalizeAddOnPayload(addOn) {
1010
577
  if (!isRecord(addOn)) return addOn;
@@ -1457,6 +1024,110 @@ var MOBILE_MONEY_PROVIDER = {
1457
1024
  AIRTEL: "airtel"
1458
1025
  };
1459
1026
 
1027
+ // src/utils/price.ts
1028
+ var CURRENCY_SYMBOLS = {
1029
+ // Major world currencies
1030
+ USD: "$",
1031
+ EUR: "\u20AC",
1032
+ GBP: "\xA3",
1033
+ JPY: "\xA5",
1034
+ CNY: "\xA5",
1035
+ CHF: "CHF",
1036
+ CAD: "C$",
1037
+ AUD: "A$",
1038
+ NZD: "NZ$",
1039
+ HKD: "HK$",
1040
+ SGD: "S$",
1041
+ INR: "\u20B9",
1042
+ BRL: "R$",
1043
+ MXN: "MX$",
1044
+ KRW: "\u20A9",
1045
+ RUB: "\u20BD",
1046
+ TRY: "\u20BA",
1047
+ THB: "\u0E3F",
1048
+ PLN: "z\u0142",
1049
+ SEK: "kr",
1050
+ NOK: "kr",
1051
+ DKK: "kr",
1052
+ CZK: "K\u010D",
1053
+ HUF: "Ft",
1054
+ ILS: "\u20AA",
1055
+ AED: "\u062F.\u0625",
1056
+ SAR: "\uFDFC",
1057
+ MYR: "RM",
1058
+ PHP: "\u20B1",
1059
+ IDR: "Rp",
1060
+ VND: "\u20AB",
1061
+ TWD: "NT$",
1062
+ // African currencies
1063
+ GHS: "GH\u20B5",
1064
+ NGN: "\u20A6",
1065
+ KES: "KSh",
1066
+ ZAR: "R",
1067
+ XOF: "CFA",
1068
+ XAF: "FCFA",
1069
+ EGP: "E\xA3",
1070
+ MAD: "MAD",
1071
+ TZS: "TSh",
1072
+ UGX: "USh",
1073
+ RWF: "FRw",
1074
+ ETB: "Br",
1075
+ ZMW: "ZK",
1076
+ BWP: "P",
1077
+ MUR: "\u20A8",
1078
+ SCR: "\u20A8",
1079
+ NAD: "N$",
1080
+ SZL: "E",
1081
+ LSL: "L",
1082
+ MWK: "MK",
1083
+ AOA: "Kz",
1084
+ CDF: "FC",
1085
+ GMD: "D",
1086
+ GNF: "FG",
1087
+ LRD: "L$",
1088
+ SLL: "Le",
1089
+ MZN: "MT",
1090
+ SDG: "SDG",
1091
+ SSP: "SSP",
1092
+ SOS: "Sh.So.",
1093
+ DJF: "Fdj",
1094
+ ERN: "Nfk",
1095
+ CVE: "$",
1096
+ STN: "Db",
1097
+ KMF: "CF",
1098
+ BIF: "FBu"
1099
+ };
1100
+ function getCurrencySymbol(currencyCode2) {
1101
+ return CURRENCY_SYMBOLS[currencyCode2.toUpperCase()] || currencyCode2;
1102
+ }
1103
+ function formatPrice(amount, currency = "GHS", locale = "en-US") {
1104
+ const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
1105
+ if (isNaN(numAmount)) {
1106
+ return `${getCurrencySymbol(currency)}0.00`;
1107
+ }
1108
+ try {
1109
+ return new Intl.NumberFormat(locale, {
1110
+ style: "currency",
1111
+ currency: currency.toUpperCase(),
1112
+ minimumFractionDigits: 2,
1113
+ maximumFractionDigits: 2
1114
+ }).format(numAmount);
1115
+ } catch {
1116
+ return `${getCurrencySymbol(currency)}${numAmount.toFixed(2)}`;
1117
+ }
1118
+ }
1119
+ function parsePrice(value) {
1120
+ if (value === void 0 || value === null) {
1121
+ return 0;
1122
+ }
1123
+ if (typeof value === "number") {
1124
+ return isNaN(value) ? 0 : value;
1125
+ }
1126
+ const cleaned = value.replace(/[^\d.-]/g, "");
1127
+ const parsed = parseFloat(cleaned);
1128
+ return isNaN(parsed) ? 0 : parsed;
1129
+ }
1130
+
1460
1131
  // src/utils/payment.ts
1461
1132
  var PAYMENT_SUCCESS_STATUSES = /* @__PURE__ */ new Set([
1462
1133
  "success",
@@ -3114,6 +2785,48 @@ var FxService = class {
3114
2785
  }
3115
2786
  };
3116
2787
 
2788
+ // src/types/elements.ts
2789
+ var ELEMENT_TYPES = {
2790
+ AUTH: "auth",
2791
+ ADDRESS: "address",
2792
+ PAYMENT: "payment",
2793
+ CHECKOUT: "checkout"
2794
+ };
2795
+ var MESSAGE_TYPES = {
2796
+ // Parent → Iframe
2797
+ INIT: "init",
2798
+ SET_TOKEN: "set_token",
2799
+ SET_CART: "set_cart",
2800
+ GET_DATA: "get_data",
2801
+ PROCESS_CHECKOUT: "process_checkout",
2802
+ ABORT_CHECKOUT: "abort_checkout",
2803
+ // Iframe → Parent
2804
+ READY: "ready",
2805
+ HEIGHT_CHANGE: "height_change",
2806
+ AUTHENTICATED: "authenticated",
2807
+ REQUIRES_OTP: "requires_otp",
2808
+ ERROR: "error",
2809
+ ADDRESS_CHANGED: "address_changed",
2810
+ ADDRESS_SELECTED: "address_selected",
2811
+ PAYMENT_METHOD_SELECTED: "payment_method_selected",
2812
+ TOKEN_REFRESHED: "token_refreshed",
2813
+ LOGOUT_COMPLETE: "logout_complete",
2814
+ CONTACT_PROVIDED: "contact_provided",
2815
+ CHECKOUT_STATUS: "checkout_status",
2816
+ CHECKOUT_COMPLETE: "checkout_complete",
2817
+ ORDER_TYPE_CHANGED: "order_type_changed",
2818
+ REQUEST_SUBMIT: "request_submit"
2819
+ };
2820
+ var EVENT_TYPES = {
2821
+ READY: "ready",
2822
+ AUTHENTICATED: "authenticated",
2823
+ REQUIRES_OTP: "requires_otp",
2824
+ ERROR: "error",
2825
+ CHANGE: "change",
2826
+ ORDER_TYPE_CHANGED: "order_type_changed",
2827
+ REQUEST_SUBMIT: "request_submit"
2828
+ };
2829
+
3117
2830
  // src/elements.ts
3118
2831
  function toCheckoutError(code, message, recoverable) {
3119
2832
  const hint = getErrorHint(code);
@@ -4400,247 +4113,610 @@ var CimplifyClient = class {
4400
4113
  }
4401
4114
  return createElements(this, businessId ?? this.businessId ?? void 0, options);
4402
4115
  }
4403
- };
4404
- function createCimplifyClient(config = {}) {
4405
- return new CimplifyClient(config);
4116
+ };
4117
+ function createCimplifyClient(config = {}) {
4118
+ return new CimplifyClient(config);
4119
+ }
4120
+ var LOCATION_STORAGE_KEY = "cimplify_location_id";
4121
+ var DISPLAY_CURRENCY_STORAGE_KEY = "cimplify_display_currency";
4122
+ var FX_REFRESH_INTERVAL = 12e4;
4123
+ var DEFAULT_CURRENCY = "USD";
4124
+ var DEFAULT_COUNTRY = "US";
4125
+ function createDefaultClient() {
4126
+ const processRef = globalThis.process;
4127
+ const envPublicKey = processRef?.env?.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY || "";
4128
+ return createCimplifyClient({ publicKey: envPublicKey });
4129
+ }
4130
+ function getStoredLocationId() {
4131
+ if (typeof window === "undefined" || !window.localStorage) {
4132
+ return null;
4133
+ }
4134
+ const value = window.localStorage.getItem(LOCATION_STORAGE_KEY);
4135
+ if (!value) {
4136
+ return null;
4137
+ }
4138
+ const normalized = value.trim();
4139
+ return normalized.length > 0 ? normalized : null;
4140
+ }
4141
+ function setStoredLocationId(locationId) {
4142
+ if (typeof window === "undefined" || !window.localStorage) {
4143
+ return;
4144
+ }
4145
+ if (!locationId) {
4146
+ window.localStorage.removeItem(LOCATION_STORAGE_KEY);
4147
+ return;
4148
+ }
4149
+ window.localStorage.setItem(LOCATION_STORAGE_KEY, locationId);
4150
+ }
4151
+ function getStoredDisplayCurrency() {
4152
+ if (typeof window === "undefined" || !window.localStorage) {
4153
+ return null;
4154
+ }
4155
+ const value = window.localStorage.getItem(DISPLAY_CURRENCY_STORAGE_KEY);
4156
+ if (!value) {
4157
+ return null;
4158
+ }
4159
+ const normalized = value.trim().toUpperCase();
4160
+ return normalized.length > 0 ? normalized : null;
4161
+ }
4162
+ function setStoredDisplayCurrency(currency) {
4163
+ if (typeof window === "undefined" || !window.localStorage) {
4164
+ return;
4165
+ }
4166
+ if (!currency) {
4167
+ window.localStorage.removeItem(DISPLAY_CURRENCY_STORAGE_KEY);
4168
+ return;
4169
+ }
4170
+ window.localStorage.setItem(DISPLAY_CURRENCY_STORAGE_KEY, currency.toUpperCase());
4171
+ }
4172
+ function resolveInitialLocation(locations) {
4173
+ if (locations.length === 0) {
4174
+ return null;
4175
+ }
4176
+ const storedId = getStoredLocationId();
4177
+ if (storedId) {
4178
+ const matched = locations.find((location) => location.id === storedId);
4179
+ if (matched) {
4180
+ return matched;
4181
+ }
4182
+ }
4183
+ return locations[0];
4184
+ }
4185
+ var CimplifyContext = createContext(null);
4186
+ function CimplifyProvider({
4187
+ client,
4188
+ children,
4189
+ onLocationChange
4190
+ }) {
4191
+ const resolvedClient = useMemo(() => client ?? createDefaultClient(), [client]);
4192
+ const onLocationChangeRef = useRef(onLocationChange);
4193
+ const [business, setBusiness] = useState(null);
4194
+ const [locations, setLocations] = useState([]);
4195
+ const [currentLocation, setCurrentLocationState] = useState(null);
4196
+ const [isReady, setIsReady] = useState(false);
4197
+ useEffect(() => {
4198
+ onLocationChangeRef.current = onLocationChange;
4199
+ }, [onLocationChange]);
4200
+ const isDemoMode = resolvedClient.getPublicKey().trim().length === 0;
4201
+ const baseCurrency = business?.default_currency || DEFAULT_CURRENCY;
4202
+ const [displayCurrencyOverride, setDisplayCurrencyOverride] = useState(
4203
+ () => getStoredDisplayCurrency()
4204
+ );
4205
+ const [fxRate, setFxRate] = useState(null);
4206
+ const displayCurrency = displayCurrencyOverride && displayCurrencyOverride !== baseCurrency ? displayCurrencyOverride : baseCurrency;
4207
+ const setDisplayCurrency = useCallback(
4208
+ (currency) => {
4209
+ const normalized = currency?.trim().toUpperCase() || null;
4210
+ if (normalized && !isSupportedCurrency(normalized)) {
4211
+ return;
4212
+ }
4213
+ setDisplayCurrencyOverride(normalized);
4214
+ setStoredDisplayCurrency(normalized);
4215
+ if (!normalized || normalized === baseCurrency) {
4216
+ setFxRate(null);
4217
+ }
4218
+ },
4219
+ [baseCurrency]
4220
+ );
4221
+ useEffect(() => {
4222
+ if (displayCurrency === baseCurrency || isDemoMode) {
4223
+ setFxRate(null);
4224
+ return;
4225
+ }
4226
+ let cancelled = false;
4227
+ async function fetchRate() {
4228
+ const result = await resolvedClient.fx.getRate(
4229
+ baseCurrency,
4230
+ displayCurrency
4231
+ );
4232
+ if (cancelled) return;
4233
+ if (result.ok) {
4234
+ setFxRate(result.value.rate);
4235
+ } else {
4236
+ setFxRate(null);
4237
+ }
4238
+ }
4239
+ void fetchRate();
4240
+ const intervalId = setInterval(() => void fetchRate(), FX_REFRESH_INTERVAL);
4241
+ return () => {
4242
+ cancelled = true;
4243
+ clearInterval(intervalId);
4244
+ };
4245
+ }, [resolvedClient, baseCurrency, displayCurrency, isDemoMode]);
4246
+ const convertPrice = useCallback(
4247
+ (amount) => {
4248
+ const num = typeof amount === "string" ? parseFloat(amount) : amount;
4249
+ if (isNaN(num)) return 0;
4250
+ if (!fxRate || displayCurrency === baseCurrency) return num;
4251
+ return Math.round(num * fxRate * 100) / 100;
4252
+ },
4253
+ [fxRate, displayCurrency, baseCurrency]
4254
+ );
4255
+ const setCurrentLocation = useCallback(
4256
+ (location) => {
4257
+ setCurrentLocationState(location);
4258
+ resolvedClient.setLocationId(location.id);
4259
+ setStoredLocationId(location.id);
4260
+ onLocationChangeRef.current?.(location);
4261
+ },
4262
+ [resolvedClient]
4263
+ );
4264
+ useEffect(() => {
4265
+ let cancelled = false;
4266
+ async function bootstrap() {
4267
+ setIsReady(false);
4268
+ if (isDemoMode) {
4269
+ if (!cancelled) {
4270
+ setBusiness(null);
4271
+ setLocations([]);
4272
+ setCurrentLocationState(null);
4273
+ resolvedClient.setLocationId(null);
4274
+ setStoredLocationId(null);
4275
+ setIsReady(true);
4276
+ }
4277
+ return;
4278
+ }
4279
+ const [businessResult, locationsResult] = await Promise.all([
4280
+ resolvedClient.business.getInfo(),
4281
+ resolvedClient.business.getLocations()
4282
+ ]);
4283
+ if (cancelled) {
4284
+ return;
4285
+ }
4286
+ const nextBusiness = businessResult.ok ? businessResult.value : null;
4287
+ const nextLocations = locationsResult.ok && Array.isArray(locationsResult.value) ? locationsResult.value : [];
4288
+ const initialLocation = resolveInitialLocation(nextLocations);
4289
+ setBusiness(nextBusiness);
4290
+ if (nextBusiness?.id) {
4291
+ resolvedClient.setBusinessId(nextBusiness.id);
4292
+ }
4293
+ setLocations(nextLocations);
4294
+ if (initialLocation) {
4295
+ setCurrentLocationState(initialLocation);
4296
+ resolvedClient.setLocationId(initialLocation.id);
4297
+ setStoredLocationId(initialLocation.id);
4298
+ } else {
4299
+ setCurrentLocationState(null);
4300
+ resolvedClient.setLocationId(null);
4301
+ setStoredLocationId(null);
4302
+ }
4303
+ setIsReady(true);
4304
+ }
4305
+ bootstrap().catch(() => {
4306
+ if (cancelled) {
4307
+ return;
4308
+ }
4309
+ setBusiness(null);
4310
+ setLocations([]);
4311
+ setCurrentLocationState(null);
4312
+ resolvedClient.setLocationId(null);
4313
+ setStoredLocationId(null);
4314
+ setIsReady(true);
4315
+ });
4316
+ return () => {
4317
+ cancelled = true;
4318
+ };
4319
+ }, [resolvedClient, isDemoMode]);
4320
+ const contextValue = useMemo(
4321
+ () => ({
4322
+ client: resolvedClient,
4323
+ business,
4324
+ currency: baseCurrency,
4325
+ country: business?.country_code || DEFAULT_COUNTRY,
4326
+ locations,
4327
+ currentLocation,
4328
+ setCurrentLocation,
4329
+ isReady,
4330
+ isDemoMode,
4331
+ baseCurrency,
4332
+ displayCurrency,
4333
+ setDisplayCurrency,
4334
+ convertPrice,
4335
+ fxRate
4336
+ }),
4337
+ [
4338
+ resolvedClient,
4339
+ business,
4340
+ baseCurrency,
4341
+ locations,
4342
+ currentLocation,
4343
+ setCurrentLocation,
4344
+ isReady,
4345
+ isDemoMode,
4346
+ displayCurrency,
4347
+ setDisplayCurrency,
4348
+ convertPrice,
4349
+ fxRate
4350
+ ]
4351
+ );
4352
+ return /* @__PURE__ */ jsx(CimplifyContext.Provider, { value: contextValue, children });
4353
+ }
4354
+ function useCimplify() {
4355
+ const context = useContext(CimplifyContext);
4356
+ if (!context) {
4357
+ throw new Error("useCimplify must be used within CimplifyProvider");
4358
+ }
4359
+ return context;
4406
4360
  }
4407
- var LOCATION_STORAGE_KEY = "cimplify_location_id";
4408
- var DISPLAY_CURRENCY_STORAGE_KEY = "cimplify_display_currency";
4409
- var FX_REFRESH_INTERVAL = 12e4;
4410
- var DEFAULT_CURRENCY = "USD";
4411
- var DEFAULT_COUNTRY = "US";
4412
- function createDefaultClient() {
4413
- const processRef = globalThis.process;
4414
- const envPublicKey = processRef?.env?.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY || "";
4415
- return createCimplifyClient({ publicKey: envPublicKey });
4361
+ function useOptionalCimplify() {
4362
+ return useContext(CimplifyContext);
4416
4363
  }
4417
- function getStoredLocationId() {
4418
- if (typeof window === "undefined" || !window.localStorage) {
4419
- return null;
4420
- }
4421
- const value = window.localStorage.getItem(LOCATION_STORAGE_KEY);
4422
- if (!value) {
4423
- return null;
4424
- }
4425
- const normalized = value.trim();
4426
- return normalized.length > 0 ? normalized : null;
4364
+ var SPACE = { sm: 8};
4365
+ function shellColors(isDark, primaryColor) {
4366
+ return {
4367
+ text: isDark ? "#f4f4f5" : "#1a1a1a",
4368
+ textSecondary: isDark ? "#a1a1aa" : "#52525b",
4369
+ textMuted: isDark ? "#71717a" : "#a1a1aa",
4370
+ border: isDark ? "#27272a" : "#e4e4e7",
4371
+ surface: isDark ? "#18181b" : "#fafafa",
4372
+ error: "#dc2626",
4373
+ primary: primaryColor
4374
+ };
4427
4375
  }
4428
- function setStoredLocationId(locationId) {
4429
- if (typeof window === "undefined" || !window.localStorage) {
4430
- return;
4376
+ function statusToLabel(status) {
4377
+ if (!status) {
4378
+ return "";
4431
4379
  }
4432
- if (!locationId) {
4433
- window.localStorage.removeItem(LOCATION_STORAGE_KEY);
4434
- return;
4380
+ if (status === "preparing") {
4381
+ return "Preparing checkout";
4435
4382
  }
4436
- window.localStorage.setItem(LOCATION_STORAGE_KEY, locationId);
4437
- }
4438
- function getStoredDisplayCurrency() {
4439
- if (typeof window === "undefined" || !window.localStorage) {
4440
- return null;
4383
+ if (status === "recovering") {
4384
+ return "Resuming payment";
4441
4385
  }
4442
- const value = window.localStorage.getItem(DISPLAY_CURRENCY_STORAGE_KEY);
4443
- if (!value) {
4444
- return null;
4386
+ if (status === "processing") {
4387
+ return "Processing payment";
4445
4388
  }
4446
- const normalized = value.trim().toUpperCase();
4447
- return normalized.length > 0 ? normalized : null;
4448
- }
4449
- function setStoredDisplayCurrency(currency) {
4450
- if (typeof window === "undefined" || !window.localStorage) {
4451
- return;
4389
+ if (status === "awaiting_authorization") {
4390
+ return "Waiting for authorization";
4452
4391
  }
4453
- if (!currency) {
4454
- window.localStorage.removeItem(DISPLAY_CURRENCY_STORAGE_KEY);
4455
- return;
4392
+ if (status === "polling") {
4393
+ return "Confirming payment";
4456
4394
  }
4457
- window.localStorage.setItem(DISPLAY_CURRENCY_STORAGE_KEY, currency.toUpperCase());
4458
- }
4459
- function resolveInitialLocation(locations) {
4460
- if (locations.length === 0) {
4461
- return null;
4395
+ if (status === "finalizing") {
4396
+ return "Finalizing order";
4462
4397
  }
4463
- const storedId = getStoredLocationId();
4464
- if (storedId) {
4465
- const matched = locations.find((location) => location.id === storedId);
4466
- if (matched) {
4467
- return matched;
4468
- }
4398
+ if (status === "success") {
4399
+ return "Payment complete";
4469
4400
  }
4470
- return locations[0];
4401
+ return "Payment failed";
4471
4402
  }
4472
- var CimplifyContext = createContext(null);
4473
- function CimplifyProvider({
4403
+ function CimplifyCheckout({
4474
4404
  client,
4475
- children,
4476
- onLocationChange
4405
+ businessId,
4406
+ cartId,
4407
+ locationId,
4408
+ linkUrl,
4409
+ orderTypes,
4410
+ enrollInLink = true,
4411
+ onComplete,
4412
+ onError,
4413
+ onStatusChange,
4414
+ appearance,
4415
+ demoMode,
4416
+ className
4477
4417
  }) {
4478
- const resolvedClient = useMemo(() => client ?? createDefaultClient(), [client]);
4479
- const onLocationChangeRef = useRef(onLocationChange);
4480
- const [business, setBusiness] = useState(null);
4481
- const [locations, setLocations] = useState([]);
4482
- const [currentLocation, setCurrentLocationState] = useState(null);
4483
- const [isReady, setIsReady] = useState(false);
4484
- useEffect(() => {
4485
- onLocationChangeRef.current = onLocationChange;
4486
- }, [onLocationChange]);
4487
- const isDemoMode = resolvedClient.getPublicKey().trim().length === 0;
4488
- const baseCurrency = business?.default_currency || DEFAULT_CURRENCY;
4489
- const [displayCurrencyOverride, setDisplayCurrencyOverride] = useState(
4490
- () => getStoredDisplayCurrency()
4418
+ const resolvedOrderTypes = useMemo(
4419
+ () => orderTypes && orderTypes.length > 0 ? orderTypes : ["pickup", "delivery"],
4420
+ [orderTypes]
4491
4421
  );
4492
- const [fxRate, setFxRate] = useState(null);
4493
- const displayCurrency = displayCurrencyOverride && displayCurrencyOverride !== baseCurrency ? displayCurrencyOverride : baseCurrency;
4494
- const setDisplayCurrency = useCallback(
4495
- (currency) => {
4496
- const normalized = currency?.trim().toUpperCase() || null;
4497
- setDisplayCurrencyOverride(normalized);
4498
- setStoredDisplayCurrency(normalized);
4499
- if (!normalized || normalized === baseCurrency) {
4500
- setFxRate(null);
4501
- }
4502
- },
4503
- [baseCurrency]
4422
+ const [orderType, setOrderType] = useState(resolvedOrderTypes[0] || "pickup");
4423
+ const [status, setStatus] = useState(null);
4424
+ const [statusText, setStatusText] = useState("");
4425
+ const [isSubmitting, setIsSubmitting] = useState(false);
4426
+ const [isInitializing, setIsInitializing] = useState(false);
4427
+ const [errorMessage, setErrorMessage] = useState(null);
4428
+ const [resolvedBusinessId, setResolvedBusinessId] = useState(businessId ?? null);
4429
+ const [resolvedCartId, setResolvedCartId] = useState(cartId ?? null);
4430
+ const [resolvedCart, setResolvedCart] = useState(null);
4431
+ const checkoutMountRef = useRef(null);
4432
+ const elementsRef = useRef(null);
4433
+ const activeCheckoutRef = useRef(null);
4434
+ const initialAppearanceRef = useRef(appearance);
4435
+ const hasWarnedInlineAppearanceRef = useRef(false);
4436
+ const isMountedRef = useRef(true);
4437
+ const demoRunRef = useRef(0);
4438
+ const isDemoCheckout = demoMode ?? client.getPublicKey().trim().length === 0;
4439
+ const isTestMode = client.isTestMode();
4440
+ const cimplifyCtx = useOptionalCimplify();
4441
+ const fxOptions = useMemo(() => {
4442
+ if (!cimplifyCtx?.fxRate) return void 0;
4443
+ if (cimplifyCtx.displayCurrency === cimplifyCtx.baseCurrency) return void 0;
4444
+ return {
4445
+ displayCurrency: cimplifyCtx.displayCurrency,
4446
+ convertPrice: cimplifyCtx.convertPrice
4447
+ };
4448
+ }, [cimplifyCtx?.fxRate, cimplifyCtx?.displayCurrency, cimplifyCtx?.baseCurrency, cimplifyCtx?.convertPrice]);
4449
+ const fxOptionsRef = useRef(fxOptions);
4450
+ fxOptionsRef.current = fxOptions;
4451
+ const resolvedCartRef = useRef(resolvedCart);
4452
+ resolvedCartRef.current = resolvedCart;
4453
+ const primaryColor = appearance?.variables?.primaryColor || "#0a2540";
4454
+ const isDark = appearance?.theme === "dark";
4455
+ const emitStatus = React3.useEffectEvent(
4456
+ (nextStatus, context = {}) => {
4457
+ setStatus(nextStatus);
4458
+ setStatusText(context.display_text || "");
4459
+ onStatusChange?.(nextStatus, context);
4460
+ }
4461
+ );
4462
+ const fireError = React3.useEffectEvent(
4463
+ (error) => {
4464
+ onError?.(error);
4465
+ }
4504
4466
  );
4505
4467
  useEffect(() => {
4506
- if (displayCurrency === baseCurrency || isDemoMode) {
4507
- setFxRate(null);
4508
- return;
4468
+ if (!resolvedOrderTypes.includes(orderType)) {
4469
+ setOrderType(resolvedOrderTypes[0] || "pickup");
4509
4470
  }
4510
- let cancelled = false;
4511
- async function fetchRate() {
4512
- const result = await resolvedClient.fx.getRate(
4513
- baseCurrency,
4514
- displayCurrency
4471
+ }, [resolvedOrderTypes, orderType]);
4472
+ useEffect(() => {
4473
+ if (appearance && appearance !== initialAppearanceRef.current && !hasWarnedInlineAppearanceRef.current) {
4474
+ hasWarnedInlineAppearanceRef.current = true;
4475
+ console.warn(
4476
+ "[Cimplify] `appearance` prop reference changed after mount. Elements keep the initial appearance to avoid iframe remount. Memoize appearance with useMemo() to remove this warning."
4515
4477
  );
4516
- if (!cancelled && result.ok) {
4517
- setFxRate(result.value.rate);
4478
+ }
4479
+ }, [appearance]);
4480
+ useEffect(() => {
4481
+ let cancelled = false;
4482
+ async function bootstrap() {
4483
+ if (isDemoCheckout) {
4484
+ if (!cancelled) {
4485
+ setResolvedBusinessId(businessId ?? null);
4486
+ setResolvedCartId(cartId ?? "cart_demo");
4487
+ setIsInitializing(false);
4488
+ setErrorMessage(null);
4489
+ }
4490
+ return;
4491
+ }
4492
+ const needsBusinessResolve = !businessId;
4493
+ const needsCartResolve = !cartId;
4494
+ if (!needsBusinessResolve && !needsCartResolve) {
4495
+ if (!cancelled) {
4496
+ setResolvedBusinessId(businessId || null);
4497
+ setResolvedCartId(cartId || null);
4498
+ setIsInitializing(false);
4499
+ setErrorMessage(null);
4500
+ }
4501
+ client.cart.get().then((cartResult) => {
4502
+ if (!cancelled && cartResult.ok && cartResult.value) {
4503
+ setResolvedCart(cartResult.value);
4504
+ }
4505
+ }).catch(() => {
4506
+ });
4507
+ return;
4508
+ }
4509
+ if (!cancelled) {
4510
+ setIsInitializing(true);
4511
+ setErrorMessage(null);
4512
+ }
4513
+ let nextBusinessId = businessId ?? null;
4514
+ if (!nextBusinessId) {
4515
+ try {
4516
+ nextBusinessId = await client.resolveBusinessId();
4517
+ } catch {
4518
+ if (!cancelled) {
4519
+ const message = "Unable to initialize checkout business context.";
4520
+ setResolvedBusinessId(null);
4521
+ setResolvedCartId(null);
4522
+ setErrorMessage(message);
4523
+ setIsInitializing(false);
4524
+ fireError({ code: "BUSINESS_ID_REQUIRED", message });
4525
+ }
4526
+ return;
4527
+ }
4528
+ }
4529
+ let nextCartId = cartId ?? null;
4530
+ if (!nextCartId) {
4531
+ const cartResult = await client.cart.get();
4532
+ if (!cartResult.ok || !cartResult.value?.id || cartResult.value.items.length === 0) {
4533
+ if (!cancelled) {
4534
+ const message = "Your cart is empty. Add items before checkout.";
4535
+ setResolvedBusinessId(nextBusinessId);
4536
+ setResolvedCartId(null);
4537
+ setErrorMessage(message);
4538
+ setIsInitializing(false);
4539
+ fireError({ code: "CART_EMPTY", message });
4540
+ }
4541
+ return;
4542
+ }
4543
+ nextCartId = cartResult.value.id;
4544
+ if (!cancelled) {
4545
+ setResolvedCart(cartResult.value);
4546
+ }
4547
+ }
4548
+ if (!cancelled) {
4549
+ setResolvedBusinessId(nextBusinessId);
4550
+ setResolvedCartId(nextCartId);
4551
+ setIsInitializing(false);
4552
+ setErrorMessage(null);
4518
4553
  }
4519
4554
  }
4520
- void fetchRate();
4521
- const intervalId = setInterval(() => void fetchRate(), FX_REFRESH_INTERVAL);
4555
+ void bootstrap();
4522
4556
  return () => {
4523
4557
  cancelled = true;
4524
- clearInterval(intervalId);
4525
4558
  };
4526
- }, [resolvedClient, baseCurrency, displayCurrency, isDemoMode]);
4527
- const convertPrice = useCallback(
4528
- (amount) => {
4529
- const num = typeof amount === "string" ? parseFloat(amount) : amount;
4530
- if (isNaN(num)) return 0;
4531
- if (!fxRate || displayCurrency === baseCurrency) return num;
4532
- return Math.round(num * fxRate * 100) / 100;
4533
- },
4534
- [fxRate, displayCurrency, baseCurrency]
4535
- );
4536
- const setCurrentLocation = useCallback(
4537
- (location) => {
4538
- setCurrentLocationState(location);
4539
- resolvedClient.setLocationId(location.id);
4540
- setStoredLocationId(location.id);
4541
- onLocationChangeRef.current?.(location);
4542
- },
4543
- [resolvedClient]
4544
- );
4559
+ }, [businessId, cartId, client, isDemoCheckout]);
4545
4560
  useEffect(() => {
4546
- let cancelled = false;
4547
- async function bootstrap() {
4548
- setIsReady(false);
4549
- if (isDemoMode) {
4550
- if (!cancelled) {
4551
- setBusiness(null);
4552
- setLocations([]);
4553
- setCurrentLocationState(null);
4554
- resolvedClient.setLocationId(null);
4555
- setStoredLocationId(null);
4556
- setIsReady(true);
4561
+ return () => {
4562
+ isMountedRef.current = false;
4563
+ demoRunRef.current += 1;
4564
+ activeCheckoutRef.current?.abort();
4565
+ activeCheckoutRef.current = null;
4566
+ };
4567
+ }, []);
4568
+ const handleSubmit = React3.useEffectEvent(async () => {
4569
+ if (isSubmitting || isInitializing || !resolvedCartId) {
4570
+ if (!resolvedCartId && !isInitializing) {
4571
+ const message = "Your cart is empty. Add items before checkout.";
4572
+ setErrorMessage(message);
4573
+ fireError({ code: "CART_EMPTY", message });
4574
+ }
4575
+ return;
4576
+ }
4577
+ setErrorMessage(null);
4578
+ setIsSubmitting(true);
4579
+ emitStatus("preparing", { display_text: statusToLabel("preparing") });
4580
+ if (isDemoCheckout) {
4581
+ const runId = demoRunRef.current + 1;
4582
+ demoRunRef.current = runId;
4583
+ const wait = async (ms) => {
4584
+ await new Promise((resolve) => setTimeout(resolve, ms));
4585
+ return isMountedRef.current && runId === demoRunRef.current;
4586
+ };
4587
+ try {
4588
+ if (!await wait(400)) return;
4589
+ emitStatus("processing", { display_text: statusToLabel("processing") });
4590
+ if (!await wait(900)) return;
4591
+ emitStatus("polling", { display_text: statusToLabel("polling") });
4592
+ if (!await wait(1200)) return;
4593
+ const result = {
4594
+ success: true,
4595
+ order: {
4596
+ id: `ord_demo_${Date.now()}`,
4597
+ order_number: `DEMO-${Math.random().toString(36).slice(2, 8).toUpperCase()}`,
4598
+ status: "confirmed",
4599
+ total: "0.00",
4600
+ currency: "USD"
4601
+ }
4602
+ };
4603
+ emitStatus("success", {
4604
+ order_id: result.order?.id,
4605
+ order_number: result.order?.order_number,
4606
+ display_text: statusToLabel("success")
4607
+ });
4608
+ onComplete(result);
4609
+ } finally {
4610
+ if (isMountedRef.current && runId === demoRunRef.current) {
4611
+ setIsSubmitting(false);
4557
4612
  }
4558
- return;
4559
4613
  }
4560
- const [businessResult, locationsResult] = await Promise.all([
4561
- resolvedClient.business.getInfo(),
4562
- resolvedClient.business.getLocations()
4563
- ]);
4564
- if (cancelled) {
4614
+ return;
4615
+ }
4616
+ if (!elementsRef.current) {
4617
+ const message = "Checkout is still initializing. Please try again.";
4618
+ setErrorMessage(message);
4619
+ fireError({ code: "CHECKOUT_NOT_READY", message });
4620
+ setIsSubmitting(false);
4621
+ return;
4622
+ }
4623
+ const checkout = elementsRef.current.processCheckout({
4624
+ cart_id: resolvedCartId,
4625
+ location_id: locationId ?? client.getLocationId() ?? void 0,
4626
+ order_type: orderType,
4627
+ enroll_in_link: enrollInLink,
4628
+ on_status_change: emitStatus,
4629
+ pay_currency: fxOptions?.displayCurrency
4630
+ });
4631
+ activeCheckoutRef.current = checkout;
4632
+ try {
4633
+ const result = await checkout;
4634
+ if (result.success) {
4635
+ onComplete(result);
4565
4636
  return;
4566
4637
  }
4567
- const nextBusiness = businessResult.ok ? businessResult.value : null;
4568
- const nextLocations = locationsResult.ok && Array.isArray(locationsResult.value) ? locationsResult.value : [];
4569
- const initialLocation = resolveInitialLocation(nextLocations);
4570
- setBusiness(nextBusiness);
4571
- if (nextBusiness?.id) {
4572
- resolvedClient.setBusinessId(nextBusiness.id);
4573
- }
4574
- setLocations(nextLocations);
4575
- if (initialLocation) {
4576
- setCurrentLocationState(initialLocation);
4577
- resolvedClient.setLocationId(initialLocation.id);
4578
- setStoredLocationId(initialLocation.id);
4579
- } else {
4580
- setCurrentLocationState(null);
4581
- resolvedClient.setLocationId(null);
4582
- setStoredLocationId(null);
4638
+ const code = result.error?.code || "CHECKOUT_FAILED";
4639
+ const message = result.error?.message || "Payment failed.";
4640
+ setErrorMessage(message);
4641
+ fireError({ code, message });
4642
+ } finally {
4643
+ if (isMountedRef.current) {
4644
+ activeCheckoutRef.current = null;
4645
+ setIsSubmitting(false);
4583
4646
  }
4584
- setIsReady(true);
4585
4647
  }
4586
- bootstrap().catch(() => {
4587
- if (cancelled) {
4588
- return;
4648
+ });
4649
+ useEffect(() => {
4650
+ if (isDemoCheckout || !resolvedBusinessId) {
4651
+ elementsRef.current = null;
4652
+ return;
4653
+ }
4654
+ const elements = client.elements(resolvedBusinessId, {
4655
+ appearance: initialAppearanceRef.current,
4656
+ linkUrl
4657
+ });
4658
+ elementsRef.current = elements;
4659
+ const checkout = elements.create("checkout", {
4660
+ orderTypes: resolvedOrderTypes,
4661
+ defaultOrderType: resolvedOrderTypes[0]
4662
+ });
4663
+ if (checkoutMountRef.current) {
4664
+ checkout.mount(checkoutMountRef.current);
4665
+ }
4666
+ checkout.on("ready", () => {
4667
+ const cart = resolvedCartRef.current;
4668
+ if (cart) {
4669
+ checkout.setCart(transformToCheckoutCart(cart, fxOptionsRef.current));
4589
4670
  }
4590
- setBusiness(null);
4591
- setLocations([]);
4592
- setCurrentLocationState(null);
4593
- resolvedClient.setLocationId(null);
4594
- setStoredLocationId(null);
4595
- setIsReady(true);
4671
+ });
4672
+ checkout.on("order_type_changed", (data) => {
4673
+ const typed = data;
4674
+ if (typed.orderType) {
4675
+ setOrderType(typed.orderType);
4676
+ }
4677
+ });
4678
+ checkout.on("request_submit", () => {
4679
+ void handleSubmit();
4596
4680
  });
4597
4681
  return () => {
4598
- cancelled = true;
4682
+ activeCheckoutRef.current?.abort();
4683
+ activeCheckoutRef.current = null;
4684
+ elements.destroy();
4685
+ elementsRef.current = null;
4599
4686
  };
4600
- }, [resolvedClient, isDemoMode]);
4601
- const contextValue = useMemo(
4602
- () => ({
4603
- client: resolvedClient,
4604
- business,
4605
- currency: baseCurrency,
4606
- country: business?.country_code || DEFAULT_COUNTRY,
4607
- locations,
4608
- currentLocation,
4609
- setCurrentLocation,
4610
- isReady,
4611
- isDemoMode,
4612
- baseCurrency,
4613
- displayCurrency,
4614
- setDisplayCurrency,
4615
- convertPrice,
4616
- fxRate
4617
- }),
4618
- [
4619
- resolvedClient,
4620
- business,
4621
- baseCurrency,
4622
- locations,
4623
- currentLocation,
4624
- setCurrentLocation,
4625
- isReady,
4626
- isDemoMode,
4627
- displayCurrency,
4628
- setDisplayCurrency,
4629
- convertPrice,
4630
- fxRate
4631
- ]
4632
- );
4633
- return /* @__PURE__ */ jsx(CimplifyContext.Provider, { value: contextValue, children });
4634
- }
4635
- function useCimplify() {
4636
- const context = useContext(CimplifyContext);
4637
- if (!context) {
4638
- throw new Error("useCimplify must be used within CimplifyProvider");
4687
+ }, [client, resolvedBusinessId, isDemoCheckout]);
4688
+ useEffect(() => {
4689
+ if (!resolvedCart || !elementsRef.current) return;
4690
+ const checkoutElement = elementsRef.current.getElement("checkout");
4691
+ if (checkoutElement) {
4692
+ checkoutElement.setCart(transformToCheckoutCart(resolvedCart, fxOptions));
4693
+ }
4694
+ }, [resolvedCart, fxOptions]);
4695
+ const colors = shellColors(isDark ?? false, primaryColor);
4696
+ if (isInitializing) {
4697
+ return /* @__PURE__ */ jsx("div", { className, "data-cimplify-checkout": "", children: /* @__PURE__ */ jsx("p", { "data-cimplify-status": "", style: { fontSize: 13, color: colors.textSecondary }, children: "Preparing checkout..." }) });
4639
4698
  }
4640
- return context;
4641
- }
4642
- function useOptionalCimplify() {
4643
- return useContext(CimplifyContext);
4699
+ if (!isDemoCheckout && (!resolvedBusinessId || !resolvedCartId)) {
4700
+ return /* @__PURE__ */ jsx("div", { className, "data-cimplify-checkout": "", children: /* @__PURE__ */ jsx("p", { "data-cimplify-error": "", style: { fontSize: 13, color: colors.error }, children: errorMessage || "Unable to initialize checkout. Please refresh and try again." }) });
4701
+ }
4702
+ return /* @__PURE__ */ jsxs("div", { className, "data-cimplify-checkout": "", children: [
4703
+ isTestMode && !isDemoCheckout && /* @__PURE__ */ jsx(
4704
+ "p",
4705
+ {
4706
+ "data-cimplify-test-mode": "",
4707
+ style: {
4708
+ marginBottom: "10px",
4709
+ fontSize: "12px",
4710
+ fontWeight: 600,
4711
+ color: "#92400e"
4712
+ },
4713
+ children: "Test mode - no real charges"
4714
+ }
4715
+ ),
4716
+ /* @__PURE__ */ jsx("div", { "data-cimplify-section": "checkout", children: /* @__PURE__ */ jsx("div", { ref: isDemoCheckout ? void 0 : checkoutMountRef }) }),
4717
+ status && /* @__PURE__ */ jsx("p", { "data-cimplify-status": "", style: { marginTop: SPACE.sm, fontSize: 13, color: colors.textSecondary }, children: statusText || statusToLabel(status) }),
4718
+ errorMessage && /* @__PURE__ */ jsx("p", { "data-cimplify-error": "", style: { marginTop: SPACE.sm, fontSize: 13, color: colors.error }, children: errorMessage })
4719
+ ] });
4644
4720
  }
4645
4721
  function Price({ amount, className, prefix }) {
4646
4722
  const { displayCurrency, convertPrice } = useCimplify();