@churnkey/react 0.6.5 → 0.7.1

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 (35) hide show
  1. package/dist/{chunk-H5IHOYUF.cjs → chunk-ER6YITYC.cjs} +202 -9
  2. package/dist/chunk-ER6YITYC.cjs.map +1 -0
  3. package/dist/{chunk-W47U2HRC.cjs → chunk-FK4AJOX6.cjs} +7 -6
  4. package/dist/chunk-FK4AJOX6.cjs.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-XHXG43PW.js → chunk-GNRS7V7H.js} +4 -3
  8. package/dist/chunk-GNRS7V7H.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-F6IP3LGK.js → chunk-RFKW3LWM.js} +199 -10
  12. package/dist/chunk-RFKW3LWM.js.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 +238 -163
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +15 -15
  24. package/dist/index.d.ts +15 -15
  25. package/dist/index.js +135 -76
  26. package/dist/index.js.map +1 -1
  27. package/dist/{step-graph-Dee2cFLK.d.cts → step-graph-C7JC8Umv.d.cts} +199 -1
  28. package/dist/{step-graph-Dee2cFLK.d.ts → step-graph-C7JC8Umv.d.ts} +199 -1
  29. package/package.json +1 -1
  30. package/dist/chunk-EYIGZLVT.js.map +0 -1
  31. package/dist/chunk-F6IP3LGK.js.map +0 -1
  32. package/dist/chunk-H5IHOYUF.cjs.map +0 -1
  33. package/dist/chunk-W47U2HRC.cjs.map +0 -1
  34. package/dist/chunk-WVSNF3TN.cjs.map +0 -1
  35. package/dist/chunk-XHXG43PW.js.map +0 -1
@@ -1,5 +1,165 @@
1
1
  'use strict';
2
2
 
