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