@churnkey/react 0.2.0 → 0.3.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.
Files changed (36) hide show
  1. package/dist/{chunk-ZEYUKGAR.cjs → chunk-2VQNWZWO.cjs} +33 -23
  2. package/dist/chunk-2VQNWZWO.cjs.map +1 -0
  3. package/dist/{chunk-YSNMTOAW.cjs → chunk-42SJA62X.cjs} +9 -7
  4. package/dist/chunk-42SJA62X.cjs.map +1 -0
  5. package/dist/{chunk-EHZJAQQ5.cjs → chunk-LVUDBCQ2.cjs} +20 -2
  6. package/dist/chunk-LVUDBCQ2.cjs.map +1 -0
  7. package/dist/{chunk-SXGORJOX.js → chunk-SQ35G4LA.js} +19 -3
  8. package/dist/chunk-SQ35G4LA.js.map +1 -0
  9. package/dist/{chunk-OL5HQUE4.js → chunk-UNSSI6KX.js} +33 -23
  10. package/dist/chunk-UNSSI6KX.js.map +1 -0
  11. package/dist/{chunk-CPCWJTSG.js → chunk-YZERGV7F.js} +6 -4
  12. package/dist/chunk-YZERGV7F.js.map +1 -0
  13. package/dist/core.cjs +20 -12
  14. package/dist/core.d.cts +14 -3
  15. package/dist/core.d.ts +14 -3
  16. package/dist/core.js +2 -2
  17. package/dist/headless.cjs +3 -3
  18. package/dist/headless.d.cts +5 -1
  19. package/dist/headless.d.ts +5 -1
  20. package/dist/headless.js +2 -2
  21. package/dist/index.cjs +166 -135
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +7 -7
  24. package/dist/index.d.ts +7 -7
  25. package/dist/index.js +73 -50
  26. package/dist/index.js.map +1 -1
  27. package/dist/{step-graph-Cbi0DRHX.d.cts → step-graph-ChlSCUGk.d.cts} +39 -19
  28. package/dist/{step-graph-Cbi0DRHX.d.ts → step-graph-ChlSCUGk.d.ts} +39 -19
  29. package/dist/styles.css +50 -11
  30. package/package.json +10 -10
  31. package/dist/chunk-CPCWJTSG.js.map +0 -1
  32. package/dist/chunk-EHZJAQQ5.cjs.map +0 -1
  33. package/dist/chunk-OL5HQUE4.js.map +0 -1
  34. package/dist/chunk-SXGORJOX.js.map +0 -1
  35. package/dist/chunk-YSNMTOAW.cjs.map +0 -1
  36. package/dist/chunk-ZEYUKGAR.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { useCancelFlowMachine } from './chunk-CPCWJTSG.js';
2
- export { useCancelFlow } from './chunk-CPCWJTSG.js';
3
- import { cn, discountPhrase, formatMonthDayLong, formatPriceFromMinor, formatMonthDay, appearanceToStyle, defaultTitles } from './chunk-SXGORJOX.js';
4
- export { BUILT_IN_STEP_TYPES, appearanceToStyle, calculateDiscountedPrice, cn, defaultTitles, formatPrice } from './chunk-SXGORJOX.js';
5
- export { AnalyticsClient, CancelFlowMachine, ChurnkeyApi, decodeSessionToken } from './chunk-OL5HQUE4.js';
6
- import { useState, useRef, useCallback, useEffect } from 'react';
1
+ import { useCancelFlowMachine } from './chunk-YZERGV7F.js';
2
+ export { useCancelFlow } from './chunk-YZERGV7F.js';
3
+ import { formatPeriodEnd, cn, discountPhrase, formatMonthDayLong, formatPriceFromMinor, formatMonthDay, appearanceToStyle, defaultTitles, BUILT_IN_OFFER_TYPES } from './chunk-SQ35G4LA.js';
4
+ export { BUILT_IN_OFFER_TYPES, BUILT_IN_STEP_TYPES, appearanceToStyle, calculateDiscountedPrice, cn, defaultTitles, formatPeriodEnd, formatPrice } from './chunk-SQ35G4LA.js';
5
+ export { AnalyticsClient, CancelFlowMachine, ChurnkeyApi, decodeSessionToken } from './chunk-UNSSI6KX.js';
6
+ import { useState, useRef, useEffect, useCallback } from 'react';
7
7
  import { jsxs, jsx } from 'react/jsx-runtime';
