Add prebuilt custom-offer recipes to @churnkey/react#41
Conversation
Ship TermExtensionOffer and PassthroughOffer behind a new @churnkey/react/recipes subpath export so merchants can render merchant-executed custom offers (term extension, transfers) without building the UI themselves. Recipes reuse the built-in offer styling and pass their result through onAccept. Also fixes two SDK bugs surfaced while testing the recipes end to end: - machine: customer/subscriptions props were dropped in local mode unless appId was set, so components never saw the period end - transform: survey-attached offers lost server-resolved copy and decisionId in token mode, rendering default copy instead of the dashboard's and breaking the presented/accepted analytics join ReasonConfig.offer now accepts OfferDecision to match the runtime, the copy-paste templates moved from recipes/ to examples/ to free the name, and the playground gained a prebuilt-recipes demo section plus recipe wiring in token mode.
robert-moore
left a comment
There was a problem hiding this comment.
Looks good. Went through the diff against main — the recipes ride the existing customComponents contract with no core changes, and both drive-by fixes check out: initializeFromConfig still rebuilds token-mode state from the server config so the machine change is safe, and toDecision already passes through anything carrying copy so the transform change matches how standalone offer steps work. The declineCta ordering had me for a second but it's the same pattern all the built-ins use (defaultMessages.offer.declineCta is '' by design).
Two small things, fine as follow-ups:
- The transform fix got a regression test but the machine fix didn't. Worth one asserting state.subscriptions populates in local mode without appId — that's the exact case that was broken.
license_plate: 'kekis'in the playground token demo reads like leftover test data — drop it or swap in something self-explanatory.
The i18n gap and the registry not being self-serve are noted in the description; agreed both are fine for v1, but let's get a ticket up for registry access before the TMF rollout.
…ibute - Add a regression test asserting customer/subscriptions seed local-mode state without an appId — the exact case the machine fix restored - Replace the leftover license_plate test attribute in the playground token demo with a self-explanatory plan_tier example
Summary
Adds a prebuilt tier of custom-offer components behind a new
@churnkey/react/recipessubpath export, so merchants who own the billing action but don't want to build or maintain offer UI (the Motley Fool ask) get a one-import integration:TermExtensionOffer— merchant-executed "delay your next charge by N days". Readsoffer.data.days, shows the extension badge and the computed new renewal date (current period end + N, hidden when no period end is available), renders the flow-authored copy, and passes{ days }throughonAccept.PassthroughOffer— copy + accept/decline for offer types whose substance lives entirely in the builder copy (scheduled/immediate transfer). Passesoffer.datathrough as the result; one component covers any number of such types.Recipes reuse the built-in
ck-*styling system, so they inherit theming, appearance variables, and dark mode wholesale.Two SDK bugs surfaced during end-to-end testing and are fixed here:
customer/subscriptionsprops were stored only whenappIdwas set, so pure local-mode integrations never saw subscription data (recipes couldn't compute the renewal date, Confirm couldn't show the period-end notice).copyanddecisionIdin token mode — flows rendered synthesized default copy ("Before you go...") instead of the dashboard's, for every offer type, and the presented/accepted analytics join ondecisionIdwas broken.Supporting changes:
ReasonConfig.offerwidened toOfferConfig | OfferDecision(the runtime already handled decisions viatoDecision); the copy-paste templates moved frompackages/react/recipes/topackages/react/examples/to free therecipesname (they were repo-only: unpublished and unreferenced); playground gained a "5b. Prebuilt Recipes" demo section, recipe wiring +customerAttributesin the token-mode section, and a@churnkey/react/recipesvite alias.Notes
defaultMessages— integrator i18n overrides don't reach them becauseCustomOfferPropsdoesn't carrymessages. Acceptable for v1; fixable later by extendingCustomOfferProps.Test plan
vitest: 235 passing (10 new recipe tests, 1 new transform regression test)tscclean inpackages/reactandapps/playground; biome cleandist/recipes.*emitted, recipes absent from the main bundleonAccept({ days })/datapassthrough payloadscustomerAttributes, builder copy anddata.daysreach the recipe, date computed from the live Stripe period end, accept payload{ type, data, result }correct; passthrough pipeline verified with a registered showcase offer type