diff --git a/apps/playground/src/TestHarness.tsx b/apps/playground/src/TestHarness.tsx
index 7386b74..5a1a580 100644
--- a/apps/playground/src/TestHarness.tsx
+++ b/apps/playground/src/TestHarness.tsx
@@ -202,7 +202,9 @@ const allOffersSteps: Step[] = [
amountMinor: 500,
currency: 'USD',
amountPaidMinor: 10890,
- netAfterRebateMinor: 10390,
+ // Net reflects the full refund (rebate + tax): $5.00 rebate + $0.50
+ // tax → $5.50 back, so the "(incl. $0.50 tax)" note shows.
+ netAfterRebateMinor: 10340,
paymentMethodBrand: 'visa',
paymentMethodLast4: '4242',
},
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index 60f4598..ccc99db 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -2,6 +2,12 @@
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Expect breaking changes in minor versions while we're pre-1.0.
+## 0.4.2 — 2026-06-01
+
+### Changed
+
+- `DefaultRebateOffer` shows the actual cash refunded. On a taxed invoice the rebate comes back with the tax paid on it, so the middle line reads "Money back" with the full refund and an "(incl. $X tax)" note, and the net reflects it. No-tax invoices are unchanged — the note is hidden and the refund equals the rebate.
+
## 0.4.1 — 2026-05-30
### Changed
diff --git a/packages/react/package.json b/packages/react/package.json
index ac762e1..c2644cf 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@churnkey/react",
- "version": "0.4.1",
+ "version": "0.4.2",
"description": "Production-ready cancel flow for React. Drop-in component, headless hook, or full customization. Works standalone or with Churnkey for AI-powered retention.",
"license": "MIT",
"repository": {
diff --git a/packages/react/src/components/steps/offer/default-rebate-offer.tsx b/packages/react/src/components/steps/offer/default-rebate-offer.tsx
index 5de7d6e..7f99a83 100644
--- a/packages/react/src/components/steps/offer/default-rebate-offer.tsx
+++ b/packages/react/src/components/steps/offer/default-rebate-offer.tsx
@@ -24,6 +24,11 @@ export function DefaultRebateOffer({
const body = description ?? offer.copy.body
const currency = o.currency ?? 'usd'
const amount = o.amountMinor ?? 0
+ // refund = paid - net; tax = refund - rebate. The server's net already
+ // accounts for tax refunded on the rebate, so no tax means refund == rebate.
+ const refund =
+ o.amountPaidMinor != null && o.netAfterRebateMinor != null ? o.amountPaidMinor - o.netAfterRebateMinor : amount
+ const taxRefunded = refund - amount
return (
@@ -31,10 +36,7 @@ export function DefaultRebateOffer({
{body &&
}
- {/* Itemized like an invoice: what they already paid this period, the
- rebate we credit (the accented line), and the net after the refund.
- Paid and net are server-resolved in token mode, so each renders
- only when present. */}
+ {/* paid / money back / net, like an invoice. Paid and net only exist in token mode. */}
{o.amountPaidMinor != null && (
@@ -43,8 +45,13 @@ export function DefaultRebateOffer({
)}
- Cancellation rebate
- −{formatPriceFromMinor(amount, currency)}
+
+ Money back
+ {taxRefunded > 0 && (
+ (incl. {formatPriceFromMinor(taxRefunded, currency)} tax)
+ )}
+
+ −{formatPriceFromMinor(refund, currency)}
{o.netAfterRebateMinor != null && (
diff --git a/packages/react/src/core/api-types.ts b/packages/react/src/core/api-types.ts
index 42be6ad..168fb31 100644
--- a/packages/react/src/core/api-types.ts
+++ b/packages/react/src/core/api-types.ts
@@ -114,12 +114,12 @@ export interface SdkContactOffer extends SdkOfferBase {
export interface SdkRebateOffer extends SdkOfferBase {
type: 'rebate'
- /** Cash refunded to the card, smallest currency unit. */
+ /** The rebate amount (pre-tax). The card is refunded this plus any tax charged on it. */
amountMinor: number
currency: string
/** Gross amount paid on the target invoice — the "you paid" row. */
amountPaidMinor: number
- /** amountPaidMinor − amountMinor — the "your net" row. */
+ /** amountPaidMinor minus the full refund (rebate plus its tax) — the "your net" row. */
netAfterRebateMinor: number
paymentMethodBrand?: string
paymentMethodLast4?: string
diff --git a/packages/react/src/core/types.ts b/packages/react/src/core/types.ts
index c362056..79ab9c3 100644
--- a/packages/react/src/core/types.ts
+++ b/packages/react/src/core/types.ts
@@ -163,12 +163,12 @@ export interface RedirectOffer {
export interface RebateOffer {
type: 'rebate'
- /** Cash refunded to the card, smallest currency unit. */
+ /** The rebate amount (pre-tax), smallest currency unit. The card is refunded this plus any tax charged on it. */
amountMinor: number
currency: string
/** Gross paid on the target invoice. Server-resolved in token mode. */
amountPaidMinor?: number
- /** amountPaidMinor − amountMinor. Server-resolved in token mode. */
+ /** amountPaidMinor minus the full refund (the rebate plus its tax). Server-resolved in token mode. */
netAfterRebateMinor?: number
paymentMethodBrand?: string
paymentMethodLast4?: string
diff --git a/packages/react/src/styles/cancel-flow.css b/packages/react/src/styles/cancel-flow.css
index a6d7d94..cec577f 100644
--- a/packages/react/src/styles/cancel-flow.css
+++ b/packages/react/src/styles/cancel-flow.css
@@ -482,13 +482,20 @@
color: var(--ck-color-text-muted);
}
-/* The rebate line — the figure we want the eye to land on. */
+/* The money-back amount — the accented line. */
.ck-cancel-flow .ck-offer-rebate-credit {
font-size: 14px;
font-weight: 600;
color: var(--ck-color-primary);
}
+/* The "(incl. tax)" note, kept quieter than the amount. */
+.ck-cancel-flow .ck-offer-rebate-tax {
+ font-size: 13px;
+ font-weight: 400;
+ color: var(--ck-color-text-muted);
+}
+
.ck-cancel-flow .ck-offer-rebate-total {
margin-top: 6px;
padding-top: 14px;