@churnkey/react 0.2.0 → 0.4.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 (37) hide show
  1. package/dist/{chunk-YSNMTOAW.cjs → chunk-4BMB42WI.cjs} +9 -7
  2. package/dist/chunk-4BMB42WI.cjs.map +1 -0
  3. package/dist/{chunk-EHZJAQQ5.cjs → chunk-BDBZ5OS3.cjs} +21 -2
  4. package/dist/chunk-BDBZ5OS3.cjs.map +1 -0
  5. package/dist/{chunk-CPCWJTSG.js → chunk-F4ESJBOS.js} +6 -4
  6. package/dist/chunk-F4ESJBOS.js.map +1 -0
  7. package/dist/{chunk-SXGORJOX.js → chunk-G57YLYM5.js} +20 -3
  8. package/dist/chunk-G57YLYM5.js.map +1 -0
  9. package/dist/{chunk-OL5HQUE4.js → chunk-QYNW3HF3.js} +66 -26
  10. package/dist/chunk-QYNW3HF3.js.map +1 -0
  11. package/dist/{chunk-ZEYUKGAR.cjs → chunk-SNTKIKHU.cjs} +66 -26
  12. package/dist/chunk-SNTKIKHU.cjs.map +1 -0
  13. package/dist/core.cjs +20 -12
  14. package/dist/core.d.cts +30 -5
  15. package/dist/core.d.ts +30 -5
  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 +218 -135
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +10 -8
  24. package/dist/index.d.ts +10 -8
  25. package/dist/index.js +125 -51
  26. package/dist/index.js.map +1 -1
  27. package/dist/{step-graph-Cbi0DRHX.d.cts → step-graph-DKvaV3Dg.d.cts} +56 -21
  28. package/dist/{step-graph-Cbi0DRHX.d.ts → step-graph-DKvaV3Dg.d.ts} +56 -21
  29. package/dist/styles.css +95 -19
  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
  37. package/dist/styles.css.d.ts +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-F4ESJBOS.js';
2
+ export { useCancelFlow } from './chunk-F4ESJBOS.js';
3
+ import { formatPeriodEnd, cn, discountPhrase, formatMonthDayLong, formatPriceFromMinor, formatMonthDay, appearanceToStyle, defaultTitles, BUILT_IN_OFFER_TYPES } from './chunk-G57YLYM5.js';
4
+ export { BUILT_IN_OFFER_TYPES, BUILT_IN_STEP_TYPES, appearanceToStyle, calculateDiscountedPrice, cn, defaultTitles, formatPeriodEnd, formatPrice } from './chunk-G57YLYM5.js';
5
+ export { AnalyticsClient, CancelFlowMachine, ChurnkeyApi, decodeSessionToken } from './chunk-QYNW3HF3.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) }),
@@ -313,6 +326,55 @@ function DefaultPlanChangeOffer({
313
326
  ] })
314
327
  ] });
315
328
  }
