All recipes

Passthrough

A prebuilt component for custom offers whose substance lives entirely in the flow-authored copy — transfers, waitlists, concierge handoffs. It renders the headline, body, and CTAs, and passes offer.data through onAccept verbatim. Register the same component under as many offer types as you need; your onAccept tells them apart by offer.type. Import it from @churnkey/react/recipes.

prebuilt component

Switch plans at renewal

We'll move you to the annual plan when this period ends — nothing changes today.

The full source

For reference — this component ships in the package, so import it from @churnkey/react/recipes instead of copying. Copy it only when you want to fork the presentation and own it from there.

@churnkey/react/recipestsx
import { RichText } from '../components/rich-text'
import { defaultMessages } from '../core/messages'
import type { CustomOfferProps } from '../core/types'

/**
 * Prebuilt renderer for custom offers whose substance lives entirely in the
 * builder copy — the customer reads the pitch and accepts or declines,
 * nothing else. One component covers any number of such offer types; the
 * per-type messaging comes from the server-resolved `offer.copy`:
 *
 *   <CancelFlow
 *     customComponents={{
 *       scheduled_transfer: PassthroughOffer,
 *       immediate_transfer: PassthroughOffer,
 *     }}
 *     onAccept={handleAccept}
 *   />
 *
 * On accept, `offer.data` (the builder-configured field values, if any)
 * passes through as the result; the merchant's handler tells offer types
 * apart via `customOfferType` on the accepted payload.
 */
export function PassthroughOffer({ offer, onAccept, onDecline, isProcessing }: CustomOfferProps) {
  const msg = defaultMessages
  // OfferDecision is a union; only the custom member carries `data`.
  const data = (offer as { data?: Record<string, unknown> }).data

  return (
    <div className="ck-step ck-step-offer">
      {offer.copy.headline && <h2 className="ck-step-title">{offer.copy.headline}</h2>}
      {offer.copy.body && <RichText as="div" html={offer.copy.body} className="ck-step-description" />}

      <div className="ck-offer-card">
        <button
          type="button"
          className="ck-button ck-button-primary"
          onClick={() => onAccept(data)}
          disabled={isProcessing}
        >
          {isProcessing ? msg.common.processing : offer.copy.cta}
        </button>
        <button type="button" className="ck-button-link" onClick={onDecline}>
          {msg.offer.declineCta || offer.copy.declineCta}
        </button>
      </div>
    </div>
  )
}

Wire it into a flow

Import the component and register it on customComponents under your offer type's key.

CancelButton.tsxtsx
import { CancelFlow } from '@churnkey/react'
import { PassthroughOffer } from '@churnkey/react/recipes'

<CancelFlow
  session={token}
  customComponents={{
    // One component, as many copy-only offer types as you need.
    scheduled_transfer: PassthroughOffer,
    immediate_transfer: PassthroughOffer,
  }}
  onAccept={async (offer) => {
    switch (offer.type) {
      case 'scheduled_transfer':  await myBilling.scheduleTransfer(); break
      case 'immediate_transfer':  await myBilling.transferNow(); break
    }
  }}
/>