All recipes
Recipe
Name your own price
A custom step that asks the customer what they'd pay if they could set the price themselves. Useful right after a 'too expensive' survey reason, or at the end of the flow as a willingness-to-pay signal. The submitted amount lands on the recorded session under customStepResults['name-your-price'] as { amount, currency } — or { amount: null } if the customer picks 'I'd rather not say'. Edit the input formatting and currency symbol logic in the source to fit your locale.
custom recipe
Question 1 of 1
What price would feel fair for what you used?
We'll consider every answer when we set next year's pricing.
The full source
Copy this file into your project. It's the same file the showcase preview above renders from.
components/NameYourPrice.tsxtsx
/**
* "Name your own price" custom step. Asks the customer what they think the
* subscription is worth — useful right after a survey reason like "too
* expensive" or at the end of a flow as a willingness-to-pay signal.
*
* The answer lands on the recorded session under
* `customStepResults['name-your-price']` (or whatever step `type` you
* register), with shape `{ amount: number | null, currency: string }`.
* `amount: null` means the customer picked "I'd rather not say".
*
* Wire it as a custom step:
*
* <CancelFlow
* steps={[
* ...,
* {
* type: 'name-your-price',
* title: 'What price would feel fair for what you used?',
* description: "We'll consider every answer when we set next year's pricing.",
* data: { initial: 14, currency: 'USD', interval: 'mo' },
* },
* ]}
* customComponents={{ 'name-your-price': NameYourPrice }}
* />
*
* The "QUESTION N OF M" eyebrow is a brand pattern, not an SDK feature —
* pass `step.data.questionNumber` / `step.data.totalQuestions` to show it
* dynamically, or hardcode in the JSX below.
*/
import type { CustomStepProps } from '@churnkey/react/core'
import { useState } from 'react'
export function NameYourPrice({ step, onNext }: CustomStepProps) {
const initial = typeof step.data?.initial === 'number' ? step.data.initial : 0
const currency = (step.data?.currency as string | undefined) ?? 'USD'
const interval = (step.data?.interval as string | undefined) ?? 'mo'
const questionNumber = step.data?.questionNumber as number | undefined
const totalQuestions = step.data?.totalQuestions as number | undefined
const [amount, setAmount] = useState<number>(initial)
const symbol = currencySymbol(currency)
return (
<div className="ck-step">
{questionNumber != null && totalQuestions != null && (
<div
style={{
fontSize: 11,
fontWeight: 600,
letterSpacing: '0.1em',
textTransform: 'uppercase',
color: 'var(--ck-color-text-muted)',
marginBottom: 16,
}}
>
Question {questionNumber} of {totalQuestions}
</div>
)}
<h2 className="ck-step-title">{step.title ?? 'What price would feel fair?'}</h2>
{step.description && <p className="ck-step-description">{step.description}</p>}
<label
style={{
display: 'grid',
gridTemplateColumns: 'auto 1fr auto',
alignItems: 'center',
gap: 12,
padding: '20px 24px',
marginTop: 8,
marginBottom: 20,
border: '1.5px solid var(--ck-color-text)',
borderRadius: 'var(--ck-radius-lg)',
background: 'var(--ck-color-surface)',
cursor: 'text',
}}
>
<span
aria-hidden
style={{
fontFamily: 'var(--ck-font-display)',
fontSize: 28,
fontWeight: 500,
color: 'var(--ck-color-text)',
}}
>
{symbol}
</span>
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={Number.isFinite(amount) && amount > 0 ? amount : ''}
onChange={(e) => {
const cleaned = e.target.value.replace(/[^0-9]/g, '')
setAmount(cleaned ? Number(cleaned) : 0)
}}
aria-label={`Amount in ${currency}`}
style={{
appearance: 'none',
border: 'none',
background: 'transparent',
outline: 'none',
fontFamily: 'var(--ck-font-display)',
fontSize: 28,
fontWeight: 500,
color: 'var(--ck-color-text)',
width: '100%',
padding: 0,
}}
/>
<span
aria-hidden
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
color: 'var(--ck-color-text-muted)',
fontSize: 14,
fontFamily: 'var(--ck-font-display)',
whiteSpace: 'nowrap',
}}
>
<span style={{ fontSize: 22, lineHeight: 1, transform: 'translateY(-2px)' }}>/</span>
<span>{interval}</span>
</span>
</label>
<button
type="button"
onClick={() => onNext({ amount: amount > 0 ? amount : null, currency })}
disabled={amount <= 0}
style={{
width: '100%',
padding: '14px 20px',
background: 'var(--ck-color-primary)',
color: '#fff',
border: 'none',
borderRadius: 'var(--ck-radius-md)',
fontSize: 15,
fontWeight: 600,
fontFamily: 'inherit',
textAlign: 'center',
cursor: amount > 0 ? 'pointer' : 'not-allowed',
opacity: amount > 0 ? 1 : 0.4,
marginBottom: 14,
}}
>
Submit and continue
</button>
<div style={{ textAlign: 'center' }}>
<button
type="button"
onClick={() => onNext({ amount: null, currency })}
style={{
padding: '4px 8px',
background: 'transparent',
border: 'none',
color: 'var(--ck-color-text-secondary)',
fontSize: 14,
fontFamily: 'inherit',
cursor: 'pointer',
}}
>
I'd rather not say
</button>
</div>
</div>
)
}
function currencySymbol(code: string): string {
switch (code.toUpperCase()) {
case 'USD':
case 'CAD':
case 'AUD':
case 'NZD':
return '$'
case 'EUR':
return '€'
case 'GBP':
return '£'
case 'JPY':
return '¥'
default:
return code + ' '
}
}
Wire it into a flow
Drop the file into your codebase and reference it from the appropriate prop on <CancelFlow>.
CancelButton.tsxtsx
import { CancelFlow } from '@churnkey/react'
import '@churnkey/react/styles.css'
import { NameYourPrice } from './components/NameYourPrice'
<CancelFlow
steps={[
{ type: 'survey', reasons: [/* ... */] },
{
type: 'name-your-price',
title: 'What price would feel fair for what you used?',
description: "We'll consider every answer when we set next year's pricing.",
data: { initial: 14, currency: 'USD', interval: 'mo' },
},
{ type: 'confirm' },
]}
customComponents={{ 'name-your-price': NameYourPrice }}
handleCancel={async () => myBilling.cancel()}
/>
// The submitted price lands on the session as:
// customStepResults['name-your-price'] = { amount: 14, currency: 'USD' }
//
// or { amount: null } if the customer chose "I'd rather not say".