8
8
 
9
9
  function RichText({ html, as = "p", className }) {
@@ -20,17 +20,18 @@ function RichText({ html, as = "p", className }) {
20
20
  function DefaultConfirm({
21
21
  title,
22
22
  description,
23
+ subscriptions,
23
24
  losses,
24
25
  lossesLabel,
25
26
  confirmLabel,
26
27
  goBackLabel,
27
- periodEnd,
28
28
  onConfirm,
29
29
  onGoBack,
30
30
  isProcessing,
31
31
  classNames
32
32
  }) {
33
33
  const hasLosses = Array.isArray(losses) && losses.length > 0;
34
+ const periodEnd = formatPeriodEnd(subscriptions);
34
35
  return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-confirm", classNames?.root), children: [
35
36
  /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: title }),
36
37
  description && /* @__PURE__ */ jsx(RichText, { html: description, className: cn("ck-step-description", classNames?.description) }),
@@ -251,6 +252,7 @@ function Checkmark({ color = "currentColor", size = 14 }) {
251
252
  function DefaultPlanChangeOffer({
252
253
  title,
253
254
  description,
255
+ subscriptions,
254
256
  offer,
255
257
  onAccept,
256
258
  onDecline,
@@ -259,7 +261,9 @@ function DefaultPlanChangeOffer({
259
261
  }) {
260
262
  const o = offer;
261
263
  const plans = o.plans ?? [];
262
- const [selectedPlanId, setSelectedPlanId] = useState(plans[0]?.id ?? null);
264
+ const currentPlanId = subscriptions[0]?.items[0]?.price.id;
265
+ const initialPlanId = plans.find((p) => p.id !== currentPlanId)?.id ?? null;
266
+ const [selectedPlanId, setSelectedPlanId] = useState(initialPlanId);
263
267
  const selectedPlan = plans.find((p) => p.id === selectedPlanId) ?? null;
264
268
  const headline = title ?? offer.copy.headline;
265
269
  const body = description ?? offer.copy.body;
@@ -272,15 +276,24 @@ function DefaultPlanChangeOffer({
272
276
  const interval = plan.duration?.interval ?? "month";
273
277
  const currency = plan.amount.currency ?? "USD";
274
278
  const isSelected = plan.id === selectedPlanId;
279
+ const isCurrent = plan.id === currentPlanId;
275
280
  return /* @__PURE__ */ jsxs(
276
281
  "button",
277
282
  {
278
283
  type: "button",
279
284
  onClick: () => setSelectedPlanId(plan.id),
280
- className: cn("ck-plan-card", isSelected && "ck-plan-card--selected"),
285
+ disabled: isCurrent,
286
+ className: cn(
287
+ "ck-plan-card",
288
+ isSelected && "ck-plan-card--selected",
289
+ isCurrent && "ck-plan-card--current"
290
+ ),
281
291
  "aria-pressed": isSelected,
282
292
  children: [
283
- /* @__PURE__ */ jsx("div", { className: "ck-plan-name", children: plan.name ?? plan.id }),
293
+ /* @__PURE__ */ jsxs("div", { className: "ck-plan-name", children: [
294
+ plan.name ?? plan.id,
295
+ isCurrent && /* @__PURE__ */ jsx("span", { className: "ck-plan-current-badge", children: "Current" })
296
+ ] }),
284
297
  plan.tagline && /* @__PURE__ */ jsx("div", { className: "ck-plan-tagline", children: plan.tagline }),
285
298
  /* @__PURE__ */ jsxs("div", { className: "ck-plan-price-row", children: [
286
299
  /* @__PURE__ */ jsx("span", { className: "ck-plan-amount", children: formatPriceFromMinor(plan.amount.value, currency) }),
@@ -443,7 +456,6 @@ function DefaultReasonButton({ reason, index, isSelected, onSelect }) {
443
456
  type: "button",
444
457
  role: "radio",
445
458
  "aria-checked": isSelected,
446
- tabIndex: isSelected ? 0 : -1,
447
459
  onClick: () => onSelect(reason.id),
448
460
  className: cn("ck-reason-button", isSelected && "ck-reason-button--selected"),
449
461
  children: [
@@ -459,50 +471,37 @@ function DefaultSurvey({
459
471
  reasons,
460
472
  selectedReason,
461
473
  onSelectReason,
474
+ followupResponse,
475
+ onFollowupResponseChange,
462
476
  onNext,
463
477
  classNames,
464
478
  components
465
479
  }) {
466
480
  const ReasonButton = components?.ReasonButton ?? DefaultReasonButton;
467
- const listRef = useRef(null);
468
- const handleKeyDown = useCallback(
469
- (e) => {
470
- if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
471
- e.preventDefault();
472
- const currentIdx = reasons.findIndex((r) => r.id === selectedReason);
473
- let nextIdx;
474
- if (e.key === "ArrowDown") {
475
- nextIdx = currentIdx < reasons.length - 1 ? currentIdx + 1 : 0;
476
- } else {
477
- nextIdx = currentIdx > 0 ? currentIdx - 1 : reasons.length - 1;
478
- }
479
- onSelectReason(reasons[nextIdx].id);
480
- const buttons = listRef.current?.querySelectorAll('[role="radio"]');
481
- buttons?.[nextIdx]?.focus();
482
- },
483
- [reasons, selectedReason, onSelectReason]
484
- );
481
+ const selected = reasons.find((r) => r.id === selectedReason);
482
+ const showFollowup = selected?.freeform === true;
485
483
  return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-survey", classNames?.root), children: [
486
484
  /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: title }),
487
485
  description && /* @__PURE__ */ jsx(RichText, { html: description, className: cn("ck-step-description", classNames?.description) }),
488
- /* @__PURE__ */ jsx(
489
- "div",
486
+ /* @__PURE__ */ jsx("div", { className: cn("ck-reason-list", classNames?.reasonList), role: "radiogroup", "aria-label": title, children: reasons.map((reason, i) => /* @__PURE__ */ jsx(
487
+ ReasonButton,
490
488
  {
491
- ref: listRef,
492
- className: cn("ck-reason-list", classNames?.reasonList),
493
- role: "radiogroup",
494
- "aria-label": title,
495
- onKeyDown: handleKeyDown,
496
- children: reasons.map((reason, i) => /* @__PURE__ */ jsx(
497
- ReasonButton,
498
- {
499
- reason,
500
- index: i,
501
- isSelected: selectedReason === reason.id,
502
- onSelect: onSelectReason
503
- },
504
- reason.id
505
- ))
489
+ reason,
490
+ index: i,
491
+ isSelected: selectedReason === reason.id,
492
+ onSelect: onSelectReason
493
+ },
494
+ reason.id
495
+ )) }),
496
+ showFollowup && /* @__PURE__ */ jsx(
497
+ "textarea",
498
+ {
499
+ className: cn("ck-reason-followup", classNames?.followupInput),
500
+ placeholder: "Tell us more (optional)",
501
+ rows: 3,
502
+ value: followupResponse,
503
+ onChange: (e) => onFollowupResponseChange(e.target.value),
504
+ "aria-label": "Additional detail"
506
505
  }
507
506
  ),
508
507
  /* @__PURE__ */ jsx(
@@ -563,7 +562,7 @@ function trapFocus(container) {
563
562
  container.addEventListener("keydown", handleKeyDown);
564
563
  return () => container.removeEventListener("keydown", handleKeyDown);
565
564
  }
566
- function DefaultModal({ open, onClose, children, className }) {
565
+ function DefaultModal({ open, onClose, children, className, overlayClassName }) {
567
566
  const overlayRef = useRef(null);
568
567
  const modalRef = useRef(null);
569
568
  const previousFocusRef = useRef(null);
@@ -593,7 +592,7 @@ function DefaultModal({ open, onClose, children, className }) {
593
592
  "div",
594
593
  {
595
594
  ref: overlayRef,
596
- className: "ck-overlay",
595
+ className: cn("ck-overlay", overlayClassName),
597
596
  onClick: (e) => {
598
597
  if (e.target === overlayRef.current) onClose();
599
598
  },
@@ -673,7 +672,7 @@ function LoadStatus({
673
672
  const CloseButton = components?.CloseButton ?? DefaultCloseButton;
674
673
  const handleClose = onClose ?? (() => {
675
674
  });
676
- return /* @__PURE__ */ jsx("div", { className: "ck-cancel-flow", "data-color-scheme": scheme, style: appearanceStyle, children: /* @__PURE__ */ jsxs(Modal, { open: true, onClose: handleClose, className: classNames?.modal, children: [
675
+ return /* @__PURE__ */ jsx("div", { className: "ck-cancel-flow", "data-color-scheme": scheme, style: appearanceStyle, children: /* @__PURE__ */ jsxs(Modal, { open: true, onClose: handleClose, className: classNames?.modal, overlayClassName: classNames?.overlay, children: [
677
676
  /* @__PURE__ */ jsx(CloseButton, { onClose: handleClose, className: classNames?.closeButton }),
678
677
  /* @__PURE__ */ jsxs("div", { className: "ck-content", children: [
679
678
  isLoading && /* @__PURE__ */ jsxs("div", { className: "ck-loading", style: { padding: "32px", textAlign: "center" }, children: [
@@ -725,7 +724,7 @@ function FlowShell({ machine, state, appearance, classNames, components, customC
725
724
  const Modal = components?.Modal ?? DefaultModal;
726
725
  const CloseButton = components?.CloseButton ?? DefaultCloseButton;
727
726
  const BackButton = components?.BackButton ?? DefaultBackButton;
728
- return /* @__PURE__ */ jsx("div", { className: "ck-cancel-flow", "data-color-scheme": scheme, style: appearanceStyle, children: /* @__PURE__ */ jsxs(Modal, { open: true, onClose: machine.close, className: classNames?.modal, children: [
727
+ return /* @__PURE__ */ jsx("div", { className: "ck-cancel-flow", "data-color-scheme": scheme, style: appearanceStyle, children: /* @__PURE__ */ jsxs(Modal, { open: true, onClose: machine.close, className: classNames?.modal, overlayClassName: classNames?.overlay, children: [
729
728
  /* @__PURE__ */ jsx(CloseButton, { onClose: machine.close, className: classNames?.closeButton }),
730
729
  /* @__PURE__ */ jsxs("div", { className: "ck-content", children: [
731
730
  machine.canGoBack && /* @__PURE__ */ jsx(BackButton, { onBack: machine.back, className: classNames?.backButton }),
@@ -750,9 +749,13 @@ function StepRenderer({
750
749
  {
751
750
  title: config?.title ?? defaultTitles.survey,
752
751
  description: config?.description,
752
+ customer: state.customer,
753
+ subscriptions: state.subscriptions,
753
754
  reasons: machine.reasons,
754
755
  selectedReason: state.selectedReason,
755
756
  onSelectReason: machine.selectReason,
757
+ followupResponse: state.followupResponse,
758
+ onFollowupResponseChange: machine.setFollowupResponse,
756
759
  onNext: machine.next,
757
760
  classNames: config?.classNames,
758
761
  components
@@ -769,12 +772,16 @@ function StepRenderer({
769
772
  {
770
773
  offer,
771
774
  customer: state.customer,
775
+ subscriptions: state.subscriptions,
772
776
  onAccept: machine.accept,
773
777
  onDecline: machine.decline,
774
778
  isProcessing: state.isProcessing
775
779
  }
776
780
  );
777
781
  }
782
+ if (!BUILT_IN_OFFER_TYPES.includes(offer.type)) {
783
+ return /* @__PURE__ */ jsx(UnregisteredOfferFallback, { offerType: offer.type, onSkip: machine.decline });
784
+ }
778
785
  const Offer = components?.Offer ?? DefaultOffer;
779
786
  const config = stepConfig;
780
787
  return /* @__PURE__ */ jsx(
@@ -782,6 +789,8 @@ function StepRenderer({
782
789
  {
783
790
  title: config?.title,
784
791
  description: config?.description,
792
+ customer: state.customer,
793
+ subscriptions: state.subscriptions,
785
794
  offer,
786
795
  onAccept: machine.accept,
787
796
  onDecline: machine.decline,
@@ -799,6 +808,8 @@ function StepRenderer({
799
808
  {
800
809
  title: config?.title ?? defaultTitles.feedback,
801
810
  description: config?.description,
811
+ customer: state.customer,
812
+ subscriptions: state.subscriptions,
802
813
  placeholder: config?.placeholder,
803
814
  required: config?.required ?? false,
804
815
  minLength: config?.minLength ?? 0,
@@ -817,6 +828,8 @@ function StepRenderer({
817
828
  {
818
829
  title: config?.title ?? defaultTitles.confirm,
819
830
  description: config?.description,
831
+ customer: state.customer,
832
+ subscriptions: state.subscriptions,
820
833
  losses: config?.losses,
821
834
  lossesLabel: config?.lossesLabel,
822
835
  confirmLabel: config?.confirmLabel ?? "Cancel subscription",
@@ -839,6 +852,8 @@ function StepRenderer({
839
852
  offer: machine.currentOffer ?? void 0,
840
853
  title: isSaved ? config?.savedTitle ?? "Welcome back!" : config?.cancelledTitle ?? "Subscription cancelled",
841
854
  description: isSaved ? config?.savedDescription ?? "Your offer has been applied." : config?.cancelledDescription ?? "We're sorry to see you go.",
855
+ customer: state.customer,
856
+ subscriptions: state.subscriptions,
842
857
  onClose: machine.close,
843
858
  classNames: config?.classNames
844
859
  }
@@ -860,6 +875,7 @@ function StepRenderer({
860
875
  data: config?.data
861
876
  },
862
877
  customer: state.customer,
878
+ subscriptions: state.subscriptions,
863
879
  onNext: machine.next,
864
880
  onBack: machine.back
865
881
  }
@@ -874,6 +890,13 @@ function UnregisteredStepFallback({ step, onSkip }) {
874
890
  }, [step, onSkip]);
875
891
  return null;
876
892
  }
893
+ function UnregisteredOfferFallback({ offerType, onSkip }) {
894
+ useEffect(() => {
895
+ console.warn(`[churnkey] No component registered for offer type "${offerType}". Skipping.`);
896
+ onSkip();
897
+ }, [offerType, onSkip]);
898
+ return null;
899
+ }
877
900
 
878
901
  export { CancelFlow, DefaultBackButton, DefaultCloseButton, DefaultConfirm, DefaultContactOffer, DefaultDiscountOffer, DefaultFeedback, DefaultModal, DefaultOffer, DefaultPauseOffer, DefaultPlanChangeOffer, DefaultReasonButton, DefaultRedirectOffer, DefaultSuccess, DefaultSurvey, DefaultTrialExtensionOffer };
879
902
  //# sourceMappingURL=index.js.map