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;