@churnkey/react 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,901 @@
1
+ import { useCancelFlowMachine } from './chunk-QTMZI5I2.js';
2
+ export { useCancelFlow } from './chunk-QTMZI5I2.js';
3
+ import { cn, discountPhrase, formatShortDate, formatPriceFromMinor, formatMonthDay, appearanceToStyle, defaultTitles } from './chunk-GCQ75J4G.js';
4
+ export { BUILT_IN_STEP_TYPES, appearanceToStyle, calculateDiscountedPrice, cn, defaultTitles, formatPrice } from './chunk-GCQ75J4G.js';
5
+ export { AnalyticsClient, CancelFlowMachine, ChurnkeyApi, decodeSessionToken } from './chunk-IFVMM2LB.js';
6
+ import { useState, useRef, useCallback, useEffect } from 'react';
7
+ import { jsxs, jsx } from 'react/jsx-runtime';
8
+
9
+ function RichText({ html, as = "p", className }) {
10
+ if (!html) return null;
11
+ const Tag = as;
12
+ return /* @__PURE__ */ jsx(
13
+ Tag,
14
+ {
15
+ className,
16
+ dangerouslySetInnerHTML: { __html: html }
17
+ }
18
+ );
19
+ }
20
+ function DefaultConfirm({
21
+ title,
22
+ description,
23
+ confirmLabel,
24
+ goBackLabel,
25
+ periodEnd,
26
+ onConfirm,
27
+ onGoBack,
28
+ isProcessing,
29
+ classNames
30
+ }) {
31
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-confirm", classNames?.root), children: [
32
+ /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: title }),
33
+ description && /* @__PURE__ */ jsx(RichText, { html: description, className: cn("ck-step-description", classNames?.description) }),
34
+ periodEnd && /* @__PURE__ */ jsxs("p", { className: cn("ck-period-end", classNames?.periodEndNotice), children: [
35
+ "Your access continues until ",
36
+ periodEnd,
37
+ "."
38
+ ] }),
39
+ /* @__PURE__ */ jsx(
40
+ "button",
41
+ {
42
+ type: "button",
43
+ className: cn("ck-button ck-button-danger", classNames?.confirmButton),
44
+ onClick: onConfirm,
45
+ disabled: isProcessing,
46
+ children: isProcessing ? "Processing..." : confirmLabel
47
+ }
48
+ ),
49
+ /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button-link", classNames?.goBackButton), onClick: onGoBack, children: goBackLabel })
50
+ ] });
51
+ }
52
+ function DefaultFeedback({
53
+ title,
54
+ description,
55
+ placeholder,
56
+ required,
57
+ minLength,
58
+ value,
59
+ onChange,
60
+ onSubmit,
61
+ classNames
62
+ }) {
63
+ const [focused, setFocused] = useState(false);
64
+ const hasMin = minLength > 0;
65
+ const isUnderMin = hasMin && value.length > 0 && value.length < minLength;
66
+ const isValid = !required || value.length >= minLength;
67
+ const placeholderText = placeholder ?? (hasMin ? `At least ${minLength} characters\u2026` : "Type your thoughts\u2026");
68
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-feedback", classNames?.root), children: [
69
+ /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: title }),
70
+ description && /* @__PURE__ */ jsx(RichText, { html: description, className: cn("ck-step-description", classNames?.description) }),
71
+ /* @__PURE__ */ jsxs(
72
+ "div",
73
+ {
74
+ className: cn(
75
+ "ck-feedback-field",
76
+ focused && "ck-feedback-field--focused",
77
+ isUnderMin && "ck-feedback-field--invalid"
78
+ ),
79
+ children: [
80
+ /* @__PURE__ */ jsx(
81
+ "textarea",
82
+ {
83
+ className: cn("ck-textarea", classNames?.textarea),
84
+ placeholder: placeholderText,
85
+ value,
86
+ onChange: (e) => onChange(e.target.value),
87
+ onFocus: () => setFocused(true),
88
+ onBlur: () => setFocused(false),
89
+ rows: 3
90
+ }
91
+ ),
92
+ hasMin && /* @__PURE__ */ jsxs(
93
+ "div",
94
+ {
95
+ className: cn(
96
+ "ck-character-count",
97
+ isUnderMin && "ck-character-count--invalid",
98
+ classNames?.characterCount
99
+ ),
100
+ children: [
101
+ value.length,
102
+ " / ",
103
+ minLength
104
+ ]
105
+ }
106
+ )
107
+ ]
108
+ }
109
+ ),
110
+ /* @__PURE__ */ jsx(
111
+ "button",
112
+ {
113
+ type: "button",
114
+ className: cn("ck-button ck-button-primary", classNames?.submitButton),
115
+ onClick: onSubmit,
116
+ disabled: !isValid,
117
+ children: "Continue"
118
+ }
119
+ )
120
+ ] });
121
+ }
122
+ function DefaultContactOffer({
123
+ title,
124
+ description,
125
+ offer,
126
+ onAccept,
127
+ onDecline,
128
+ isProcessing,
129
+ classNames
130
+ }) {
131
+ const headline = title ?? offer.copy.headline;
132
+ const body = description ?? offer.copy.body;
133
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-offer", classNames?.root), children: [
134
+ headline && /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: headline }),
135
+ body && /* @__PURE__ */ jsx(RichText, { html: body, className: cn("ck-step-description", classNames?.description) }),
136
+ /* @__PURE__ */ jsxs("div", { className: cn("ck-offer-card", classNames?.card), children: [
137
+ /* @__PURE__ */ jsx(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ className: cn("ck-button ck-button-primary", classNames?.acceptButton),
142
+ onClick: () => onAccept(),
143
+ disabled: isProcessing,
144
+ children: isProcessing ? "Processing..." : offer.copy.cta
145
+ }
146
+ ),
147
+ /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button-link", classNames?.declineButton), onClick: onDecline, children: offer.copy.declineCta })
148
+ ] })
149
+ ] });
150
+ }
151
+ function DefaultDiscountOffer({
152
+ title,
153
+ description,
154
+ offer,
155
+ onAccept,
156
+ onDecline,
157
+ isProcessing,
158
+ classNames
159
+ }) {
160
+ const o = offer;
161
+ const headline = title ?? offer.copy.headline;
162
+ const body = description ?? offer.copy.body;
163
+ const phrase = discountPhrase(o);
164
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-offer", classNames?.root), children: [
165
+ headline && /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: headline }),
166
+ body && /* @__PURE__ */ jsx(RichText, { html: body, className: cn("ck-step-description", classNames?.description) }),
167
+ /* @__PURE__ */ jsxs("div", { className: cn("ck-offer-card", classNames?.card), children: [
168
+ /* @__PURE__ */ jsxs("div", { className: "ck-offer-details ck-offer-discount", children: [
169
+ /* @__PURE__ */ jsx("div", { className: "ck-offer-discount-eyebrow", children: "Limited-time offer" }),
170
+ /* @__PURE__ */ jsx("div", { className: "ck-offer-discount-phrase", children: phrase })
171
+ ] }),
172
+ /* @__PURE__ */ jsx(
173
+ "button",
174
+ {
175
+ type: "button",
176
+ className: cn("ck-button ck-button-primary", classNames?.acceptButton),
177
+ onClick: () => onAccept(),
178
+ disabled: isProcessing,
179
+ children: isProcessing ? "Processing..." : offer.copy.cta
180
+ }
181
+ ),
182
+ /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button-link", classNames?.declineButton), onClick: onDecline, children: offer.copy.declineCta })
183
+ ] })
184
+ ] });
185
+ }
186
+ function DefaultPauseOffer({
187
+ title,
188
+ description,
189
+ offer,
190
+ onAccept,
191
+ onDecline,
192
+ isProcessing,
193
+ classNames
194
+ }) {
195
+ const o = offer;
196
+ const max = Math.max(1, o.months);
197
+ const [months, setMonths] = useState(Math.min(2, max));
198
+ const headline = title ?? offer.copy.headline;
199
+ const body = description ?? offer.copy.body;
200
+ const options = Array.from({ length: max }, (_, i) => i + 1);
201
+ const resumeAt = /* @__PURE__ */ new Date();
202
+ resumeAt.setMonth(resumeAt.getMonth() + months);
203
+ const resumeDate = formatShortDate(resumeAt);
204
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-offer", classNames?.root), children: [
205
+ headline && /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: headline }),
206
+ body && /* @__PURE__ */ jsx(RichText, { html: body, className: cn("ck-step-description", classNames?.description) }),
207
+ /* @__PURE__ */ jsxs("div", { className: cn("ck-offer-card", classNames?.card), children: [
208
+ /* @__PURE__ */ jsxs("div", { className: "ck-offer-details ck-offer-pause", children: [
209
+ /* @__PURE__ */ jsx("div", { className: "ck-pause-segments", style: { gridTemplateColumns: `repeat(${max}, 1fr)` }, children: options.map((m) => {
210
+ const isSelected = m === months;
211
+ return /* @__PURE__ */ jsxs(
212
+ "button",
213
+ {
214
+ type: "button",
215
+ onClick: () => setMonths(m),
216
+ className: cn("ck-pause-segment", isSelected && "ck-pause-segment--selected"),
217
+ children: [
218
+ m,
219
+ " ",
220
+ m === 1 ? "month" : "months"
221
+ ]
222
+ },
223
+ m
224
+ );
225
+ }) }),
226
+ /* @__PURE__ */ jsxs("div", { className: "ck-pause-resume", children: [
227
+ /* @__PURE__ */ jsxs("div", { children: [
228
+ /* @__PURE__ */ jsx("div", { className: "ck-pause-resume-label", children: "Billing resumes" }),
229
+ /* @__PURE__ */ jsx("div", { className: "ck-pause-resume-date", children: resumeDate })
230
+ ] }),
231
+ /* @__PURE__ */ jsx(CalendarIcon, {})
232
+ ] })
233
+ ] }),
234
+ /* @__PURE__ */ jsx(
235
+ "button",
236
+ {
237
+ type: "button",
238
+ className: cn("ck-button ck-button-primary", classNames?.acceptButton),
239
+ onClick: () => onAccept({ months }),
240
+ disabled: isProcessing,
241
+ children: isProcessing ? "Processing..." : offer.copy.cta
242
+ }
243
+ ),
244
+ /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button-link", classNames?.declineButton), onClick: onDecline, children: offer.copy.declineCta })
245
+ ] })
246
+ ] });
247
+ }
248
+ function CalendarIcon() {
249
+ return /* @__PURE__ */ jsxs(
250
+ "svg",
251
+ {
252
+ className: "ck-pause-resume-icon",
253
+ width: "18",
254
+ height: "18",
255
+ viewBox: "0 0 24 24",
256
+ fill: "none",
257
+ stroke: "currentColor",
258
+ strokeWidth: "1.75",
259
+ strokeLinecap: "round",
260
+ strokeLinejoin: "round",
261
+ "aria-hidden": "true",
262
+ children: [
263
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "4", width: "18", height: "18", rx: "3" }),
264
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "2", x2: "16", y2: "6" }),
265
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "2", x2: "8", y2: "6" }),
266
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "10", x2: "21", y2: "10" })
267
+ ]
268
+ }
269
+ );
270
+ }
271
+ function Checkmark({ color = "currentColor", size = 14 }) {
272
+ return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M3 8.5L6.5 12L13 5", stroke: color, strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) });
273
+ }
274
+ function DefaultPlanChangeOffer({
275
+ title,
276
+ description,
277
+ offer,
278
+ onAccept,
279
+ onDecline,
280
+ isProcessing,
281
+ classNames
282
+ }) {
283
+ const o = offer;
284
+ const plans = o.plans ?? [];
285
+ const [selectedPlanId, setSelectedPlanId] = useState(plans[0]?.id ?? null);
286
+ const selectedPlan = plans.find((p) => p.id === selectedPlanId) ?? null;
287
+ const headline = title ?? offer.copy.headline;
288
+ const body = description ?? offer.copy.body;
289
+ const ctaLabel = isProcessing ? "Processing..." : selectedPlan?.name ? `Switch to ${selectedPlan.name}` : offer.copy.cta;
290
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-offer", classNames?.root), children: [
291
+ headline && /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: headline }),
292
+ body && /* @__PURE__ */ jsx(RichText, { html: body, className: cn("ck-step-description", classNames?.description) }),
293
+ /* @__PURE__ */ jsxs("div", { className: cn("ck-offer-card", classNames?.card), children: [
294
+ /* @__PURE__ */ jsx("div", { className: "ck-offer-details ck-plan-grid", children: plans.map((plan) => {
295
+ const interval = plan.duration?.interval ?? "month";
296
+ const currency = plan.amount.currency ?? "USD";
297
+ const isSelected = plan.id === selectedPlanId;
298
+ return /* @__PURE__ */ jsxs(
299
+ "button",
300
+ {
301
+ type: "button",
302
+ onClick: () => setSelectedPlanId(plan.id),
303
+ className: cn("ck-plan-card", isSelected && "ck-plan-card--selected"),
304
+ "aria-pressed": isSelected,
305
+ children: [
306
+ /* @__PURE__ */ jsx("div", { className: "ck-plan-name", children: plan.name ?? plan.id }),
307
+ plan.tagline && /* @__PURE__ */ jsx("div", { className: "ck-plan-tagline", children: plan.tagline }),
308
+ /* @__PURE__ */ jsxs("div", { className: "ck-plan-price-row", children: [
309
+ /* @__PURE__ */ jsx("span", { className: "ck-plan-amount", children: formatPriceFromMinor(plan.amount.value, currency) }),
310
+ /* @__PURE__ */ jsxs("span", { className: "ck-plan-period", children: [
311
+ "/",
312
+ interval
313
+ ] }),
314
+ plan.msrp && /* @__PURE__ */ jsx("span", { className: "ck-plan-msrp", children: plan.msrp })
315
+ ] }),
316
+ plan.features && plan.features.length > 0 && /* @__PURE__ */ jsx("ul", { className: "ck-plan-features", children: plan.features.map((feature, i) => /* @__PURE__ */ jsxs("li", { className: "ck-plan-feature", children: [
317
+ /* @__PURE__ */ jsx("span", { className: "ck-plan-feature-check", children: /* @__PURE__ */ jsx(Checkmark, { size: 11 }) }),
318
+ feature
319
+ ] }, `${plan.id}-feature-${i}`)) })
320
+ ]
321
+ },
322
+ plan.id
323
+ );
324
+ }) }),
325
+ /* @__PURE__ */ jsx(
326
+ "button",
327
+ {
328
+ type: "button",
329
+ className: cn("ck-button ck-button-primary", classNames?.acceptButton),
330
+ onClick: () => selectedPlanId && onAccept({ planId: selectedPlanId }),
331
+ disabled: isProcessing || !selectedPlanId,
332
+ children: ctaLabel
333
+ }
334
+ ),
335
+ /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button-link", classNames?.declineButton), onClick: onDecline, children: offer.copy.declineCta })
336
+ ] })
337
+ ] });
338
+ }
339
+ function DefaultRedirectOffer({ title, description, offer, onDecline, classNames }) {
340
+ const url = offer.url;
341
+ const headline = title ?? offer.copy.headline;
342
+ const body = description ?? offer.copy.body;
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__ */ jsx("div", { className: "ck-offer-details", children: /* @__PURE__ */ jsxs("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "ck-redirect-link", children: [
348
+ /* @__PURE__ */ jsx("span", { children: offer.copy.cta }),
349
+ /* @__PURE__ */ jsx(ExternalIcon, {})
350
+ ] }) }),
351
+ /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button-link", classNames?.declineButton), onClick: onDecline, children: offer.copy.declineCta })
352
+ ] })
353
+ ] });
354
+ }
355
+ function ExternalIcon() {
356
+ return /* @__PURE__ */ jsxs(
357
+ "svg",
358
+ {
359
+ width: "14",
360
+ height: "14",
361
+ viewBox: "0 0 24 24",
362
+ fill: "none",
363
+ stroke: "currentColor",
364
+ strokeWidth: "2",
365
+ strokeLinecap: "round",
366
+ strokeLinejoin: "round",
367
+ className: "ck-redirect-icon",
368
+ "aria-hidden": "true",
369
+ children: [
370
+ /* @__PURE__ */ jsx("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
371
+ /* @__PURE__ */ jsx("polyline", { points: "15 3 21 3 21 9" }),
372
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
373
+ ]
374
+ }
375
+ );
376
+ }
377
+ function DefaultTrialExtensionOffer({
378
+ title,
379
+ description,
380
+ offer,
381
+ onAccept,
382
+ onDecline,
383
+ isProcessing,
384
+ classNames
385
+ }) {
386
+ const o = offer;
387
+ const headline = title ?? offer.copy.headline;
388
+ const body = description ?? offer.copy.body;
389
+ const end = /* @__PURE__ */ new Date();
390
+ end.setDate(end.getDate() + o.days);
391
+ const newEnd = formatMonthDay(end);
392
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-offer", classNames?.root), children: [
393
+ headline && /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: headline }),
394
+ body && /* @__PURE__ */ jsx(RichText, { html: body, className: cn("ck-step-description", classNames?.description) }),
395
+ /* @__PURE__ */ jsxs("div", { className: cn("ck-offer-card", classNames?.card), children: [
396
+ /* @__PURE__ */ jsxs("div", { className: "ck-offer-details ck-trial-block", children: [
397
+ /* @__PURE__ */ jsxs("div", { className: "ck-trial-badge", children: [
398
+ /* @__PURE__ */ jsxs("div", { className: "ck-trial-days", children: [
399
+ "+",
400
+ o.days
401
+ ] }),
402
+ /* @__PURE__ */ jsx("div", { className: "ck-trial-unit", children: o.days === 1 ? "day" : "days" })
403
+ ] }),
404
+ /* @__PURE__ */ jsxs("div", { children: [
405
+ /* @__PURE__ */ jsx("div", { className: "ck-trial-end-label", children: "New end date" }),
406
+ /* @__PURE__ */ jsx("div", { className: "ck-trial-end-date", children: newEnd })
407
+ ] })
408
+ ] }),
409
+ /* @__PURE__ */ jsx(
410
+ "button",
411
+ {
412
+ type: "button",
413
+ className: cn("ck-button ck-button-primary", classNames?.acceptButton),
414
+ onClick: () => onAccept(),
415
+ disabled: isProcessing,
416
+ children: isProcessing ? "Processing..." : offer.copy.cta
417
+ }
418
+ ),
419
+ /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button-link", classNames?.declineButton), onClick: onDecline, children: offer.copy.declineCta })
420
+ ] })
421
+ ] });
422
+ }
423
+ function DefaultOffer(props) {
424
+ const { offer, components } = props;
425
+ const Component = pickOfferComponent(offer.type, components);
426
+ if (!Component) return null;
427
+ return /* @__PURE__ */ jsx(
428
+ Component,
429
+ {
430
+ ...props
431
+ },
432
+ offer.decisionId ?? offer.type
433
+ );
434
+ }
435
+ function pickOfferComponent(type, components) {
436
+ switch (type) {
437
+ case "discount":
438
+ return components?.DiscountOffer ?? DefaultDiscountOffer;
439
+ case "pause":
440
+ return components?.PauseOffer ?? DefaultPauseOffer;
441
+ case "trial_extension":
442
+ return components?.TrialExtensionOffer ?? DefaultTrialExtensionOffer;
443
+ case "plan_change":
444
+ return components?.PlanChangeOffer ?? DefaultPlanChangeOffer;
445
+ case "contact":
446
+ return components?.ContactOffer ?? DefaultContactOffer;
447
+ case "redirect":
448
+ return components?.RedirectOffer ?? DefaultRedirectOffer;
449
+ default:
450
+ return null;
451
+ }
452
+ }
453
+ function DefaultSuccess({ title, description, onClose, classNames }) {
454
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-success", classNames?.root), children: [
455
+ /* @__PURE__ */ jsx("div", { className: cn("ck-success-icon", classNames?.icon), children: /* @__PURE__ */ jsx(Checkmark, { color: "currentColor", size: 26 }) }),
456
+ /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: title }),
457
+ description && /* @__PURE__ */ jsx(RichText, { html: description, className: cn("ck-step-description", classNames?.description) }),
458
+ /* @__PURE__ */ jsx("div", { className: "ck-success-actions", children: /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-button ck-button-primary", classNames?.closeButton), onClick: onClose, children: "Done" }) })
459
+ ] });
460
+ }
461
+ function DefaultReasonButton({ reason, index, isSelected, onSelect }) {
462
+ const letter = String.fromCharCode(65 + index);
463
+ return /* @__PURE__ */ jsxs(
464
+ "button",
465
+ {
466
+ type: "button",
467
+ role: "radio",
468
+ "aria-checked": isSelected,
469
+ tabIndex: isSelected ? 0 : -1,
470
+ onClick: () => onSelect(reason.id),
471
+ className: cn("ck-reason-button", isSelected && "ck-reason-button--selected"),
472
+ children: [
473
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "ck-reason-badge", children: isSelected ? /* @__PURE__ */ jsx(Checkmark, { color: "#fff", size: 12 }) : letter }),
474
+ /* @__PURE__ */ jsx("span", { className: "ck-reason-label", children: reason.label })
475
+ ]
476
+ }
477
+ );
478
+ }
479
+ function DefaultSurvey({
480
+ title,
481
+ description,
482
+ reasons,
483
+ selectedReason,
484
+ onSelectReason,
485
+ onNext,
486
+ classNames,
487
+ components
488
+ }) {
489
+ const ReasonButton = components?.ReasonButton ?? DefaultReasonButton;
490
+ const listRef = useRef(null);
491
+ const handleKeyDown = useCallback(
492
+ (e) => {
493
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
494
+ e.preventDefault();
495
+ const currentIdx = reasons.findIndex((r) => r.id === selectedReason);
496
+ let nextIdx;
497
+ if (e.key === "ArrowDown") {
498
+ nextIdx = currentIdx < reasons.length - 1 ? currentIdx + 1 : 0;
499
+ } else {
500
+ nextIdx = currentIdx > 0 ? currentIdx - 1 : reasons.length - 1;
501
+ }
502
+ onSelectReason(reasons[nextIdx].id);
503
+ const buttons = listRef.current?.querySelectorAll('[role="radio"]');
504
+ buttons?.[nextIdx]?.focus();
505
+ },
506
+ [reasons, selectedReason, onSelectReason]
507
+ );
508
+ return /* @__PURE__ */ jsxs("div", { className: cn("ck-step ck-step-survey", classNames?.root), children: [
509
+ /* @__PURE__ */ jsx("h2", { className: cn("ck-step-title", classNames?.title), children: title }),
510
+ description && /* @__PURE__ */ jsx(RichText, { html: description, className: cn("ck-step-description", classNames?.description) }),
511
+ /* @__PURE__ */ jsx(
512
+ "div",
513
+ {
514
+ ref: listRef,
515
+ className: cn("ck-reason-list", classNames?.reasonList),
516
+ role: "radiogroup",
517
+ "aria-label": title,
518
+ onKeyDown: handleKeyDown,
519
+ children: reasons.map((reason, i) => /* @__PURE__ */ jsx(
520
+ ReasonButton,
521
+ {
522
+ reason,
523
+ index: i,
524
+ isSelected: selectedReason === reason.id,
525
+ onSelect: onSelectReason
526
+ },
527
+ reason.id
528
+ ))
529
+ }
530
+ ),
531
+ /* @__PURE__ */ jsx(
532
+ "button",
533
+ {
534
+ type: "button",
535
+ className: cn("ck-button ck-button-primary", classNames?.continueButton),
536
+ onClick: onNext,
537
+ disabled: !selectedReason,
538
+ children: "Continue"
539
+ }
540
+ )
541
+ ] });
542
+ }
543
+ function DefaultBackButton({ onBack, className }) {
544
+ return /* @__PURE__ */ jsxs("button", { type: "button", className: cn("ck-back-button", className), onClick: onBack, children: [
545
+ /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
546
+ "path",
547
+ {
548
+ d: "M12.5 4L6.5 10l6 6",
549
+ stroke: "currentColor",
550
+ strokeWidth: "1.5",
551
+ strokeLinecap: "round",
552
+ strokeLinejoin: "round"
553
+ }
554
+ ) }),
555
+ /* @__PURE__ */ jsx("span", { children: "Back" })
556
+ ] });
557
+ }
558
+ function DefaultCloseButton({ onClose, className }) {
559
+ return /* @__PURE__ */ jsx("button", { type: "button", className: cn("ck-close-button", className), onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M15 5L5 15M5 5l10 10", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }) }) });
560
+ }
561
+ function trapFocus(container) {
562
+ const focusableSelector = [
563
+ "a[href]",
564
+ "button:not(:disabled)",
565
+ "input:not(:disabled)",
566
+ "textarea:not(:disabled)",
567
+ "select:not(:disabled)",
568
+ '[tabindex]:not([tabindex="-1"])'
569
+ ].join(", ");
570
+ function handleKeyDown(e) {
571
+ if (e.key !== "Tab") return;
572
+ const focusable = Array.from(container.querySelectorAll(focusableSelector)).filter(
573
+ (el) => el.offsetParent !== null
574
+ );
575
+ if (focusable.length === 0) return;
576
+ const first = focusable[0];
577
+ const last = focusable[focusable.length - 1];
578
+ if (e.shiftKey && document.activeElement === first) {
579
+ e.preventDefault();
580
+ last.focus();
581
+ } else if (!e.shiftKey && document.activeElement === last) {
582
+ e.preventDefault();
583
+ first.focus();
584
+ }
585
+ }
586
+ container.addEventListener("keydown", handleKeyDown);
587
+ return () => container.removeEventListener("keydown", handleKeyDown);
588
+ }
589
+ function DefaultModal({ open, onClose, children, className }) {
590
+ const overlayRef = useRef(null);
591
+ const modalRef = useRef(null);
592
+ const previousFocusRef = useRef(null);
593
+ useEffect(() => {
594
+ if (!open || !modalRef.current) return;
595
+ previousFocusRef.current = document.activeElement;
596
+ modalRef.current.focus();
597
+ const cleanup = trapFocus(modalRef.current);
598
+ return () => {
599
+ cleanup();
600
+ previousFocusRef.current?.focus();
601
+ };
602
+ }, [open]);
603
+ const handleKeyDown = useCallback(
604
+ (e) => {
605
+ if (e.key === "Escape") onClose();
606
+ },
607
+ [onClose]
608
+ );
609
+ useEffect(() => {
610
+ if (!open) return;
611
+ document.addEventListener("keydown", handleKeyDown);
612
+ return () => document.removeEventListener("keydown", handleKeyDown);
613
+ }, [open, handleKeyDown]);
614
+ if (!open) return null;
615
+ return /* @__PURE__ */ jsx(
616
+ "div",
617
+ {
618
+ ref: overlayRef,
619
+ className: "ck-overlay",
620
+ onClick: (e) => {
621
+ if (e.target === overlayRef.current) onClose();
622
+ },
623
+ children: /* @__PURE__ */ jsx(
624
+ "div",
625
+ {
626
+ ref: modalRef,
627
+ role: "dialog",
628
+ "aria-modal": "true",
629
+ "aria-labelledby": "ck-dialog-title",
630
+ tabIndex: -1,
631
+ className: cn("ck-modal", className),
632
+ children
633
+ }
634
+ )
635
+ }
636
+ );
637
+ }
638
+ var query = "(prefers-color-scheme: dark)";
639
+ function getSystemScheme() {
640
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return "light";
641
+ return window.matchMedia(query).matches ? "dark" : "light";
642
+ }
643
+ function useColorScheme(preference) {
644
+ const [system, setSystem] = useState(getSystemScheme);
645
+ useEffect(() => {
646
+ if (preference !== "auto" || typeof window === "undefined" || typeof window.matchMedia !== "function") return;
647
+ const mql = window.matchMedia(query);
648
+ const onChange = () => setSystem(mql.matches ? "dark" : "light");
649
+ mql.addEventListener("change", onChange);
650
+ return () => mql.removeEventListener("change", onChange);
651
+ }, [preference]);
652
+ if (!preference || preference === "light") return "light";
653
+ if (preference === "dark") return "dark";
654
+ return system;
655
+ }
656
+ function CancelFlow(props) {
657
+ const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(props);
658
+ if (isLoading || loadError) {
659
+ return /* @__PURE__ */ jsx(
660
+ LoadStatus,
661
+ {
662
+ appearance: props.appearance,
663
+ classNames: props.classNames,
664
+ components: props.components,
665
+ onClose: props.onClose,
666
+ isLoading,
667
+ loadError,
668
+ onRetry: retry
669
+ }
670
+ );
671
+ }
672
+ return /* @__PURE__ */ jsx(
673
+ FlowShell,
674
+ {
675
+ machine,
676
+ state,
677
+ appearance: props.appearance,
678
+ classNames: props.classNames,
679
+ components: props.components,
680
+ customComponents: props.customComponents
681
+ }
682
+ );
683
+ }
684
+ function LoadStatus({
685
+ appearance,
686
+ classNames,
687
+ components,
688
+ onClose,
689
+ isLoading,
690
+ loadError,
691
+ onRetry
692
+ }) {
693
+ const scheme = useColorScheme(appearance?.colorScheme);
694
+ const appearanceStyle = appearanceToStyle(appearance);
695
+ const Modal = components?.Modal ?? DefaultModal;
696
+ const CloseButton = components?.CloseButton ?? DefaultCloseButton;
697
+ const handleClose = onClose ?? (() => {
698
+ });
699
+ 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: [
700
+ /* @__PURE__ */ jsx(CloseButton, { onClose: handleClose, className: classNames?.closeButton }),
701
+ /* @__PURE__ */ jsxs("div", { className: "ck-content", children: [
702
+ isLoading && /* @__PURE__ */ jsxs("div", { className: "ck-loading", style: { padding: "32px", textAlign: "center" }, children: [
703
+ /* @__PURE__ */ jsx(
704
+ "div",
705
+ {
706
+ className: "ck-loading-spinner",
707
+ style: {
708
+ width: 32,
709
+ height: 32,
710
+ border: "3px solid var(--ck-color-border, #e5e7eb)",
711
+ borderTopColor: "var(--ck-color-primary, #2563eb)",
712
+ borderRadius: "50%",
713
+ animation: "ck-spin 0.6s linear infinite",
714
+ margin: "0 auto 16px"
715
+ }
716
+ }
717
+ ),
718
+ /* @__PURE__ */ jsx("p", { style: { color: "var(--ck-color-text-secondary, #6b7280)" }, children: "Loading your options..." })
719
+ ] }),
720
+ loadError && /* @__PURE__ */ jsxs("div", { className: "ck-error", role: "alert", style: { padding: "32px", textAlign: "center" }, children: [
721
+ /* @__PURE__ */ jsx("p", { className: "ck-error-message", style: { marginBottom: 16 }, children: "We couldn't load your cancellation options. Please try again." }),
722
+ /* @__PURE__ */ jsx(
723
+ "button",
724
+ {
725
+ type: "button",
726
+ className: "ck-retry-button",
727
+ onClick: onRetry,
728
+ style: {
729
+ padding: "8px 20px",
730
+ fontSize: 14,
731
+ fontWeight: 600,
732
+ background: "var(--ck-color-primary, #2563eb)",
733
+ color: "#fff",
734
+ border: "none",
735
+ borderRadius: "var(--ck-border-radius, 8px)",
736
+ cursor: "pointer"
737
+ },
738
+ children: "Try again"
739
+ }
740
+ )
741
+ ] })
742
+ ] })
743
+ ] }) });
744
+ }
745
+ function FlowShell({ machine, state, appearance, classNames, components, customComponents }) {
746
+ const scheme = useColorScheme(appearance?.colorScheme);
747
+ const appearanceStyle = appearanceToStyle(appearance);
748
+ const Modal = components?.Modal ?? DefaultModal;
749
+ const CloseButton = components?.CloseButton ?? DefaultCloseButton;
750
+ const BackButton = components?.BackButton ?? DefaultBackButton;
751
+ 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: [
752
+ /* @__PURE__ */ jsx(CloseButton, { onClose: machine.close, className: classNames?.closeButton }),
753
+ /* @__PURE__ */ jsxs("div", { className: "ck-content", children: [
754
+ machine.canGoBack && /* @__PURE__ */ jsx(BackButton, { onBack: machine.back, className: classNames?.backButton }),
755
+ state.error && /* @__PURE__ */ jsx("div", { className: "ck-error", role: "alert", children: /* @__PURE__ */ jsx("p", { className: "ck-error-message", children: "Something went wrong. Please try again." }) }),
756
+ /* @__PURE__ */ jsx(StepRenderer, { state, machine, components, customComponents })
757
+ ] })
758
+ ] }) });
759
+ }
760
+ function StepRenderer({
761
+ state,
762
+ machine,
763
+ components,
764
+ customComponents
765
+ }) {
766
+ const stepConfig = machine.currentStep;
767
+ switch (state.step) {
768
+ case "survey": {
769
+ const Survey = components?.Survey ?? DefaultSurvey;
770
+ const config = stepConfig;
771
+ return /* @__PURE__ */ jsx(
772
+ Survey,
773
+ {
774
+ title: config?.title ?? defaultTitles.survey,
775
+ description: config?.description,
776
+ reasons: machine.reasons,
777
+ selectedReason: state.selectedReason,
778
+ onSelectReason: machine.selectReason,
779
+ onNext: machine.next,
780
+ classNames: config?.classNames,
781
+ components
782
+ }
783
+ );
784
+ }
785
+ case "offer": {
786
+ const offer = machine.currentOffer;
787
+ if (!offer) return null;
788
+ const CustomOffer = customComponents?.[offer.type];
789
+ if (CustomOffer) {
790
+ return /* @__PURE__ */ jsx(
791
+ CustomOffer,
792
+ {
793
+ offer,
794
+ customer: state.customer,
795
+ onAccept: machine.accept,
796
+ onDecline: machine.decline,
797
+ isProcessing: state.isProcessing
798
+ }
799
+ );
800
+ }
801
+ const Offer = components?.Offer ?? DefaultOffer;
802
+ const config = stepConfig;
803
+ return /* @__PURE__ */ jsx(
804
+ Offer,
805
+ {
806
+ title: config?.title,
807
+ description: config?.description,
808
+ offer,
809
+ onAccept: machine.accept,
810
+ onDecline: machine.decline,
811
+ isProcessing: state.isProcessing,
812
+ classNames: config?.classNames,
813
+ components
814
+ }
815
+ );
816
+ }
817
+ case "feedback": {
818
+ const Feedback = components?.Feedback ?? DefaultFeedback;
819
+ const config = stepConfig;
820
+ return /* @__PURE__ */ jsx(
821
+ Feedback,
822
+ {
823
+ title: config?.title ?? defaultTitles.feedback,
824
+ description: config?.description,
825
+ placeholder: config?.placeholder,
826
+ required: config?.required ?? false,
827
+ minLength: config?.minLength ?? 0,
828
+ value: state.feedback,
829
+ onChange: machine.setFeedback,
830
+ onSubmit: machine.next,
831
+ classNames: config?.classNames
832
+ }
833
+ );
834
+ }
835
+ case "confirm": {
836
+ const Confirm = components?.Confirm ?? DefaultConfirm;
837
+ const config = stepConfig;
838
+ return /* @__PURE__ */ jsx(
839
+ Confirm,
840
+ {
841
+ title: config?.title ?? defaultTitles.confirm,
842
+ description: config?.description,
843
+ confirmLabel: config?.confirmLabel ?? "Cancel subscription",
844
+ goBackLabel: config?.goBackLabel ?? "Go back",
845
+ onConfirm: machine.cancel,
846
+ onGoBack: machine.back,
847
+ isProcessing: state.isProcessing,
848
+ classNames: config?.classNames
849
+ }
850
+ );
851
+ }
852
+ case "success": {
853
+ const Success = components?.Success ?? DefaultSuccess;
854
+ const config = stepConfig;
855
+ const isSaved = state.outcome === "saved";
856
+ return /* @__PURE__ */ jsx(
857
+ Success,
858
+ {
859
+ outcome: state.outcome ?? "cancelled",
860
+ offer: machine.currentOffer ?? void 0,
861
+ title: isSaved ? config?.savedTitle ?? "Welcome back!" : config?.cancelledTitle ?? "Subscription cancelled",
862
+ description: isSaved ? config?.savedDescription ?? "Your offer has been applied." : config?.cancelledDescription ?? "We're sorry to see you go.",
863
+ onClose: machine.close,
864
+ classNames: config?.classNames
865
+ }
866
+ );
867
+ }
868
+ default: {
869
+ const CustomStep = customComponents?.[state.step];
870
+ if (!CustomStep) {
871
+ return /* @__PURE__ */ jsx(UnregisteredStepFallback, { step: state.step, onSkip: machine.next });
872
+ }
873
+ const config = stepConfig;
874
+ return /* @__PURE__ */ jsx(
875
+ CustomStep,
876
+ {
877
+ step: {
878
+ type: state.step,
879
+ title: config?.title,
880
+ description: config?.description,
881
+ data: config?.data
882
+ },
883
+ customer: state.customer,
884
+ onNext: machine.next,
885
+ onBack: machine.back
886
+ }
887
+ );
888
+ }
889
+ }
890
+ }
891
+ function UnregisteredStepFallback({ step, onSkip }) {
892
+ useEffect(() => {
893
+ console.warn(`[churnkey] No component registered for step type "${step}". Skipping.`);
894
+ onSkip();
895
+ }, [step, onSkip]);
896
+ return null;
897
+ }
898
+
899
+ export { CancelFlow, DefaultBackButton, DefaultCloseButton, DefaultConfirm, DefaultContactOffer, DefaultDiscountOffer, DefaultFeedback, DefaultModal, DefaultOffer, DefaultPauseOffer, DefaultPlanChangeOffer, DefaultReasonButton, DefaultRedirectOffer, DefaultSuccess, DefaultSurvey, DefaultTrialExtensionOffer };
900
+ //# sourceMappingURL=index.js.map
901
+ //# sourceMappingURL=index.js.map