All recipes
Recipe
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
May 19
Extended to
Jun 2
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()}
/>