@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
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkZEYUKGAR_cjs = require('./chunk-ZEYUKGAR.cjs');
3
+ var chunkSNTKIKHU_cjs = require('./chunk-SNTKIKHU.cjs');
4
4
  var react = require('react');
5
5
 
6
6
  function useCancelFlowMachine(config) {
@@ -23,7 +23,7 @@ function useCancelFlowMachine(config) {
23
23
  onClose: () => cb.current.onClose?.(),
24
24
  onStepChange: (step, prevStep) => cb.current.onStepChange?.(step, prevStep)
25
25
  };
26
- return new chunkZEYUKGAR_cjs.CancelFlowMachine({ ...config, ...dispatch });
26
+ return new chunkSNTKIKHU_cjs.CancelFlowMachine({ ...config, ...dispatch });
27
27
  });
28
28
  const [state, setState] = react.useState(() => machine.getSnapshot());
29
29
  const [isLoading, setIsLoading] = react.useState(!!config.session);
@@ -37,8 +37,8 @@ function useCancelFlowMachine(config) {
37
37
  setLoadError(null);
38
38
  setIsLoading(true);
39
39
  let cancelled = false;
40
- const creds = chunkZEYUKGAR_cjs.decodeSessionToken(config.session);
41
- const api = new chunkZEYUKGAR_cjs.ChurnkeyApi(creds, config.apiBaseUrl);
40
+ const creds = chunkSNTKIKHU_cjs.decodeSessionToken(config.session);
41
+ const api = new chunkSNTKIKHU_cjs.ChurnkeyApi(creds, config.apiBaseUrl);
42
42
  api.fetchConfig().then((sdkConfig) => {
43
43
  if (cancelled) return;
44
44
  machine.initializeFromConfig(sdkConfig, api, creds);
@@ -61,17 +61,19 @@ function useCancelFlowMachine(config) {
61
61
 
62
62
  // src/headless/use-cancel-flow.ts
63
63
  function useCancelFlow(config) {
64
- const { machine, state, isLoading, loadError } = useCancelFlowMachine(config);
64
+ const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(config);
65
65
  return {
66
66
  ...state,
67
67
  isLoading,
68
68
  loadError,
69
+ retry,
69
70
  reasons: machine.reasons,
70
71
  currentStep: machine.currentStep,
71
72
  currentOffer: machine.currentOffer,
72
73
  stepIndex: machine.stepIndex,
73
74
  totalSteps: machine.totalSteps,
74
75
  selectReason: machine.selectReason,
76
+ setFollowupResponse: machine.setFollowupResponse,
75
77
  setFeedback: machine.setFeedback,
76
78
  accept: machine.accept,
77
79
  decline: machine.decline,
@@ -84,5 +86,5 @@ function useCancelFlow(config) {
84
86
 
85
87
  exports.useCancelFlow = useCancelFlow;
86
88
  exports.useCancelFlowMachine = useCancelFlowMachine;
87
- //# sourceMappingURL=chunk-YSNMTOAW.cjs.map
88
- //# sourceMappingURL=chunk-YSNMTOAW.cjs.map
89
+ //# sourceMappingURL=chunk-4BMB42WI.cjs.map
90
+ //# sourceMappingURL=chunk-4BMB42WI.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/headless/use-cancel-flow-machine.ts","../src/headless/use-cancel-flow.ts"],"names":["useRef","useState","CancelFlowMachine","useEffect","useCallback","decodeSessionToken","ChurnkeyApi"],"mappings":"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","file":"chunk-4BMB42WI.cjs","sourcesContent":["// Internal — shared by `useCancelFlow` (headless) and the `CancelFlow`\n// component. Lives under headless/ since it's the headless layer of the\n// SDK; the component imports it because there's no separate public/private\n// split worth maintaining for one shared file.\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { ChurnkeyApi } from '../core/api'\nimport { CancelFlowMachine } from '../core/machine'\nimport { decodeSessionToken } from '../core/token'\nimport type { FlowCallbacks, FlowConfig, FlowState } from '../core/types'\n\nexport interface CancelFlowMachineHandle {\n machine: CancelFlowMachine\n state: FlowState\n isLoading: boolean\n loadError: Error | null\n /** Re-fetch the flow config (token mode only). No-op without a session. */\n retry: () => void\n}\n\n/**\n * Wires a CancelFlowMachine to React lifecycle. Used internally by both the\n * `CancelFlow` component and the `useCancelFlow` hook — not part of the\n * public API.\n *\n * Callbacks reach the machine via thunks that dereference a ref updated each\n * render. This buys two things: the consumer's latest closure always runs,\n * and the fetch effect's dep list stays stable so inline-arrow handlers\n * don't trigger a re-fetch and reset the flow to step 1.\n */\nexport function useCancelFlowMachine(config: FlowConfig): CancelFlowMachineHandle {\n // FlowConfig extends FlowCallbacks, so storing the whole config gives us\n // access to every callback by name without a separate copy.\n const callbacksRef = useRef(config)\n callbacksRef.current = config\n\n const [machine] = useState(() => {\n const cb = callbacksRef\n const dispatch: FlowCallbacks = {\n handleDiscount: (o, c) => cb.current.handleDiscount?.(o, c),\n handlePause: (o, c) => cb.current.handlePause?.(o, c),\n handlePlanChange: (o, c) => cb.current.handlePlanChange?.(o, c),\n handleTrialExtension: (o, c) => cb.current.handleTrialExtension?.(o, c),\n handleCancel: (c) => cb.current.handleCancel?.(c),\n onAccept: (o, c) => cb.current.onAccept?.(o, c),\n onDiscount: (o, c) => cb.current.onDiscount?.(o, c),\n onPause: (o, c) => cb.current.onPause?.(o, c),\n onPlanChange: (o, c) => cb.current.onPlanChange?.(o, c),\n onTrialExtension: (o, c) => cb.current.onTrialExtension?.(o, c),\n onCancel: (c) => cb.current.onCancel?.(c),\n onClose: () => cb.current.onClose?.(),\n onStepChange: (step, prevStep) => cb.current.onStepChange?.(step, prevStep),\n }\n return new CancelFlowMachine({ ...config, ...dispatch })\n })\n\n const [state, setState] = useState<FlowState>(() => machine.getSnapshot())\n const [isLoading, setIsLoading] = useState(!!config.session)\n const [loadError, setLoadError] = useState<Error | null>(null)\n\n useEffect(() => {\n setState(machine.getSnapshot())\n return machine.subscribe(() => setState(machine.getSnapshot()))\n }, [machine])\n\n const loadConfig = useCallback(() => {\n if (!config.session) return\n setLoadError(null)\n setIsLoading(true)\n let cancelled = false\n const creds = decodeSessionToken(config.session)\n const api = new ChurnkeyApi(creds, config.apiBaseUrl)\n api\n .fetchConfig()\n .then((sdkConfig) => {\n if (cancelled) return\n machine.initializeFromConfig(sdkConfig, api, creds)\n setIsLoading(false)\n })\n .catch((err) => {\n if (cancelled) return\n setLoadError(err instanceof Error ? err : new Error(String(err)))\n setIsLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [config.session, config.apiBaseUrl, machine])\n\n useEffect(() => {\n if (!config.session) return\n return loadConfig()\n }, [config.session, loadConfig])\n\n return { machine, state, isLoading, loadError, retry: loadConfig }\n}\n","import type { FlowConfig } from '../core/types'\nimport { useCancelFlowMachine } from './use-cancel-flow-machine'\n\nexport function useCancelFlow(config: FlowConfig) {\n const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(config)\n\n return {\n ...state,\n isLoading,\n loadError,\n retry,\n reasons: machine.reasons,\n currentStep: machine.currentStep,\n currentOffer: machine.currentOffer,\n stepIndex: machine.stepIndex,\n totalSteps: machine.totalSteps,\n selectReason: machine.selectReason,\n setFollowupResponse: machine.setFollowupResponse,\n setFeedback: machine.setFeedback,\n accept: machine.accept,\n decline: machine.decline,\n cancel: machine.cancel,\n next: machine.next,\n back: machine.back,\n close: machine.close,\n }\n}\n"]}
@@ -58,6 +58,14 @@ function formatMonthDay(date, locale) {
58
58
  function formatMonthDayLong(date, locale) {
59
59
  return date.toLocaleDateString(locale, { month: "long", day: "numeric" });
60
60
  }
61
+ function formatPeriodEnd(subscriptions, locale) {
62
+ const status = subscriptions?.[0]?.status;
63
+ if (!status || !("currentPeriod" in status) || !status.currentPeriod?.end) return null;
64
+ const end = status.currentPeriod.end;
65
+ const d = end instanceof Date ? end : new Date(end);
66
+ if (Number.isNaN(d.getTime())) return null;
67
+ return d.toLocaleDateString(locale, { month: "long", day: "numeric", year: "numeric" });
68
+ }
61
69
  function discountPhrase(opts) {
62
70
  const { percentOff, amountOff, currency, duration, durationInMonths } = opts;
63
71
  const amount = percentOff != null ? `${percentOff}% off` : amountOff != null ? `${formatPriceFromMinor(amountOff, currency)} off` : "discount";
@@ -67,6 +75,15 @@ function discountPhrase(opts) {
67
75
 
68
76
  // src/core/utils.ts
69
77
  var BUILT_IN_STEP_TYPES = ["survey", "offer", "feedback", "confirm", "success"];
78
+ var BUILT_IN_OFFER_TYPES = [
79
+ "discount",
80
+ "pause",
81
+ "trial_extension",
82
+ "plan_change",
83
+ "contact",
84
+ "redirect",
85
+ "rebate"
86
+ ];
70
87
  function cn(...classes) {
71
88
  return classes.filter(Boolean).join(" ");
72
89
  }
@@ -127,6 +144,7 @@ var defaultTitles = {
127
144
  confirm: "Confirm cancellation"
128
145
  };
129
146
 
147
+ exports.BUILT_IN_OFFER_TYPES = BUILT_IN_OFFER_TYPES;
130
148
  exports.BUILT_IN_STEP_TYPES = BUILT_IN_STEP_TYPES;
131
149
  exports.appearanceToStyle = appearanceToStyle;
132
150
  exports.calculateDiscountedPrice = calculateDiscountedPrice;
@@ -135,7 +153,8 @@ exports.defaultTitles = defaultTitles;
135
153
  exports.discountPhrase = discountPhrase;
136
154
  exports.formatMonthDay = formatMonthDay;
137
155
  exports.formatMonthDayLong = formatMonthDayLong;
156
+ exports.formatPeriodEnd = formatPeriodEnd;
138
157
  exports.formatPrice = formatPrice;
139
158
  exports.formatPriceFromMinor = formatPriceFromMinor;
140
- //# sourceMappingURL=chunk-EHZJAQQ5.cjs.map
141
- //# sourceMappingURL=chunk-EHZJAQQ5.cjs.map
159
+ //# sourceMappingURL=chunk-BDBZ5OS3.cjs.map
160
+ //# sourceMappingURL=chunk-BDBZ5OS3.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/format.ts","../src/core/utils.ts"],"names":[],"mappings":";;;AAIA,IAAM,uBAAA,uBAAmD,GAAA,CAAI;AAAA,EAC3D,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA;AACF,CAAC,CAAA;AAID,IAAM,wBAAA,mBAAgD,IAAI,GAAA,CAAI,CAAC,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,KAAA,EAAO,KAAK,CAAC,CAAA;AAExG,SAAS,sBAAsB,QAAA,EAA2B;AAC/D,EAAA,OAAO,uBAAA,CAAwB,GAAA,CAAI,QAAA,CAAS,WAAA,EAAa,CAAA;AAC3D;AAEO,SAAS,uBAAuB,QAAA,EAA2B;AAChE,EAAA,OAAO,wBAAA,CAAyB,GAAA,CAAI,QAAA,CAAS,WAAA,EAAa,CAAA;AAC5D;AAQO,SAAS,kBAAA,CAAmB,WAAW,KAAA,EAAe;AAC3D,EAAA,IAAI,qBAAA,CAAsB,QAAQ,CAAA,EAAG,OAAO,CAAA;AAC5C,EAAA,IAAI,sBAAA,CAAuB,QAAQ,CAAA,EAAG,OAAO,GAAA;AAC7C,EAAA,OAAO,GAAA;AACT;AAMO,SAAS,qBAAA,CAAsB,KAAA,EAAe,QAAA,GAAW,KAAA,EAAe;AAC7E,EAAA,OAAO,KAAA,GAAQ,mBAAmB,QAAQ,CAAA;AAC5C;AAOO,SAAS,WAAA,CAAY,MAAA,EAAgB,QAAA,GAAW,KAAA,EAAO,MAAA,EAAyB;AACrF,EAAA,IAAI;AACF,IAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ;AAAA,MAC9C,KAAA,EAAO,UAAA;AAAA,MACP,QAAA;AAAA,MACA,eAAA,EAAiB;AAAA,KAClB,CAAA,CAAE,MAAA,CAAO,MAAM,CAAA;AAChB,IAAA,OAAO,SAAA,CAAU,OAAA,CAAQ,SAAA,EAAW,EAAE,CAAA;AAAA,EACxC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,GAAG,QAAQ,CAAA,CAAA,EAAI,MAAA,CAAO,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA;AAAA,EACzC;AACF;AAGO,SAAS,oBAAA,CAAqB,KAAA,EAAe,QAAA,GAAW,KAAA,EAAO,MAAA,EAAyB;AAC7F,EAAA,OAAO,YAAY,qBAAA,CAAsB,KAAA,EAAO,QAAQ,CAAA,EAAG,UAAU,MAAM,CAAA;AAC7E;AAEO,SAAS,wBAAA,CAAyB,eAAuB,UAAA,EAA4B;AAC1F,EAAA,OAAO,aAAA,IAAiB,IAAI,UAAA,GAAa,GAAA,CAAA;AAC3C;AAUO,SAAS,cAAA,CAAe,MAAY,MAAA,EAAyB;AAClE,EAAA,OAAO,IAAA,CAAK,mBAAmB,MAAA,EAAQ,EAAE,OAAO,OAAA,EAAS,GAAA,EAAK,WAAW,CAAA;AAC3E;AAGO,SAAS,kBAAA,CAAmB,MAAY,MAAA,EAAyB;AACtE,EAAA,OAAO,IAAA,CAAK,mBAAmB,MAAA,EAAQ,EAAE,OAAO,MAAA,EAAQ,GAAA,EAAK,WAAW,CAAA;AAC1E;AAQO,SAAS,eAAA,CACd,eACA,MAAA,EACe;AACf,EAAA,MAAM,MAAA,GAAS,aAAA,GAAgB,CAAC,CAAA,EAAG,MAAA;AACnC,EAAA,IAAI,CAAC,UAAU,EAAE,eAAA,IAAmB,WAAW,CAAC,MAAA,CAAO,aAAA,EAAe,GAAA,EAAK,OAAO,IAAA;AAClF,EAAA,MAAM,GAAA,GAAM,OAAO,aAAA,CAAc,GAAA;AACjC,EAAA,MAAM,IAAI,GAAA,YAAe,IAAA,GAAO,GAAA,GAAM,IAAI,KAAK,GAAG,CAAA;AAClD,EAAA,IAAI,OAAO,KAAA,CAAM,CAAA,CAAE,OAAA,EAAS,GAAG,OAAO,IAAA;AACtC,EAAA,OAAO,CAAA,CAAE,kBAAA,CAAmB,MAAA,EAAQ,EAAE,KAAA,EAAO,QAAQ,GAAA,EAAK,SAAA,EAAW,IAAA,EAAM,SAAA,EAAW,CAAA;AACxF;AAWO,SAAS,eAAe,IAAA,EAOpB;AACT,EAAA,MAAM,EAAE,UAAA,EAAY,SAAA,EAAW,QAAA,EAAU,QAAA,EAAU,kBAAiB,GAAI,IAAA;AACxE,EAAA,MAAM,MAAA,GACJ,UAAA,IAAc,IAAA,GACV,CAAA,EAAG,UAAU,CAAA,KAAA,CAAA,GACb,SAAA,IAAa,IAAA,GACX,CAAA,EAAG,oBAAA,CAAqB,SAAA,EAAW,QAAQ,CAAC,CAAA,IAAA,CAAA,GAC5C,UAAA;AACR,EAAA,MAAM,IAAA,GACJ,QAAA,KAAa,MAAA,GACT,mBAAA,GACA,aAAa,SAAA,GACX,UAAA,GACA,gBAAA,GACE,CAAA,IAAA,EAAO,gBAAgB,CAAA,CAAA,EAAI,gBAAA,KAAqB,CAAA,GAAI,OAAA,GAAU,QAAQ,CAAA,CAAA,GACtE,EAAA;AACV,EAAA,OAAO,IAAA,GAAO,CAAA,EAAG,MAAM,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA,GAAK,MAAA;AACtC;;;ACnJO,IAAM,sBAAyC,CAAC,QAAA,EAAU,OAAA,EAAS,UAAA,EAAY,WAAW,SAAS;AACnG,IAAM,oBAAA,GAA0C;AAAA,EACrD,UAAA;AAAA,EACA,OAAA;AAAA,EACA,iBAAA;AAAA,EACA,aAAA;AAAA,EACA,SAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF;AAEO,SAAS,MAAM,OAAA,EAAwD;AAC5E,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,OAAO,CAAA,CAAE,KAAK,GAAG,CAAA;AACzC;AAKA,IAAM,OAAA,GAAqD;AAAA;AAAA,EAEzD,eAAA,EAAiB,iBAAA;AAAA,EACjB,oBAAA,EAAsB,cAAA;AAAA,EACtB,0BAAA,EAA4B,mBAAA;AAAA;AAAA,EAG5B,mBAAA,EAAqB,aAAA;AAAA,EACrB,0BAAA,EAA4B,mBAAA;AAAA;AAAA,EAG5B,iBAAA,EAAmB,WAAA;AAAA,EACnB,2BAAA,EAA6B,oBAAA;AAAA,EAC7B,uBAAA,EAAyB,gBAAA;AAAA;AAAA,EAGzB,oBAAA,EAAsB,cAAA;AAAA,EACtB,0BAAA,EAA4B,mBAAA;AAAA,EAC5B,yBAAA,EAA2B,kBAAA;AAAA;AAAA,EAG3B,oBAAA,EAAsB,cAAA;AAAA,EACtB,yBAAA,EAA2B,kBAAA;AAAA,EAC3B,mBAAA,EAAqB,aAAA;AAAA,EACrB,yBAAA,EAA2B,kBAAA;AAAA,EAC3B,wBAAA,EAA0B,iBAAA;AAAA;AAAA,EAG1B,kBAAA,EAAoB,YAAA;AAAA,EACpB,gBAAA,EAAkB,gBAAA;AAAA,EAClB,mBAAA,EAAqB,mBAAA;AAAA,EACrB,gBAAA,EAAkB,UAAA;AAAA,EAClB,wBAAA,EAA0B,mBAAA;AAAA,EAC1B,gCAAA,EAAkC,sBAAA;AAAA;AAAA,EAGlC,oBAAA,EAAsB,cAAA;AAAA,EACtB,gBAAA,EAAkB,UAAA;AAAA,EAClB,gBAAA,EAAkB,UAAA;AAAA,EAClB,gBAAA,EAAkB,UAAA;AAAA,EAClB,gBAAA,EAAkB,UAAA;AAAA;AAAA,EAGlB,mBAAA,EAAqB,aAAA;AAAA,EACrB,kBAAA,EAAoB,YAAA;AAAA;AAAA,EAGpB,oBAAA,EAAsB;AACxB,CAAA;AAEO,SAAS,kBAAkB,UAAA,EAAoD;AACpF,EAAA,MAAM,YAAY,UAAA,EAAY,SAAA;AAC9B,EAAA,IAAI,CAAC,WAAW,OAAO,MAAA;AAEvB,EAAA,MAAM,QAAgC,EAAC;AACvC,EAAA,KAAA,MAAW,CAAC,OAAA,EAAS,MAAM,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AACvD,IAAA,MAAM,KAAA,GAAQ,UAAU,MAAM,CAAA;AAC9B,IAAA,IAAI,KAAA,EAAO,KAAA,CAAM,OAAO,CAAA,GAAI,KAAA;AAAA,EAC9B;AAEA,EAAA,OAAO,OAAO,IAAA,CAAK,KAAK,CAAA,CAAE,MAAA,GAAS,IAAK,KAAA,GAA0B,MAAA;AACpE;AAKO,IAAM,aAAA,GAAgB;AAAA,EAC3B,MAAA,EAAQ,yBAAA;AAAA,EACR,QAAA,EAAU,qBAAA;AAAA,EACV,OAAA,EAAS;AACX","file":"chunk-BDBZ5OS3.cjs","sourcesContent":["import type { DirectSubscription } from './types'\n\n// Currencies whose smallest unit equals one major unit (no fractional part).\n// Stripe and most billing providers store these without an implicit /100.\nconst ZERO_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n])\n\n// Middle-Eastern currencies that use three fractional digits (1000 divisor).\n// Without this bucket, JOD/KWD amounts would be off by 10x in display.\nconst THREE_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'])\n\nexport function isZeroDecimalCurrency(currency: string): boolean {\n return ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase())\n}\n\nexport function isThreeDecimalCurrency(currency: string): boolean {\n return THREE_DECIMAL_CURRENCIES.has(currency.toUpperCase())\n}\n\n/**\n * Smallest-unit divisor for a currency. 1 for zero-decimal (JPY), 1000 for\n * three-decimal (KWD), 100 for everything else. Used to convert provider\n * amounts (which are always in the smallest unit) into the major unit for\n * display.\n */\nexport function getCurrencyDivisor(currency = 'USD'): number {\n if (isZeroDecimalCurrency(currency)) return 1\n if (isThreeDecimalCurrency(currency)) return 1000\n return 100\n}\n\n/**\n * Converts a Direct.Price.amount.value (smallest currency unit) into the\n * major unit used for display.\n */\nexport function convertFromMinorUnits(value: number, currency = 'USD'): number {\n return value / getCurrencyDivisor(currency)\n}\n\n/**\n * Format an amount already in the major unit. Uses `narrowSymbol` so output\n * reads \"$29\" rather than \"US$29\", and trims trailing zero fractions so\n * whole amounts render compactly.\n */\nexport function formatPrice(amount: number, currency = 'USD', locale?: string): string {\n try {\n const formatted = new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n }).format(amount)\n return formatted.replace(/[.,]00$/, '')\n } catch {\n return `${currency} ${amount.toFixed(2)}`\n }\n}\n\n/** Convenience: format a Direct-shape minor-unit amount in one call. */\nexport function formatPriceFromMinor(value: number, currency = 'USD', locale?: string): string {\n return formatPrice(convertFromMinorUnits(value, currency), currency, locale)\n}\n\nexport function calculateDiscountedPrice(originalPrice: number, percentOff: number): number {\n return originalPrice * (1 - percentOff / 100)\n}\n\n// ─── Dates ────────────────────────────────────────────────────────────────\n\n/** \"Apr 30, 2026\" — used for resume dates and other full-date contexts. */\nexport function formatShortDate(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })\n}\n\n/** \"Apr 30\" — used when the year is implied by context. */\nexport function formatMonthDay(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric' })\n}\n\n/** \"April 30\" — long month form for prominent date displays. */\nexport function formatMonthDayLong(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'long', day: 'numeric' })\n}\n\n/**\n * Long-form access period end (\"June 14, 2026\") for the first subscription's\n * current period. Returns null for canceled subscriptions, missing periods,\n * and unparseable dates so Confirm can drop the \"access continues until\"\n * notice cleanly instead of rendering \"Invalid Date\".\n */\nexport function formatPeriodEnd(\n subscriptions: DirectSubscription[] | null | undefined,\n locale?: string,\n): string | null {\n const status = subscriptions?.[0]?.status\n if (!status || !('currentPeriod' in status) || !status.currentPeriod?.end) return null\n const end = status.currentPeriod.end\n const d = end instanceof Date ? end : new Date(end)\n if (Number.isNaN(d.getTime())) return null\n return d.toLocaleDateString(locale, { month: 'long', day: 'numeric', year: 'numeric' })\n}\n\n// ─── Discount phrasing ─────────────────────────────────────────────────────\n\n/**\n * Renders the discount as a single human-readable sentence:\n * \"25% off for 3 months\" / \"$5 off your next renewal\" / \"20% off for life\"\n *\n * Mirrors the embed's couponToString helper so SDK and embed use the same\n * phrasing for the same offer shape.\n */\nexport function discountPhrase(opts: {\n percentOff?: number\n /** Smallest currency unit (cents for USD). */\n amountOff?: number\n currency?: string\n duration?: 'once' | 'repeating' | 'forever'\n durationInMonths?: number\n}): string {\n const { percentOff, amountOff, currency, duration, durationInMonths } = opts\n const amount =\n percentOff != null\n ? `${percentOff}% off`\n : amountOff != null\n ? `${formatPriceFromMinor(amountOff, currency)} off`\n : 'discount'\n const tail =\n duration === 'once'\n ? 'your next renewal'\n : duration === 'forever'\n ? 'for life'\n : durationInMonths\n ? `for ${durationInMonths} ${durationInMonths === 1 ? 'month' : 'months'}`\n : ''\n return tail ? `${amount} ${tail}` : amount\n}\n","import type { CSSProperties } from 'react'\nimport type { Appearance, AppearanceVariables } from './types'\n\nexport const BUILT_IN_STEP_TYPES: readonly string[] = ['survey', 'offer', 'feedback', 'confirm', 'success']\nexport const BUILT_IN_OFFER_TYPES: readonly string[] = [\n 'discount',\n 'pause',\n 'trial_extension',\n 'plan_change',\n 'contact',\n 'redirect',\n 'rebate',\n]\n\nexport function cn(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(' ')\n}\n\n// Maps the typed `appearance.variables` field names to the underlying\n// CSS custom properties. Light/dark variants live in CSS — these\n// overrides apply on top of whichever scheme is active.\nconst VAR_MAP: Record<string, keyof AppearanceVariables> = {\n // Surfaces\n '--ck-color-bg': 'colorBackground',\n '--ck-color-surface': 'colorSurface',\n '--ck-color-surface-muted': 'colorSurfaceMuted',\n\n // Borders\n '--ck-color-border': 'colorBorder',\n '--ck-color-border-strong': 'colorBorderStrong',\n\n // Text\n '--ck-color-text': 'colorText',\n '--ck-color-text-secondary': 'colorTextSecondary',\n '--ck-color-text-muted': 'colorTextMuted',\n\n // Primary\n '--ck-color-primary': 'colorPrimary',\n '--ck-color-primary-hover': 'colorPrimaryHover',\n '--ck-color-primary-soft': 'colorPrimarySoft',\n\n // Semantic\n '--ck-color-success': 'colorSuccess',\n '--ck-color-success-soft': 'colorSuccessSoft',\n '--ck-color-danger': 'colorDanger',\n '--ck-color-danger-hover': 'colorDangerHover',\n '--ck-color-danger-soft': 'colorDangerSoft',\n\n // Typography\n '--ck-font-family': 'fontFamily',\n '--ck-font-mono': 'fontFamilyMono',\n '--ck-font-display': 'fontFamilyDisplay',\n '--ck-font-size': 'fontSize',\n '--ck-step-title-weight': 'fontWeightDisplay',\n '--ck-step-title-letter-spacing': 'letterSpacingDisplay',\n\n // Geometry\n '--ck-border-radius': 'borderRadius',\n '--ck-radius-sm': 'radiusSm',\n '--ck-radius-md': 'radiusMd',\n '--ck-radius-lg': 'radiusLg',\n '--ck-radius-xl': 'radiusXl',\n\n // Elevation\n '--ck-shadow-modal': 'shadowModal',\n '--ck-shadow-card': 'shadowCard',\n\n // Overlay\n '--ck-overlay-color': 'overlayColor',\n}\n\nexport function appearanceToStyle(appearance?: Appearance): CSSProperties | undefined {\n const variables = appearance?.variables\n if (!variables) return undefined\n\n const style: Record<string, string> = {}\n for (const [cssProp, varKey] of Object.entries(VAR_MAP)) {\n const value = variables[varKey]\n if (value) style[cssProp] = value\n }\n\n return Object.keys(style).length > 0 ? (style as CSSProperties) : undefined\n}\n\n// Step-level fallback titles when neither token-mode config nor local\n// step config provide one. Offer and Success are absent — Offer falls back\n// to `offer.copy.headline`, Success branches on outcome.\nexport const defaultTitles = {\n survey: 'Why are you cancelling?',\n feedback: 'Any other feedback?',\n confirm: 'Confirm cancellation',\n} as const\n"]}
@@ -1,4 +1,4 @@
1
- import { CancelFlowMachine, decodeSessionToken, ChurnkeyApi } from './chunk-OL5HQUE4.js';
1
+ import { CancelFlowMachine, decodeSessionToken, ChurnkeyApi } from './chunk-QYNW3HF3.js';
2
2
  import { useRef, useState, useEffect, useCallback } from 'react';
3
3
 
4
4
  function useCancelFlowMachine(config) {
@@ -59,17 +59,19 @@ function useCancelFlowMachine(config) {
59
59
 
60
60
  // src/headless/use-cancel-flow.ts
61
61
  function useCancelFlow(config) {
62
- const { machine, state, isLoading, loadError } = useCancelFlowMachine(config);
62
+ const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(config);
63
63
  return {
64
64
  ...state,
65
65
  isLoading,
66
66
  loadError,
67
+ retry,
67
68
  reasons: machine.reasons,
68
69
  currentStep: machine.currentStep,
69
70
  currentOffer: machine.currentOffer,
70
71
  stepIndex: machine.stepIndex,
71
72
  totalSteps: machine.totalSteps,
72
73
  selectReason: machine.selectReason,
74
+ setFollowupResponse: machine.setFollowupResponse,
73
75
  setFeedback: machine.setFeedback,
74
76
  accept: machine.accept,
75
77
  decline: machine.decline,
@@ -81,5 +83,5 @@ function useCancelFlow(config) {
81
83
  }
82
84
 
83
85
  export { useCancelFlow, useCancelFlowMachine };
84
- //# sourceMappingURL=chunk-CPCWJTSG.js.map
85
- //# sourceMappingURL=chunk-CPCWJTSG.js.map
86
+ //# sourceMappingURL=chunk-F4ESJBOS.js.map
87
+ //# sourceMappingURL=chunk-F4ESJBOS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/headless/use-cancel-flow-machine.ts","../src/headless/use-cancel-flow.ts"],"names":[],"mappings":";;;AA8BO,SAAS,qBAAqB,MAAA,EAA6C;AAGhF,EAAA,MAAM,YAAA,GAAe,OAAO,MAAM,CAAA;AAClC,EAAA,YAAA,CAAa,OAAA,GAAU,MAAA;AAEvB,EAAA,MAAM,CAAC,OAAO,CAAA,GAAI,QAAA,CAAS,MAAM;AAC/B,IAAA,MAAM,EAAA,GAAK,YAAA;AACX,IAAA,MAAM,QAAA,GAA0B;AAAA,MAC9B,cAAA,EAAgB,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,cAAA,GAAiB,GAAG,CAAC,CAAA;AAAA,MAC1D,WAAA,EAAa,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,WAAA,GAAc,GAAG,CAAC,CAAA;AAAA,MACpD,gBAAA,EAAkB,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,gBAAA,GAAmB,GAAG,CAAC,CAAA;AAAA,MAC9D,oBAAA,EAAsB,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,oBAAA,GAAuB,GAAG,CAAC,CAAA;AAAA,MACtE,cAAc,CAAC,CAAA,KAAM,EAAA,CAAG,OAAA,CAAQ,eAAe,CAAC,CAAA;AAAA,MAChD,QAAA,EAAU,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,QAAA,GAAW,GAAG,CAAC,CAAA;AAAA,MAC9C,UAAA,EAAY,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,UAAA,GAAa,GAAG,CAAC,CAAA;AAAA,MAClD,OAAA,EAAS,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,OAAA,GAAU,GAAG,CAAC,CAAA;AAAA,MAC5C,YAAA,EAAc,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,YAAA,GAAe,GAAG,CAAC,CAAA;AAAA,MACtD,gBAAA,EAAkB,CAAC,CAAA,EAAG,CAAA,KAAM,GAAG,OAAA,CAAQ,gBAAA,GAAmB,GAAG,CAAC,CAAA;AAAA,MAC9D,UAAU,CAAC,CAAA,KAAM,EAAA,CAAG,OAAA,CAAQ,WAAW,CAAC,CAAA;AAAA,MACxC,OAAA,EAAS,MAAM,EAAA,CAAG,OAAA,CAAQ,OAAA,IAAU;AAAA,MACpC,YAAA,EAAc,CAAC,IAAA,EAAM,QAAA,KAAa,GAAG,OAAA,CAAQ,YAAA,GAAe,MAAM,QAAQ;AAAA,KAC5E;AACA,IAAA,OAAO,IAAI,iBAAA,CAAkB,EAAE,GAAG,MAAA,EAAQ,GAAG,UAAU,CAAA;AAAA,EACzD,CAAC,CAAA;AAED,EAAA,MAAM,CAAC,OAAO,QAAQ,CAAA,GAAI,SAAoB,MAAM,OAAA,CAAQ,aAAa,CAAA;AACzE,EAAA,MAAM,CAAC,WAAW,YAAY,CAAA,GAAI,SAAS,CAAC,CAAC,OAAO,OAAO,CAAA;AAC3D,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAI,SAAuB,IAAI,CAAA;AAE7D,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,QAAA,CAAS,OAAA,CAAQ,aAAa,CAAA;AAC9B,IAAA,OAAO,QAAQ,SAAA,CAAU,MAAM,SAAS,OAAA,CAAQ,WAAA,EAAa,CAAC,CAAA;AAAA,EAChE,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,MAAM,UAAA,GAAa,YAAY,MAAM;AACnC,IAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACrB,IAAA,YAAA,CAAa,IAAI,CAAA;AACjB,IAAA,YAAA,CAAa,IAAI,CAAA;AACjB,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,MAAM,KAAA,GAAQ,kBAAA,CAAmB,MAAA,CAAO,OAAO,CAAA;AAC/C,IAAA,MAAM,GAAA,GAAM,IAAI,WAAA,CAAY,KAAA,EAAO,OAAO,UAAU,CAAA;AACpD,IAAA,GAAA,CACG,WAAA,EAAY,CACZ,IAAA,CAAK,CAAC,SAAA,KAAc;AACnB,MAAA,IAAI,SAAA,EAAW;AACf,MAAA,OAAA,CAAQ,oBAAA,CAAqB,SAAA,EAAW,GAAA,EAAK,KAAK,CAAA;AAClD,MAAA,YAAA,CAAa,KAAK,CAAA;AAAA,IACpB,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,GAAA,KAAQ;AACd,MAAA,IAAI,SAAA,EAAW;AACf,MAAA,YAAA,CAAa,GAAA,YAAe,QAAQ,GAAA,GAAM,IAAI,MAAM,MAAA,CAAO,GAAG,CAAC,CAAC,CAAA;AAChE,MAAA,YAAA,CAAa,KAAK,CAAA;AAAA,IACpB,CAAC,CAAA;AACH,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EACF,GAAG,CAAC,MAAA,CAAO,SAAS,MAAA,CAAO,UAAA,EAAY,OAAO,CAAC,CAAA;AAE/C,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACrB,IAAA,OAAO,UAAA,EAAW;AAAA,EACpB,CAAA,EAAG,CAAC,MAAA,CAAO,OAAA,EAAS,UAAU,CAAC,CAAA;AAE/B,EAAA,OAAO,EAAE,OAAA,EAAS,KAAA,EAAO,SAAA,EAAW,SAAA,EAAW,OAAO,UAAA,EAAW;AACnE;;;AC5FO,SAAS,cAAc,MAAA,EAAoB;AAChD,EAAA,MAAM,EAAE,SAAS,KAAA,EAAO,SAAA,EAAW,WAAW,KAAA,EAAM,GAAI,qBAAqB,MAAM,CAAA;AAEnF,EAAA,OAAO;AAAA,IACL,GAAG,KAAA;AAAA,IACH,SAAA;AAAA,IACA,SAAA;AAAA,IACA,KAAA;AAAA,IACA,SAAS,OAAA,CAAQ,OAAA;AAAA,IACjB,aAAa,OAAA,CAAQ,WAAA;AAAA,IACrB,cAAc,OAAA,CAAQ,YAAA;AAAA,IACtB,WAAW,OAAA,CAAQ,SAAA;AAAA,IACnB,YAAY,OAAA,CAAQ,UAAA;AAAA,IACpB,cAAc,OAAA,CAAQ,YAAA;AAAA,IACtB,qBAAqB,OAAA,CAAQ,mBAAA;AAAA,IAC7B,aAAa,OAAA,CAAQ,WAAA;AAAA,IACrB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,SAAS,OAAA,CAAQ,OAAA;AAAA,IACjB,QAAQ,OAAA,CAAQ,MAAA;AAAA,IAChB,MAAM,OAAA,CAAQ,IAAA;AAAA,IACd,MAAM,OAAA,CAAQ,IAAA;AAAA,IACd,OAAO,OAAA,CAAQ;AAAA,GACjB;AACF","file":"chunk-F4ESJBOS.js","sourcesContent":["// Internal — shared by `useCancelFlow` (headless) and the `CancelFlow`\n// component. Lives under headless/ since it's the headless layer of the\n// SDK; the component imports it because there's no separate public/private\n// split worth maintaining for one shared file.\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { ChurnkeyApi } from '../core/api'\nimport { CancelFlowMachine } from '../core/machine'\nimport { decodeSessionToken } from '../core/token'\nimport type { FlowCallbacks, FlowConfig, FlowState } from '../core/types'\n\nexport interface CancelFlowMachineHandle {\n machine: CancelFlowMachine\n state: FlowState\n isLoading: boolean\n loadError: Error | null\n /** Re-fetch the flow config (token mode only). No-op without a session. */\n retry: () => void\n}\n\n/**\n * Wires a CancelFlowMachine to React lifecycle. Used internally by both the\n * `CancelFlow` component and the `useCancelFlow` hook — not part of the\n * public API.\n *\n * Callbacks reach the machine via thunks that dereference a ref updated each\n * render. This buys two things: the consumer's latest closure always runs,\n * and the fetch effect's dep list stays stable so inline-arrow handlers\n * don't trigger a re-fetch and reset the flow to step 1.\n */\nexport function useCancelFlowMachine(config: FlowConfig): CancelFlowMachineHandle {\n // FlowConfig extends FlowCallbacks, so storing the whole config gives us\n // access to every callback by name without a separate copy.\n const callbacksRef = useRef(config)\n callbacksRef.current = config\n\n const [machine] = useState(() => {\n const cb = callbacksRef\n const dispatch: FlowCallbacks = {\n handleDiscount: (o, c) => cb.current.handleDiscount?.(o, c),\n handlePause: (o, c) => cb.current.handlePause?.(o, c),\n handlePlanChange: (o, c) => cb.current.handlePlanChange?.(o, c),\n handleTrialExtension: (o, c) => cb.current.handleTrialExtension?.(o, c),\n handleCancel: (c) => cb.current.handleCancel?.(c),\n onAccept: (o, c) => cb.current.onAccept?.(o, c),\n onDiscount: (o, c) => cb.current.onDiscount?.(o, c),\n onPause: (o, c) => cb.current.onPause?.(o, c),\n onPlanChange: (o, c) => cb.current.onPlanChange?.(o, c),\n onTrialExtension: (o, c) => cb.current.onTrialExtension?.(o, c),\n onCancel: (c) => cb.current.onCancel?.(c),\n onClose: () => cb.current.onClose?.(),\n onStepChange: (step, prevStep) => cb.current.onStepChange?.(step, prevStep),\n }\n return new CancelFlowMachine({ ...config, ...dispatch })\n })\n\n const [state, setState] = useState<FlowState>(() => machine.getSnapshot())\n const [isLoading, setIsLoading] = useState(!!config.session)\n const [loadError, setLoadError] = useState<Error | null>(null)\n\n useEffect(() => {\n setState(machine.getSnapshot())\n return machine.subscribe(() => setState(machine.getSnapshot()))\n }, [machine])\n\n const loadConfig = useCallback(() => {\n if (!config.session) return\n setLoadError(null)\n setIsLoading(true)\n let cancelled = false\n const creds = decodeSessionToken(config.session)\n const api = new ChurnkeyApi(creds, config.apiBaseUrl)\n api\n .fetchConfig()\n .then((sdkConfig) => {\n if (cancelled) return\n machine.initializeFromConfig(sdkConfig, api, creds)\n setIsLoading(false)\n })\n .catch((err) => {\n if (cancelled) return\n setLoadError(err instanceof Error ? err : new Error(String(err)))\n setIsLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [config.session, config.apiBaseUrl, machine])\n\n useEffect(() => {\n if (!config.session) return\n return loadConfig()\n }, [config.session, loadConfig])\n\n return { machine, state, isLoading, loadError, retry: loadConfig }\n}\n","import type { FlowConfig } from '../core/types'\nimport { useCancelFlowMachine } from './use-cancel-flow-machine'\n\nexport function useCancelFlow(config: FlowConfig) {\n const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(config)\n\n return {\n ...state,\n isLoading,\n loadError,\n retry,\n reasons: machine.reasons,\n currentStep: machine.currentStep,\n currentOffer: machine.currentOffer,\n stepIndex: machine.stepIndex,\n totalSteps: machine.totalSteps,\n selectReason: machine.selectReason,\n setFollowupResponse: machine.setFollowupResponse,\n setFeedback: machine.setFeedback,\n accept: machine.accept,\n decline: machine.decline,\n cancel: machine.cancel,\n next: machine.next,\n back: machine.back,\n close: machine.close,\n }\n}\n"]}
@@ -56,6 +56,14 @@ function formatMonthDay(date, locale) {
56
56
  function formatMonthDayLong(date, locale) {
57
57
  return date.toLocaleDateString(locale, { month: "long", day: "numeric" });
58
58
  }
59
+ function formatPeriodEnd(subscriptions, locale) {
60
+ const status = subscriptions?.[0]?.status;
61
+ if (!status || !("currentPeriod" in status) || !status.currentPeriod?.end) return null;
62
+ const end = status.currentPeriod.end;
63
+ const d = end instanceof Date ? end : new Date(end);
64
+ if (Number.isNaN(d.getTime())) return null;
65
+ return d.toLocaleDateString(locale, { month: "long", day: "numeric", year: "numeric" });
66
+ }
59
67
  function discountPhrase(opts) {
60
68
  const { percentOff, amountOff, currency, duration, durationInMonths } = opts;
61
69
  const amount = percentOff != null ? `${percentOff}% off` : amountOff != null ? `${formatPriceFromMinor(amountOff, currency)} off` : "discount";
@@ -65,6 +73,15 @@ function discountPhrase(opts) {
65
73
 
66
74
  // src/core/utils.ts
67
75
  var BUILT_IN_STEP_TYPES = ["survey", "offer", "feedback", "confirm", "success"];
76
+ var BUILT_IN_OFFER_TYPES = [
77
+ "discount",
78
+ "pause",
79
+ "trial_extension",
80
+ "plan_change",
81
+ "contact",
82
+ "redirect",
83
+ "rebate"
84
+ ];
68
85
  function cn(...classes) {
69
86
  return classes.filter(Boolean).join(" ");
70
87
  }
@@ -125,6 +142,6 @@ var defaultTitles = {
125
142
  confirm: "Confirm cancellation"
126
143
  };
127
144
 
128
- export { BUILT_IN_STEP_TYPES, appearanceToStyle, calculateDiscountedPrice, cn, defaultTitles, discountPhrase, formatMonthDay, formatMonthDayLong, formatPrice, formatPriceFromMinor };
129
- //# sourceMappingURL=chunk-SXGORJOX.js.map
130
- //# sourceMappingURL=chunk-SXGORJOX.js.map
145
+ export { BUILT_IN_OFFER_TYPES, BUILT_IN_STEP_TYPES, appearanceToStyle, calculateDiscountedPrice, cn, defaultTitles, discountPhrase, formatMonthDay, formatMonthDayLong, formatPeriodEnd, formatPrice, formatPriceFromMinor };
146
+ //# sourceMappingURL=chunk-G57YLYM5.js.map
147
+ //# sourceMappingURL=chunk-G57YLYM5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/format.ts","../src/core/utils.ts"],"names":[],"mappings":"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","file":"chunk-G57YLYM5.js","sourcesContent":["import type { DirectSubscription } from './types'\n\n// Currencies whose smallest unit equals one major unit (no fractional part).\n// Stripe and most billing providers store these without an implicit /100.\nconst ZERO_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n])\n\n// Middle-Eastern currencies that use three fractional digits (1000 divisor).\n// Without this bucket, JOD/KWD amounts would be off by 10x in display.\nconst THREE_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'])\n\nexport function isZeroDecimalCurrency(currency: string): boolean {\n return ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase())\n}\n\nexport function isThreeDecimalCurrency(currency: string): boolean {\n return THREE_DECIMAL_CURRENCIES.has(currency.toUpperCase())\n}\n\n/**\n * Smallest-unit divisor for a currency. 1 for zero-decimal (JPY), 1000 for\n * three-decimal (KWD), 100 for everything else. Used to convert provider\n * amounts (which are always in the smallest unit) into the major unit for\n * display.\n */\nexport function getCurrencyDivisor(currency = 'USD'): number {\n if (isZeroDecimalCurrency(currency)) return 1\n if (isThreeDecimalCurrency(currency)) return 1000\n return 100\n}\n\n/**\n * Converts a Direct.Price.amount.value (smallest currency unit) into the\n * major unit used for display.\n */\nexport function convertFromMinorUnits(value: number, currency = 'USD'): number {\n return value / getCurrencyDivisor(currency)\n}\n\n/**\n * Format an amount already in the major unit. Uses `narrowSymbol` so output\n * reads \"$29\" rather than \"US$29\", and trims trailing zero fractions so\n * whole amounts render compactly.\n */\nexport function formatPrice(amount: number, currency = 'USD', locale?: string): string {\n try {\n const formatted = new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n }).format(amount)\n return formatted.replace(/[.,]00$/, '')\n } catch {\n return `${currency} ${amount.toFixed(2)}`\n }\n}\n\n/** Convenience: format a Direct-shape minor-unit amount in one call. */\nexport function formatPriceFromMinor(value: number, currency = 'USD', locale?: string): string {\n return formatPrice(convertFromMinorUnits(value, currency), currency, locale)\n}\n\nexport function calculateDiscountedPrice(originalPrice: number, percentOff: number): number {\n return originalPrice * (1 - percentOff / 100)\n}\n\n// ─── Dates ────────────────────────────────────────────────────────────────\n\n/** \"Apr 30, 2026\" — used for resume dates and other full-date contexts. */\nexport function formatShortDate(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })\n}\n\n/** \"Apr 30\" — used when the year is implied by context. */\nexport function formatMonthDay(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric' })\n}\n\n/** \"April 30\" — long month form for prominent date displays. */\nexport function formatMonthDayLong(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'long', day: 'numeric' })\n}\n\n/**\n * Long-form access period end (\"June 14, 2026\") for the first subscription's\n * current period. Returns null for canceled subscriptions, missing periods,\n * and unparseable dates so Confirm can drop the \"access continues until\"\n * notice cleanly instead of rendering \"Invalid Date\".\n */\nexport function formatPeriodEnd(\n subscriptions: DirectSubscription[] | null | undefined,\n locale?: string,\n): string | null {\n const status = subscriptions?.[0]?.status\n if (!status || !('currentPeriod' in status) || !status.currentPeriod?.end) return null\n const end = status.currentPeriod.end\n const d = end instanceof Date ? end : new Date(end)\n if (Number.isNaN(d.getTime())) return null\n return d.toLocaleDateString(locale, { month: 'long', day: 'numeric', year: 'numeric' })\n}\n\n// ─── Discount phrasing ─────────────────────────────────────────────────────\n\n/**\n * Renders the discount as a single human-readable sentence:\n * \"25% off for 3 months\" / \"$5 off your next renewal\" / \"20% off for life\"\n *\n * Mirrors the embed's couponToString helper so SDK and embed use the same\n * phrasing for the same offer shape.\n */\nexport function discountPhrase(opts: {\n percentOff?: number\n /** Smallest currency unit (cents for USD). */\n amountOff?: number\n currency?: string\n duration?: 'once' | 'repeating' | 'forever'\n durationInMonths?: number\n}): string {\n const { percentOff, amountOff, currency, duration, durationInMonths } = opts\n const amount =\n percentOff != null\n ? `${percentOff}% off`\n : amountOff != null\n ? `${formatPriceFromMinor(amountOff, currency)} off`\n : 'discount'\n const tail =\n duration === 'once'\n ? 'your next renewal'\n : duration === 'forever'\n ? 'for life'\n : durationInMonths\n ? `for ${durationInMonths} ${durationInMonths === 1 ? 'month' : 'months'}`\n : ''\n return tail ? `${amount} ${tail}` : amount\n}\n","import type { CSSProperties } from 'react'\nimport type { Appearance, AppearanceVariables } from './types'\n\nexport const BUILT_IN_STEP_TYPES: readonly string[] = ['survey', 'offer', 'feedback', 'confirm', 'success']\nexport const BUILT_IN_OFFER_TYPES: readonly string[] = [\n 'discount',\n 'pause',\n 'trial_extension',\n 'plan_change',\n 'contact',\n 'redirect',\n 'rebate',\n]\n\nexport function cn(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(' ')\n}\n\n// Maps the typed `appearance.variables` field names to the underlying\n// CSS custom properties. Light/dark variants live in CSS — these\n// overrides apply on top of whichever scheme is active.\nconst VAR_MAP: Record<string, keyof AppearanceVariables> = {\n // Surfaces\n '--ck-color-bg': 'colorBackground',\n '--ck-color-surface': 'colorSurface',\n '--ck-color-surface-muted': 'colorSurfaceMuted',\n\n // Borders\n '--ck-color-border': 'colorBorder',\n '--ck-color-border-strong': 'colorBorderStrong',\n\n // Text\n '--ck-color-text': 'colorText',\n '--ck-color-text-secondary': 'colorTextSecondary',\n '--ck-color-text-muted': 'colorTextMuted',\n\n // Primary\n '--ck-color-primary': 'colorPrimary',\n '--ck-color-primary-hover': 'colorPrimaryHover',\n '--ck-color-primary-soft': 'colorPrimarySoft',\n\n // Semantic\n '--ck-color-success': 'colorSuccess',\n '--ck-color-success-soft': 'colorSuccessSoft',\n '--ck-color-danger': 'colorDanger',\n '--ck-color-danger-hover': 'colorDangerHover',\n '--ck-color-danger-soft': 'colorDangerSoft',\n\n // Typography\n '--ck-font-family': 'fontFamily',\n '--ck-font-mono': 'fontFamilyMono',\n '--ck-font-display': 'fontFamilyDisplay',\n '--ck-font-size': 'fontSize',\n '--ck-step-title-weight': 'fontWeightDisplay',\n '--ck-step-title-letter-spacing': 'letterSpacingDisplay',\n\n // Geometry\n '--ck-border-radius': 'borderRadius',\n '--ck-radius-sm': 'radiusSm',\n '--ck-radius-md': 'radiusMd',\n '--ck-radius-lg': 'radiusLg',\n '--ck-radius-xl': 'radiusXl',\n\n // Elevation\n '--ck-shadow-modal': 'shadowModal',\n '--ck-shadow-card': 'shadowCard',\n\n // Overlay\n '--ck-overlay-color': 'overlayColor',\n}\n\nexport function appearanceToStyle(appearance?: Appearance): CSSProperties | undefined {\n const variables = appearance?.variables\n if (!variables) return undefined\n\n const style: Record<string, string> = {}\n for (const [cssProp, varKey] of Object.entries(VAR_MAP)) {\n const value = variables[varKey]\n if (value) style[cssProp] = value\n }\n\n return Object.keys(style).length > 0 ? (style as CSSProperties) : undefined\n}\n\n// Step-level fallback titles when neither token-mode config nor local\n// step config provide one. Offer and Success are absent — Offer falls back\n// to `offer.copy.headline`, Success branches on outcome.\nexport const defaultTitles = {\n survey: 'Why are you cancelling?',\n feedback: 'Any other feedback?',\n confirm: 'Confirm cancellation',\n} as const\n"]}
@@ -63,6 +63,9 @@ var ChurnkeyApi = class {
63
63
  async extendTrial(days, blueprintId) {
64
64
  await this.request(this.orgUrl("cancel-flow/actions/extend-trial"), { days, blueprintId });
65
65
  }
66
+ async applyRebate(blueprintId, offerGuid) {
67
+ await this.request(this.orgUrl("cancel-flow/actions/rebate"), { blueprintId, offerGuid });
68
+ }
66
69
  async createSession(payload) {
67
70
  await this.request(`${this.baseUrl}/api/sessions/sdk`, payload);
68
71
  }
@@ -154,7 +157,7 @@ function mergeStep(step, attrs) {
154
157
  ...s,
155
158
  title: maybeMerge(s.title, attrs),
156
159
  description: maybeMerge(s.description, attrs),
157
- offer: s.offer ? mergeOfferDecision(s.offer, attrs) : void 0
160
+ offer: s.offer ? mergeOffer(s.offer, attrs) : void 0
158
161
  };
159
162
  }
160
163
  case "feedback": {
@@ -177,7 +180,8 @@ function mergeStep(step, attrs) {
177
180
  function mergeReason(r, attrs) {
178
181
  return { ...r, label: applyMergeFields(r.label, attrs) };
179
182
  }
180
- function mergeOfferDecision(o, attrs) {
183
+ function mergeOffer(o, attrs) {
184
+ if (!("copy" in o)) return o;
181
185
  return {
182
186
  ...o,
183
187
  copy: {
@@ -196,7 +200,7 @@ function maybeMerge(text, attrs) {
196
200
  var EMPTY_GRAPH = { stepMap: {}, firstStepId: "", orderedStepIds: [] };
197
201
  function buildStepGraph(steps, defaultOfferCopy2) {
198
202
  if (steps.length === 0) return EMPTY_GRAPH;
199
- const resolved = steps.map((step, i) => normalizeStep(step, i));
203
+ const resolved = steps.map((step, i) => normalizeStep(step, i, defaultOfferCopy2));
200
204
  linkNeighbors(resolved);
201
205
  const stepMap = {};
202
206
  for (const step of resolved) stepMap[step.guid] = step;
@@ -226,7 +230,7 @@ function buildStepGraph(steps, defaultOfferCopy2) {
226
230
  surveyStepId: surveyStep?.guid
227
231
  };
228
232
  }
229
- function normalizeStep(step, index) {
233
+ function normalizeStep(step, index, defaultOfferCopy2) {
230
234
  const guid = step.guid ?? `step-${index}-${step.type}`;
231
235
  const base = { guid, type: step.type };
232
236
  switch (step.type) {
@@ -247,7 +251,10 @@ function normalizeStep(step, index) {
247
251
  ...base,
248
252
  title: s.title,
249
253
  description: s.description,
250
- offer: s.offer,
254
+ // Standalone offers may arrive without `copy` — toDecision fills it
255
+ // in from defaultOfferCopy. Token-mode offers already carry copy
256
+ // from the server, so toDecision passes them through unchanged.
257
+ offer: s.offer ? toDecision(s.offer, defaultOfferCopy2) : void 0,
251
258
  classNames: s.classNames
252
259
  };
253
260
  }
@@ -356,9 +363,9 @@ function transformStep(step) {
356
363
  function transformReason(r) {
357
364
  const out = {
358
365
  id: r.id,
359
- label: r.label,
360
- freeform: r.freeform
366
+ label: r.label
361
367
  };
368
+ if (r.freeform) out.freeform = true;
362
369
  if (r.offer) out.offer = transformOfferConfig(r.offer);
363
370
  return out;
364
371
  }
@@ -382,8 +389,7 @@ function transformOfferConfig(o) {
382
389
  return {
383
390
  type: "pause",
384
391
  months: o.months,
385
- interval: o.interval,
386
- datePicker: o.datePicker
392
+ interval: o.interval
387
393
  };
388
394
  case "plan_change":
389
395
  return {
@@ -407,6 +413,16 @@ function transformOfferConfig(o) {
407
413
  url: o.url,
408
414
  label: o.label
409
415
  };
416
+ case "rebate":
417
+ return {
418
+ type: "rebate",
419
+ amountMinor: o.amountMinor,
420
+ currency: o.currency,
421
+ amountPaidMinor: o.amountPaidMinor,
422
+ netAfterRebateMinor: o.netAfterRebateMinor,
423
+ paymentMethodBrand: o.paymentMethodBrand,
424
+ paymentMethodLast4: o.paymentMethodLast4
425
+ };
410
426
  }
411
427
  }
412
428
  function defaultOfferCopy(offer) {
@@ -454,6 +470,13 @@ function defaultOfferCopy(offer) {
454
470
  cta: o.label,
455
471
  declineCta: "No thanks"
456
472
  };
473
+ case "rebate":
474
+ return {
475
+ headline: "Get money back",
476
+ body: "Stay subscribed and we'll refund part of your most recent payment.",
477
+ cta: "Accept refund",
478
+ declineCta: "No thanks"
479
+ };
457
480
  default:
458
481
  return {
459
482
  headline: "Before you go...",
@@ -480,6 +503,8 @@ function handlerFor(offerType, cb) {
480
503
  return cb.handlePlanChange;
481
504
  case "trial_extension":
482
505
  return cb.handleTrialExtension;
506
+ case "rebate":
507
+ return cb.handleRebate;
483
508
  default:
484
509
  return void 0;
485
510
  }
@@ -494,6 +519,8 @@ function listenerFor(offerType, cb) {
494
519
  return cb.onPlanChange;
495
520
  case "trial_extension":
496
521
  return cb.onTrialExtension;
522
+ case "rebate":
523
+ return cb.onRebate;
497
524
  default:
498
525
  return void 0;
499
526
  }
@@ -520,7 +547,8 @@ var OFFER_TYPE_API_MAP = {
520
547
  plan_change: "PLAN_CHANGE",
521
548
  trial_extension: "TRIAL_EXTENSION",
522
549
  contact: "CONTACT",
523
- redirect: "REDIRECT"
550
+ redirect: "REDIRECT",
551
+ rebate: "REBATE"
524
552
  };
525
553
  function toApiStepType(step) {
526
554
  if (step === "success") return null;
@@ -600,6 +628,8 @@ function toAcceptedOfferPayload(rec, result) {
600
628
  return { ...base, trialExtensionDays: o.days };
601
629
  case "redirect":
602
630
  return { ...base, redirectUrl: o.url };
631
+ case "rebate":
632
+ return { ...base, rebateAmount: o.amountMinor };
603
633
  default:
604
634
  return base;
605
635
  }
@@ -642,7 +672,7 @@ var CancelFlowMachine = class {
642
672
  const merged = applyMergeFieldsToSteps(config.steps, this.directCustomer);
643
673
  this.graph = buildStepGraph(merged, defaultOfferCopy);
644
674
  }
645
- this.state = this.buildInitialState(null);
675
+ this.state = this.buildInitialState(this.directCustomer, this.directSubscriptions ?? []);
646
676
  this.cachedSnapshot = { ...this.state };
647
677
  const first = this.firstStep();
648
678
  if (!config.session && first) this.trackStepEnter(first);
@@ -696,7 +726,10 @@ var CancelFlowMachine = class {
696
726
  }
697
727
  selectReason = (id) => {
698
728
  if (!this.reasons.find((r) => r.id === id)) return;
699
- this.setState({ selectedReason: id });
729
+ this.setState({ selectedReason: id, followupResponse: "" });
730
+ };
731
+ setFollowupResponse = (text) => {
732
+ this.setState({ followupResponse: text });
700
733
  };
701
734
  next = (result) => {
702
735
  if (isPlainObject(result)) {
@@ -727,7 +760,7 @@ var CancelFlowMachine = class {
727
760
  if (handler) {
728
761
  await handler(acceptedOffer, customer);
729
762
  } else if (this.isTokenMode()) {
730
- await this.executeTokenAction(acceptedOffer);
763
+ await this.executeTokenAction(acceptedOffer, offer.decisionId);
731
764
  }
732
765
  const listener = listenerFor(offer.type, this.callbacks);
733
766
  if (listener) await runListener(listener, acceptedOffer, customer);
@@ -784,7 +817,7 @@ var CancelFlowMachine = class {
784
817
  const merged = this.localSteps ? mergeLocalSteps(result.steps, this.localSteps) : result.steps;
785
818
  const steps = applyMergeFieldsToSteps(merged, config.customer ?? null);
786
819
  this.graph = buildStepGraph(steps, defaultOfferCopy);
787
- this.state = this.buildInitialState(config.customer ?? null);
820
+ this.state = this.buildInitialState(config.customer ?? null, config.subscriptions ?? []);
788
821
  this.cachedSnapshot = { ...this.state };
789
822
  const first = this.firstStep();
790
823
  if (first) this.trackStepEnter(first);
@@ -800,17 +833,19 @@ var CancelFlowMachine = class {
800
833
  surveyStep() {
801
834
  return this.graph.surveyStepId ? this.graph.stepMap[this.graph.surveyStepId] : void 0;
802
835
  }
803
- buildInitialState(customer) {
836
+ buildInitialState(customer, subscriptions) {
804
837
  const first = this.firstStep();
805
838
  return {
806
839
  step: first?.type ?? "survey",
807
840
  currentStepId: this.graph.firstStepId,
808
841
  selectedReason: null,
842
+ followupResponse: "",
809
843
  feedback: "",
810
844
  outcome: null,
811
845
  isProcessing: false,
812
846
  error: null,
813
- customer
847
+ customer,
848
+ subscriptions
814
849
  };
815
850
  }
816
851
  // On a survey step with a selected reason that has an attached offer,
@@ -879,7 +914,7 @@ var CancelFlowMachine = class {
879
914
  listener();
880
915
  }
881
916
  }
882
- async executeTokenAction(offer) {
917
+ async executeTokenAction(offer, decisionId) {
883
918
  if (!this.apiClient) return;
884
919
  const o = offer;
885
920
  switch (o.type) {
@@ -897,6 +932,9 @@ var CancelFlowMachine = class {
897
932
  case "trial_extension":
898
933
  await this.apiClient.extendTrial(o.days, this.blueprintId ?? void 0);
899
934
  break;
935
+ case "rebate":
936
+ await this.apiClient.applyRebate(this.blueprintId ?? void 0, decisionId);
937
+ break;
900
938
  }
901
939
  }
902
940
  trackStepView(stepType, guid, numChoices) {
@@ -921,14 +959,14 @@ var CancelFlowMachine = class {
921
959
  }
922
960
  }
923
961
  // Offer passed in so accept() can capture it before enterSuccessStep
924
- // potentially moves currentStepId off the offer step.
962
+ // potentially moves currentStepId off the offer step. copy and decisionId
963
+ // are SDK-internal — strip them from the consumer-facing payload.
925
964
  buildAcceptedOffer(offer, result) {
926
- const { copy: _, ...offerConfig } = offer;
927
- return {
928
- ...offerConfig,
929
- reasonId: this.state.selectedReason,
930
- ...result ? { result } : {}
931
- };
965
+ const { copy: _copy, decisionId: _decisionId, ...offerConfig } = offer;
966
+ const accepted = offerConfig;
967
+ if (this.state.selectedReason) accepted.reasonId = this.state.selectedReason;
968
+ if (result) accepted.result = result;
969
+ return accepted;
932
970
  }
933
971
  // Terminal transition. Prefer moving currentStepId to a declared success
934
972
  // step so the developer's savedTitle / classNames / custom component
@@ -948,12 +986,14 @@ var CancelFlowMachine = class {
948
986
  }
949
987
  buildBasePayload() {
950
988
  const selectedReason = this.reasons.find((r) => r.id === this.state.selectedReason);
989
+ const followupResponse = selectedReason?.freeform && this.state.followupResponse ? this.state.followupResponse : void 0;
951
990
  const payload = {
952
991
  blueprintId: this.blueprintId ?? void 0,
953
992
  customer: this.resolveSessionCustomer(),
954
993
  canceled: false,
955
994
  surveyChoiceId: this.state.selectedReason ?? void 0,
956
995
  surveyChoiceValue: selectedReason?.label,
996
+ followupResponse,
957
997
  feedback: this.state.feedback || void 0,
958
998
  presentedOffers: this.presentedOffers,
959
999
  stepsViewed: this.stepsViewed,
@@ -1052,5 +1092,5 @@ function decodeSessionToken(token) {
1052
1092
  }
1053
1093
 
1054
1094
  export { AnalyticsClient, CancelFlowMachine, ChurnkeyApi, decodeSessionToken };
1055
- //# sourceMappingURL=chunk-OL5HQUE4.js.map
1056
- //# sourceMappingURL=chunk-OL5HQUE4.js.map
1095
+ //# sourceMappingURL=chunk-QYNW3HF3.js.map
1096
+ //# sourceMappingURL=chunk-QYNW3HF3.js.map