@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.
- package/dist/{chunk-H5IHOYUF.cjs → chunk-ER6YITYC.cjs} +202 -9
- package/dist/chunk-ER6YITYC.cjs.map +1 -0
- package/dist/{chunk-W47U2HRC.cjs → chunk-FK4AJOX6.cjs} +7 -6
- package/dist/chunk-FK4AJOX6.cjs.map +1 -0
- package/dist/{chunk-EYIGZLVT.js → chunk-GH5EKETT.js} +6 -3
- package/dist/chunk-GH5EKETT.js.map +1 -0
- package/dist/{chunk-XHXG43PW.js → chunk-GNRS7V7H.js} +4 -3
- package/dist/chunk-GNRS7V7H.js.map +1 -0
- package/dist/{chunk-WVSNF3TN.cjs → chunk-M6Z3BHEH.cjs} +6 -2
- package/dist/chunk-M6Z3BHEH.cjs.map +1 -0
- package/dist/{chunk-F6IP3LGK.js → chunk-RFKW3LWM.js} +199 -10
- package/dist/chunk-RFKW3LWM.js.map +1 -0
- package/dist/core.cjs +30 -14
- package/dist/core.d.cts +22 -6
- package/dist/core.d.ts +22 -6
- package/dist/core.js +2 -2
- package/dist/headless.cjs +3 -3
- package/dist/headless.d.cts +4 -1
- package/dist/headless.d.ts +4 -1
- package/dist/headless.js +2 -2
- package/dist/index.cjs +238 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -15
- package/dist/index.d.ts +15 -15
- package/dist/index.js +135 -76
- package/dist/index.js.map +1 -1
- package/dist/{step-graph-Dee2cFLK.d.cts → step-graph-C7JC8Umv.d.cts} +199 -1
- package/dist/{step-graph-Dee2cFLK.d.ts → step-graph-C7JC8Umv.d.ts} +199 -1
- package/package.json +1 -1
- package/dist/chunk-EYIGZLVT.js.map +0 -1
- package/dist/chunk-F6IP3LGK.js.map +0 -1
- package/dist/chunk-H5IHOYUF.cjs.map +0 -1
- package/dist/chunk-W47U2HRC.cjs.map +0 -1
- package/dist/chunk-WVSNF3TN.cjs.map +0 -1
- package/dist/chunk-XHXG43PW.js.map +0 -1
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
import { ReactNode, ReactElement, ComponentType } from 'react';
|
|
2
2
|
|
|
3
|
+
/** Both timing variants of a message. Which one renders is decided by the
|
|
4
|
+
* flow's effective cancel timing (`FlowState.cancelAtPeriodEnd`). */
|
|
5
|
+
interface TimingVariants {
|
|
6
|
+
immediate: string;
|
|
7
|
+
atPeriodEnd: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A message that may differ by cancellation timing. Plain strings render
|
|
11
|
+
* as-is in both cases; pass the object form when the wording should change
|
|
12
|
+
* (e.g. "Cancel" vs "Turn off auto-renew").
|
|
13
|
+
*/
|
|
14
|
+
type TimingAware = string | TimingVariants;
|
|
15
|
+
/** Success-screen copy for one accepted offer type. */
|
|
16
|
+
interface SavedOfferCopy {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
interface CancelFlowMessages {
|
|
21
|
+
common: {
|
|
22
|
+
continue: string;
|
|
23
|
+
back: string;
|
|
24
|
+
close: string;
|
|
25
|
+
done: string;
|
|
26
|
+
processing: string;
|
|
27
|
+
tryAgain: string;
|
|
28
|
+
loading: string;
|
|
29
|
+
loadError: string;
|
|
30
|
+
error: string;
|
|
31
|
+
day: string;
|
|
32
|
+
days: string;
|
|
33
|
+
month: string;
|
|
34
|
+
months: string;
|
|
35
|
+
};
|
|
36
|
+
survey: {
|
|
37
|
+
title: string;
|
|
38
|
+
followupPlaceholder: string;
|
|
39
|
+
followupAriaLabel: string;
|
|
40
|
+
};
|
|
41
|
+
feedback: {
|
|
42
|
+
title: string;
|
|
43
|
+
placeholder: string;
|
|
44
|
+
/** Used instead of `placeholder` when the step sets `minLength`. Supports `{minLength}`. */
|
|
45
|
+
placeholderWithMin: string;
|
|
46
|
+
};
|
|
47
|
+
confirm: {
|
|
48
|
+
title: string;
|
|
49
|
+
lossesLabel: string;
|
|
50
|
+
cta: TimingAware;
|
|
51
|
+
goBack: string;
|
|
52
|
+
/** Shown between the description and the confirm button. Supports
|
|
53
|
+
* `{periodEnd}`. The default immediate variant is empty, which
|
|
54
|
+
* suppresses the notice for immediate timing; empty OVERRIDE values are
|
|
55
|
+
* dropped like any blank override, so to remove the notice entirely
|
|
56
|
+
* replace the `Confirm` component or hide it via
|
|
57
|
+
* `classNames.periodEndNotice`. */
|
|
58
|
+
periodEndNotice: TimingAware;
|
|
59
|
+
};
|
|
60
|
+
success: {
|
|
61
|
+
saved: {
|
|
62
|
+
/** Generic fallback — used for offer types without their own entry
|
|
63
|
+
* (contact, redirect, custom types). */
|
|
64
|
+
title: string;
|
|
65
|
+
description: string;
|
|
66
|
+
discount: SavedOfferCopy;
|
|
67
|
+
pause: SavedOfferCopy & {
|
|
68
|
+
/** Used instead of `description` when the resume date is known
|
|
69
|
+
* (the built-in pause UI always supplies it). Supports `{resumeDate}`. */
|
|
70
|
+
resumeDescription: string;
|
|
71
|
+
};
|
|
72
|
+
trial_extension: SavedOfferCopy;
|
|
73
|
+
plan_change: SavedOfferCopy;
|
|
74
|
+
rebate: SavedOfferCopy;
|
|
75
|
+
};
|
|
76
|
+
cancelled: {
|
|
77
|
+
title: TimingAware;
|
|
78
|
+
description: TimingAware;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
offer: {
|
|
82
|
+
limitedTimeEyebrow: string;
|
|
83
|
+
pauseEyebrow: string;
|
|
84
|
+
newEndDateLabel: string;
|
|
85
|
+
currentPlanBadge: string;
|
|
86
|
+
/** CTA when a plan is selected in a plan-change offer. Supports `{planName}`. */
|
|
87
|
+
switchToCta: string;
|
|
88
|
+
/**
|
|
89
|
+
* Accept-button override per offer type. Defaults are empty: an unset
|
|
90
|
+
* key falls through to the offer's own `copy.cta` (today a
|
|
91
|
+
* server-synthesized default like "Accept refund"; authored per-offer
|
|
92
|
+
* copy, when it exists, keeps winning over an unset key).
|
|
93
|
+
*/
|
|
94
|
+
acceptCta: {
|
|
95
|
+
discount: string;
|
|
96
|
+
pause: string;
|
|
97
|
+
trial_extension: string;
|
|
98
|
+
plan_change: string;
|
|
99
|
+
contact: string;
|
|
100
|
+
redirect: string;
|
|
101
|
+
rebate: string;
|
|
102
|
+
};
|
|
103
|
+
/** Decline-link override for every offer type. Empty default falls
|
|
104
|
+
* through to the offer's own `copy.declineCta`. */
|
|
105
|
+
declineCta: string;
|
|
106
|
+
rebate: {
|
|
107
|
+
paidLabel: string;
|
|
108
|
+
moneyBackLabel: string;
|
|
109
|
+
/** Tax parenthetical after the money-back label. Supports `{amount}`. */
|
|
110
|
+
inclTax: string;
|
|
111
|
+
netLabel: string;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Recursive partial of the catalog for overrides. Timing-aware fields also
|
|
117
|
+
* accept a plain string (applies to both timings) or a partial variant pair
|
|
118
|
+
* (the missing variant keeps the base value).
|
|
119
|
+
*/
|
|
120
|
+
type MessagesPatch<T = CancelFlowMessages> = {
|
|
121
|
+
[K in keyof T]?: T[K] extends string ? string : T[K] extends TimingAware ? string | Partial<TimingVariants> : MessagesPatch<T[K]>;
|
|
122
|
+
};
|
|
123
|
+
interface I18nConfig {
|
|
124
|
+
/** UI language. Defaults to the browser language, falling back to `'en'`. */
|
|
125
|
+
locale?: string;
|
|
126
|
+
/** Per-locale message overrides, e.g. `{ en: { confirm: { cta: 'Turn off auto-renew' } } }`. */
|
|
127
|
+
messages?: Record<string, MessagesPatch>;
|
|
128
|
+
}
|
|
129
|
+
declare const defaultMessages: CancelFlowMessages;
|
|
130
|
+
/**
|
|
131
|
+
* Pick the string for the flow's effective cancel timing. `null` (timing
|
|
132
|
+
* unknown — local mode) behaves like period-end, matching the default the
|
|
133
|
+
* cancel action itself uses.
|
|
134
|
+
*/
|
|
135
|
+
declare function selectTiming(value: TimingAware, cancelAtPeriodEnd: boolean | null): string;
|
|
136
|
+
/** Replace `{name}` tokens. Unknown tokens are left in place. */
|
|
137
|
+
declare function formatMessage(template: string, vars: Record<string, string | number>): string;
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the full catalog for a locale. Layers, lowest to highest: built-in
|
|
140
|
+
* defaults, then `orgMessages` (dashboard-configured overrides delivered on
|
|
141
|
+
* the token-mode config), then the config's own per-locale messages — the
|
|
142
|
+
* developer's overrides always end up on top. Both per-language layers run
|
|
143
|
+
* through the same locale fallback chain.
|
|
144
|
+
*/
|
|
145
|
+
declare function buildMessages(i18n?: I18nConfig, orgMessages?: Record<string, MessagesPatch>): CancelFlowMessages;
|
|
146
|
+
|
|
3
147
|
interface DirectAddress {
|
|
4
148
|
line1?: string;
|
|
5
149
|
line2?: string;
|
|
@@ -444,10 +588,14 @@ interface ModalProps {
|
|
|
444
588
|
interface CloseButtonProps {
|
|
445
589
|
onClose: () => void;
|
|
446
590
|
className?: string;
|
|
591
|
+
/** Accessible label. Defaults to "Close". */
|
|
592
|
+
label?: string;
|
|
447
593
|
}
|
|
448
594
|
interface BackButtonProps {
|
|
449
595
|
onBack: () => void;
|
|
450
596
|
className?: string;
|
|
597
|
+
/** Visible label. Defaults to "Back". */
|
|
598
|
+
label?: string;
|
|
451
599
|
}
|
|
452
600
|
interface SurveyStepProps {
|
|
453
601
|
title: string;
|
|
@@ -464,6 +612,8 @@ interface SurveyStepProps {
|
|
|
464
612
|
onNext: () => void;
|
|
465
613
|
classNames?: SurveyClassNames;
|
|
466
614
|
components?: Partial<ComponentOverrides>;
|
|
615
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
616
|
+
messages?: CancelFlowMessages;
|
|
467
617
|
}
|
|
468
618
|
interface OfferStepProps {
|
|
469
619
|
title?: string;
|
|
@@ -486,6 +636,8 @@ interface OfferStepProps {
|
|
|
486
636
|
* implementations can ignore this.
|
|
487
637
|
*/
|
|
488
638
|
components?: Partial<ComponentOverrides>;
|
|
639
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
640
|
+
messages?: CancelFlowMessages;
|
|
489
641
|
}
|
|
490
642
|
interface FeedbackStepProps {
|
|
491
643
|
title: string;
|
|
@@ -499,6 +651,8 @@ interface FeedbackStepProps {
|
|
|
499
651
|
onChange: (text: string) => void;
|
|
500
652
|
onSubmit: () => void;
|
|
501
653
|
classNames?: FeedbackClassNames;
|
|
654
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
655
|
+
messages?: CancelFlowMessages;
|
|
502
656
|
}
|
|
503
657
|
interface ConfirmStepProps {
|
|
504
658
|
title: string;
|
|
@@ -509,20 +663,33 @@ interface ConfirmStepProps {
|
|
|
509
663
|
lossesLabel?: string;
|
|
510
664
|
confirmLabel: string;
|
|
511
665
|
goBackLabel: string;
|
|
666
|
+
/**
|
|
667
|
+
* Timing-resolved access notice ("Your access continues until June 14, 2026."),
|
|
668
|
+
* rendered between the loss list and the confirm button. Absent when the
|
|
669
|
+
* period end is unknown or the resolved message is empty.
|
|
670
|
+
*/
|
|
671
|
+
periodEndNotice?: string;
|
|
512
672
|
onConfirm: () => Promise<void>;
|
|
513
673
|
onGoBack: () => void;
|
|
514
674
|
isProcessing: boolean;
|
|
515
675
|
classNames?: ConfirmClassNames;
|
|
676
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
677
|
+
messages?: CancelFlowMessages;
|
|
516
678
|
}
|
|
517
679
|
interface SuccessStepProps {
|
|
518
680
|
outcome: 'saved' | 'cancelled';
|
|
519
681
|
offer?: OfferDecision;
|
|
682
|
+
/** The accepted offer (consumer-facing shape, incl. any accept result).
|
|
683
|
+
* Present when outcome is 'saved'. */
|
|
684
|
+
acceptedOffer?: AcceptedOffer;
|
|
520
685
|
title: string;
|
|
521
686
|
description?: string;
|
|
522
687
|
customer: DirectCustomer | null;
|
|
523
688
|
subscriptions: DirectSubscription[];
|
|
524
689
|
onClose: () => void;
|
|
525
690
|
classNames?: SuccessClassNames;
|
|
691
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
692
|
+
messages?: CancelFlowMessages;
|
|
526
693
|
}
|
|
527
694
|
interface ReasonButtonProps {
|
|
528
695
|
reason: ReasonConfig;
|
|
@@ -537,10 +704,21 @@ interface FlowState {
|
|
|
537
704
|
followupResponse: string;
|
|
538
705
|
feedback: string;
|
|
539
706
|
outcome: 'saved' | 'cancelled' | null;
|
|
707
|
+
/** The offer the customer accepted, set alongside outcome 'saved'. Drives
|
|
708
|
+
* per-offer success copy; `null` until then and for cancellations. */
|
|
709
|
+
acceptedOffer: AcceptedOffer | null;
|
|
540
710
|
isProcessing: boolean;
|
|
541
711
|
error: Error | null;
|
|
542
712
|
customer: DirectCustomer | null;
|
|
543
713
|
subscriptions: DirectSubscription[];
|
|
714
|
+
/**
|
|
715
|
+
* Effective cancel timing for this flow: the server-resolved value per
|
|
716
|
+
* blueprint (token mode), else the developer's `cancelAtPeriodEnd`
|
|
717
|
+
* declaration (local mode), else `null` — unknown. Drives timing-aware
|
|
718
|
+
* messages; `null` reads as period-end, matching the default the cancel
|
|
719
|
+
* action uses.
|
|
720
|
+
*/
|
|
721
|
+
cancelAtPeriodEnd: boolean | null;
|
|
544
722
|
}
|
|
545
723
|
declare const MODES: readonly ["live", "test", "sandbox"];
|
|
546
724
|
/**
|
|
@@ -574,6 +752,22 @@ interface FlowConfig extends FlowCallbacks {
|
|
|
574
752
|
* this field.
|
|
575
753
|
*/
|
|
576
754
|
mode?: Mode;
|
|
755
|
+
/**
|
|
756
|
+
* Locale and message overrides for the SDK's own strings (buttons, loading
|
|
757
|
+
* and confirmation chrome). Flow content — step titles, offer copy — is
|
|
758
|
+
* authored in the dashboard and localized server-side; override it there or
|
|
759
|
+
* via `steps`. Read once at mount, like `steps`.
|
|
760
|
+
*/
|
|
761
|
+
i18n?: I18nConfig;
|
|
762
|
+
/**
|
|
763
|
+
* Local-mode declaration of your billing behavior: does cancellation take
|
|
764
|
+
* effect at the end of the billing period (`true`) or immediately
|
|
765
|
+
* (`false`)? Drives timing-aware messages and the confirm step's
|
|
766
|
+
* access-until notice. In token mode this is ignored: the server-resolved
|
|
767
|
+
* value is authoritative, and if the server omits it the timing is treated
|
|
768
|
+
* as unknown rather than falling back to this declaration.
|
|
769
|
+
*/
|
|
770
|
+
cancelAtPeriodEnd?: boolean;
|
|
577
771
|
}
|
|
578
772
|
type OfferCallback = (offer: AcceptedOffer, customer: DirectCustomer | null) => Promise<void> | void;
|
|
579
773
|
type CancelCallback = (customer: DirectCustomer | null) => Promise<void> | void;
|
|
@@ -617,6 +811,10 @@ interface CancelFlowProps extends FlowCallbacks {
|
|
|
617
811
|
apiBaseUrl?: string;
|
|
618
812
|
/** See FlowConfig.mode. Ignored in token mode (the token is authoritative). */
|
|
619
813
|
mode?: Mode;
|
|
814
|
+
/** See FlowConfig.i18n. */
|
|
815
|
+
i18n?: I18nConfig;
|
|
816
|
+
/** See FlowConfig.cancelAtPeriodEnd. Local mode only; the token config wins. */
|
|
817
|
+
cancelAtPeriodEnd?: boolean;
|
|
620
818
|
appearance?: Appearance;
|
|
621
819
|
classNames?: StructuralClassNames;
|
|
622
820
|
components?: Partial<ComponentOverrides>;
|
|
@@ -649,4 +847,4 @@ interface ResolvedStep {
|
|
|
649
847
|
classNames?: unknown;
|
|
650
848
|
}
|
|
651
849
|
|
|
652
|
-
export type
|
|
850
|
+
export { type Step as $, type AcceptedOffer as A, type BackButtonProps as B, type CancelFlowProps as C, type DirectAddress as D, type FlowConfig as E, type FeedbackStepProps as F, type FlowState as G, type MessagesPatch as H, type I18nConfig as I, type Mode as J, type OfferClassNames as K, type OfferConfig as L, type ModalProps as M, type OfferCopy as N, type OfferStepProps as O, type OfferDecision as P, type OfferStep as Q, type ReasonButtonProps as R, type SuccessStepProps as S, type PauseOffer as T, type PlanChangeOffer as U, type PlanOption as V, type ReasonConfig as W, type RebateOffer as X, type RedirectOffer as Y, type ResolvedStep as Z, type SavedOfferCopy as _, type ConfirmStepProps as a, type StructuralClassNames as a0, type SubscriptionStatus as a1, type SuccessClassNames as a2, type SuccessStep as a3, type SurveyClassNames as a4, type SurveyStep as a5, type TimingAware as a6, type TimingVariants as a7, type TrialExtensionOffer as a8, buildMessages as a9, defaultMessages as aa, formatMessage as ab, selectTiming as ac, type SurveyStepProps as b, type CloseButtonProps as c, type Appearance as d, type AppearanceVariables as e, type BuiltInOfferConfig as f, type BuiltInStep as g, type BuiltInStepType as h, type CancelFlowMessages as i, type ComponentOverrides as j, type ConfirmClassNames as k, type ConfirmStep as l, type ContactOffer as m, type CustomComponents as n, type CustomOfferConfig as o, type CustomOfferProps as p, type CustomStepConfig as q, type CustomStepProps as r, type DirectCoupon as s, type DirectCustomer as t, type DirectPrice as u, type DirectSubscription as v, type DirectSubscriptionItem as w, type DiscountOffer as x, type FeedbackClassNames as y, type FeedbackStep as z };
|
|
@@ -1,5 +1,149 @@
|
|
|
1
1
|
import { ReactNode, ReactElement, ComponentType } from 'react';
|
|
2
2
|
|
|
3
|
+
/** Both timing variants of a message. Which one renders is decided by the
|
|
4
|
+
* flow's effective cancel timing (`FlowState.cancelAtPeriodEnd`). */
|
|
5
|
+
interface TimingVariants {
|
|
6
|
+
immediate: string;
|
|
7
|
+
atPeriodEnd: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A message that may differ by cancellation timing. Plain strings render
|
|
11
|
+
* as-is in both cases; pass the object form when the wording should change
|
|
12
|
+
* (e.g. "Cancel" vs "Turn off auto-renew").
|
|
13
|
+
*/
|
|
14
|
+
type TimingAware = string | TimingVariants;
|
|
15
|
+
/** Success-screen copy for one accepted offer type. */
|
|
16
|
+
interface SavedOfferCopy {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
interface CancelFlowMessages {
|
|
21
|
+
common: {
|
|
22
|
+
continue: string;
|
|
23
|
+
back: string;
|
|
24
|
+
close: string;
|
|
25
|
+
done: string;
|
|
26
|
+
processing: string;
|
|
27
|
+
tryAgain: string;
|
|
28
|
+
loading: string;
|
|
29
|
+
loadError: string;
|
|
30
|
+
error: string;
|
|
31
|
+
day: string;
|
|
32
|
+
days: string;
|
|
33
|
+
month: string;
|
|
34
|
+
months: string;
|
|
35
|
+
};
|
|
36
|
+
survey: {
|
|
37
|
+
title: string;
|
|
38
|
+
followupPlaceholder: string;
|
|
39
|
+
followupAriaLabel: string;
|
|
40
|
+
};
|
|
41
|
+
feedback: {
|
|
42
|
+
title: string;
|
|
43
|
+
placeholder: string;
|
|
44
|
+
/** Used instead of `placeholder` when the step sets `minLength`. Supports `{minLength}`. */
|
|
45
|
+
placeholderWithMin: string;
|
|
46
|
+
};
|
|
47
|
+
confirm: {
|
|
48
|
+
title: string;
|
|
49
|
+
lossesLabel: string;
|
|
50
|
+
cta: TimingAware;
|
|
51
|
+
goBack: string;
|
|
52
|
+
/** Shown between the description and the confirm button. Supports
|
|
53
|
+
* `{periodEnd}`. The default immediate variant is empty, which
|
|
54
|
+
* suppresses the notice for immediate timing; empty OVERRIDE values are
|
|
55
|
+
* dropped like any blank override, so to remove the notice entirely
|
|
56
|
+
* replace the `Confirm` component or hide it via
|
|
57
|
+
* `classNames.periodEndNotice`. */
|
|
58
|
+
periodEndNotice: TimingAware;
|
|
59
|
+
};
|
|
60
|
+
success: {
|
|
61
|
+
saved: {
|
|
62
|
+
/** Generic fallback — used for offer types without their own entry
|
|
63
|
+
* (contact, redirect, custom types). */
|
|
64
|
+
title: string;
|
|
65
|
+
description: string;
|
|
66
|
+
discount: SavedOfferCopy;
|
|
67
|
+
pause: SavedOfferCopy & {
|
|
68
|
+
/** Used instead of `description` when the resume date is known
|
|
69
|
+
* (the built-in pause UI always supplies it). Supports `{resumeDate}`. */
|
|
70
|
+
resumeDescription: string;
|
|
71
|
+
};
|
|
72
|
+
trial_extension: SavedOfferCopy;
|
|
73
|
+
plan_change: SavedOfferCopy;
|
|
74
|
+
rebate: SavedOfferCopy;
|
|
75
|
+
};
|
|
76
|
+
cancelled: {
|
|
77
|
+
title: TimingAware;
|
|
78
|
+
description: TimingAware;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
offer: {
|
|
82
|
+
limitedTimeEyebrow: string;
|
|
83
|
+
pauseEyebrow: string;
|
|
84
|
+
newEndDateLabel: string;
|
|
85
|
+
currentPlanBadge: string;
|
|
86
|
+
/** CTA when a plan is selected in a plan-change offer. Supports `{planName}`. */
|
|
87
|
+
switchToCta: string;
|
|
88
|
+
/**
|
|
89
|
+
* Accept-button override per offer type. Defaults are empty: an unset
|
|
90
|
+
* key falls through to the offer's own `copy.cta` (today a
|
|
91
|
+
* server-synthesized default like "Accept refund"; authored per-offer
|
|
92
|
+
* copy, when it exists, keeps winning over an unset key).
|
|
93
|
+
*/
|
|
94
|
+
acceptCta: {
|
|
95
|
+
discount: string;
|
|
96
|
+
pause: string;
|
|
97
|
+
trial_extension: string;
|
|
98
|
+
plan_change: string;
|
|
99
|
+
contact: string;
|
|
100
|
+
redirect: string;
|
|
101
|
+
rebate: string;
|
|
102
|
+
};
|
|
103
|
+
/** Decline-link override for every offer type. Empty default falls
|
|
104
|
+
* through to the offer's own `copy.declineCta`. */
|
|
105
|
+
declineCta: string;
|
|
106
|
+
rebate: {
|
|
107
|
+
paidLabel: string;
|
|
108
|
+
moneyBackLabel: string;
|
|
109
|
+
/** Tax parenthetical after the money-back label. Supports `{amount}`. */
|
|
110
|
+
inclTax: string;
|
|
111
|
+
netLabel: string;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Recursive partial of the catalog for overrides. Timing-aware fields also
|
|
117
|
+
* accept a plain string (applies to both timings) or a partial variant pair
|
|
118
|
+
* (the missing variant keeps the base value).
|
|
119
|
+
*/
|
|
120
|
+
type MessagesPatch<T = CancelFlowMessages> = {
|
|
121
|
+
[K in keyof T]?: T[K] extends string ? string : T[K] extends TimingAware ? string | Partial<TimingVariants> : MessagesPatch<T[K]>;
|
|
122
|
+
};
|
|
123
|
+
interface I18nConfig {
|
|
124
|
+
/** UI language. Defaults to the browser language, falling back to `'en'`. */
|
|
125
|
+
locale?: string;
|
|
126
|
+
/** Per-locale message overrides, e.g. `{ en: { confirm: { cta: 'Turn off auto-renew' } } }`. */
|
|
127
|
+
messages?: Record<string, MessagesPatch>;
|
|
128
|
+
}
|
|
129
|
+
declare const defaultMessages: CancelFlowMessages;
|
|
130
|
+
/**
|
|
131
|
+
* Pick the string for the flow's effective cancel timing. `null` (timing
|
|
132
|
+
* unknown — local mode) behaves like period-end, matching the default the
|
|
133
|
+
* cancel action itself uses.
|
|
134
|
+
*/
|
|
135
|
+
declare function selectTiming(value: TimingAware, cancelAtPeriodEnd: boolean | null): string;
|
|
136
|
+
/** Replace `{name}` tokens. Unknown tokens are left in place. */
|
|
137
|
+
declare function formatMessage(template: string, vars: Record<string, string | number>): string;
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the full catalog for a locale. Layers, lowest to highest: built-in
|
|
140
|
+
* defaults, then `orgMessages` (dashboard-configured overrides delivered on
|
|
141
|
+
* the token-mode config), then the config's own per-locale messages — the
|
|
142
|
+
* developer's overrides always end up on top. Both per-language layers run
|
|
143
|
+
* through the same locale fallback chain.
|
|
144
|
+
*/
|
|
145
|
+
declare function buildMessages(i18n?: I18nConfig, orgMessages?: Record<string, MessagesPatch>): CancelFlowMessages;
|
|
146
|
+
|
|
3
147
|
interface DirectAddress {
|
|
4
148
|
line1?: string;
|
|
5
149
|
line2?: string;
|
|
@@ -444,10 +588,14 @@ interface ModalProps {
|
|
|
444
588
|
interface CloseButtonProps {
|
|
445
589
|
onClose: () => void;
|
|
446
590
|
className?: string;
|
|
591
|
+
/** Accessible label. Defaults to "Close". */
|
|
592
|
+
label?: string;
|
|
447
593
|
}
|
|
448
594
|
interface BackButtonProps {
|
|
449
595
|
onBack: () => void;
|
|
450
596
|
className?: string;
|
|
597
|
+
/** Visible label. Defaults to "Back". */
|
|
598
|
+
label?: string;
|
|
451
599
|
}
|
|
452
600
|
interface SurveyStepProps {
|
|
453
601
|
title: string;
|
|
@@ -464,6 +612,8 @@ interface SurveyStepProps {
|
|
|
464
612
|
onNext: () => void;
|
|
465
613
|
classNames?: SurveyClassNames;
|
|
466
614
|
components?: Partial<ComponentOverrides>;
|
|
615
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
616
|
+
messages?: CancelFlowMessages;
|
|
467
617
|
}
|
|
468
618
|
interface OfferStepProps {
|
|
469
619
|
title?: string;
|
|
@@ -486,6 +636,8 @@ interface OfferStepProps {
|
|
|
486
636
|
* implementations can ignore this.
|
|
487
637
|
*/
|
|
488
638
|
components?: Partial<ComponentOverrides>;
|
|
639
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
640
|
+
messages?: CancelFlowMessages;
|
|
489
641
|
}
|
|
490
642
|
interface FeedbackStepProps {
|
|
491
643
|
title: string;
|
|
@@ -499,6 +651,8 @@ interface FeedbackStepProps {
|
|
|
499
651
|
onChange: (text: string) => void;
|
|
500
652
|
onSubmit: () => void;
|
|
501
653
|
classNames?: FeedbackClassNames;
|
|
654
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
655
|
+
messages?: CancelFlowMessages;
|
|
502
656
|
}
|
|
503
657
|
interface ConfirmStepProps {
|
|
504
658
|
title: string;
|
|
@@ -509,20 +663,33 @@ interface ConfirmStepProps {
|
|
|
509
663
|
lossesLabel?: string;
|
|
510
664
|
confirmLabel: string;
|
|
511
665
|
goBackLabel: string;
|
|
666
|
+
/**
|
|
667
|
+
* Timing-resolved access notice ("Your access continues until June 14, 2026."),
|
|
668
|
+
* rendered between the loss list and the confirm button. Absent when the
|
|
669
|
+
* period end is unknown or the resolved message is empty.
|
|
670
|
+
*/
|
|
671
|
+
periodEndNotice?: string;
|
|
512
672
|
onConfirm: () => Promise<void>;
|
|
513
673
|
onGoBack: () => void;
|
|
514
674
|
isProcessing: boolean;
|
|
515
675
|
classNames?: ConfirmClassNames;
|
|
676
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
677
|
+
messages?: CancelFlowMessages;
|
|
516
678
|
}
|
|
517
679
|
interface SuccessStepProps {
|
|
518
680
|
outcome: 'saved' | 'cancelled';
|
|
519
681
|
offer?: OfferDecision;
|
|
682
|
+
/** The accepted offer (consumer-facing shape, incl. any accept result).
|
|
683
|
+
* Present when outcome is 'saved'. */
|
|
684
|
+
acceptedOffer?: AcceptedOffer;
|
|
520
685
|
title: string;
|
|
521
686
|
description?: string;
|
|
522
687
|
customer: DirectCustomer | null;
|
|
523
688
|
subscriptions: DirectSubscription[];
|
|
524
689
|
onClose: () => void;
|
|
525
690
|
classNames?: SuccessClassNames;
|
|
691
|
+
/** Resolved message catalog. Default components fall back to `defaultMessages` when absent. */
|
|
692
|
+
messages?: CancelFlowMessages;
|
|
526
693
|
}
|
|
527
694
|
interface ReasonButtonProps {
|
|
528
695
|
reason: ReasonConfig;
|
|
@@ -537,10 +704,21 @@ interface FlowState {
|
|
|
537
704
|
followupResponse: string;
|
|
538
705
|
feedback: string;
|
|
539
706
|
outcome: 'saved' | 'cancelled' | null;
|
|
707
|
+
/** The offer the customer accepted, set alongside outcome 'saved'. Drives
|
|
708
|
+
* per-offer success copy; `null` until then and for cancellations. */
|
|
709
|
+
acceptedOffer: AcceptedOffer | null;
|
|
540
710
|
isProcessing: boolean;
|
|
541
711
|
error: Error | null;
|
|
542
712
|
customer: DirectCustomer | null;
|
|
543
713
|
subscriptions: DirectSubscription[];
|
|
714
|
+
/**
|
|
715
|
+
* Effective cancel timing for this flow: the server-resolved value per
|
|
716
|
+
* blueprint (token mode), else the developer's `cancelAtPeriodEnd`
|
|
717
|
+
* declaration (local mode), else `null` — unknown. Drives timing-aware
|
|
718
|
+
* messages; `null` reads as period-end, matching the default the cancel
|
|
719
|
+
* action uses.
|
|
720
|
+
*/
|
|
721
|
+
cancelAtPeriodEnd: boolean | null;
|
|
544
722
|
}
|
|
545
723
|
declare const MODES: readonly ["live", "test", "sandbox"];
|
|
546
724
|
/**
|
|
@@ -574,6 +752,22 @@ interface FlowConfig extends FlowCallbacks {
|
|
|
574
752
|
* this field.
|
|
575
753
|
*/
|
|
576
754
|
mode?: Mode;
|
|
755
|
+
/**
|
|
756
|
+
* Locale and message overrides for the SDK's own strings (buttons, loading
|
|
757
|
+
* and confirmation chrome). Flow content — step titles, offer copy — is
|
|
758
|
+
* authored in the dashboard and localized server-side; override it there or
|
|
759
|
+
* via `steps`. Read once at mount, like `steps`.
|
|
760
|
+
*/
|
|
761
|
+
i18n?: I18nConfig;
|
|
762
|
+
/**
|
|
763
|
+
* Local-mode declaration of your billing behavior: does cancellation take
|
|
764
|
+
* effect at the end of the billing period (`true`) or immediately
|
|
765
|
+
* (`false`)? Drives timing-aware messages and the confirm step's
|
|
766
|
+
* access-until notice. In token mode this is ignored: the server-resolved
|
|
767
|
+
* value is authoritative, and if the server omits it the timing is treated
|
|
768
|
+
* as unknown rather than falling back to this declaration.
|
|
769
|
+
*/
|
|
770
|
+
cancelAtPeriodEnd?: boolean;
|
|
577
771
|
}
|
|
578
772
|
type OfferCallback = (offer: AcceptedOffer, customer: DirectCustomer | null) => Promise<void> | void;
|
|
579
773
|
type CancelCallback = (customer: DirectCustomer | null) => Promise<void> | void;
|
|
@@ -617,6 +811,10 @@ interface CancelFlowProps extends FlowCallbacks {
|
|
|
617
811
|
apiBaseUrl?: string;
|
|
618
812
|
/** See FlowConfig.mode. Ignored in token mode (the token is authoritative). */
|
|
619
813
|
mode?: Mode;
|
|
814
|
+
/** See FlowConfig.i18n. */
|
|
815
|
+
i18n?: I18nConfig;
|
|
816
|
+
/** See FlowConfig.cancelAtPeriodEnd. Local mode only; the token config wins. */
|
|
817
|
+
cancelAtPeriodEnd?: boolean;
|
|
620
818
|
appearance?: Appearance;
|
|
621
819
|
classNames?: StructuralClassNames;
|
|
622
820
|
components?: Partial<ComponentOverrides>;
|
|
@@ -649,4 +847,4 @@ interface ResolvedStep {
|
|
|
649
847
|
classNames?: unknown;
|
|
650
848
|
}
|
|
651
849
|
|
|
652
|
-
export type
|
|
850
|
+
export { type Step as $, type AcceptedOffer as A, type BackButtonProps as B, type CancelFlowProps as C, type DirectAddress as D, type FlowConfig as E, type FeedbackStepProps as F, type FlowState as G, type MessagesPatch as H, type I18nConfig as I, type Mode as J, type OfferClassNames as K, type OfferConfig as L, type ModalProps as M, type OfferCopy as N, type OfferStepProps as O, type OfferDecision as P, type OfferStep as Q, type ReasonButtonProps as R, type SuccessStepProps as S, type PauseOffer as T, type PlanChangeOffer as U, type PlanOption as V, type ReasonConfig as W, type RebateOffer as X, type RedirectOffer as Y, type ResolvedStep as Z, type SavedOfferCopy as _, type ConfirmStepProps as a, type StructuralClassNames as a0, type SubscriptionStatus as a1, type SuccessClassNames as a2, type SuccessStep as a3, type SurveyClassNames as a4, type SurveyStep as a5, type TimingAware as a6, type TimingVariants as a7, type TrialExtensionOffer as a8, buildMessages as a9, defaultMessages as aa, formatMessage as ab, selectTiming as ac, type SurveyStepProps as b, type CloseButtonProps as c, type Appearance as d, type AppearanceVariables as e, type BuiltInOfferConfig as f, type BuiltInStep as g, type BuiltInStepType as h, type CancelFlowMessages as i, type ComponentOverrides as j, type ConfirmClassNames as k, type ConfirmStep as l, type ContactOffer as m, type CustomComponents as n, type CustomOfferConfig as o, type CustomOfferProps as p, type CustomStepConfig as q, type CustomStepProps as r, type DirectCoupon as s, type DirectCustomer as t, type DirectPrice as u, type DirectSubscription as v, type DirectSubscriptionItem as w, type DiscountOffer as x, type FeedbackClassNames as y, type FeedbackStep as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@churnkey/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Production-ready cancel flow for React. Drop-in component, headless hook, or full customization. Works standalone or with Churnkey for AI-powered retention.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/format.ts"],"names":[],"mappings":"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","file":"chunk-EYIGZLVT.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"]}
|