Onboarding
DeliciousKit's onboarding is built around what actually moves install-to-paid conversion: value first, then ask. You describe the flow as data; DeliciousKit renders it, captures the answers, personalizes the paywall, and fires analytics on every step.
Describe the flow
extension OnboardingConfig {
static var myApp: OnboardingConfig {
OnboardingConfig(
theme: DeliciousKitTheme(accent: .blue),
steps: [
.feature(title: "Welcome", subtitle: "What this app does.", systemImage: "sparkles"),
.singleSelect(key: "goal", question: "What's your goal?", options: [
OnboardingOption(id: "a", label: "Get fit", emoji: "💪"),
OnboardingOption(id: "b", label: "Lose weight", emoji: "🥗")
]),
.commitment(key: "mins", question: "Minutes a day?", range: 5...60,
step: 5, unit: " min", defaultValue: 15),
.building(title: "Building your plan…", subtitle: "One moment.",
messages: ["Analyzing", "Personalizing", "Done!"]),
.paywall
],
paywall: PaywallConfig(/* see Paywall & purchases */) // omit entirely if you don't use `.paywall` in steps
)
}
}
The step types
.feature— a value screen (title, subtitle, SF Symbol). Show what the app does, not a feature list..singleSelect— a one-tap question that personalizes the experience and/or qualifies the pitch..commitment— a slider ("minutes a day?"). A small investment that lifts ownership..building— a short "building your plan…" beat right before the ask..permission— a system-permission ask, with App-Review-compliant three-state handling built in (see below)..paywall— the upsell, with its headline personalized from the user's answers. Optional — omit it fromstepsfor a usage-gated paywall shown later instead (see Paywall & purchases).
Permission steps
.permission generalizes the three-state pattern every permission ask needs — not-yet-asked, authorized, denied — so you don't hand-roll it per permission, per app:
.permission(
key: "push",
title: "Stay in the loop",
subtitle: "Get notified when it matters.",
details: "We'll only send what's genuinely useful.",
deniedDetails: "Notifications are off. Enable them in Settings anytime.",
systemImage: "bell.badge.fill",
checkStatus: {
let settings = await UNUserNotificationCenter.current().notificationSettings()
switch settings.authorizationStatus {
case .authorized, .provisional: return .authorized
case .denied: return .denied
default: return .notRequested
}
},
request: {
let granted = (try? await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
return granted ? .authorized : .denied
}
)
DeliciousKit doesn't hardcode which permission this is — checkStatus/request are your own closures, so the same step works for push, location, camera, contacts, anything with an OS authorization API. The rendered step handles the rest:
- Not yet asked — the button triggers your
request()closure (which shows the system dialog). - Authorized — a green checkmark; the button advances.
- Denied — the button becomes "Open Settings" (deep-links out), with a small "Continue" link below it. There's deliberately no button that skips before the system dialog — Apple's Guideline 5.1.1(iv) only permits a way forward after a denial, never a bypass of the dialog itself.
Fires .permissionRequested / .permissionGranted / .permissionDenied analytics events automatically, keyed by the key you pass in.
Why this order works
Value before commitment; small commitments before the ask; a "building" moment to deepen investment; then a paywall framed around what the user just told you. It defaults to a soft (skippable) paywall — flip allowSkip: false for a hard one. The deeper craft (when to use a questionnaire vs. value-paging, permission priming) lives in the Delicious Onboarding skill.
Next: Paywall & purchases.