@churnkey/react 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-YSNMTOAW.cjs → chunk-4BMB42WI.cjs} +9 -7
- package/dist/chunk-4BMB42WI.cjs.map +1 -0
- package/dist/{chunk-EHZJAQQ5.cjs → chunk-BDBZ5OS3.cjs} +21 -2
- package/dist/chunk-BDBZ5OS3.cjs.map +1 -0
- package/dist/{chunk-CPCWJTSG.js → chunk-F4ESJBOS.js} +6 -4
- package/dist/chunk-F4ESJBOS.js.map +1 -0
- package/dist/{chunk-SXGORJOX.js → chunk-G57YLYM5.js} +20 -3
- package/dist/chunk-G57YLYM5.js.map +1 -0
- package/dist/{chunk-OL5HQUE4.js → chunk-QYNW3HF3.js} +66 -26
- package/dist/chunk-QYNW3HF3.js.map +1 -0
- package/dist/{chunk-ZEYUKGAR.cjs → chunk-SNTKIKHU.cjs} +66 -26
- package/dist/chunk-SNTKIKHU.cjs.map +1 -0
- package/dist/core.cjs +20 -12
- package/dist/core.d.cts +30 -5
- package/dist/core.d.ts +30 -5
- package/dist/core.js +2 -2
- package/dist/headless.cjs +3 -3
- package/dist/headless.d.cts +5 -1
- package/dist/headless.d.ts +5 -1
- package/dist/headless.js +2 -2
- package/dist/index.cjs +218 -135
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -8
- package/dist/index.d.ts +10 -8
- package/dist/index.js +125 -51
- package/dist/index.js.map +1 -1
- package/dist/{step-graph-Cbi0DRHX.d.cts → step-graph-DKvaV3Dg.d.cts} +56 -21
- package/dist/{step-graph-Cbi0DRHX.d.ts → step-graph-DKvaV3Dg.d.ts} +56 -21
- package/dist/styles.css +95 -19
- package/package.json +10 -10
- package/dist/chunk-CPCWJTSG.js.map +0 -1
- package/dist/chunk-EHZJAQQ5.cjs.map +0 -1
- package/dist/chunk-OL5HQUE4.js.map +0 -1
- package/dist/chunk-SXGORJOX.js.map +0 -1
- package/dist/chunk-YSNMTOAW.cjs.map +0 -1
- package/dist/chunk-ZEYUKGAR.cjs.map +0 -1
- package/dist/styles.css.d.ts +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkSNTKIKHU_cjs = require('./chunk-SNTKIKHU.cjs');
|
|
4
4
|
var react = require('react');
|
|
5
5
|
|
|
6
6
|
function useCancelFlowMachine(config) {
|
|
@@ -23,7 +23,7 @@ function useCancelFlowMachine(config) {
|
|
|
23
23
|
onClose: () => cb.current.onClose?.(),
|
|
24
24
|
onStepChange: (step, prevStep) => cb.current.onStepChange?.(step, prevStep)
|
|
25
25
|
};
|
|
26
|
-
return new
|
|
26
|
+
return new chunkSNTKIKHU_cjs.CancelFlowMachine({ ...config, ...dispatch });
|
|
27
27
|
});
|
|
28
28
|
const [state, setState] = react.useState(() => machine.getSnapshot());
|
|
29
29
|
const [isLoading, setIsLoading] = react.useState(!!config.session);
|
|
@@ -37,8 +37,8 @@ function useCancelFlowMachine(config) {
|
|
|
37
37
|
setLoadError(null);
|
|
38
38
|
setIsLoading(true);
|
|
39
39
|
let cancelled = false;
|
|
40
|
-
const creds =
|
|
41
|
-
const api = new
|
|
40
|
+
const creds = chunkSNTKIKHU_cjs.decodeSessionToken(config.session);
|
|
41
|
+
const api = new chunkSNTKIKHU_cjs.ChurnkeyApi(creds, config.apiBaseUrl);
|
|
42
42
|
api.fetchConfig().then((sdkConfig) => {
|
|
43
43
|
if (cancelled) return;
|
|
44
44
|
machine.initializeFromConfig(sdkConfig, api, creds);
|
|
@@ -61,17 +61,19 @@ function useCancelFlowMachine(config) {
|
|
|
61
61
|
|
|
62
62
|
// src/headless/use-cancel-flow.ts
|
|
63
63
|
function useCancelFlow(config) {
|
|
64
|
-
const { machine, state, isLoading, loadError } = useCancelFlowMachine(config);
|
|
64
|
+
const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(config);
|
|
65
65
|
return {
|
|
66
66
|
...state,
|
|
67
67
|
isLoading,
|
|
68
68
|
loadError,
|
|
69
|
+
retry,
|
|
69
70
|
reasons: machine.reasons,
|
|
70
71
|
currentStep: machine.currentStep,
|
|
71
72
|
currentOffer: machine.currentOffer,
|
|
72
73
|
stepIndex: machine.stepIndex,
|
|
73
74
|
totalSteps: machine.totalSteps,
|
|
74
75
|
selectReason: machine.selectReason,
|
|
76
|
+
setFollowupResponse: machine.setFollowupResponse,
|
|
75
77
|
setFeedback: machine.setFeedback,
|
|
76
78
|
accept: machine.accept,
|
|
77
79
|
decline: machine.decline,
|
|
@@ -84,5 +86,5 @@ function useCancelFlow(config) {
|
|
|
84
86
|
|
|
85
87
|
exports.useCancelFlow = useCancelFlow;
|
|
86
88
|
exports.useCancelFlowMachine = useCancelFlowMachine;
|
|
87
|
-
//# sourceMappingURL=chunk-
|
|
88
|
-
//# sourceMappingURL=chunk-
|
|
89
|
+
//# sourceMappingURL=chunk-4BMB42WI.cjs.map
|
|
90
|
+
//# sourceMappingURL=chunk-4BMB42WI.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/headless/use-cancel-flow-machine.ts","../src/headless/use-cancel-flow.ts"],"names":["useRef","useState","CancelFlowMachine","useEffect","useCallback","decodeSessionToken","ChurnkeyApi"],"mappings":"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","file":"chunk-4BMB42WI.cjs","sourcesContent":["// Internal — shared by `useCancelFlow` (headless) and the `CancelFlow`\n// component. Lives under headless/ since it's the headless layer of the\n// SDK; the component imports it because there's no separate public/private\n// split worth maintaining for one shared file.\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { ChurnkeyApi } from '../core/api'\nimport { CancelFlowMachine } from '../core/machine'\nimport { decodeSessionToken } from '../core/token'\nimport type { FlowCallbacks, FlowConfig, FlowState } from '../core/types'\n\nexport interface CancelFlowMachineHandle {\n machine: CancelFlowMachine\n state: FlowState\n isLoading: boolean\n loadError: Error | null\n /** Re-fetch the flow config (token mode only). No-op without a session. */\n retry: () => void\n}\n\n/**\n * Wires a CancelFlowMachine to React lifecycle. Used internally by both the\n * `CancelFlow` component and the `useCancelFlow` hook — not part of the\n * public API.\n *\n * Callbacks reach the machine via thunks that dereference a ref updated each\n * render. This buys two things: the consumer's latest closure always runs,\n * and the fetch effect's dep list stays stable so inline-arrow handlers\n * don't trigger a re-fetch and reset the flow to step 1.\n */\nexport function useCancelFlowMachine(config: FlowConfig): CancelFlowMachineHandle {\n // FlowConfig extends FlowCallbacks, so storing the whole config gives us\n // access to every callback by name without a separate copy.\n const callbacksRef = useRef(config)\n callbacksRef.current = config\n\n const [machine] = useState(() => {\n const cb = callbacksRef\n const dispatch: FlowCallbacks = {\n handleDiscount: (o, c) => cb.current.handleDiscount?.(o, c),\n handlePause: (o, c) => cb.current.handlePause?.(o, c),\n handlePlanChange: (o, c) => cb.current.handlePlanChange?.(o, c),\n handleTrialExtension: (o, c) => cb.current.handleTrialExtension?.(o, c),\n handleCancel: (c) => cb.current.handleCancel?.(c),\n onAccept: (o, c) => cb.current.onAccept?.(o, c),\n onDiscount: (o, c) => cb.current.onDiscount?.(o, c),\n onPause: (o, c) => cb.current.onPause?.(o, c),\n onPlanChange: (o, c) => cb.current.onPlanChange?.(o, c),\n onTrialExtension: (o, c) => cb.current.onTrialExtension?.(o, c),\n onCancel: (c) => cb.current.onCancel?.(c),\n onClose: () => cb.current.onClose?.(),\n onStepChange: (step, prevStep) => cb.current.onStepChange?.(step, prevStep),\n }\n return new CancelFlowMachine({ ...config, ...dispatch })\n })\n\n const [state, setState] = useState<FlowState>(() => machine.getSnapshot())\n const [isLoading, setIsLoading] = useState(!!config.session)\n const [loadError, setLoadError] = useState<Error | null>(null)\n\n useEffect(() => {\n setState(machine.getSnapshot())\n return machine.subscribe(() => setState(machine.getSnapshot()))\n }, [machine])\n\n const loadConfig = useCallback(() => {\n if (!config.session) return\n setLoadError(null)\n setIsLoading(true)\n let cancelled = false\n const creds = decodeSessionToken(config.session)\n const api = new ChurnkeyApi(creds, config.apiBaseUrl)\n api\n .fetchConfig()\n .then((sdkConfig) => {\n if (cancelled) return\n machine.initializeFromConfig(sdkConfig, api, creds)\n setIsLoading(false)\n })\n .catch((err) => {\n if (cancelled) return\n setLoadError(err instanceof Error ? err : new Error(String(err)))\n setIsLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [config.session, config.apiBaseUrl, machine])\n\n useEffect(() => {\n if (!config.session) return\n return loadConfig()\n }, [config.session, loadConfig])\n\n return { machine, state, isLoading, loadError, retry: loadConfig }\n}\n","import type { FlowConfig } from '../core/types'\nimport { useCancelFlowMachine } from './use-cancel-flow-machine'\n\nexport function useCancelFlow(config: FlowConfig) {\n const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(config)\n\n return {\n ...state,\n isLoading,\n loadError,\n retry,\n reasons: machine.reasons,\n currentStep: machine.currentStep,\n currentOffer: machine.currentOffer,\n stepIndex: machine.stepIndex,\n totalSteps: machine.totalSteps,\n selectReason: machine.selectReason,\n setFollowupResponse: machine.setFollowupResponse,\n setFeedback: machine.setFeedback,\n accept: machine.accept,\n decline: machine.decline,\n cancel: machine.cancel,\n next: machine.next,\n back: machine.back,\n close: machine.close,\n }\n}\n"]}
|
|
@@ -58,6 +58,14 @@ function formatMonthDay(date, locale) {
|
|
|
58
58
|
function formatMonthDayLong(date, locale) {
|
|
59
59
|
return date.toLocaleDateString(locale, { month: "long", day: "numeric" });
|
|
60
60
|
}
|
|
61
|
+
function formatPeriodEnd(subscriptions, locale) {
|
|
62
|
+
const status = subscriptions?.[0]?.status;
|
|
63
|
+
if (!status || !("currentPeriod" in status) || !status.currentPeriod?.end) return null;
|
|
64
|
+
const end = status.currentPeriod.end;
|
|
65
|
+
const d = end instanceof Date ? end : new Date(end);
|
|
66
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
67
|
+
return d.toLocaleDateString(locale, { month: "long", day: "numeric", year: "numeric" });
|
|
68
|
+
}
|
|
61
69
|
function discountPhrase(opts) {
|
|
62
70
|
const { percentOff, amountOff, currency, duration, durationInMonths } = opts;
|
|
63
71
|
const amount = percentOff != null ? `${percentOff}% off` : amountOff != null ? `${formatPriceFromMinor(amountOff, currency)} off` : "discount";
|
|
@@ -67,6 +75,15 @@ function discountPhrase(opts) {
|
|
|
67
75
|
|
|
68
76
|
// src/core/utils.ts
|
|
69
77
|
var BUILT_IN_STEP_TYPES = ["survey", "offer", "feedback", "confirm", "success"];
|
|
78
|
+
var BUILT_IN_OFFER_TYPES = [
|
|
79
|
+
"discount",
|
|
80
|
+
"pause",
|
|
81
|
+
"trial_extension",
|
|
82
|
+
"plan_change",
|
|
83
|
+
"contact",
|
|
84
|
+
"redirect",
|
|
85
|
+
"rebate"
|
|
86
|
+
];
|
|
70
87
|
function cn(...classes) {
|
|
71
88
|
return classes.filter(Boolean).join(" ");
|
|
72
89
|
}
|
|
@@ -127,6 +144,7 @@ var defaultTitles = {
|
|
|
127
144
|
confirm: "Confirm cancellation"
|
|
128
145
|
};
|
|
129
146
|
|
|
147
|
+
exports.BUILT_IN_OFFER_TYPES = BUILT_IN_OFFER_TYPES;
|
|
130
148
|
exports.BUILT_IN_STEP_TYPES = BUILT_IN_STEP_TYPES;
|
|
131
149
|
exports.appearanceToStyle = appearanceToStyle;
|
|
132
150
|
exports.calculateDiscountedPrice = calculateDiscountedPrice;
|
|
@@ -135,7 +153,8 @@ exports.defaultTitles = defaultTitles;
|
|
|
135
153
|
exports.discountPhrase = discountPhrase;
|
|
136
154
|
exports.formatMonthDay = formatMonthDay;
|
|
137
155
|
exports.formatMonthDayLong = formatMonthDayLong;
|
|
156
|
+
exports.formatPeriodEnd = formatPeriodEnd;
|
|
138
157
|
exports.formatPrice = formatPrice;
|
|
139
158
|
exports.formatPriceFromMinor = formatPriceFromMinor;
|
|
140
|
-
//# sourceMappingURL=chunk-
|
|
141
|
-
//# sourceMappingURL=chunk-
|
|
159
|
+
//# sourceMappingURL=chunk-BDBZ5OS3.cjs.map
|
|
160
|
+
//# sourceMappingURL=chunk-BDBZ5OS3.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/format.ts","../src/core/utils.ts"],"names":[],"mappings":"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","file":"chunk-BDBZ5OS3.cjs","sourcesContent":["import type { DirectSubscription } from './types'\n\n// Currencies whose smallest unit equals one major unit (no fractional part).\n// Stripe and most billing providers store these without an implicit /100.\nconst ZERO_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n])\n\n// Middle-Eastern currencies that use three fractional digits (1000 divisor).\n// Without this bucket, JOD/KWD amounts would be off by 10x in display.\nconst THREE_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'])\n\nexport function isZeroDecimalCurrency(currency: string): boolean {\n return ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase())\n}\n\nexport function isThreeDecimalCurrency(currency: string): boolean {\n return THREE_DECIMAL_CURRENCIES.has(currency.toUpperCase())\n}\n\n/**\n * Smallest-unit divisor for a currency. 1 for zero-decimal (JPY), 1000 for\n * three-decimal (KWD), 100 for everything else. Used to convert provider\n * amounts (which are always in the smallest unit) into the major unit for\n * display.\n */\nexport function getCurrencyDivisor(currency = 'USD'): number {\n if (isZeroDecimalCurrency(currency)) return 1\n if (isThreeDecimalCurrency(currency)) return 1000\n return 100\n}\n\n/**\n * Converts a Direct.Price.amount.value (smallest currency unit) into the\n * major unit used for display.\n */\nexport function convertFromMinorUnits(value: number, currency = 'USD'): number {\n return value / getCurrencyDivisor(currency)\n}\n\n/**\n * Format an amount already in the major unit. Uses `narrowSymbol` so output\n * reads \"$29\" rather than \"US$29\", and trims trailing zero fractions so\n * whole amounts render compactly.\n */\nexport function formatPrice(amount: number, currency = 'USD', locale?: string): string {\n try {\n const formatted = new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n }).format(amount)\n return formatted.replace(/[.,]00$/, '')\n } catch {\n return `${currency} ${amount.toFixed(2)}`\n }\n}\n\n/** Convenience: format a Direct-shape minor-unit amount in one call. */\nexport function formatPriceFromMinor(value: number, currency = 'USD', locale?: string): string {\n return formatPrice(convertFromMinorUnits(value, currency), currency, locale)\n}\n\nexport function calculateDiscountedPrice(originalPrice: number, percentOff: number): number {\n return originalPrice * (1 - percentOff / 100)\n}\n\n// ─── Dates ────────────────────────────────────────────────────────────────\n\n/** \"Apr 30, 2026\" — used for resume dates and other full-date contexts. */\nexport function formatShortDate(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })\n}\n\n/** \"Apr 30\" — used when the year is implied by context. */\nexport function formatMonthDay(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric' })\n}\n\n/** \"April 30\" — long month form for prominent date displays. */\nexport function formatMonthDayLong(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'long', day: 'numeric' })\n}\n\n/**\n * Long-form access period end (\"June 14, 2026\") for the first subscription's\n * current period. Returns null for canceled subscriptions, missing periods,\n * and unparseable dates so Confirm can drop the \"access continues until\"\n * notice cleanly instead of rendering \"Invalid Date\".\n */\nexport function formatPeriodEnd(\n subscriptions: DirectSubscription[] | null | undefined,\n locale?: string,\n): string | null {\n const status = subscriptions?.[0]?.status\n if (!status || !('currentPeriod' in status) || !status.currentPeriod?.end) return null\n const end = status.currentPeriod.end\n const d = end instanceof Date ? end : new Date(end)\n if (Number.isNaN(d.getTime())) return null\n return d.toLocaleDateString(locale, { month: 'long', day: 'numeric', year: 'numeric' })\n}\n\n// ─── Discount phrasing ─────────────────────────────────────────────────────\n\n/**\n * Renders the discount as a single human-readable sentence:\n * \"25% off for 3 months\" / \"$5 off your next renewal\" / \"20% off for life\"\n *\n * Mirrors the embed's couponToString helper so SDK and embed use the same\n * phrasing for the same offer shape.\n */\nexport function discountPhrase(opts: {\n percentOff?: number\n /** Smallest currency unit (cents for USD). */\n amountOff?: number\n currency?: string\n duration?: 'once' | 'repeating' | 'forever'\n durationInMonths?: number\n}): string {\n const { percentOff, amountOff, currency, duration, durationInMonths } = opts\n const amount =\n percentOff != null\n ? `${percentOff}% off`\n : amountOff != null\n ? `${formatPriceFromMinor(amountOff, currency)} off`\n : 'discount'\n const tail =\n duration === 'once'\n ? 'your next renewal'\n : duration === 'forever'\n ? 'for life'\n : durationInMonths\n ? `for ${durationInMonths} ${durationInMonths === 1 ? 'month' : 'months'}`\n : ''\n return tail ? `${amount} ${tail}` : amount\n}\n","import type { CSSProperties } from 'react'\nimport type { Appearance, AppearanceVariables } from './types'\n\nexport const BUILT_IN_STEP_TYPES: readonly string[] = ['survey', 'offer', 'feedback', 'confirm', 'success']\nexport const BUILT_IN_OFFER_TYPES: readonly string[] = [\n 'discount',\n 'pause',\n 'trial_extension',\n 'plan_change',\n 'contact',\n 'redirect',\n 'rebate',\n]\n\nexport function cn(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(' ')\n}\n\n// Maps the typed `appearance.variables` field names to the underlying\n// CSS custom properties. Light/dark variants live in CSS — these\n// overrides apply on top of whichever scheme is active.\nconst VAR_MAP: Record<string, keyof AppearanceVariables> = {\n // Surfaces\n '--ck-color-bg': 'colorBackground',\n '--ck-color-surface': 'colorSurface',\n '--ck-color-surface-muted': 'colorSurfaceMuted',\n\n // Borders\n '--ck-color-border': 'colorBorder',\n '--ck-color-border-strong': 'colorBorderStrong',\n\n // Text\n '--ck-color-text': 'colorText',\n '--ck-color-text-secondary': 'colorTextSecondary',\n '--ck-color-text-muted': 'colorTextMuted',\n\n // Primary\n '--ck-color-primary': 'colorPrimary',\n '--ck-color-primary-hover': 'colorPrimaryHover',\n '--ck-color-primary-soft': 'colorPrimarySoft',\n\n // Semantic\n '--ck-color-success': 'colorSuccess',\n '--ck-color-success-soft': 'colorSuccessSoft',\n '--ck-color-danger': 'colorDanger',\n '--ck-color-danger-hover': 'colorDangerHover',\n '--ck-color-danger-soft': 'colorDangerSoft',\n\n // Typography\n '--ck-font-family': 'fontFamily',\n '--ck-font-mono': 'fontFamilyMono',\n '--ck-font-display': 'fontFamilyDisplay',\n '--ck-font-size': 'fontSize',\n '--ck-step-title-weight': 'fontWeightDisplay',\n '--ck-step-title-letter-spacing': 'letterSpacingDisplay',\n\n // Geometry\n '--ck-border-radius': 'borderRadius',\n '--ck-radius-sm': 'radiusSm',\n '--ck-radius-md': 'radiusMd',\n '--ck-radius-lg': 'radiusLg',\n '--ck-radius-xl': 'radiusXl',\n\n // Elevation\n '--ck-shadow-modal': 'shadowModal',\n '--ck-shadow-card': 'shadowCard',\n\n // Overlay\n '--ck-overlay-color': 'overlayColor',\n}\n\nexport function appearanceToStyle(appearance?: Appearance): CSSProperties | undefined {\n const variables = appearance?.variables\n if (!variables) return undefined\n\n const style: Record<string, string> = {}\n for (const [cssProp, varKey] of Object.entries(VAR_MAP)) {\n const value = variables[varKey]\n if (value) style[cssProp] = value\n }\n\n return Object.keys(style).length > 0 ? (style as CSSProperties) : undefined\n}\n\n// Step-level fallback titles when neither token-mode config nor local\n// step config provide one. Offer and Success are absent — Offer falls back\n// to `offer.copy.headline`, Success branches on outcome.\nexport const defaultTitles = {\n survey: 'Why are you cancelling?',\n feedback: 'Any other feedback?',\n confirm: 'Confirm cancellation',\n} as const\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CancelFlowMachine, decodeSessionToken, ChurnkeyApi } from './chunk-
|
|
1
|
+
import { CancelFlowMachine, decodeSessionToken, ChurnkeyApi } from './chunk-QYNW3HF3.js';
|
|
2
2
|
import { useRef, useState, useEffect, useCallback } from 'react';
|
|
3
3
|
|
|
4
4
|
function useCancelFlowMachine(config) {
|
|
@@ -59,17 +59,19 @@ function useCancelFlowMachine(config) {
|
|
|
59
59
|
|
|
60
60
|
// src/headless/use-cancel-flow.ts
|
|
61
61
|
function useCancelFlow(config) {
|
|
62
|
-
const { machine, state, isLoading, loadError } = useCancelFlowMachine(config);
|
|
62
|
+
const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(config);
|
|
63
63
|
return {
|
|
64
64
|
...state,
|
|
65
65
|
isLoading,
|
|
66
66
|
loadError,
|
|
67
|
+
retry,
|
|
67
68
|
reasons: machine.reasons,
|
|
68
69
|
currentStep: machine.currentStep,
|
|
69
70
|
currentOffer: machine.currentOffer,
|
|
70
71
|
stepIndex: machine.stepIndex,
|
|
71
72
|
totalSteps: machine.totalSteps,
|
|
72
73
|
selectReason: machine.selectReason,
|
|
74
|
+
setFollowupResponse: machine.setFollowupResponse,
|
|
73
75
|
setFeedback: machine.setFeedback,
|
|
74
76
|
accept: machine.accept,
|
|
75
77
|
decline: machine.decline,
|
|
@@ -81,5 +83,5 @@ function useCancelFlow(config) {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
export { useCancelFlow, useCancelFlowMachine };
|
|
84
|
-
//# sourceMappingURL=chunk-
|
|
85
|
-
//# sourceMappingURL=chunk-
|
|
86
|
+
//# sourceMappingURL=chunk-F4ESJBOS.js.map
|
|
87
|
+
//# sourceMappingURL=chunk-F4ESJBOS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/headless/use-cancel-flow-machine.ts","../src/headless/use-cancel-flow.ts"],"names":[],"mappings":"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","file":"chunk-F4ESJBOS.js","sourcesContent":["// Internal — shared by `useCancelFlow` (headless) and the `CancelFlow`\n// component. Lives under headless/ since it's the headless layer of the\n// SDK; the component imports it because there's no separate public/private\n// split worth maintaining for one shared file.\n\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { ChurnkeyApi } from '../core/api'\nimport { CancelFlowMachine } from '../core/machine'\nimport { decodeSessionToken } from '../core/token'\nimport type { FlowCallbacks, FlowConfig, FlowState } from '../core/types'\n\nexport interface CancelFlowMachineHandle {\n machine: CancelFlowMachine\n state: FlowState\n isLoading: boolean\n loadError: Error | null\n /** Re-fetch the flow config (token mode only). No-op without a session. */\n retry: () => void\n}\n\n/**\n * Wires a CancelFlowMachine to React lifecycle. Used internally by both the\n * `CancelFlow` component and the `useCancelFlow` hook — not part of the\n * public API.\n *\n * Callbacks reach the machine via thunks that dereference a ref updated each\n * render. This buys two things: the consumer's latest closure always runs,\n * and the fetch effect's dep list stays stable so inline-arrow handlers\n * don't trigger a re-fetch and reset the flow to step 1.\n */\nexport function useCancelFlowMachine(config: FlowConfig): CancelFlowMachineHandle {\n // FlowConfig extends FlowCallbacks, so storing the whole config gives us\n // access to every callback by name without a separate copy.\n const callbacksRef = useRef(config)\n callbacksRef.current = config\n\n const [machine] = useState(() => {\n const cb = callbacksRef\n const dispatch: FlowCallbacks = {\n handleDiscount: (o, c) => cb.current.handleDiscount?.(o, c),\n handlePause: (o, c) => cb.current.handlePause?.(o, c),\n handlePlanChange: (o, c) => cb.current.handlePlanChange?.(o, c),\n handleTrialExtension: (o, c) => cb.current.handleTrialExtension?.(o, c),\n handleCancel: (c) => cb.current.handleCancel?.(c),\n onAccept: (o, c) => cb.current.onAccept?.(o, c),\n onDiscount: (o, c) => cb.current.onDiscount?.(o, c),\n onPause: (o, c) => cb.current.onPause?.(o, c),\n onPlanChange: (o, c) => cb.current.onPlanChange?.(o, c),\n onTrialExtension: (o, c) => cb.current.onTrialExtension?.(o, c),\n onCancel: (c) => cb.current.onCancel?.(c),\n onClose: () => cb.current.onClose?.(),\n onStepChange: (step, prevStep) => cb.current.onStepChange?.(step, prevStep),\n }\n return new CancelFlowMachine({ ...config, ...dispatch })\n })\n\n const [state, setState] = useState<FlowState>(() => machine.getSnapshot())\n const [isLoading, setIsLoading] = useState(!!config.session)\n const [loadError, setLoadError] = useState<Error | null>(null)\n\n useEffect(() => {\n setState(machine.getSnapshot())\n return machine.subscribe(() => setState(machine.getSnapshot()))\n }, [machine])\n\n const loadConfig = useCallback(() => {\n if (!config.session) return\n setLoadError(null)\n setIsLoading(true)\n let cancelled = false\n const creds = decodeSessionToken(config.session)\n const api = new ChurnkeyApi(creds, config.apiBaseUrl)\n api\n .fetchConfig()\n .then((sdkConfig) => {\n if (cancelled) return\n machine.initializeFromConfig(sdkConfig, api, creds)\n setIsLoading(false)\n })\n .catch((err) => {\n if (cancelled) return\n setLoadError(err instanceof Error ? err : new Error(String(err)))\n setIsLoading(false)\n })\n return () => {\n cancelled = true\n }\n }, [config.session, config.apiBaseUrl, machine])\n\n useEffect(() => {\n if (!config.session) return\n return loadConfig()\n }, [config.session, loadConfig])\n\n return { machine, state, isLoading, loadError, retry: loadConfig }\n}\n","import type { FlowConfig } from '../core/types'\nimport { useCancelFlowMachine } from './use-cancel-flow-machine'\n\nexport function useCancelFlow(config: FlowConfig) {\n const { machine, state, isLoading, loadError, retry } = useCancelFlowMachine(config)\n\n return {\n ...state,\n isLoading,\n loadError,\n retry,\n reasons: machine.reasons,\n currentStep: machine.currentStep,\n currentOffer: machine.currentOffer,\n stepIndex: machine.stepIndex,\n totalSteps: machine.totalSteps,\n selectReason: machine.selectReason,\n setFollowupResponse: machine.setFollowupResponse,\n setFeedback: machine.setFeedback,\n accept: machine.accept,\n decline: machine.decline,\n cancel: machine.cancel,\n next: machine.next,\n back: machine.back,\n close: machine.close,\n }\n}\n"]}
|
|
@@ -56,6 +56,14 @@ function formatMonthDay(date, locale) {
|
|
|
56
56
|
function formatMonthDayLong(date, locale) {
|
|
57
57
|
return date.toLocaleDateString(locale, { month: "long", day: "numeric" });
|
|
58
58
|
}
|
|
59
|
+
function formatPeriodEnd(subscriptions, locale) {
|
|
60
|
+
const status = subscriptions?.[0]?.status;
|
|
61
|
+
if (!status || !("currentPeriod" in status) || !status.currentPeriod?.end) return null;
|
|
62
|
+
const end = status.currentPeriod.end;
|
|
63
|
+
const d = end instanceof Date ? end : new Date(end);
|
|
64
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
65
|
+
return d.toLocaleDateString(locale, { month: "long", day: "numeric", year: "numeric" });
|
|
66
|
+
}
|
|
59
67
|
function discountPhrase(opts) {
|
|
60
68
|
const { percentOff, amountOff, currency, duration, durationInMonths } = opts;
|
|
61
69
|
const amount = percentOff != null ? `${percentOff}% off` : amountOff != null ? `${formatPriceFromMinor(amountOff, currency)} off` : "discount";
|
|
@@ -65,6 +73,15 @@ function discountPhrase(opts) {
|
|
|
65
73
|
|
|
66
74
|
// src/core/utils.ts
|
|
67
75
|
var BUILT_IN_STEP_TYPES = ["survey", "offer", "feedback", "confirm", "success"];
|
|
76
|
+
var BUILT_IN_OFFER_TYPES = [
|
|
77
|
+
"discount",
|
|
78
|
+
"pause",
|
|
79
|
+
"trial_extension",
|
|
80
|
+
"plan_change",
|
|
81
|
+
"contact",
|
|
82
|
+
"redirect",
|
|
83
|
+
"rebate"
|
|
84
|
+
];
|
|
68
85
|
function cn(...classes) {
|
|
69
86
|
return classes.filter(Boolean).join(" ");
|
|
70
87
|
}
|
|
@@ -125,6 +142,6 @@ var defaultTitles = {
|
|
|
125
142
|
confirm: "Confirm cancellation"
|
|
126
143
|
};
|
|
127
144
|
|
|
128
|
-
export { BUILT_IN_STEP_TYPES, appearanceToStyle, calculateDiscountedPrice, cn, defaultTitles, discountPhrase, formatMonthDay, formatMonthDayLong, formatPrice, formatPriceFromMinor };
|
|
129
|
-
//# sourceMappingURL=chunk-
|
|
130
|
-
//# sourceMappingURL=chunk-
|
|
145
|
+
export { BUILT_IN_OFFER_TYPES, BUILT_IN_STEP_TYPES, appearanceToStyle, calculateDiscountedPrice, cn, defaultTitles, discountPhrase, formatMonthDay, formatMonthDayLong, formatPeriodEnd, formatPrice, formatPriceFromMinor };
|
|
146
|
+
//# sourceMappingURL=chunk-G57YLYM5.js.map
|
|
147
|
+
//# sourceMappingURL=chunk-G57YLYM5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/format.ts","../src/core/utils.ts"],"names":[],"mappings":"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","file":"chunk-G57YLYM5.js","sourcesContent":["import type { DirectSubscription } from './types'\n\n// Currencies whose smallest unit equals one major unit (no fractional part).\n// Stripe and most billing providers store these without an implicit /100.\nconst ZERO_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set([\n 'BIF',\n 'CLP',\n 'DJF',\n 'GNF',\n 'JPY',\n 'KMF',\n 'KRW',\n 'MGA',\n 'PYG',\n 'RWF',\n 'UGX',\n 'VND',\n 'VUV',\n 'XAF',\n 'XOF',\n 'XPF',\n])\n\n// Middle-Eastern currencies that use three fractional digits (1000 divisor).\n// Without this bucket, JOD/KWD amounts would be off by 10x in display.\nconst THREE_DECIMAL_CURRENCIES: ReadonlySet<string> = new Set(['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'])\n\nexport function isZeroDecimalCurrency(currency: string): boolean {\n return ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase())\n}\n\nexport function isThreeDecimalCurrency(currency: string): boolean {\n return THREE_DECIMAL_CURRENCIES.has(currency.toUpperCase())\n}\n\n/**\n * Smallest-unit divisor for a currency. 1 for zero-decimal (JPY), 1000 for\n * three-decimal (KWD), 100 for everything else. Used to convert provider\n * amounts (which are always in the smallest unit) into the major unit for\n * display.\n */\nexport function getCurrencyDivisor(currency = 'USD'): number {\n if (isZeroDecimalCurrency(currency)) return 1\n if (isThreeDecimalCurrency(currency)) return 1000\n return 100\n}\n\n/**\n * Converts a Direct.Price.amount.value (smallest currency unit) into the\n * major unit used for display.\n */\nexport function convertFromMinorUnits(value: number, currency = 'USD'): number {\n return value / getCurrencyDivisor(currency)\n}\n\n/**\n * Format an amount already in the major unit. Uses `narrowSymbol` so output\n * reads \"$29\" rather than \"US$29\", and trims trailing zero fractions so\n * whole amounts render compactly.\n */\nexport function formatPrice(amount: number, currency = 'USD', locale?: string): string {\n try {\n const formatted = new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n }).format(amount)\n return formatted.replace(/[.,]00$/, '')\n } catch {\n return `${currency} ${amount.toFixed(2)}`\n }\n}\n\n/** Convenience: format a Direct-shape minor-unit amount in one call. */\nexport function formatPriceFromMinor(value: number, currency = 'USD', locale?: string): string {\n return formatPrice(convertFromMinorUnits(value, currency), currency, locale)\n}\n\nexport function calculateDiscountedPrice(originalPrice: number, percentOff: number): number {\n return originalPrice * (1 - percentOff / 100)\n}\n\n// ─── Dates ────────────────────────────────────────────────────────────────\n\n/** \"Apr 30, 2026\" — used for resume dates and other full-date contexts. */\nexport function formatShortDate(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' })\n}\n\n/** \"Apr 30\" — used when the year is implied by context. */\nexport function formatMonthDay(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric' })\n}\n\n/** \"April 30\" — long month form for prominent date displays. */\nexport function formatMonthDayLong(date: Date, locale?: string): string {\n return date.toLocaleDateString(locale, { month: 'long', day: 'numeric' })\n}\n\n/**\n * Long-form access period end (\"June 14, 2026\") for the first subscription's\n * current period. Returns null for canceled subscriptions, missing periods,\n * and unparseable dates so Confirm can drop the \"access continues until\"\n * notice cleanly instead of rendering \"Invalid Date\".\n */\nexport function formatPeriodEnd(\n subscriptions: DirectSubscription[] | null | undefined,\n locale?: string,\n): string | null {\n const status = subscriptions?.[0]?.status\n if (!status || !('currentPeriod' in status) || !status.currentPeriod?.end) return null\n const end = status.currentPeriod.end\n const d = end instanceof Date ? end : new Date(end)\n if (Number.isNaN(d.getTime())) return null\n return d.toLocaleDateString(locale, { month: 'long', day: 'numeric', year: 'numeric' })\n}\n\n// ─── Discount phrasing ─────────────────────────────────────────────────────\n\n/**\n * Renders the discount as a single human-readable sentence:\n * \"25% off for 3 months\" / \"$5 off your next renewal\" / \"20% off for life\"\n *\n * Mirrors the embed's couponToString helper so SDK and embed use the same\n * phrasing for the same offer shape.\n */\nexport function discountPhrase(opts: {\n percentOff?: number\n /** Smallest currency unit (cents for USD). */\n amountOff?: number\n currency?: string\n duration?: 'once' | 'repeating' | 'forever'\n durationInMonths?: number\n}): string {\n const { percentOff, amountOff, currency, duration, durationInMonths } = opts\n const amount =\n percentOff != null\n ? `${percentOff}% off`\n : amountOff != null\n ? `${formatPriceFromMinor(amountOff, currency)} off`\n : 'discount'\n const tail =\n duration === 'once'\n ? 'your next renewal'\n : duration === 'forever'\n ? 'for life'\n : durationInMonths\n ? `for ${durationInMonths} ${durationInMonths === 1 ? 'month' : 'months'}`\n : ''\n return tail ? `${amount} ${tail}` : amount\n}\n","import type { CSSProperties } from 'react'\nimport type { Appearance, AppearanceVariables } from './types'\n\nexport const BUILT_IN_STEP_TYPES: readonly string[] = ['survey', 'offer', 'feedback', 'confirm', 'success']\nexport const BUILT_IN_OFFER_TYPES: readonly string[] = [\n 'discount',\n 'pause',\n 'trial_extension',\n 'plan_change',\n 'contact',\n 'redirect',\n 'rebate',\n]\n\nexport function cn(...classes: (string | undefined | null | false)[]): string {\n return classes.filter(Boolean).join(' ')\n}\n\n// Maps the typed `appearance.variables` field names to the underlying\n// CSS custom properties. Light/dark variants live in CSS — these\n// overrides apply on top of whichever scheme is active.\nconst VAR_MAP: Record<string, keyof AppearanceVariables> = {\n // Surfaces\n '--ck-color-bg': 'colorBackground',\n '--ck-color-surface': 'colorSurface',\n '--ck-color-surface-muted': 'colorSurfaceMuted',\n\n // Borders\n '--ck-color-border': 'colorBorder',\n '--ck-color-border-strong': 'colorBorderStrong',\n\n // Text\n '--ck-color-text': 'colorText',\n '--ck-color-text-secondary': 'colorTextSecondary',\n '--ck-color-text-muted': 'colorTextMuted',\n\n // Primary\n '--ck-color-primary': 'colorPrimary',\n '--ck-color-primary-hover': 'colorPrimaryHover',\n '--ck-color-primary-soft': 'colorPrimarySoft',\n\n // Semantic\n '--ck-color-success': 'colorSuccess',\n '--ck-color-success-soft': 'colorSuccessSoft',\n '--ck-color-danger': 'colorDanger',\n '--ck-color-danger-hover': 'colorDangerHover',\n '--ck-color-danger-soft': 'colorDangerSoft',\n\n // Typography\n '--ck-font-family': 'fontFamily',\n '--ck-font-mono': 'fontFamilyMono',\n '--ck-font-display': 'fontFamilyDisplay',\n '--ck-font-size': 'fontSize',\n '--ck-step-title-weight': 'fontWeightDisplay',\n '--ck-step-title-letter-spacing': 'letterSpacingDisplay',\n\n // Geometry\n '--ck-border-radius': 'borderRadius',\n '--ck-radius-sm': 'radiusSm',\n '--ck-radius-md': 'radiusMd',\n '--ck-radius-lg': 'radiusLg',\n '--ck-radius-xl': 'radiusXl',\n\n // Elevation\n '--ck-shadow-modal': 'shadowModal',\n '--ck-shadow-card': 'shadowCard',\n\n // Overlay\n '--ck-overlay-color': 'overlayColor',\n}\n\nexport function appearanceToStyle(appearance?: Appearance): CSSProperties | undefined {\n const variables = appearance?.variables\n if (!variables) return undefined\n\n const style: Record<string, string> = {}\n for (const [cssProp, varKey] of Object.entries(VAR_MAP)) {\n const value = variables[varKey]\n if (value) style[cssProp] = value\n }\n\n return Object.keys(style).length > 0 ? (style as CSSProperties) : undefined\n}\n\n// Step-level fallback titles when neither token-mode config nor local\n// step config provide one. Offer and Success are absent — Offer falls back\n// to `offer.copy.headline`, Success branches on outcome.\nexport const defaultTitles = {\n survey: 'Why are you cancelling?',\n feedback: 'Any other feedback?',\n confirm: 'Confirm cancellation',\n} as const\n"]}
|
|
@@ -63,6 +63,9 @@ var ChurnkeyApi = class {
|
|
|
63
63
|
async extendTrial(days, blueprintId) {
|
|
64
64
|
await this.request(this.orgUrl("cancel-flow/actions/extend-trial"), { days, blueprintId });
|
|
65
65
|
}
|
|
66
|
+
async applyRebate(blueprintId, offerGuid) {
|
|
67
|
+
await this.request(this.orgUrl("cancel-flow/actions/rebate"), { blueprintId, offerGuid });
|
|
68
|
+
}
|
|
66
69
|
async createSession(payload) {
|
|
67
70
|
await this.request(`${this.baseUrl}/api/sessions/sdk`, payload);
|
|
68
71
|
}
|
|
@@ -154,7 +157,7 @@ function mergeStep(step, attrs) {
|
|
|
154
157
|
...s,
|
|
155
158
|
title: maybeMerge(s.title, attrs),
|
|
156
159
|
description: maybeMerge(s.description, attrs),
|
|
157
|
-
offer: s.offer ?
|
|
160
|
+
offer: s.offer ? mergeOffer(s.offer, attrs) : void 0
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
163
|
case "feedback": {
|
|
@@ -177,7 +180,8 @@ function mergeStep(step, attrs) {
|
|
|
177
180
|
function mergeReason(r, attrs) {
|
|
178
181
|
return { ...r, label: applyMergeFields(r.label, attrs) };
|
|
179
182
|
}
|
|
180
|
-
function
|
|
183
|
+
function mergeOffer(o, attrs) {
|
|
184
|
+
if (!("copy" in o)) return o;
|
|
181
185
|
return {
|
|
182
186
|
...o,
|
|
183
187
|
copy: {
|
|
@@ -196,7 +200,7 @@ function maybeMerge(text, attrs) {
|
|
|
196
200
|
var EMPTY_GRAPH = { stepMap: {}, firstStepId: "", orderedStepIds: [] };
|
|
197
201
|
function buildStepGraph(steps, defaultOfferCopy2) {
|
|
198
202
|
if (steps.length === 0) return EMPTY_GRAPH;
|
|
199
|
-
const resolved = steps.map((step, i) => normalizeStep(step, i));
|
|
203
|
+
const resolved = steps.map((step, i) => normalizeStep(step, i, defaultOfferCopy2));
|
|
200
204
|
linkNeighbors(resolved);
|
|
201
205
|
const stepMap = {};
|
|
202
206
|
for (const step of resolved) stepMap[step.guid] = step;
|
|
@@ -226,7 +230,7 @@ function buildStepGraph(steps, defaultOfferCopy2) {
|
|
|
226
230
|
surveyStepId: surveyStep?.guid
|
|
227
231
|
};
|
|
228
232
|
}
|
|
229
|
-
function normalizeStep(step, index) {
|
|
233
|
+
function normalizeStep(step, index, defaultOfferCopy2) {
|
|
230
234
|
const guid = step.guid ?? `step-${index}-${step.type}`;
|
|
231
235
|
const base = { guid, type: step.type };
|
|
232
236
|
switch (step.type) {
|
|
@@ -247,7 +251,10 @@ function normalizeStep(step, index) {
|
|
|
247
251
|
...base,
|
|
248
252
|
title: s.title,
|
|
249
253
|
description: s.description,
|
|
250
|
-
|
|
254
|
+
// Standalone offers may arrive without `copy` — toDecision fills it
|
|
255
|
+
// in from defaultOfferCopy. Token-mode offers already carry copy
|
|
256
|
+
// from the server, so toDecision passes them through unchanged.
|
|
257
|
+
offer: s.offer ? toDecision(s.offer, defaultOfferCopy2) : void 0,
|
|
251
258
|
classNames: s.classNames
|
|
252
259
|
};
|
|
253
260
|
}
|
|
@@ -356,9 +363,9 @@ function transformStep(step) {
|
|
|
356
363
|
function transformReason(r) {
|
|
357
364
|
const out = {
|
|
358
365
|
id: r.id,
|
|
359
|
-
label: r.label
|
|
360
|
-
freeform: r.freeform
|
|
366
|
+
label: r.label
|
|
361
367
|
};
|
|
368
|
+
if (r.freeform) out.freeform = true;
|
|
362
369
|
if (r.offer) out.offer = transformOfferConfig(r.offer);
|
|
363
370
|
return out;
|
|
364
371
|
}
|
|
@@ -382,8 +389,7 @@ function transformOfferConfig(o) {
|
|
|
382
389
|
return {
|
|
383
390
|
type: "pause",
|
|
384
391
|
months: o.months,
|
|
385
|
-
interval: o.interval
|
|
386
|
-
datePicker: o.datePicker
|
|
392
|
+
interval: o.interval
|
|
387
393
|
};
|
|
388
394
|
case "plan_change":
|
|
389
395
|
return {
|
|
@@ -407,6 +413,16 @@ function transformOfferConfig(o) {
|
|
|
407
413
|
url: o.url,
|
|
408
414
|
label: o.label
|
|
409
415
|
};
|
|
416
|
+
case "rebate":
|
|
417
|
+
return {
|
|
418
|
+
type: "rebate",
|
|
419
|
+
amountMinor: o.amountMinor,
|
|
420
|
+
currency: o.currency,
|
|
421
|
+
amountPaidMinor: o.amountPaidMinor,
|
|
422
|
+
netAfterRebateMinor: o.netAfterRebateMinor,
|
|
423
|
+
paymentMethodBrand: o.paymentMethodBrand,
|
|
424
|
+
paymentMethodLast4: o.paymentMethodLast4
|
|
425
|
+
};
|
|
410
426
|
}
|
|
411
427
|
}
|
|
412
428
|
function defaultOfferCopy(offer) {
|
|
@@ -454,6 +470,13 @@ function defaultOfferCopy(offer) {
|
|
|
454
470
|
cta: o.label,
|
|
455
471
|
declineCta: "No thanks"
|
|
456
472
|
};
|
|
473
|
+
case "rebate":
|
|
474
|
+
return {
|
|
475
|
+
headline: "Get money back",
|
|
476
|
+
body: "Stay subscribed and we'll refund part of your most recent payment.",
|
|
477
|
+
cta: "Accept refund",
|
|
478
|
+
declineCta: "No thanks"
|
|
479
|
+
};
|
|
457
480
|
default:
|
|
458
481
|
return {
|
|
459
482
|
headline: "Before you go...",
|
|
@@ -480,6 +503,8 @@ function handlerFor(offerType, cb) {
|
|
|
480
503
|
return cb.handlePlanChange;
|
|
481
504
|
case "trial_extension":
|
|
482
505
|
return cb.handleTrialExtension;
|
|
506
|
+
case "rebate":
|
|
507
|
+
return cb.handleRebate;
|
|
483
508
|
default:
|
|
484
509
|
return void 0;
|
|
485
510
|
}
|
|
@@ -494,6 +519,8 @@ function listenerFor(offerType, cb) {
|
|
|
494
519
|
return cb.onPlanChange;
|
|
495
520
|
case "trial_extension":
|
|
496
521
|
return cb.onTrialExtension;
|
|
522
|
+
case "rebate":
|
|
523
|
+
return cb.onRebate;
|
|
497
524
|
default:
|
|
498
525
|
return void 0;
|
|
499
526
|
}
|
|
@@ -520,7 +547,8 @@ var OFFER_TYPE_API_MAP = {
|
|
|
520
547
|
plan_change: "PLAN_CHANGE",
|
|
521
548
|
trial_extension: "TRIAL_EXTENSION",
|
|
522
549
|
contact: "CONTACT",
|
|
523
|
-
redirect: "REDIRECT"
|
|
550
|
+
redirect: "REDIRECT",
|
|
551
|
+
rebate: "REBATE"
|
|
524
552
|
};
|
|
525
553
|
function toApiStepType(step) {
|
|
526
554
|
if (step === "success") return null;
|
|
@@ -600,6 +628,8 @@ function toAcceptedOfferPayload(rec, result) {
|
|
|
600
628
|
return { ...base, trialExtensionDays: o.days };
|
|
601
629
|
case "redirect":
|
|
602
630
|
return { ...base, redirectUrl: o.url };
|
|
631
|
+
case "rebate":
|
|
632
|
+
return { ...base, rebateAmount: o.amountMinor };
|
|
603
633
|
default:
|
|
604
634
|
return base;
|
|
605
635
|
}
|
|
@@ -642,7 +672,7 @@ var CancelFlowMachine = class {
|
|
|
642
672
|
const merged = applyMergeFieldsToSteps(config.steps, this.directCustomer);
|
|
643
673
|
this.graph = buildStepGraph(merged, defaultOfferCopy);
|
|
644
674
|
}
|
|
645
|
-
this.state = this.buildInitialState(
|
|
675
|
+
this.state = this.buildInitialState(this.directCustomer, this.directSubscriptions ?? []);
|
|
646
676
|
this.cachedSnapshot = { ...this.state };
|
|
647
677
|
const first = this.firstStep();
|
|
648
678
|
if (!config.session && first) this.trackStepEnter(first);
|
|
@@ -696,7 +726,10 @@ var CancelFlowMachine = class {
|
|
|
696
726
|
}
|
|
697
727
|
selectReason = (id) => {
|
|
698
728
|
if (!this.reasons.find((r) => r.id === id)) return;
|
|
699
|
-
this.setState({ selectedReason: id });
|
|
729
|
+
this.setState({ selectedReason: id, followupResponse: "" });
|
|
730
|
+
};
|
|
731
|
+
setFollowupResponse = (text) => {
|
|
732
|
+
this.setState({ followupResponse: text });
|
|
700
733
|
};
|
|
701
734
|
next = (result) => {
|
|
702
735
|
if (isPlainObject(result)) {
|
|
@@ -727,7 +760,7 @@ var CancelFlowMachine = class {
|
|
|
727
760
|
if (handler) {
|
|
728
761
|
await handler(acceptedOffer, customer);
|
|
729
762
|
} else if (this.isTokenMode()) {
|
|
730
|
-
await this.executeTokenAction(acceptedOffer);
|
|
763
|
+
await this.executeTokenAction(acceptedOffer, offer.decisionId);
|
|
731
764
|
}
|
|
732
765
|
const listener = listenerFor(offer.type, this.callbacks);
|
|
733
766
|
if (listener) await runListener(listener, acceptedOffer, customer);
|
|
@@ -784,7 +817,7 @@ var CancelFlowMachine = class {
|
|
|
784
817
|
const merged = this.localSteps ? mergeLocalSteps(result.steps, this.localSteps) : result.steps;
|
|
785
818
|
const steps = applyMergeFieldsToSteps(merged, config.customer ?? null);
|
|
786
819
|
this.graph = buildStepGraph(steps, defaultOfferCopy);
|
|
787
|
-
this.state = this.buildInitialState(config.customer ?? null);
|
|
820
|
+
this.state = this.buildInitialState(config.customer ?? null, config.subscriptions ?? []);
|
|
788
821
|
this.cachedSnapshot = { ...this.state };
|
|
789
822
|
const first = this.firstStep();
|
|
790
823
|
if (first) this.trackStepEnter(first);
|
|
@@ -800,17 +833,19 @@ var CancelFlowMachine = class {
|
|
|
800
833
|
surveyStep() {
|
|
801
834
|
return this.graph.surveyStepId ? this.graph.stepMap[this.graph.surveyStepId] : void 0;
|
|
802
835
|
}
|
|
803
|
-
buildInitialState(customer) {
|
|
836
|
+
buildInitialState(customer, subscriptions) {
|
|
804
837
|
const first = this.firstStep();
|
|
805
838
|
return {
|
|
806
839
|
step: first?.type ?? "survey",
|
|
807
840
|
currentStepId: this.graph.firstStepId,
|
|
808
841
|
selectedReason: null,
|
|
842
|
+
followupResponse: "",
|
|
809
843
|
feedback: "",
|
|
810
844
|
outcome: null,
|
|
811
845
|
isProcessing: false,
|
|
812
846
|
error: null,
|
|
813
|
-
customer
|
|
847
|
+
customer,
|
|
848
|
+
subscriptions
|
|
814
849
|
};
|
|
815
850
|
}
|
|
816
851
|
// On a survey step with a selected reason that has an attached offer,
|
|
@@ -879,7 +914,7 @@ var CancelFlowMachine = class {
|
|
|
879
914
|
listener();
|
|
880
915
|
}
|
|
881
916
|
}
|
|
882
|
-
async executeTokenAction(offer) {
|
|
917
|
+
async executeTokenAction(offer, decisionId) {
|
|
883
918
|
if (!this.apiClient) return;
|
|
884
919
|
const o = offer;
|
|
885
920
|
switch (o.type) {
|
|
@@ -897,6 +932,9 @@ var CancelFlowMachine = class {
|
|
|
897
932
|
case "trial_extension":
|
|
898
933
|
await this.apiClient.extendTrial(o.days, this.blueprintId ?? void 0);
|
|
899
934
|
break;
|
|
935
|
+
case "rebate":
|
|
936
|
+
await this.apiClient.applyRebate(this.blueprintId ?? void 0, decisionId);
|
|
937
|
+
break;
|
|
900
938
|
}
|
|
901
939
|
}
|
|
902
940
|
trackStepView(stepType, guid, numChoices) {
|
|
@@ -921,14 +959,14 @@ var CancelFlowMachine = class {
|
|
|
921
959
|
}
|
|
922
960
|
}
|
|
923
961
|
// Offer passed in so accept() can capture it before enterSuccessStep
|
|
924
|
-
// potentially moves currentStepId off the offer step.
|
|
962
|
+
// potentially moves currentStepId off the offer step. copy and decisionId
|
|
963
|
+
// are SDK-internal — strip them from the consumer-facing payload.
|
|
925
964
|
buildAcceptedOffer(offer, result) {
|
|
926
|
-
const { copy:
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
};
|
|
965
|
+
const { copy: _copy, decisionId: _decisionId, ...offerConfig } = offer;
|
|
966
|
+
const accepted = offerConfig;
|
|
967
|
+
if (this.state.selectedReason) accepted.reasonId = this.state.selectedReason;
|
|
968
|
+
if (result) accepted.result = result;
|
|
969
|
+
return accepted;
|
|
932
970
|
}
|
|
933
971
|
// Terminal transition. Prefer moving currentStepId to a declared success
|
|
934
972
|
// step so the developer's savedTitle / classNames / custom component
|
|
@@ -948,12 +986,14 @@ var CancelFlowMachine = class {
|
|
|
948
986
|
}
|
|
949
987
|
buildBasePayload() {
|
|
950
988
|
const selectedReason = this.reasons.find((r) => r.id === this.state.selectedReason);
|
|
989
|
+
const followupResponse = selectedReason?.freeform && this.state.followupResponse ? this.state.followupResponse : void 0;
|
|
951
990
|
const payload = {
|
|
952
991
|
blueprintId: this.blueprintId ?? void 0,
|
|
953
992
|
customer: this.resolveSessionCustomer(),
|
|
954
993
|
canceled: false,
|
|
955
994
|
surveyChoiceId: this.state.selectedReason ?? void 0,
|
|
956
995
|
surveyChoiceValue: selectedReason?.label,
|
|
996
|
+
followupResponse,
|
|
957
997
|
feedback: this.state.feedback || void 0,
|
|
958
998
|
presentedOffers: this.presentedOffers,
|
|
959
999
|
stepsViewed: this.stepsViewed,
|
|
@@ -1052,5 +1092,5 @@ function decodeSessionToken(token) {
|
|
|
1052
1092
|
}
|
|
1053
1093
|
|
|
1054
1094
|
export { AnalyticsClient, CancelFlowMachine, ChurnkeyApi, decodeSessionToken };
|
|
1055
|
-
//# sourceMappingURL=chunk-
|
|
1056
|
-
//# sourceMappingURL=chunk-
|
|
1095
|
+
//# sourceMappingURL=chunk-QYNW3HF3.js.map
|
|
1096
|
+
//# sourceMappingURL=chunk-QYNW3HF3.js.map
|