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()}
/>