All recipes
Default
Default discount offer
DefaultDiscountOffer renders any offer of type 'discount'. The SDK builds a phrase from the offer's display fields (percentOff, amountOff, durationInMonths, currency) via its discountPhrase formatter and renders it inside the standard offer card. Fork it when the discount needs more visual weight — a struck-through price, a savings calculation, a countdown timer.
default component
Stay for less
Take 20% off the next three months.
Limited-time offer
20% off for 3 months
The full source
Copy this file into your project. It's the same file the showcase preview above renders from.
components/MyDiscountOffer.tsxtsx
import { discountPhrase } from '../../../core/format'
import type { OfferDecision, OfferStepProps } from '../../../core/types'
import { cn } from '../../../core/utils'
import { RichText } from '../../rich-text'
export function DefaultDiscountOffer({
title,
description,
offer,
onAccept,
onDecline,
isProcessing,
classNames,
}: OfferStepProps) {
const o = offer as OfferDecision & {
percentOff?: number
amountOff?: number
currency?: string
durationInMonths?: number
}
const headline = title ?? offer.copy.headline
const body = description ?? offer.copy.body
const phrase = discountPhrase(o)
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', classNames?.card)}>
<div className="ck-offer-details ck-offer-discount">
<div className="ck-offer-discount-eyebrow">Limited-time offer</div>
<div className="ck-offer-discount-phrase">{phrase}</div>
</div>
<button
type="button"
className={cn('ck-button ck-button-primary', classNames?.acceptButton)}
onClick={() => onAccept()}
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>
</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 DefaultDiscountOffer when an offer of type 'discount' renders.
// Copy the source to fork from this baseline.
import { CancelFlow } from '@churnkey/react'
import { MyDiscountOffer } from './components/MyDiscountOffer'
<CancelFlow
steps={[
{
type: 'survey',
reasons: [{
id: 'expensive',
label: 'Too expensive',
offer: {
type: 'discount',
couponId: 'STRIPE_SAVE20',
percentOff: 20,
durationInMonths: 3,
},
}],
},
{ type: 'confirm' },
]}
components={{ DiscountOffer: MyDiscountOffer }}
handleDiscount={async (offer) => myBilling.applyCoupon(offer.couponId)}
handleCancel={async () => myBilling.cancel()}
/>