All recipes
Default
Default pause offer
DefaultPauseOffer renders any offer of type 'pause'. The resume date sits at the top in display type (We'll see you back on July 15), with month chips (1 through offer.months) below. Picking a chip recomputes the date. Accepting passes { months } to onAccept. Fork it to swap the chips for a date picker, lock the length to a single value, or add a 'we miss you' email opt-in below the controls.
default component
Take a break instead?
We'll keep your data exactly where it is.
We'll see you back on
June 19
The full source
Copy this file into your project. It's the same file the showcase preview above renders from.
components/MyPauseOffer.tsxtsx
import { useState } from 'react'
import { formatMonthDayLong } from '../../../core/format'
import type { OfferDecision, OfferStepProps } from '../../../core/types'
import { cn } from '../../../core/utils'
import { RichText } from '../../rich-text'
// Pause offer rendered with the resume date as the typographic anchor.
// Foregrounding the date — "We'll see you back on July 15" — frames the
// decision in terms of the outcome the customer cares about, with the
// month-length chips as a subordinate adjustment.
export function DefaultPauseOffer({
title,
description,
offer,
onAccept,
onDecline,
isProcessing,
classNames,
}: OfferStepProps) {
const o = offer as OfferDecision & { months: number }
const max = Math.max(1, o.months)
// Start at the shortest option — pre-selecting a longer pause feels
// presumptuous, and the upgrade is one click away.
const [months, setMonths] = useState<number>(1)
const headline = title ?? offer.copy.headline
const body = description ?? offer.copy.body
const resume = new Date()
resume.setMonth(resume.getMonth() + months)
const resumeDate = formatMonthDayLong(resume)
return (
<div className={cn('ck-step ck-step-offer', classNames?.root)}>
{headline && <h2 className={cn('ck-step-title', classNames?.title)}>{headline}</h2>}
{body && <RichText html={body} className={cn('ck-step-description', classNames?.description)} />}
<div className={cn('ck-offer-card ck-pause-card', classNames?.card)}>
<div className="ck-pause-eyebrow">We'll see you back on</div>
<div className="ck-pause-date">{resumeDate}</div>
<div className={cn('ck-pause-chips', classNames?.pauseSlider)}>
{Array.from({ length: max }, (_, i) => i + 1).map((m) => (
<button
key={m}
type="button"
onClick={() => setMonths(m)}
className={cn('ck-pause-chip', m === months && 'ck-pause-chip--selected')}
aria-pressed={m === months}
>
{m} {m === 1 ? 'month' : 'months'}
</button>
))}
</div>
</div>
<button
type="button"
className={cn('ck-button ck-button-primary', classNames?.acceptButton)}
onClick={() => onAccept({ months })}
disabled={isProcessing}
>
{isProcessing ? 'Processing...' : offer.copy.cta}
</button>
<button type="button" className={cn('ck-button-link', classNames?.declineButton)} onClick={onDecline}>
{offer.copy.declineCta}
</button>
</div>
)
}
Wire it into a flow
Drop the file into your codebase and reference it from the appropriate prop on <CancelFlow>.
CancelButton.tsxtsx
// The SDK uses DefaultPauseOffer when an offer of type 'pause' renders.
// Copy the source to fork from this baseline.
import { CancelFlow } from '@churnkey/react'
import { MyPauseOffer } from './components/MyPauseOffer'
<CancelFlow
steps={[
{
type: 'survey',
reasons: [{
id: 'not-using',
label: 'Not using it enough',
offer: { type: 'pause', months: 3 }, // max selectable length
}],
},
{ type: 'confirm' },
]}
components={{ PauseOffer: MyPauseOffer }}
handlePause={async (offer) => myBilling.pause({ months: offer.result.months })}
handleCancel={async () => myBilling.cancel()}
/>