Skip to Content
ReferenceEvents

Events

Every event emitted by the widget, with payload shapes and subscription instructions.


Event names

Sourced from packages/widget/src/events.ts:

// packages/widget/src/events.ts export const WIDGET_EVENTS = Object.freeze({ STEP_VIEWED: 'unchurn:step_viewed', STEP_ACCEPTED: 'unchurn:step_accepted', STEP_DECLINED: 'unchurn:step_declined', } as const);
ConstantEvent nameWhen it fires
WIDGET_EVENTS.STEP_VIEWEDunchurn:step_viewedA flow step becomes visible to the customer
WIDGET_EVENTS.STEP_ACCEPTEDunchurn:step_acceptedThe customer accepts the offer on the current step
WIDGET_EVENTS.STEP_DECLINEDunchurn:step_declinedThe customer declines the offer on the current step

Payload shapes

unchurn:step_viewed

// packages/widget/src/events.ts export type StepViewedPayload = { step: Step; sessionId: string; };
FieldTypeDescription
stepStepThe step that became visible (see Step union below)
sessionIdstringSession identifier from createSession

unchurn:step_accepted

// packages/widget/src/events.ts export type StepAcceptedPayload = StepAcceptedData & { sessionId: string }; export type StepAcceptedData = | { step: 'discount'; kind: WidgetDiscountTerms['kind']; durationMonths: WidgetDiscountTerms['durationMonths']; percentOff: WidgetDiscountTerms['percentOff']; } | { step: 'pause'; durationMonths: WidgetPauseDurations[number] } | { step: 'plan-switch'; targetPriceId: WidgetPlanSwitchTarget['priceId'] } | { step: Exclude<Step, 'discount' | 'pause' | 'plan-switch'> };
FieldTypeWhen presentDescription
stepStepalwaysThe step that was accepted
sessionIdstringalwaysSession identifier
kind'percent'step === 'discount'Discount kind (only 'percent' ships at launch)
percentOffnumberstep === 'discount'Discount percentage
durationMonthsnumberstep === 'discount' or step === 'pause'Duration in months
targetPriceIdstringstep === 'plan-switch'Stripe price ID of the selected plan

unchurn:step_declined

// packages/widget/src/events.ts export type StepDeclinedPayload = { step: Step; sessionId: string; };
FieldTypeDescription
stepStepThe step that was declined
sessionIdstringSession identifier

Step union

All valid values for the step field across all event payloads:

// packages/widget/src/flow-routing.ts export type Step = | 'feedback' | 'discount' | 'pause' | 'missing-feature' | 'technical-issue' | 'switching-product' | 'confirm' | 'uncancel-prompt' | 'resume-prompt' | 'trial-extension' | 'plan-switch' | 'success';
StepDescription
feedbackInitial reason-selection screen
discountDiscount offer screen
pausePause offer screen
missing-featureMissing feature capture screen
technical-issueTechnical issue capture screen
switching-productSwitching product capture screen
confirmCancel confirmation screen
uncancel-promptPrompt for subscriptions already scheduled to cancel
resume-promptPrompt for paused subscriptions
trial-extensionTrial extension offer screen
plan-switchPlan switch (downgrade) offer screen
successFinal success screen

Subscribing from merchant code

Events are emitted on window.unchurn.events. Only on is exposed publicly — emit is not, to prevent co-resident scripts from injecting authenticated telemetry.

// your-analytics.ts window.unchurn.events.on( window.unchurn.events.EVENTS.STEP_VIEWED, (data) => { myAnalytics.track('cancel_step_viewed', { step: data.step, sessionId: data.sessionId, }); } ); window.unchurn.events.on( window.unchurn.events.EVENTS.STEP_ACCEPTED, (data) => { myAnalytics.track('cancel_step_accepted', data); } );

The on function returns an unsubscribe function:

// packages/widget/src/events.ts export function on<K extends EventKey>( event: K, handler: (data: Payloads[K]) => void, ): () => void

Call the returned function to stop receiving events.


Security contract

  • Event payloads never contain the session JWT. Any merchant-origin script can subscribe, so including the JWT would be a cross-script exfiltration surface.
  • emit is not exposed on window.unchurn.events. A public emit would let co-resident scripts inject authenticated telemetry under the user’s session.
  • Handlers run in a try/catch. A throwing handler never halts other subscribers or the emitter.
  • Events are deferred to a macrotask (setTimeout 0) so handlers never block React renders.

Last updated on