@churnkey/react 0.6.4 → 0.7.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-WTFB6T53.cjs → chunk-4ZAJYXG2.cjs} +7 -6
  2. package/dist/chunk-4ZAJYXG2.cjs.map +1 -0
  3. package/dist/{chunk-4WYZSTRI.js → chunk-BODEVZ3K.js} +189 -10
  4. package/dist/chunk-BODEVZ3K.js.map +1 -0
  5. package/dist/{chunk-EYIGZLVT.js → chunk-GH5EKETT.js} +6 -3
  6. package/dist/chunk-GH5EKETT.js.map +1 -0
  7. package/dist/{chunk-OC5H64F4.js → chunk-KS3RWFRH.js} +4 -3
  8. package/dist/chunk-KS3RWFRH.js.map +1 -0
  9. package/dist/{chunk-WVSNF3TN.cjs → chunk-M6Z3BHEH.cjs} +6 -2
  10. package/dist/chunk-M6Z3BHEH.cjs.map +1 -0
  11. package/dist/{chunk-WGS432VT.cjs → chunk-SWEPUHIN.cjs} +192 -9
  12. package/dist/chunk-SWEPUHIN.cjs.map +1 -0
  13. package/dist/core.cjs +30 -14
  14. package/dist/core.d.cts +22 -6
  15. package/dist/core.d.ts +22 -6
  16. package/dist/core.js +2 -2
  17. package/dist/headless.cjs +3 -3
  18. package/dist/headless.d.cts +4 -1
  19. package/dist/headless.d.ts +4 -1
  20. package/dist/headless.js +2 -2
  21. package/dist/index.cjs +235 -161
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +14 -14
  24. package/dist/index.d.ts +14 -14
  25. package/dist/index.js +136 -78
  26. package/dist/index.js.map +1 -1
  27. package/dist/{step-graph-BpoAhd0d.d.cts → step-graph-s1L1wV-P.d.cts} +184 -2
  28. package/dist/{step-graph-BpoAhd0d.d.ts → step-graph-s1L1wV-P.d.ts} +184 -2
  29. package/dist/styles.css +13 -0
  30. package/package.json +1 -1
  31. package/dist/chunk-4WYZSTRI.js.map +0 -1
  32. package/dist/chunk-EYIGZLVT.js.map +0 -1
  33. package/dist/chunk-OC5H64F4.js.map +0 -1
  34. package/dist/chunk-WGS432VT.cjs.map +0 -1
  35. package/dist/chunk-WTFB6T53.cjs.map +0 -1
  36. package/dist/chunk-WVSNF3TN.cjs.map +0 -1
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkWGS432VT_cjs = require('./chunk-WGS432VT.cjs');
3
+ var chunkSWEPUHIN_cjs = require('./chunk-SWEPUHIN.cjs');
4
4
  var react = require('react');
5
5
 
6
6
  function useCancelFlowMachine(config) {
@@ -25,7 +25,7 @@ function useCancelFlowMachine(config) {
25
25
  onClose: () => cb.current.onClose?.(),
26
26
  onStepChange: (step, prevStep) => cb.current.onStepChange?.(step, prevStep)
27
27
  };
28
- return new chunkWGS432VT_cjs.CancelFlowMachine({ ...config, ...dispatch });
28
+ return new chunkSWEPUHIN_cjs.CancelFlowMachine({ ...config, ...dispatch });
29
29
  });
30
30
  const [state, setState] = react.useState(() => machine.getSnapshot());
31
31
  const [isLoading, setIsLoading] = react.useState(!!config.session);
