All recipes
Default

Default contact offer

DefaultContactOffer is the simplest offer renderer — title, description, accept button (which usually opens the contact URL), decline link. Title and description carry the pitch; the button is the action. Fork it when 'talk to us first' is a real save channel and you want it to compete visually with discount and pause offers — see the Contact with support card recipe.

default component

Talk to us first?

Most cancellations we hear about have an easy fix. We'd love to help.

The full source

Copy this file into your project. It's the same file the showcase preview above renders from.

components/MyContactOffer.tsxtsx
import type { OfferStepProps } from '../../../core/types'
import { cn } from '../../../core/utils'
import { RichText } from '../../rich-text'

// Contact offers render no body by default — title and description carry the
// pitch, the accept button triggers the consumer's handleContact callback
// (which usually opens the offer's url). Override the slot to add an
// avatar/SLA/etc. specific to your support team.
export function DefaultContactOffer({
  title,
  description,
  offer,
  onAccept,
  onDecline,
  isProcessing,
  classNames,
}: OfferStepProps) {
  const headline = title ?? offer.copy.headline
  const body = description ?? offer.copy.body

  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)}>
        <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 DefaultContactOffer when an offer of type 'contact' renders.
// Copy the source to fork from this baseline.

import { CancelFlow } from '@churnkey/react'
import { MyContactOffer } from './components/MyContactOffer'

<CancelFlow
  steps={[
    {
      type: 'survey',
      reasons: [{
        id: 'need-help',
        label: 'I need help with something',
        offer: {
          type: 'contact',
          url: 'mailto:support@example.com',
          label: 'Email support',
        },
      }],
    },
    { type: 'confirm' },
  ]}
  components={{ ContactOffer: MyContactOffer }}
  handleCancel={async () => myBilling.cancel()}
/>