All recipes
Trial extension — before / after dates
Shows the trial transition — current end date, arrow, extended date — rather than just the days gained. The framing reads as a billing detail, which fits B2B trials tied to deployments, deliverable deadlines, or multi-product funnels.
custom recipe
Have more time on us
Your trial gets extended by 14 days.
Trial ends
Jun 10
Extended to
Jun 24
The full source
Copy this file into your project. It's the same file the showcase preview above renders from.
components/TrialExtensionDateArrow.tsxtsx
/**
* Trial-extension offer that shows the *transition* — the current end date,
* an arrow, and the new end date — instead of just the days gained. Reads
* more "before / after" than "+N days." Use it when the actual dates
* matter to your customer (billing pipelines, planned launches, deliverable
* deadlines).
*
* <CancelFlow
* ...
* components={{ TrialExtensionOffer: TrialExtensionDateArrow }}
* />
*
* Pass `currentEndsAt` as a `Date` via `step.data` or close over your own
* billing data when you instantiate the override — the demo below assumes
* the trial ends today.
*/
import type { OfferDecision, OfferStepProps } from '@churnkey/react/core'
export function TrialExtensionDateArrow({
title,
description,
offer,
onAccept,
onDecline,
isProcessing,
}: OfferStepProps) {
const o = offer as OfferDecision & { days: number }
const headline = title ?? offer.copy.headline
const body = description ?? offer.copy.body
const current = new Date()
const extended = new Date()
extended.setDate(extended.getDate() + o.days)
return (
<div className="ck-step ck-step-offer">
{headline && <h2 className="ck-step-title">{headline}</h2>}
{body && <p className="ck-step-description">{body}</p>}
<div
style={{
display: 'grid',
gridTemplateColumns: '1fr auto 1fr',
alignItems: 'center',
gap: 12,
padding: '24px 20px',
background: 'var(--ck-color-surface-muted)',
borderRadius: 'var(--ck-radius-lg)',
margin: '8px 0 20px',
}}
>
<DateBlock label="Trial ends" date={current} dim />
<ArrowGlyph />
<DateBlock label="Extended to" date={extended} />
</div>
<button
type="button"
className="ck-button ck-button-primary"
onClick={() => onAccept()}
disabled={isProcessing}
>
{isProcessing ? 'Processing...' : offer.copy.cta}
</button>
<button type="button" className="ck-button-link" onClick={onDecline}>
{offer.copy.declineCta}
</button>
</div>
)
}
function DateBlock({ label, date, dim }: { label: string; date: Date; dim?: boolean }) {
const month = date.toLocaleString('en-US', { month: 'short' })
const day = date.getDate()
return (
<div style={{ textAlign: 'center' }}>
<div
style={{
fontSize: 11,
fontWeight: 600,
letterSpacing: '0.08em',
textTransform: 'uppercase',
color: 'var(--ck-color-text-muted)',
marginBottom: 4,
}}
>
{label}
</div>
<div
style={{
fontFamily: 'var(--ck-font-display)',
fontSize: 26,
fontWeight: 500,
color: dim ? 'var(--ck-color-text-muted)' : 'var(--ck-color-text)',
letterSpacing: '-0.012em',
fontVariantNumeric: 'tabular-nums',
}}
>
{month} {day}
</div>
</div>
)
}
function ArrowGlyph() {
return (
<svg
width="22"
height="14"
viewBox="0 0 22 14"
fill="none"
stroke="var(--ck-color-text-muted)"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<line x1="2" y1="7" x2="19" y2="7" />
<polyline points="14 2 19 7 14 12" />
</svg>
)
}
Wire it into a flow
Drop the file into your codebase and reference it from the appropriate prop on <CancelFlow>.
CancelButton.tsxtsx
import { CancelFlow } from '@churnkey/react'
import '@churnkey/react/styles.css'
import { TrialExtensionDateArrow } from './components/TrialExtensionDateArrow'
<CancelFlow
steps={[
{
type: 'survey',
reasons: [
{
id: 'not-ready',
label: 'Not ready to commit yet',
offer: { type: 'trial_extension', days: 14 },
},
/* ... */
],
},
{ type: 'confirm' },
]}
components={{ TrialExtensionOffer: TrialExtensionDateArrow }}
handleTrialExtension={async (o) => myBilling.extendTrial({ days: o.days })}
handleCancel={async () => myBilling.cancel()}
/>