3
+ // src/core/messages.ts
4
+ var defaultMessages = {
5
+ common: {
6
+ continue: "Continue",
7
+ back: "Back",
8
+ close: "Close",
9
+ done: "Done",
10
+ processing: "Processing...",
11
+ tryAgain: "Try again",
12
+ loading: "Loading your options...",
13
+ loadError: "We couldn't load your cancellation options. Please try again.",
14
+ error: "Something went wrong. Please try again.",
15
+ day: "day",
16
+ days: "days",
17
+ month: "month",
18
+ months: "months"
19
+ },
20
+ survey: {
21
+ title: "Why are you cancelling?",
22
+ followupPlaceholder: "Tell us more (optional)",
23
+ followupAriaLabel: "Additional detail"
24
+ },
25
+ feedback: {
26
+ title: "Any other feedback?",
27
+ placeholder: "Type your thoughts\u2026",
28
+ placeholderWithMin: "At least {minLength} characters\u2026"
29
+ },
30
+ confirm: {
31
+ title: "Confirm cancellation",
32
+ lossesLabel: "You'll lose access to:",
33
+ cta: "Cancel subscription",
34
+ goBack: "Go back",
35
+ periodEndNotice: {
36
+ immediate: "",
37
+ atPeriodEnd: "Your access continues until {periodEnd}."
38
+ }
39
+ },
40
+ success: {
41
+ // Per-offer defaults mirror the embed's *Applied keys so the two
42
+ // surfaces confirm the same outcome with the same words.
43
+ saved: {
44
+ title: "Welcome back!",
45
+ description: "Your offer has been applied.",
46
+ discount: { title: "Discount applied.", description: "We're so happy you're still here." },
47
+ pause: {
48
+ title: "Subscription paused.",
49
+ description: "You won't be billed until your subscription resumes.",
50
+ resumeDescription: "You won't be billed again until {resumeDate}."
51
+ },
52
+ trial_extension: { title: "Trial extended.", description: "Your trial has been extended successfully." },
53
+ plan_change: { title: "Plan changed.", description: "Your new plan is now in effect." },
54
+ rebate: { title: "Refund issued.", description: "Your money's on its way back." }
55
+ },
56
+ cancelled: {
57
+ title: "Subscription cancelled",
58
+ description: "We're sorry to see you go."
59
+ }
60
+ },
61
+ offer: {
62
+ limitedTimeEyebrow: "Limited-time offer",
63
+ pauseEyebrow: "We'll see you back on",
64
+ newEndDateLabel: "New end date",
65
+ currentPlanBadge: "Current",
66
+ switchToCta: "Switch to {planName}",
67
+ acceptCta: {
68
+ discount: "",
69
+ pause: "",
70
+ trial_extension: "",
71
+ plan_change: "",
72
+ contact: "",
73
+ redirect: "",
74
+ rebate: ""
75
+ },
76
+ declineCta: "",
77
+ rebate: {
78
+ paidLabel: "You paid this period",
79
+ moneyBackLabel: "Money back",
80
+ inclTax: "(incl. {amount} tax)",
81
+ netLabel: "Your net for this period"
82
+ }
83
+ }
84
+ };
85
+ function isTimingVariants(value) {
86
+ return typeof value === "object" && value !== null && ("immediate" in value || "atPeriodEnd" in value) && !Array.isArray(value);
87
+ }
88
+ function selectTiming(value, cancelAtPeriodEnd) {
89
+ if (typeof value === "string") return value;
90
+ return cancelAtPeriodEnd ?? true ? value.atPeriodEnd : value.immediate;
91
+ }
92
+ function formatMessage(template, vars) {
93
+ return template.replace(/\{(\w+)\}/g, (match, name) => {
94
+ const value = vars[name];
95
+ return value == null ? match : String(value);
96
+ });
97
+ }
98
+ var TIMING_AWARE_PATHS = /* @__PURE__ */ new Set([
99
+ "confirm.cta",
100
+ "confirm.periodEndNotice",
101
+ "success.cancelled.title",
102
+ "success.cancelled.description"
103
+ ]);
104
+ function mergeValue(base, patch, path) {
105
+ if (typeof base === "string" || isTimingVariants(base)) {
106
+ if (typeof patch === "string") return patch === "" ? base : patch;
107
+ if (isTimingVariants(patch) && TIMING_AWARE_PATHS.has(path)) {
108
+ const result = typeof base === "string" ? { immediate: base, atPeriodEnd: base } : { immediate: "", atPeriodEnd: "", ...base };
109
+ let applied = false;
110
+ if (typeof patch.immediate === "string" && patch.immediate) {
111
+ result.immediate = patch.immediate;
112
+ applied = true;
113
+ }
114
+ if (typeof patch.atPeriodEnd === "string" && patch.atPeriodEnd) {
115
+ result.atPeriodEnd = patch.atPeriodEnd;
116
+ applied = true;
117
+ }
118
+ return applied ? result : base;
119
+ }
120
+ return base;
121
+ }
122
+ if (typeof base === "object" && base !== null && typeof patch === "object" && patch !== null && !Array.isArray(patch) && !isTimingVariants(patch)) {
123
+ const merged = { ...base };
124
+ for (const [key, value] of Object.entries(patch)) {
125
+ if (value == null || !(key in merged)) continue;
126
+ merged[key] = mergeValue(merged[key], value, path ? `${path}.${key}` : key);
127
+ }
128
+ return merged;
129
+ }
130
+ return base;
131
+ }
132
+ function mergeMessages(base, patch) {
133
+ if (!patch) return base;
134
+ return mergeValue(base, patch, "");
135
+ }
136
+ function resolveLocale(explicit) {
137
+ if (explicit) return explicit;
138
+ if (typeof navigator !== "undefined" && navigator.language) return navigator.language;
139
+ return "en";
140
+ }
141
+ function localeChain(locale) {
142
+ const exact = locale.toLowerCase();
143
+ const base = exact.split("-")[0];
144
+ const chain = ["en"];
145
+ if (base !== "en") chain.push(base);
146
+ if (exact !== base) chain.push(exact);
147
+ return chain;
148
+ }
149
+ function applyLocaleLayer(base, byLang, chain) {
150
+ if (!byLang) return base;
151
+ const byLowerKey = new Map(Object.entries(byLang).map(([k, v]) => [k.toLowerCase(), v]));
152
+ let resolved = base;
153
+ for (const lang of chain) {
154
+ resolved = mergeMessages(resolved, byLowerKey.get(lang));
155
+ }
156
+ return resolved;
157
+ }
158
+ function buildMessages(i18n, orgMessages) {
159
+ const chain = localeChain(resolveLocale(i18n?.locale));
160
+ return applyLocaleLayer(applyLocaleLayer(defaultMessages, orgMessages, chain), i18n?.messages, chain);
161
+ }
162
+
3
163
  // src/core/utils.ts
4
164
  var BUILT_IN_STEP_TYPES = ["survey", "offer", "feedback", "confirm", "success"];
