All recipes
Default

Default redirect offer

DefaultRedirectOffer renders any offer of type 'redirect'. It's a link to an external URL — useful for sending the customer to onboarding, a recent product update, or a help article. The accept action is a regular anchor with target="_blank", so no handle<Type> callback runs. Fork to add a destination preview, SSO/auth parameters in the URL, or click analytics.

default component

Have you seen the new dashboard?

The redesign launched last week. Take a quick tour before you go.

The full source

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

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

export function DefaultRedirectOffer({ title, description, offer, onDecline, classNames }: OfferStepProps) {
  const url = (offer as OfferDecision & { url: string }).url
  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)}>
        <div className="ck-offer-details">
          <a href={url} target="_blank" rel="noopener noreferrer" className="ck-redirect-link">
            <span>{offer.copy.cta}</span>
            <ExternalIcon />
          </a>
        </div>
        <button type="button" className={cn('ck-button-link', classNames?.declineButton)} onClick={onDecline}>
          {offer.copy.declineCta}
        </button>
      </div>
    </div>
  )
}

function ExternalIcon() {
  return (
    <svg
      width="14"
      height="14"
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
      className="ck-redirect-icon"
      aria-hidden="true"
    >
      <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
      <polyline points="15 3 21 3 21 9" />
      <line x1="10" y1="14" x2="21" y2="3" />
    </svg>
  )
}

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

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

<CancelFlow
  steps={[
    {
      type: 'survey',
      reasons: [{
        id: 'missing-features',
        label: 'Missing features',
        offer: {
          type: 'redirect',
          url: 'https://example.com/whats-new',
          label: 'See the latest updates',
        },
      }],
    },
    { type: 'confirm' },
  ]}
  components={{ RedirectOffer: MyRedirectOffer }}
  handleCancel={async () => myBilling.cancel()}
/>