@cimplify/sdk 0.8.1 → 0.8.2

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,4 @@
1
- import { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react';
1
+ import React2, { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react';
2
2
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
3
 
4
4
  // src/react/index.tsx
@@ -14,6 +14,7 @@ var MESSAGE_TYPES = {
14
14
  // Parent → Iframe
15
15
  INIT: "init",
16
16
  SET_TOKEN: "set_token",
17
+ SET_CART: "set_cart",
17
18
  GET_DATA: "get_data",
18
19
  PROCESS_CHECKOUT: "process_checkout",
19
20
  ABORT_CHECKOUT: "abort_checkout",
@@ -343,6 +344,61 @@ function Ad({
343
344
  }
344
345
  );
345
346
  }
347
+
348
+ // src/utils/cart-transform.ts
349
+ function getSelections(item) {
350
+ if (item.bundle_resolved?.selections?.length) {
351
+ return item.bundle_resolved.selections.map((s) => ({
352
+ name: s.product_name || s.component_id,
353
+ quantity: s.quantity,
354
+ variant_name: s.variant_name
355
+ }));
356
+ }
357
+ if (item.composite_resolved?.selections?.length) {
358
+ return item.composite_resolved.selections.map((s) => ({
359
+ name: s.component_name || s.component_id,
360
+ quantity: s.quantity
361
+ }));
362
+ }
363
+ return void 0;
364
+ }
365
+ function mapItem(item) {
366
+ const result = {
367
+ name: item.name,
368
+ quantity: item.quantity,
369
+ unit_price: String(item.base_price),
370
+ total_price: String(item.total_price),
371
+ line_type: item.line_type
372
+ };
373
+ if (item.image_url) result.image_url = item.image_url;
374
+ const variantName = item.variant_info?.name || item.variant_name || void 0;
375
+ if (variantName) result.variant_name = variantName;
376
+ if (item.scheduled_start) result.scheduled_start = item.scheduled_start;
377
+ if (item.scheduled_end) result.scheduled_end = item.scheduled_end;
378
+ const selections = getSelections(item);
379
+ if (selections) result.selections = selections;
380
+ if (item.add_on_options?.length) {
381
+ result.add_ons = item.add_on_options.map((opt) => ({
382
+ name: opt.name,
383
+ price: String(opt.price ?? "0")
384
+ }));
385
+ }
386
+ if (item.special_instructions) {
387
+ result.special_instructions = item.special_instructions;
388
+ }
389
+ return result;
390
+ }
391
+ function transformToCheckoutCart(cart) {
392
+ 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
400
+ };
401
+ }
346
402
  var SPACE = { sm: 8};
347
403
  function shellColors(isDark, primaryColor) {
348
404
  return {
@@ -409,6 +465,7 @@ function CimplifyCheckout({
409
465
  const [errorMessage, setErrorMessage] = useState(null);
410
466
  const [resolvedBusinessId, setResolvedBusinessId] = useState(businessId ?? null);
411
467
  const [resolvedCartId, setResolvedCartId] = useState(cartId ?? null);
468
+ const [resolvedCart, setResolvedCart] = useState(null);
412
469
  const checkoutMountRef = useRef(null);
413
470
  const elementsRef = useRef(null);
414
471
  const activeCheckoutRef = useRef(null);
@@ -416,25 +473,21 @@ function CimplifyCheckout({
416
473
  const hasWarnedInlineAppearanceRef = useRef(false);
417
474
  const isMountedRef = useRef(true);
418
475
  const demoRunRef = useRef(0);
419
- const onCompleteRef = useRef(onComplete);
420
- const onErrorRef = useRef(onError);
421
- const onStatusChangeRef = useRef(onStatusChange);
422
- const handleSubmitRef = useRef(async () => {
423
- });
424
- onCompleteRef.current = onComplete;
425
- onErrorRef.current = onError;
426
- onStatusChangeRef.current = onStatusChange;
427
476
  const isDemoCheckout = demoMode ?? client.getPublicKey().trim().length === 0;
428
477
  const isTestMode = client.isTestMode();
429
478
  const primaryColor = appearance?.variables?.primaryColor || "#0a2540";
430
479
  const isDark = appearance?.theme === "dark";
431
- const emitStatus = useCallback(
480
+ const emitStatus = React2.useEffectEvent(
432
481
  (nextStatus, context = {}) => {
433
482
  setStatus(nextStatus);
434
483
  setStatusText(context.display_text || "");
435
- onStatusChangeRef.current?.(nextStatus, context);
436
- },
437
- []
484
+ onStatusChange?.(nextStatus, context);
485
+ }
486
+ );
487
+ const fireError = React2.useEffectEvent(
488
+ (error) => {
489
+ onError?.(error);
490
+ }
438
491
  );
439
492
  useEffect(() => {
440
493
  if (!resolvedOrderTypes.includes(orderType)) {
@@ -470,6 +523,12 @@ function CimplifyCheckout({
470
523
  setIsInitializing(false);
471
524
  setErrorMessage(null);
472
525
  }
526
+ client.cart.get().then((cartResult) => {
527
+ if (!cancelled && cartResult.ok && cartResult.value) {
528
+ setResolvedCart(cartResult.value);
529
+ }
530
+ }).catch(() => {
531
+ });
473
532
  return;
474
533
  }
475
534
  if (!cancelled) {
@@ -487,7 +546,7 @@ function CimplifyCheckout({
487
546
  setResolvedCartId(null);
488
547
  setErrorMessage(message);
489
548
  setIsInitializing(false);
490
- onErrorRef.current?.({ code: "BUSINESS_ID_REQUIRED", message });
549
+ fireError({ code: "BUSINESS_ID_REQUIRED", message });
491
550
  }
492
551
  return;
493
552
  }
@@ -502,11 +561,14 @@ function CimplifyCheckout({
502
561
  setResolvedCartId(null);
503
562
  setErrorMessage(message);
504
563
  setIsInitializing(false);
505
- onErrorRef.current?.({ code: "CART_EMPTY", message });
564
+ fireError({ code: "CART_EMPTY", message });
506
565
  }
507
566
  return;
508
567
  }
509
568
  nextCartId = cartResult.value.id;
569
+ if (!cancelled) {
570
+ setResolvedCart(cartResult.value);
571
+ }
510
572
  }
511
573
  if (!cancelled) {
512
574
  setResolvedBusinessId(nextBusinessId);
@@ -528,45 +590,12 @@ function CimplifyCheckout({
528
590
  activeCheckoutRef.current = null;
529
591
  };
530
592
  }, []);
531
- useEffect(() => {
532
- if (isDemoCheckout || !resolvedBusinessId) {
533
- elementsRef.current = null;
534
- return;
535
- }
536
- const elements = client.elements(resolvedBusinessId, {
537
- appearance: initialAppearanceRef.current,
538
- linkUrl
539
- });
540
- elementsRef.current = elements;
541
- const checkout = elements.create("checkout", {
542
- orderTypes: resolvedOrderTypes,
543
- defaultOrderType: resolvedOrderTypes[0]
544
- });
545
- if (checkoutMountRef.current) {
546
- checkout.mount(checkoutMountRef.current);
547
- }
548
- checkout.on("order_type_changed", (data) => {
549
- const typed = data;
550
- if (typed.orderType) {
551
- setOrderType(typed.orderType);
552
- }
553
- });
554
- checkout.on("request_submit", () => {
555
- void handleSubmitRef.current();
556
- });
557
- return () => {
558
- activeCheckoutRef.current?.abort();
559
- activeCheckoutRef.current = null;
560
- elements.destroy();
561
- elementsRef.current = null;
562
- };
563
- }, [client, resolvedBusinessId, isDemoCheckout]);
564
- const handleSubmit = useCallback(async () => {
593
+ const handleSubmit = React2.useEffectEvent(async () => {
565
594
  if (isSubmitting || isInitializing || !resolvedCartId) {
566
595
  if (!resolvedCartId && !isInitializing) {
567
596
  const message = "Your cart is empty. Add items before checkout.";
568
597
  setErrorMessage(message);
569
- onErrorRef.current?.({ code: "CART_EMPTY", message });
598
+ fireError({ code: "CART_EMPTY", message });
570
599
  }
571
600
  return;
572
601
  }
@@ -601,7 +630,7 @@ function CimplifyCheckout({
601
630
  order_number: result.order?.order_number,
602
631
  display_text: statusToLabel("success")
603
632
  });
604
- onCompleteRef.current(result);
633
+ onComplete(result);
605
634
  } finally {
606
635
  if (isMountedRef.current && runId === demoRunRef.current) {
607
636
  setIsSubmitting(false);
@@ -612,7 +641,7 @@ function CimplifyCheckout({
612
641
  if (!elementsRef.current) {
613
642
  const message = "Checkout is still initializing. Please try again.";
614
643
  setErrorMessage(message);
615
- onErrorRef.current?.({ code: "CHECKOUT_NOT_READY", message });
644
+ fireError({ code: "CHECKOUT_NOT_READY", message });
616
645
  setIsSubmitting(false);
617
646
  return;
618
647
  }
@@ -627,31 +656,65 @@ function CimplifyCheckout({
627
656
  try {
628
657
  const result = await checkout;
629
658
  if (result.success) {
630
- onCompleteRef.current(result);
659
+ onComplete(result);
631
660
  return;
632
661
  }
633
662
  const code = result.error?.code || "CHECKOUT_FAILED";
634
663
  const message = result.error?.message || "Payment failed.";
635
664
  setErrorMessage(message);
636
- onErrorRef.current?.({ code, message });
665
+ fireError({ code, message });
637
666
  } finally {
638
667
  if (isMountedRef.current) {
639
668
  activeCheckoutRef.current = null;
640
669
  setIsSubmitting(false);
641
670
  }
642
671
  }
643
- }, [
644
- resolvedCartId,
645
- client,
646
- enrollInLink,
647
- emitStatus,
648
- isDemoCheckout,
649
- isInitializing,
650
- isSubmitting,
651
- locationId,
652
- orderType
653
- ]);
654
- handleSubmitRef.current = handleSubmit;
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]);
655
718
  const colors = shellColors(isDark ?? false, primaryColor);
656
719
  if (isInitializing) {
657
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..." }) });
@@ -1283,6 +1346,13 @@ var CartOperations = class {
1283
1346
  }
1284
1347
  };
1285
1348
 
1349
+ // src/constants.ts
1350
+ var MOBILE_MONEY_PROVIDER = {
1351
+ MTN: "mtn",
1352
+ VODAFONE: "vodafone",
1353
+ AIRTEL: "airtel"
1354
+ };
1355
+
1286
1356
  // src/utils/price.ts
1287
1357
  function parsePrice(value) {
1288
1358
  if (value === void 0 || value === null) {
@@ -1521,6 +1591,10 @@ async function openCardPopup(provider, checkoutResult, email, currency, signal)
1521
1591
  }
1522
1592
  return { success: false, error: "PROVIDER_UNAVAILABLE" };
1523
1593
  }
1594
+ var VALID_MOBILE_MONEY_PROVIDERS = new Set(Object.values(MOBILE_MONEY_PROVIDER));
1595
+ function isValidMobileMoneyProvider(value) {
1596
+ return VALID_MOBILE_MONEY_PROVIDERS.has(value);
1597
+ }
1524
1598
  function normalizeAuthorizationType(value) {
1525
1599
  return value === "otp" || value === "pin" ? value : void 0;
1526
1600
  }
@@ -1753,6 +1827,16 @@ var CheckoutResolver = class {
1753
1827
  }
1754
1828
  await this.wait(this.pollIntervalMs);
1755
1829
  }
1830
+ try {
1831
+ const finalResult = await this.client.checkout.pollPaymentStatus(input.orderId);
1832
+ if (finalResult.ok) {
1833
+ const normalized = normalizeStatusResponse(finalResult.value);
1834
+ if (normalized.paid || isPaymentStatusSuccess(normalized.status)) {
1835
+ return this.finalizeSuccess(latestCheckoutResult);
1836
+ }
1837
+ }
1838
+ } catch {
1839
+ }
1756
1840
  return this.fail(
1757
1841
  "PAYMENT_TIMEOUT",
1758
1842
  "Payment confirmation timed out. Please retry checkout.",
@@ -1883,7 +1967,7 @@ var CheckoutResolver = class {
1883
1967
  };
1884
1968
  }
1885
1969
  getEnrollmentMobileMoney() {
1886
- if (this.paymentData?.type === "mobile_money" && this.paymentData.phone_number && this.paymentData.provider) {
1970
+ if (this.paymentData?.type === "mobile_money" && this.paymentData.phone_number && this.paymentData.provider && isValidMobileMoneyProvider(this.paymentData.provider)) {
1887
1971
  return {
1888
1972
  phone_number: this.paymentData.phone_number,
1889
1973
  provider: this.paymentData.provider,
@@ -2957,8 +3041,10 @@ var CimplifyElements = class {
2957
3041
  false
2958
3042
  );
2959
3043
  }
3044
+ this.checkoutInProgress = true;
2960
3045
  if (!options.cart_id) {
2961
3046
  console.debug("[cimplify:checkout] BLOCKED: no cart_id");
3047
+ this.checkoutInProgress = false;
2962
3048
  return toCheckoutError(
2963
3049
  "INVALID_CART",
2964
3050
  "A valid cart is required before checkout can start.",
@@ -2967,6 +3053,7 @@ var CimplifyElements = class {
2967
3053
  }
2968
3054
  if (!options.order_type) {
2969
3055
  console.debug("[cimplify:checkout] BLOCKED: no order_type");
3056
+ this.checkoutInProgress = false;
2970
3057
  return toCheckoutError(
2971
3058
  "ORDER_TYPE_REQUIRED",
2972
3059
  "Order type is required before checkout can start.",
@@ -2976,6 +3063,7 @@ var CimplifyElements = class {
2976
3063
  const checkoutElement = this.elements.get(ELEMENT_TYPES.CHECKOUT) || this.elements.get(ELEMENT_TYPES.PAYMENT);
2977
3064
  if (!checkoutElement) {
2978
3065
  console.debug("[cimplify:checkout] BLOCKED: no checkout element");
3066
+ this.checkoutInProgress = false;
2979
3067
  return toCheckoutError(
2980
3068
  "NO_PAYMENT_ELEMENT",
2981
3069
  "Checkout element must be mounted before checkout.",
@@ -2984,6 +3072,7 @@ var CimplifyElements = class {
2984
3072
  }
2985
3073
  if (!checkoutElement.isMounted()) {
2986
3074
  console.debug("[cimplify:checkout] BLOCKED: checkout element not mounted");
3075
+ this.checkoutInProgress = false;
2987
3076
  return toCheckoutError(
2988
3077
  "PAYMENT_NOT_MOUNTED",
2989
3078
  "Checkout element must be mounted before checkout.",
@@ -2995,6 +3084,7 @@ var CimplifyElements = class {
2995
3084
  const authElement = this.elements.get(ELEMENT_TYPES.AUTH);
2996
3085
  if (authElement && !this.accessToken) {
2997
3086
  console.debug("[cimplify:checkout] BLOCKED: auth incomplete");
3087
+ this.checkoutInProgress = false;
2998
3088
  return toCheckoutError(
2999
3089
  "AUTH_INCOMPLETE",
3000
3090
  "Authentication must complete before checkout can start.",
@@ -3032,7 +3122,6 @@ var CimplifyElements = class {
3032
3122
  };
3033
3123
  const timeoutMs = options.timeout_ms ?? 18e4;
3034
3124
  const paymentWindow = checkoutElement.getContentWindow();
3035
- this.checkoutInProgress = true;
3036
3125
  return new Promise((resolve) => {
3037
3126
  let settled = false;
3038
3127
  const cleanup = () => {
@@ -3327,6 +3416,9 @@ var CimplifyElement = class {
3327
3416
  this.sendMessage({ type: MESSAGE_TYPES.GET_DATA });
3328
3417
  });
3329
3418
  }
3419
+ setCart(cart) {
3420
+ this.sendMessage({ type: MESSAGE_TYPES.SET_CART, cart });
3421
+ }
3330
3422
  sendMessage(message) {
3331
3423
  if (this.iframe?.contentWindow) {
3332
3424
  this.iframe.contentWindow.postMessage(message, this.linkUrl);
@@ -3475,6 +3567,7 @@ var ACCESS_TOKEN_STORAGE_KEY = "cimplify_access_token";
3475
3567
  var SESSION_TOKEN_STORAGE_KEY = "cimplify_session_token";
3476
3568
  var ORDER_TOKEN_PREFIX = "cimplify_ot_";
3477
3569
  var SESSION_TOKEN_HEADER = "x-session-token";
3570
+ var ORDER_TOKEN_TTL_MS = 24 * 60 * 60 * 1e3;
3478
3571
  var DEFAULT_TIMEOUT_MS = 3e4;
3479
3572
  var DEFAULT_MAX_RETRIES = 3;
3480
3573
  var DEFAULT_RETRY_DELAY_MS = 1e3;
@@ -3624,12 +3717,27 @@ var CimplifyClient = class {
3624
3717
  }
3625
3718
  setOrderToken(orderId, token) {
3626
3719
  if (typeof window !== "undefined" && window.localStorage) {
3627
- localStorage.setItem(`${ORDER_TOKEN_PREFIX}${orderId}`, token);
3720
+ try {
3721
+ const entry = JSON.stringify({ token, storedAt: Date.now() });
3722
+ localStorage.setItem(`${ORDER_TOKEN_PREFIX}${orderId}`, entry);
3723
+ } catch {
3724
+ }
3628
3725
  }
3629
3726
  }
3630
3727
  getOrderToken(orderId) {
3631
3728
  if (typeof window !== "undefined" && window.localStorage) {
3632
- return localStorage.getItem(`${ORDER_TOKEN_PREFIX}${orderId}`);
3729
+ try {
3730
+ const raw = localStorage.getItem(`${ORDER_TOKEN_PREFIX}${orderId}`);
3731
+ if (!raw) return null;
3732
+ const entry = JSON.parse(raw);
3733
+ if (Date.now() - entry.storedAt > ORDER_TOKEN_TTL_MS) {
3734
+ localStorage.removeItem(`${ORDER_TOKEN_PREFIX}${orderId}`);
3735
+ return null;
3736
+ }
3737
+ return entry.token;
3738
+ } catch {
3739
+ return null;
3740
+ }
3633
3741
  }
3634
3742
  return null;
3635
3743
  }
@@ -3705,10 +3813,13 @@ var CimplifyClient = class {
3705
3813
  }
3706
3814
  saveAccessToken(token) {
3707
3815
  if (typeof window !== "undefined" && window.localStorage) {
3708
- if (token) {
3709
- localStorage.setItem(ACCESS_TOKEN_STORAGE_KEY, token);
3710
- } else {
3711
- localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
3816
+ try {
3817
+ if (token) {
3818
+ localStorage.setItem(ACCESS_TOKEN_STORAGE_KEY, token);
3819
+ } else {
3820
+ localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY);
3821
+ }
3822
+ } catch {
3712
3823
  }
3713
3824
  }
3714
3825
  }
@@ -3781,6 +3892,19 @@ var CimplifyClient = class {
3781
3892
  });
3782
3893
  return response;
3783
3894
  }
3895
+ if (response.status === 429 && attempt < this.maxRetries) {
3896
+ retryCount++;
3897
+ const retryAfter = response.headers.get("Retry-After");
3898
+ const delay = retryAfter ? Math.min(parseInt(retryAfter, 10) * 1e3, 3e4) || this.retryDelay * Math.pow(2, attempt) : this.retryDelay * Math.pow(2, attempt);
3899
+ this.hooks.onRetry?.({
3900
+ ...context,
3901
+ attempt: retryCount,
3902
+ delayMs: delay,
3903
+ error: new Error(`Rate limited: ${response.status}`)
3904
+ });
3905
+ await sleep(delay);
3906
+ continue;
3907
+ }
3784
3908
  if (response.status >= 400 && response.status < 500) {
3785
3909
  this.hooks.onRequestError?.({
3786
3910
  ...context,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",