feat(react): customer-aware step components and freeform survey#5
Merged
Conversation
…tion to all steps, style: plan change step with awareness of current plan
Rename the field and action surface so the SDK's vocabulary matches the embed widget, and split the typed text onto its own session field so dashboard groupings by reason label stay stable. - FlowState/SurveyStepProps: freeformText → followupResponse, onFreeformChange → onFollowupResponseChange - Machine: setFreeformText → setFollowupResponse - SurveyClassNames.freeformInput → followupInput - CSS .ck-reason-freeform → .ck-reason-followup - SessionPayload: add followupResponse; surveyChoiceValue is now always the static reason label rather than dual-use for typed text ReasonConfig.freeform stays as the discriminator — only naming downstream of it moves.
There was a problem hiding this comment.
Pull request overview
This PR makes React cancel-flow steps customer/subscription aware, adds follow-up survey responses, and updates default step behavior to use subscription context for plan-change and confirmation UI.
Changes:
- Adds
followupResponsestate/payload support forfreeformsurvey reasons and exposes it through components/headless hook. - Passes
customerandsubscriptionsthrough default/custom step props and uses subscriptions in plan-change/current-plan and confirm/period-end rendering. - Adds overlay class customization, exports offer utilities/formatting, trims unused public props, and adds tests/playground updates.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
packages/react/tests/headless/use-cancel-flow.test.tsx |
Adds headless hook coverage for retry. |
packages/react/tests/core/merge-fields.test.ts |
Updates offer copy merge assertions for broader offer shape. |
packages/react/tests/core/machine.test.ts |
Adds machine tests for customer/subscriptions, follow-up state, and standalone offers. |
packages/react/tests/components/cancel-flow.test.tsx |
Adds component tests for overlay class, freeform textarea, period-end notice, unknown offers, and current plan UI. |
packages/react/src/styles/cancel-flow.css |
Adds follow-up textarea/current-plan styling and updates overlay default. |
packages/react/src/headless/use-cancel-flow.ts |
Exposes retry and setFollowupResponse. |
packages/react/src/core/utils.ts |
Exports built-in offer type list. |
packages/react/src/core/types.ts |
Updates public types for follow-up responses, subscriptions, accepted offers, and removed props. |
packages/react/src/core/transform.ts |
Maps SDK freeform/offer fields to the updated runtime shape. |
packages/react/src/core/step-graph.ts |
Normalizes standalone offer steps with synthesized copy. |
packages/react/src/core/merge-fields.ts |
Supports merge-field processing for offers without copy. |
packages/react/src/core/machine.ts |
Adds follow-up state/payload handling and customer/subscription initial state. |
packages/react/src/core/index.ts |
Exports formatPeriodEnd and BUILT_IN_OFFER_TYPES. |
packages/react/src/core/format.ts |
Adds period-end formatting helper. |
packages/react/src/core/api.ts |
Adds followupResponse to session payload. |
packages/react/src/components/structural/default-modal.tsx |
Threads overlay class name into the modal overlay. |
packages/react/src/components/steps/offer/default-plan-change-offer.tsx |
Uses subscriptions to mark/disable the current plan. |
packages/react/src/components/steps/default-survey.tsx |
Renders follow-up textarea for freeform reasons. |
packages/react/src/components/steps/default-confirm.tsx |
Derives period-end notice from subscriptions. |
packages/react/src/components/cancel-flow.tsx |
Passes customer/subscriptions to steps and skips unregistered offers. |
apps/playground/src/RecipeBrowser.tsx |
Updates playground examples for new required props. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const status = subscriptions?.[0]?.status | ||
| if (!status || !('currentPeriod' in status) || !status.currentPeriod?.end) return null | ||
| const end = status.currentPeriod.end | ||
| const d = end instanceof Date ? end : new Date(end) |
Comment on lines
+203
to
+204
| * The text the customer types lands on the session as `surveyChoiceValue` | ||
| * (the reason's `id` still travels as `surveyChoiceId`). |
| @@ -308,7 +311,12 @@ export class CancelFlowMachine { | |||
|
|
|||
| selectReason = (id: string): void => { | |||
| if (!this.reasons.find((r) => r.id === id)) return | |||
| aria-label={title} | ||
| onKeyDown={handleKeyDown} | ||
| > | ||
| <div className={cn('ck-reason-list', classNames?.reasonList)} role="radiogroup" aria-label={title}> |
Comment on lines
+191
to
+192
| /** Payload from custom offers — whatever your component passed to | ||
| * `onAccept(result)`. Built-in offer types do not populate this. */ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three threads bundled here, all consequences of the same idea: step components should see the same customer the machine sees, so they can render context-aware UI without consumer code shuttling props around.
freeform: true.customerandsubscriptionsare passed to every step, not just custom ones.While the public surface is touching, a few props that were declared but never wired to behavior come out (
PauseOffer.datePicker,PlanChangeOffer.currentPlanId,ConfirmStepProps.periodEnd,CancelFlowProps.layout,CancelFlowProps.animation).Changes
Follow-up text on survey reasons
When a reason has
freeform: true,DefaultSurveyreveals a textarea below the reason list. The typed value lands on the session payload asfollowupResponse. The reason's staticlabelcontinues to travel assurveyChoiceValueand itsidassurveyChoiceId, so dashboard groupings by reason stay stable.This mirrors the embed widget's payload shape:
surveyChoiceValueis always the label, follow-up text always lives onfollowupResponse.New on the machine:
followupResponsestate andsetFollowupResponseaction. Both surfaced on theuseCancelFlowreturn value for headless consumers.customerandsubscriptionson every stepSurveyStepProps,OfferStepProps,FeedbackStepProps,ConfirmStepProps, andSuccessStepPropsall receivecustomer: DirectCustomer | nullandsubscriptions: DirectSubscription[]. Previously these were available only onCustomStepProps/CustomOfferProps.Plan-change is current-plan aware
DefaultPlanChangeOfferreadssubscriptions[0].items[0].price.idto identify the current plan. That card renders disabled with aCurrentbadge; the initial selection seeds to the first non-current plan.PlanChangeOffer.currentPlanIdremoved since it can be derived.Confirm derives period end from subscriptions
ConfirmStepProps.periodEndremoved.DefaultConfirmnow callsformatPeriodEnd(subscriptions), which returnsnullfor canceled, missing, or unparseable cases so the "access continues until..." notice is omitted cleanly instead of renderingInvalid Date.formatPeriodEndis exported from@churnkey/react/corefor consumers reusing the logic.Unregistered offer types auto-skip
A flow whose offer
typeisn't inBUILT_IN_OFFER_TYPESand has noCustomOfferregistered now auto-declines and advances. Same shape as the existing unregistered-step fallback.BUILT_IN_OFFER_TYPESis now exported.Overlay customization
ModalPropsgainsoverlayClassName.StructuralClassNames.overlayis threaded throughCancelFlow, so the overlay can be styled without replacing the wholeModal. The default overlay color also changes from a primary-tintedcolor-mix(...)to a neutral translucent ink; brands that want the old behavior can set--ck-overlay-colorback to the color-mix expression.API trim
Removed fields (none were wired to behavior):
PauseOffer.datePickerPlanChangeOffer.currentPlanIdConfirmStepProps.periodEndCancelFlowProps.layoutCancelFlowProps.animationAcceptedOffer.reasonIdbecomes optional. It's only present when the offer was routed from a survey reason; standaloneOfferSteps have no reason to carry.decisionIdis stripped fromAcceptedOffer(it was SDK-internal leaking through).Headless hook
useCancelFlownow also returnsretry.Breaking changes
Pre-1.0 minor.
customerandsubscriptionsprops. TypeScript will flag each site.freeformText/onFreeformChangetofollowupResponse/onFollowupResponseChange. ThefreeformInputclassName slot is nowfollowupInput. The internalck-reason-freeformCSS class isck-reason-followup.setFreeformTextis nowsetFollowupResponse.periodEnd,currentPlanId,datePicker,layout,animation) are no longer accepted. None previously did anything.AcceptedOffer.reasonIdis nowstring | undefined. Consumers reading it should handle the standalone-offer case.followupResponserather than overloadingsurveyChoiceValue. Any analytics consumer that was reading the typed text fromsurveyChoiceValueshould switch tofollowupResponse. Dashboard groupings by reason label become more reliable as a result.Tests
packages/react/tests/headless/use-cancel-flow.test.tsx(new): covers the headless hook surface includingfollowupResponseandretry.cancel-flow.test.tsx: adds the follow-up textarea path and the current-plan disabled state.machine.test.ts: adds follow-up state transitions, period-end derivation, unregistered-offer auto-skip.