@@ -39,8 +39,8 @@ function useCancelFlowMachine(config) {
39
39
  setLoadError(null);
40
40
  setIsLoading(true);
41
41
  let cancelled = false;
42
- const creds = chunkWGS432VT_cjs.decodeSessionToken(config.session);
43
- const api = new chunkWGS432VT_cjs.ChurnkeyApi(creds, config.apiBaseUrl);
42
+ const creds = chunkSWEPUHIN_cjs.decodeSessionToken(config.session);
43
+ const api = new chunkSWEPUHIN_cjs.ChurnkeyApi(creds, config.apiBaseUrl);
44
44
  api.fetchConfig(callbacksRef.current.customerAttributes).then((sdkConfig) => {
45
45
  if (cancelled) return;
46
46
  machine.initializeFromConfig(sdkConfig, api, creds);
@@ -69,6 +69,7 @@ function useCancelFlow(config) {
69
69
  isLoading,
70
70
  loadError,
71
71
  retry,
72
+ messages: machine.messages,
72
73
  reasons: machine.reasons,
73
74
  currentStep: machine.currentStep,
74
75
  currentOffer: machine.currentOffer,
@@ -88,5 +89,5 @@ function useCancelFlow(config) {
88
89
 
89
90
  exports.useCancelFlow = useCancelFlow;
90
91
  exports.useCancelFlowMachine = useCancelFlowMachine;
91
- //# sourceMappingURL=chunk-WTFB6T53.cjs.map
92
- //# sourceMappingURL=chunk-WTFB6T53.cjs.map
92
+ //# sourceMappingURL=chunk-4ZAJYXG2.cjs.map
93
+ //# sourceMappingURL=chunk-4ZAJYXG2.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-4ZAJYXG2.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 handleRebate: (o, c) => cb.current.handleRebate?.(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 onRebate: (o, c) => cb.current.onRebate?.(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 // Read through the ref, not the closure: an inline `customerAttributes`\n // object literal would otherwise join the dep list and re-fetch (and\n // reset the flow) on every render.\n .fetchConfig(callbacksRef.current.customerAttributes)\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 messages: machine.messages,\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"]}
@@ -1,3 +1,153 @@
1
+ // src/core/messages.ts
2
+ var defaultMessages = {
3
+ common: {
4
+ continue: "Continue",
5
+ back: "Back",
6
+ close: "Close",
7
+ done: "Done",
8
+ processing: "Processing...",
9
+ tryAgain: "Try again",
10
+ loading: "Loading your options...",
11
+ loadError: "We couldn't load your cancellation options. Please try again.",
12
+ error: "Something went wrong. Please try again.",
13
+ day: "day",
14
+ days: "days",
15
+ month: "month",
16
+ months: "months"
17
+ },
18
+ survey: {
19
+ title: "Why are you cancelling?",
20
+ followupPlaceholder: "Tell us more (optional)",
21
+ followupAriaLabel: "Additional detail"
22
+ },
23
+ feedback: {
24
+ title: "Any other feedback?",
25
+ placeholder: "Type your thoughts\u2026",
26
+ placeholderWithMin: "At least {minLength} characters\u2026"
27
+ },
28
+ confirm: {
29
+ title: "Confirm cancellation",
30
+ lossesLabel: "You'll lose access to:",
31
+ cta: "Cancel subscription",
32
+ goBack: "Go back",
33
+ periodEndNotice: {
34
+ immediate: "",
35
+ atPeriodEnd: "Your access continues until {periodEnd}."
36
+ }
37
+ },
38
+ success: {
39
+ // Per-offer defaults mirror the embed's *Applied keys so the two
40
+ // surfaces confirm the same outcome with the same words.
41
+ saved: {
42
+ title: "Welcome back!",
43
+ description: "Your offer has been applied.",
44
+ discount: { title: "Discount applied.", description: "We're so happy you're still here." },
45
+ pause: {
46
+ title: "Subscription paused.",
47
+ description: "You won't be billed until your subscription resumes.",
48
+ resumeDescription: "You won't be billed again until {resumeDate}."
49
+ },
50
+ trial_extension: { title: "Trial extended.", description: "Your trial has been extended successfully." },
51
+ plan_change: { title: "Plan changed.", description: "Your new plan is now in effect." },
52
+ rebate: { title: "Refund issued.", description: "Your money's on its way back." }
53
+ },
54
+ cancelled: {
55
+ title: "Subscription cancelled",
56
+ description: "We're sorry to see you go."
57
+ }
58
+ },
59
+ offer: {
60
+ limitedTimeEyebrow: "Limited-time offer",
61
+ pauseEyebrow: "We'll see you back on",
62
+ newEndDateLabel: "New end date",
63
+ currentPlanBadge: "Current",
64
+ switchToCta: "Switch to {planName}",
65
+ rebate: {
66
+ paidLabel: "You paid this period",
67
+ moneyBackLabel: "Money back",
68
+ inclTax: "(incl. {amount} tax)",
69
+ netLabel: "Your net for this period"
70
+ }
71
+ }
72
+ };
73
+ function isTimingVariants(value) {
74
+ return typeof value === "object" && value !== null && ("immediate" in value || "atPeriodEnd" in value) && !Array.isArray(value);
75
+ }
76
+ function selectTiming(value, cancelAtPeriodEnd) {
77
+ if (typeof value === "string") return value;
78
+ return cancelAtPeriodEnd ?? true ? value.atPeriodEnd : value.immediate;
79
+ }
80
+ function formatMessage(template, vars) {
81
+ return template.replace(/\{(\w+)\}/g, (match, name) => {
82
+ const value = vars[name];
83
+ return value == null ? match : String(value);
84
+ });
85
+ }
86
+ var TIMING_AWARE_PATHS = /* @__PURE__ */ new Set([
87
+ "confirm.cta",
88
+ "confirm.periodEndNotice",
89
+ "success.cancelled.title",
90
+ "success.cancelled.description"
91
+ ]);
92
+ function mergeValue(base, patch, path) {
93
+ if (typeof base === "string" || isTimingVariants(base)) {
94
+ if (typeof patch === "string") return patch === "" ? base : patch;
95
+ if (isTimingVariants(patch) && TIMING_AWARE_PATHS.has(path)) {
96
+ const result = typeof base === "string" ? { immediate: base, atPeriodEnd: base } : { immediate: "", atPeriodEnd: "", ...base };
97
+ let applied = false;
98
+ if (typeof patch.immediate === "string" && patch.immediate) {
99
+ result.immediate = patch.immediate;
100
+ applied = true;
101
+ }
102
+ if (typeof patch.atPeriodEnd === "string" && patch.atPeriodEnd) {
103
+ result.atPeriodEnd = patch.atPeriodEnd;
104
+ applied = true;
105
+ }
106
+ return applied ? result : base;
107
+ }
108
+ return base;
109
+ }
110
+ if (typeof base === "object" && base !== null && typeof patch === "object" && patch !== null && !Array.isArray(patch) && !isTimingVariants(patch)) {
111
+ const merged = { ...base };
112
+ for (const [key, value] of Object.entries(patch)) {
113
+ if (value == null || !(key in merged)) continue;
114
+ merged[key] = mergeValue(merged[key], value, path ? `${path}.${key}` : key);
115
+ }
116
+ return merged;
117
+ }
118
+ return base;
119
+ }
120
+ function mergeMessages(base, patch) {
121
+ if (!patch) return base;
122
+ return mergeValue(base, patch, "");
123
+ }
124
+ function resolveLocale(explicit) {
125
+ if (explicit) return explicit;
126
+ if (typeof navigator !== "undefined" && navigator.language) return navigator.language;
127
+ return "en";
128
+ }
129
+ function localeChain(locale) {
130
+ const exact = locale.toLowerCase();
131
+ const base = exact.split("-")[0];
132
+ const chain = ["en"];
133
+ if (base !== "en") chain.push(base);
134
+ if (exact !== base) chain.push(exact);
135
+ return chain;
136
+ }
137
+ function applyLocaleLayer(base, byLang, chain) {
138
+ if (!byLang) return base;
139
+ const byLowerKey = new Map(Object.entries(byLang).map(([k, v]) => [k.toLowerCase(), v]));
140
+ let resolved = base;
141
+ for (const lang of chain) {
142
+ resolved = mergeMessages(resolved, byLowerKey.get(lang));
143
+ }
144
+ return resolved;
145
+ }
146
+ function buildMessages(i18n, orgMessages) {
147
+ const chain = localeChain(resolveLocale(i18n?.locale));
148
+ return applyLocaleLayer(applyLocaleLayer(defaultMessages, orgMessages, chain), i18n?.messages, chain);
149
+ }
150
+
1
151
  // src/core/utils.ts
2
152
  var BUILT_IN_STEP_TYPES = ["survey", "offer", "feedback", "confirm", "success"];
3
153
  var BUILT_IN_OFFER_TYPES = [
@@ -64,9 +214,9 @@ function appearanceToStyle(appearance) {
64
214
  return Object.keys(style).length > 0 ? style : void 0;
65
215
  }
66
216
  var defaultTitles = {
67
- survey: "Why are you cancelling?",
68
- feedback: "Any other feedback?",
69
- confirm: "Confirm cancellation"
217
+ survey: defaultMessages.survey.title,
218
+ feedback: defaultMessages.feedback.title,
219
+ confirm: defaultMessages.confirm.title
70
220
  };
71
221
 
72
222
  // src/core/api.ts
@@ -752,6 +902,9 @@ var CancelFlowMachine = class {
752
902
  presentedOffers = [];
753
903
  customStepResults = {};
754
904
  configMode = "live";
905
+ i18nConfig;
906
+ localCancelAtPeriodEnd = null;
907
+ resolvedMessages;
755
908
  stepEnteredAt = Date.now();
756
909
  aborted = false;
757
910
  // Visited-step stack. `back` pops this so it lands on the actually-seen
@@ -760,6 +913,11 @@ var CancelFlowMachine = class {
760
913
  history = [];
761
914
  constructor(config) {
762
915
  this.callbacks = config;
916
+ this.i18nConfig = config.i18n;
917
+ this.resolvedMessages = buildMessages(config.i18n);
918
+ if (!config.session && typeof config.cancelAtPeriodEnd === "boolean") {
919
+ this.localCancelAtPeriodEnd = config.cancelAtPeriodEnd;
920
+ }
763
921
  if (config.mode) this.configMode = config.mode;
764
922
  if (config.customerAttributes) this.customerAttributes = config.customerAttributes;
765
923
  if (config.appId && config.customer) {
@@ -800,6 +958,14 @@ var CancelFlowMachine = class {
800
958
  get currentOffer() {
801
959
  return this.currentStep?.offer ?? null;
802
960
  }
961
+ /**
962
+ * The fully resolved message catalog (defaults layered with the config's
963
+ * `i18n` overrides). Timing-aware values are still unresolved — pick a
964
+ * variant with `selectTiming(value, state.cancelAtPeriodEnd)`.
965
+ */
966
+ get messages() {
967
+ return this.resolvedMessages;
968
+ }
803
969
  /** Whether `back()` would move anywhere — false on the first step and on success. */
804
970
  get canGoBack() {
805
971
  if (this.state.step === "success") return false;
@@ -867,7 +1033,7 @@ var CancelFlowMachine = class {
867
1033
  if (listener) await runListener(listener, acceptedOffer, customer);
868
1034
  await this.callbacks.onAccept?.(acceptedOffer, customer);
869
1035
  this.markCurrentOfferAccepted();
870
- this.enterSuccessStep("saved");
1036
+ this.enterSuccessStep("saved", acceptedOffer);
871
1037
  this.recordOutcome("saved", offer, safeResult);
872
1038
  } catch (error) {
873
1039
  this.setState({ isProcessing: false, error });
@@ -916,6 +1082,9 @@ var CancelFlowMachine = class {
916
1082
  this.apiClient = apiClient;
917
1083
  this.creds = creds;
918
1084
  this.config = config;
1085
+ if (config.translations) {
1086
+ this.resolvedMessages = buildMessages(this.i18nConfig, config.translations);
1087
+ }
919
1088
  const result = transformSdkConfig(config);
920
1089
  this.blueprintId = result.blueprintId;
921
1090
  const merged = this.localSteps ? mergeLocalSteps(result.steps, this.localSteps) : result.steps;
@@ -949,7 +1118,9 @@ var CancelFlowMachine = class {
949
1118
  isProcessing: false,
950
1119
  error: null,
951
1120
  customer,
952
- subscriptions
1121
+ subscriptions,
1122
+ cancelAtPeriodEnd: this.config?.settings.cancelAtPeriodEnd ?? this.localCancelAtPeriodEnd,
1123
+ acceptedOffer: null
953
1124
  };
954
1125
  }
955
1126
  // On a survey step with a selected reason that has an attached offer,
@@ -1075,9 +1246,17 @@ var CancelFlowMachine = class {
1075
1246
  // Terminal transition. Prefer moving currentStepId to a declared success
1076
1247
  // step so the developer's savedTitle / classNames / custom component
1077
1248
  // applies; otherwise stay put and the renderer's defaults cover it.
1078
- enterSuccessStep(outcome) {
1249
+ // The accepted offer is snapshotted into state here because currentOffer
1250
+ // derives from currentStepId, which a declared success step moves off the
1251
+ // offer — per-offer success copy needs a stable source.
1252
+ enterSuccessStep(outcome, acceptedOffer) {
1079
1253
  const success = this.getStepConfig("success");
1080
- const partial = { step: "success", outcome, isProcessing: false };
1254
+ const partial = {
1255
+ step: "success",
1256
+ outcome,
1257
+ isProcessing: false,
1258
+ acceptedOffer: acceptedOffer ?? null
1259
+ };
1081
1260
  if (success) partial.currentStepId = success.guid;
1082
1261
  this.setState(partial);
1083
1262
  }
@@ -1206,6 +1385,6 @@ function decodeSessionToken(token) {
1206
1385
  };
1207
1386
  }
1208
1387
 
1209
- export { AnalyticsClient, BUILT_IN_OFFER_TYPES, BUILT_IN_STEP_TYPES, CancelFlowMachine, ChurnkeyApi, appearanceToStyle, cn, decodeSessionToken, defaultTitles };
1210
- //# sourceMappingURL=chunk-4WYZSTRI.js.map
1211
- //# sourceMappingURL=chunk-4WYZSTRI.js.map
1388
+ export { AnalyticsClient, BUILT_IN_OFFER_TYPES, BUILT_IN_STEP_TYPES, CancelFlowMachine, ChurnkeyApi, appearanceToStyle, buildMessages, cn, decodeSessionToken, defaultMessages, defaultTitles, formatMessage, selectTiming };
1389
+ //# sourceMappingURL=chunk-BODEVZ3K.js.map
1390
+ //# sourceMappingURL=chunk-BODEVZ3K.js.map