From 7d013aa5bff8470724edf805f316add18f9c73e5 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Mon, 1 Jun 2026 11:14:43 -0400 Subject: [PATCH 1/3] Show the refunded tax on the rebate panel (0.4.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a taxed invoice the rebate is refunded with the tax paid on it, so the panel now shows the actual cash back. The middle line reads 'Money back' with the full refund and a quiet '(incl. $X tax)' note, and the net reflects the full refund. Derived from the server-resolved net (refund = paid − net), so no new public fields. No-tax invoices are unchanged — the note is hidden and the refund equals the rebate. --- apps/playground/src/TestHarness.tsx | 4 +++- packages/react/CHANGELOG.md | 6 +++++ packages/react/package.json | 2 +- .../steps/offer/default-rebate-offer.tsx | 22 ++++++++++++++----- packages/react/src/styles/cancel-flow.css | 9 +++++++- 5 files changed, 35 insertions(+), 8 deletions(-) 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..026bf2e 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,13 @@ export function DefaultRebateOffer({ const body = description ?? offer.copy.body const currency = o.currency ?? 'usd' const amount = o.amountMinor ?? 0 + // The card is refunded the rebate plus the tax on it. The server resolves the + // net after that full refund, so derive the refund — and the tax on the rebate + // — from it. With no tax on top the refund equals the rebate and the tax note + // is hidden. + const refund = + o.amountPaidMinor != null && o.netAfterRebateMinor != null ? o.amountPaidMinor - o.netAfterRebateMinor : amount + const taxRefunded = refund - amount return (
@@ -32,9 +39,9 @@ export function DefaultRebateOffer({
{/* 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. */} + money back (the accented line — the rebate plus any tax refunded on + it), and the net after that. Paid and net are server-resolved in + token mode, so each renders only when present. */}
{o.amountPaidMinor != null && (
@@ -43,8 +50,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/styles/cancel-flow.css b/packages/react/src/styles/cancel-flow.css index a6d7d94..867def5 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 line — the figure we want the eye to land on. */ .ck-cancel-flow .ck-offer-rebate-credit { font-size: 14px; font-weight: 600; color: var(--ck-color-primary); } +/* The "(incl. tax)" note on the money-back line — quieter than the figure. */ +.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; From d12d3f1359ff0431f29ef390c57a5df193077ff2 Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Mon, 1 Jun 2026 11:22:03 -0400 Subject: [PATCH 2/3] Fix stale rebate type docs for the pre-tax-base semantic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit amountMinor is the rebate (pre-tax), not the cash refunded — the card gets it plus any tax. netAfterRebateMinor subtracts the full refund (rebate + tax), not just the rebate. Doc-only; the runtime already behaves this way. --- packages/react/src/core/api-types.ts | 4 ++-- packages/react/src/core/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 From 44985eb560af646a9fdbf1b234f19d9b78cc572c Mon Sep 17 00:00:00 2001 From: Rob Moore Date: Mon, 1 Jun 2026 11:27:41 -0400 Subject: [PATCH 3/3] Tighten rebate comments --- .../components/steps/offer/default-rebate-offer.tsx | 11 +++-------- packages/react/src/styles/cancel-flow.css | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) 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 026bf2e..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,10 +24,8 @@ export function DefaultRebateOffer({ const body = description ?? offer.copy.body const currency = o.currency ?? 'usd' const amount = o.amountMinor ?? 0 - // The card is refunded the rebate plus the tax on it. The server resolves the - // net after that full refund, so derive the refund — and the tax on the rebate - // — from it. With no tax on top the refund equals the rebate and the tax note - // is hidden. + // 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 @@ -38,10 +36,7 @@ export function DefaultRebateOffer({ {body && }
- {/* Itemized like an invoice: what they already paid this period, the - money back (the accented line — the rebate plus any tax refunded on - it), and the net after that. 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 && (
diff --git a/packages/react/src/styles/cancel-flow.css b/packages/react/src/styles/cancel-flow.css index 867def5..cec577f 100644 --- a/packages/react/src/styles/cancel-flow.css +++ b/packages/react/src/styles/cancel-flow.css @@ -482,14 +482,14 @@ color: var(--ck-color-text-muted); } -/* The money-back 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 on the money-back line — quieter than the figure. */ +/* The "(incl. tax)" note, kept quieter than the amount. */ .ck-cancel-flow .ck-offer-rebate-tax { font-size: 13px; font-weight: 400;