Build your own cancellation UI with headless cancel flow primitives
The built-in cancel flow (presentCancelFlow) provides a complete retention sheet out of the box. If you need full control over the UI — custom animations, native-feeling screens, or a flow that matches your app’s design system — use the headless cancel flow primitives instead.You get the same backend-driven questionnaire, save offers, and pause options, but you render every pixel yourself.
The headless cancel flow is a set of flat, standalone methods. There’s no session object or state machine — you call each primitive when you need it, in whatever order makes sense for your UI.
When a user selects a questionnaire option where triggersOffer is true, show the save offer from config.offer. If the user accepts, call acceptSaveOffer to apply the discount via Stripe.
Copy
do { let result = try await ZeroSettle.shared.acceptSaveOffer( productId: "premium_monthly", userId: currentUser.id ) showConfirmation(result.message) // "40% off for 3 months"} catch { showError(error.localizedDescription)}
Human-readable description (e.g., “40% off for 3 months”)
discountPercent
Int?
Discount percentage, if applicable
durationMonths
Int?
Duration of the discount in months, if applicable
acceptSaveOffer creates a Stripe coupon and applies it to the user’s subscription. This is a real billing action — the discount takes effect immediately.
When a user selects a questionnaire option where triggersPause is true, show the pause options from config.pause. Let the user pick a duration, then call pauseSubscription with the selected option ID.
Copy
do { let resumesAt = try await ZeroSettle.shared.pauseSubscription( productId: "premium_monthly", userId: currentUser.id, pauseOptionId: selectedPauseOption.id ) if let resumesAt { showConfirmation("Paused until \(resumesAt.formatted())") }} catch { showError(error.localizedDescription)}
Returns an ISO 8601 date string (or Date on Swift) for when the subscription will automatically resume, or null if the pause was applied without a set resume date.
If the user completes the questionnaire and still wants to cancel:
Copy
try await ZeroSettle.shared.cancelSubscription( productId: "premium_monthly", userId: currentUser.id, immediate: false // Cancel at period end (default))
Parameter
Type
Default
Description
productId
String
—
The product to cancel
userId
String
—
Your app’s user identifier
immediate
Bool
false
If true, cancel immediately. If false, cancel at the end of the current billing period.
After the user completes your cancel flow — regardless of outcome — submit a response for analytics tracking. This powers your retention funnel metrics on the dashboard.
Here’s a typical sequence for a cancel flow with a save offer:
Copy
User taps "Cancel Subscription" │ ▼Read cancelFlowConfig.questionsShow questionnaire UI │ ▼ (user selects an option where triggersOffer = true) │Show save offer (config.offer.title, body, ctaText) │ ├── User accepts → acceptSaveOffer() → Show "Thanks for staying!" │ ├── User declines, triggersPause = true → Show pause options │ ├── User pauses → pauseSubscription() → Show "Paused until..." │ └── User declines → cancelSubscription() → Show "Cancelled" │ └── User declines → cancelSubscription() → Show "Cancelled" │ ▼submitCancelFlowResponse() ← Always call this at the end
The questionnaire, save offer text, and pause options are all configured on the ZeroSettle dashboard — no code changes needed to update them.