5
165
  var BUILT_IN_OFFER_TYPES = [
@@ -66,9 +226,9 @@ function appearanceToStyle(appearance) {
66
226
  return Object.keys(style).length > 0 ? style : void 0;
67
227
  }
68
228
  var defaultTitles = {
69
- survey: "Why are you cancelling?",
70
- feedback: "Any other feedback?",
71
- confirm: "Confirm cancellation"
229
+ survey: defaultMessages.survey.title,
230
+ feedback: defaultMessages.feedback.title,
231
+ confirm: defaultMessages.confirm.title
72
232
  };
73
233
 
74
234
  // src/core/api.ts
@@ -754,6 +914,9 @@ var CancelFlowMachine = class {
754
914
  presentedOffers = [];
755
915
  customStepResults = {};
756
916
  configMode = "live";
917
+ i18nConfig;
918
+ localCancelAtPeriodEnd = null;
919
+ resolvedMessages;
757
920
  stepEnteredAt = Date.now();
758
921
  aborted = false;
759
922
  // Visited-step stack. `back` pops this so it lands on the actually-seen
@@ -762,6 +925,11 @@ var CancelFlowMachine = class {
762
925
  history = [];
763
926
  constructor(config) {
764
927
  this.callbacks = config;
928
+ this.i18nConfig = config.i18n;
929
+ this.resolvedMessages = buildMessages(config.i18n);
930
+ if (!config.session && typeof config.cancelAtPeriodEnd === "boolean") {
931
+ this.localCancelAtPeriodEnd = config.cancelAtPeriodEnd;
932
+ }
765
933
  if (config.mode) this.configMode = config.mode;
766
934
  if (config.customerAttributes) this.customerAttributes = config.customerAttributes;
767
935
  if (config.appId && config.customer) {
@@ -802,6 +970,14 @@ var CancelFlowMachine = class {
802
970
  get currentOffer() {
803
971
  return this.currentStep?.offer ?? null;
804
972
  }
973
+ /**
974
+ * The fully resolved message catalog (defaults layered with the config's
975
+ * `i18n` overrides). Timing-aware values are still unresolved — pick a
976
+ * variant with `selectTiming(value, state.cancelAtPeriodEnd)`.
977
+ */
978
+ get messages() {
979
+ return this.resolvedMessages;
980
+ }
805
981
  /** Whether `back()` would move anywhere — false on the first step and on success. */
806
982
  get canGoBack() {
807
983
  if (this.state.step === "success") return false;
@@ -869,7 +1045,7 @@ var CancelFlowMachine = class {
869
1045
  if (listener) await runListener(listener, acceptedOffer, customer);
870
1046
  await this.callbacks.onAccept?.(acceptedOffer, customer);
871
1047
  this.markCurrentOfferAccepted();
872
- this.enterSuccessStep("saved");
1048
+ this.enterSuccessStep("saved", acceptedOffer);
873
1049
  this.recordOutcome("saved", offer, safeResult);
874
1050
  } catch (error) {
875
1051
  this.setState({ isProcessing: false, error });
@@ -918,6 +1094,9 @@ var CancelFlowMachine = class {
918
1094
  this.apiClient = apiClient;
919
1095
  this.creds = creds;
920
1096
  this.config = config;
1097
+ if (config.translations) {
1098
+ this.resolvedMessages = buildMessages(this.i18nConfig, config.translations);
1099
+ }
921
1100
  const result = transformSdkConfig(config);
922
1101
  this.blueprintId = result.blueprintId;
923
1102
  const merged = this.localSteps ? mergeLocalSteps(result.steps, this.localSteps) : result.steps;
@@ -951,7 +1130,9 @@ var CancelFlowMachine = class {
951
1130
  isProcessing: false,
952
1131
  error: null,
953
1132
  customer,
954
- subscriptions
1133
+ subscriptions,
1134
+ cancelAtPeriodEnd: this.config?.settings.cancelAtPeriodEnd ?? this.localCancelAtPeriodEnd,
1135
+ acceptedOffer: null
955
1136
  };
956
1137
  }
957
1138
  // On a survey step with a selected reason that has an attached offer,
@@ -1077,9 +1258,17 @@ var CancelFlowMachine = class {
1077
1258
  // Terminal transition. Prefer moving currentStepId to a declared success
1078
1259
  // step so the developer's savedTitle / classNames / custom component
1079
1260
  // applies; otherwise stay put and the renderer's defaults cover it.
1080
- enterSuccessStep(outcome) {
1261
+ // The accepted offer is snapshotted into state here because currentOffer
1262
+ // derives from currentStepId, which a declared success step moves off the
1263
+ // offer — per-offer success copy needs a stable source.
1264
+ enterSuccessStep(outcome, acceptedOffer) {
1081
1265
  const success = this.getStepConfig("success");
1082
- const partial = { step: "success", outcome, isProcessing: false };
1266
+ const partial = {
1267
+ step: "success",
1268
+ outcome,
1269
+ isProcessing: false,
1270
+ acceptedOffer: acceptedOffer ?? null
1271
+ };
1083
1272
  if (success) partial.currentStepId = success.guid;
1084
1273
  this.setState(partial);
1085
1274
  }
@@ -1214,8 +1403,12 @@ exports.BUILT_IN_STEP_TYPES = BUILT_IN_STEP_TYPES;
1214
1403
  exports.CancelFlowMachine = CancelFlowMachine;
1215
1404
  exports.ChurnkeyApi = ChurnkeyApi;
1216
1405
  exports.appearanceToStyle = appearanceToStyle;
1406
+ exports.buildMessages = buildMessages;
1217
1407
  exports.cn = cn;
1218
1408
  exports.decodeSessionToken = decodeSessionToken;
1409
+ exports.defaultMessages = defaultMessages;
1219
1410
  exports.defaultTitles = defaultTitles;
1220
- //# sourceMappingURL=chunk-H5IHOYUF.cjs.map
1221
- //# sourceMappingURL=chunk-H5IHOYUF.cjs.map
1411
+ exports.formatMessage = formatMessage;
1412
+ exports.selectTiming = selectTiming;
1413
+ //# sourceMappingURL=chunk-ER6YITYC.cjs.map
1414
+ //# sourceMappingURL=chunk-ER6YITYC.cjs.map