329
+ function DefaultRebateOffer({
330
+ title,
331
+ description,
332
+ offer,
333
+ onAccept,
334
+ onDecline,
335
+ isProcessing,
336
+ classNames
337
+ }) {
338
+ const o = offer;
339
+ const headline = title ?? offer.copy.headline;
340
+ const body = description ?? offer.copy.body;
341
+ const currency = o.currency ?? "usd";
342
+ const amount = o.amountMinor ?? 0;
343
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-offer", classNames?.root), children: [
344
+ headline && /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: headline }),
345
+ body && /* @__PURE__ */ jsx(RichText, { html: body, className: cn("ck-step-description", classNames?.description) }),
346
+ /* @__PURE__ */ jsxs("div", { className: cn("ck-offer-card", classNames?.card), children: [
347
+ /* @__PURE__ */ jsxs("div", { className: "ck-offer-rebate", children: [
348
+ o.amountPaidMinor != null && /* @__PURE__ */ jsxs("div", { className: "ck-offer-rebate-row", children: [
349
+ /* @__PURE__ */ jsx("span", { children: "Subscription \xB7 this period" }),
350
+ /* @__PURE__ */ jsx("span", { children: formatPriceFromMinor(o.amountPaidMinor, currency) })
351
+ ] }),
352
+ /* @__PURE__ */ jsxs("div", { className: "ck-offer-rebate-row ck-offer-rebate-credit", children: [
353
+ /* @__PURE__ */ jsx("span", { children: "Cancellation rebate" }),
354
+ /* @__PURE__ */ jsxs("span", { children: [
355
+ "\u2212",
356
+ formatPriceFromMinor(amount, currency)
357
+ ] })
358
+ ] }),
359
+ o.netAfterRebateMinor != null && /* @__PURE__ */ jsxs("div", { className: "ck-offer-rebate-row ck-offer-rebate-total", children: [
360
+ /* @__PURE__ */ jsx("span", { children: "Due for this period" }),
361
+ /* @__PURE__ */ jsx("span", { children: formatPriceFromMinor(o.netAfterRebateMinor, currency) })
362
+ ] })
363
+ ] }),
364
+ /* @__PURE__ */ jsx(
365
+ "button",
366
+ {
367
+ type: "button",
368
+ className: cn("ck-button ck-button-primary", classNames?.acceptButton),
369
+ onClick: () => onAccept(),
370
+ disabled: isProcessing,
371
+ children: isProcessing ? "Processing..." : offer.copy.cta
372
+ }
373
+ ),
374
+ /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button-link", classNames?.declineButton), onClick: onDecline, children: offer.copy.declineCta })
375
+ ] })
376
+ ] });
377
+ }
316
378
  function DefaultRedirectOffer({ title, description, offer, onDecline, classNames }) {
317
379
  const url = offer.url;
318
380
  const headline = title ?? offer.copy.headline;
@@ -423,6 +485,8 @@ function pickOfferComponent(type, components) {
423
485
  return components?.ContactOffer ?? DefaultContactOffer;
424
486
  case "redirect":
425
487
  return components?.RedirectOffer ?? DefaultRedirectOffer;
488
+ case "rebate":
489
+ return components?.RebateOffer ?? DefaultRebateOffer;
426
490
  default:
427
491
  return null;
428
492
  }
@@ -443,7 +507,6 @@ function DefaultReasonButton({ reason, index, isSelected, onSelect }) {
443
507
  type: "button",
444
508
  role: "radio",
445
509
  "aria-checked": isSelected,
446
- tabIndex: isSelected ? 0 : -1,
447
510
  onClick: () => onSelect(reason.id),
448
511
  className: cn("ck-reason-button", isSelected && "ck-reason-button--selected"),
449
512
  children: [
@@ -459,50 +522,37 @@ function DefaultSurvey({
459
522
  reasons,
460
523
  selectedReason,
461
524
  onSelectReason,
525
+ followupResponse,
526
+ onFollowupResponseChange,
462
527
  onNext,
463
528
  classNames,
464
529
  components
465
530
  }) {
466
531
  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
- );
532
+ const selected = reasons.find((r) => r.id === selectedReason);
533
+ const showFollowup = selected?.freeform === true;
485
534
  return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-survey", classNames?.root), children: [
486
535
  /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: title }),
487
536
  description && /* @__PURE__ */ jsx(RichText, { html: description, className: cn("ck-step-description", classNames?.description) }),
488
- /* @__PURE__ */ jsx(
489
- "div",
537
+ /* @__PURE__ */ jsx("div", { className: cn("ck-reason-list", classNames?.reasonList), role: "radiogroup", "aria-label": title, children: reasons.map((reason, i) => /* @__PURE__ */ jsx(
538
+ ReasonButton,
490
539
  {
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
- ))
540
+ reason,
541
+ index: i,
542
+ isSelected: selectedReason === reason.id,
543
+ onSelect: onSelectReason
544
+ },
545
+ reason.id
546
+ )) }),
547
+ showFollowup && /* @__PURE__ */ jsx(
548
+ "textarea",
549
+ {
550
+ className: cn("ck-reason-followup", classNames?.followupInput),
551
+ placeholder: "Tell us more (optional)",
552
+ rows: 3,
553
+ value: followupResponse,
554
+ onChange: (e) => onFollowupResponseChange(e.target.value),
555
+ "aria-label": "Additional detail"
506
556
  }
507
557
  ),
508
558
  /* @__PURE__ */ jsx(
@@ -563,7 +613,7 @@ function trapFocus(container) {
563
613
  container.addEventListener("keydown", handleKeyDown);
564
614
  return () => container.removeEventListener("keydown", handleKeyDown);
565
615
  }
566
- function DefaultModal({ open, onClose, children, className }) {
616
+ function DefaultModal({ open, onClose, children, className, overlayClassName }) {
567
617
  const overlayRef = useRef(null);
568
618
  const modalRef = useRef(null);
569
619
  const previousFocusRef = useRef(null);
@@ -593,7 +643,7 @@ function DefaultModal({ open, onClose, children, className }) {
593
643
  "div",
594
644
  {
595
645
  ref: overlayRef,
596
- className: "ck-overlay",
646
+ className: cn("ck-overlay", overlayClassName),
597
647
  onClick: (e) => {
598
648
  if (e.target === overlayRef.current) onClose();
599
649
  },
@@ -673,7 +723,7 @@ function LoadStatus({
673
723
  const CloseButton = components?.CloseButton ?? DefaultCloseButton;
674
724
  const handleClose = onClose ?? (() => {
675
725
  });
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: [
726
+ 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
727
  /* @__PURE__ */ jsx(CloseButton, { onClose: handleClose, className: classNames?.closeButton }),
678
728
  /* @__PURE__ */ jsxs("div", { className: "ck-content", children: [
679
729
  isLoading && /* @__PURE__ */ jsxs("div", { className: "ck-loading", style: { padding: "32px", textAlign: "center" }, children: [
@@ -725,7 +775,7 @@ function FlowShell({ machine, state, appearance, classNames, components, customC
725
775
  const Modal = components?.Modal ?? DefaultModal;
726
776
  const CloseButton = components?.CloseButton ?? DefaultCloseButton;
727
777
  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: [
778
+ 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
779
  /* @__PURE__ */ jsx(CloseButton, { onClose: machine.close, className: classNames?.closeButton }),
730
780
  /* @__PURE__ */ jsxs("div", { className: "ck-content", children: [
731
781
  machine.canGoBack && /* @__PURE__ */ jsx(BackButton, { onBack: machine.back, className: classNames?.backButton }),
@@ -750,9 +800,13 @@ function StepRenderer({
750
800
  {
751
801
  title: config?.title ?? defaultTitles.survey,
752
802
  description: config?.description,
803
+ customer: state.customer,
804
+ subscriptions: state.subscriptions,
753
805
  reasons: machine.reasons,
754
806
  selectedReason: state.selectedReason,
755
807
  onSelectReason: machine.selectReason,
808
+ followupResponse: state.followupResponse,
809
+ onFollowupResponseChange: machine.setFollowupResponse,
756
810
  onNext: machine.next,
757
811
  classNames: config?.classNames,
758
812
  components
@@ -769,12 +823,16 @@ function StepRenderer({
769
823
  {
770
824
  offer,
771
825
  customer: state.customer,
826
+ subscriptions: state.subscriptions,
772
827
  onAccept: machine.accept,
773
828
  onDecline: machine.decline,
774
829
  isProcessing: state.isProcessing
775
830
  }
776
831
  );
777
832
  }
833
+ if (!BUILT_IN_OFFER_TYPES.includes(offer.type)) {
834
+ return /* @__PURE__ */ jsx(UnregisteredOfferFallback, { offerType: offer.type, onSkip: machine.decline });
835
+ }
778
836
  const Offer = components?.Offer ?? DefaultOffer;
779
837
  const config = stepConfig;
780
838
  return /* @__PURE__ */ jsx(
@@ -782,6 +840,8 @@ function StepRenderer({
782
840
  {
783
841
  title: config?.title,
784
842
  description: config?.description,
843
+ customer: state.customer,
844
+ subscriptions: state.subscriptions,
785
845
  offer,
786
846
  onAccept: machine.accept,
787
847
  onDecline: machine.decline,
@@ -799,6 +859,8 @@ function StepRenderer({
799
859
  {
800
860
  title: config?.title ?? defaultTitles.feedback,
801
861
  description: config?.description,
862
+ customer: state.customer,
863
+ subscriptions: state.subscriptions,
802
864
  placeholder: config?.placeholder,
803
865
  required: config?.required ?? false,
804
866
  minLength: config?.minLength ?? 0,
@@ -817,6 +879,8 @@ function StepRenderer({
817
879
  {
818
880
  title: config?.title ?? defaultTitles.confirm,
819
881
  description: config?.description,
882
+ customer: state.customer,
883
+ subscriptions: state.subscriptions,
820
884
  losses: config?.losses,
821
885
  lossesLabel: config?.lossesLabel,
822
886
  confirmLabel: config?.confirmLabel ?? "Cancel subscription",
@@ -839,6 +903,8 @@ function StepRenderer({
839
903
  offer: machine.currentOffer ?? void 0,
840
904
  title: isSaved ? config?.savedTitle ?? "Welcome back!" : config?.cancelledTitle ?? "Subscription cancelled",
841
905
  description: isSaved ? config?.savedDescription ?? "Your offer has been applied." : config?.cancelledDescription ?? "We're sorry to see you go.",
906
+ customer: state.customer,
907
+ subscriptions: state.subscriptions,
842
908
  onClose: machine.close,
843
909
  classNames: config?.classNames
844
910
  }
@@ -860,6 +926,7 @@ function StepRenderer({
860
926
  data: config?.data
861
927
  },
862
928
  customer: state.customer,
929
+ subscriptions: state.subscriptions,
863
930
  onNext: machine.next,
864
931
  onBack: machine.back
865
932
  }
@@ -874,7 +941,14 @@ function UnregisteredStepFallback({ step, onSkip }) {
874
941
  }, [step, onSkip]);
875
942
  return null;
876
943
  }
944
+ function UnregisteredOfferFallback({ offerType, onSkip }) {
945
+ useEffect(() => {
946
+ console.warn(`[churnkey] No component registered for offer type "${offerType}". Skipping.`);
947
+ onSkip();
948
+ }, [offerType, onSkip]);
949
+ return null;
950
+ }
877
951
 
878
- export { CancelFlow, DefaultBackButton, DefaultCloseButton, DefaultConfirm, DefaultContactOffer, DefaultDiscountOffer, DefaultFeedback, DefaultModal, DefaultOffer, DefaultPauseOffer, DefaultPlanChangeOffer, DefaultReasonButton, DefaultRedirectOffer, DefaultSuccess, DefaultSurvey, DefaultTrialExtensionOffer };
952
+ export { CancelFlow, DefaultBackButton, DefaultCloseButton, DefaultConfirm, DefaultContactOffer, DefaultDiscountOffer, DefaultFeedback, DefaultModal, DefaultOffer, DefaultPauseOffer, DefaultPlanChangeOffer, DefaultReasonButton, DefaultRebateOffer, DefaultRedirectOffer, DefaultSuccess, DefaultSurvey, DefaultTrialExtensionOffer };
879
953
  //# sourceMappingURL=index.js.map
880
954
  //# sourceMappingURL=index.js.map