2.0: mostrar los dos ultimos pasos del onboarding (aha moment + paywall)

completeOnboarding escribia settings.onboardingCompleted al terminar el paso
de platos. Ese flag es el que ContentView observa para cambiar de OnboardingView
a HomeView, asi que el save destruia el flujo antes de que nextStep() pudiera
pintar el paso 5. WeekReadyStepView (el aha moment de la 1.2.0) y PaywallStepView
no se han visto nunca. GA4 lo confirma: 3 onboarding_completed en 28 dias y cero
eventos con step 5 o 6.

Se separa persistir los datos de marcar el onboarding como terminado:
completeOnboarding(markFinished:) commitea platos y plan sin tocar el flag, y el
nuevo finishOnboarding() lo escribe al salir del paywall. HomeView ya difiere su
prompt premium a la segunda sesion, asi que no hay paywall duplicado.

De paso, los nombres de paso en AnalyticsService estaban desalineados: el indice
5 decia "5_paywall" cuando el paso 5 es WeekReady, y el 6 no tenia nombre.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Ks8uUcMA9mjypVK7F2Pkt
This commit is contained in:
alexandrev-tibco
2026-08-05 20:53:10 +02:00
parent 10a3d8bf32
commit 3694297d4f
4 changed files with 97 additions and 6 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ enum AnalyticsService {
// Keep in sync with the OnboardingView step order.
private static let onboardingStepNames = [
"0_welcome", "1_meal_windows", "2_weekends",
"3_calendar", "4_first_dishes", "5_paywall"
"3_calendar", "4_first_dishes", "5_week_ready", "6_paywall"
]
static func logOnboardingStepViewed(step: Int) {
+28 -2
View File
@@ -120,8 +120,15 @@ final class OnboardingViewModel: ObservableObject {
}
}
/// Persists the choices made during onboarding (settings, dishes, first week).
///
/// `markFinished` must stay `false` while the flow is still on screen: the
/// `onboardingCompleted` flag is what ContentView watches to swap onboarding
/// for the home, so writing it here would tear the flow down before the last
/// two steps (Week Ready and paywall) get a chance to render. Call
/// `finishOnboarding(context:)` once the user leaves the final step.
@discardableResult
func completeOnboarding(context: ModelContext) -> Bool {
func completeOnboarding(context: ModelContext, markFinished: Bool = true) -> Bool {
// Create or get settings
let descriptor = FetchDescriptor<AppSettings>()
let settings = (try? context.fetch(descriptor))?.first ?? {
@@ -137,7 +144,9 @@ final class OnboardingViewModel: ObservableObject {
settings.calendarId = selectedCalendarId
settings.lunchTime = lunchTime
settings.dinnerTime = dinnerTime
settings.onboardingCompleted = true
if markFinished {
settings.onboardingCompleted = true
}
// Create default tags if not exist
let tagDescriptor = FetchDescriptor<Tag>()
@@ -178,6 +187,23 @@ final class OnboardingViewModel: ObservableObject {
}
}
/// Flips the flag ContentView watches, handing the user over to the home.
/// Separate from `completeOnboarding` so the data can be committed at the
/// dishes step while the flow stays on screen for the last two steps.
func finishOnboarding(context: ModelContext) {
let descriptor = FetchDescriptor<AppSettings>()
guard let settings = (try? context.fetch(descriptor))?.first else { return }
guard !settings.onboardingCompleted else { return }
settings.onboardingCompleted = true
do {
try context.save()
} catch {
CrashlyticsService.record(error, context: "onboarding_finish")
print("Onboarding finish failed: \(error)")
}
}
/// Fills the first week right inside onboarding (the "aha moment" step) using
/// the same engine as the home auto-complete. Returns the number of filled slots.
@discardableResult
+10 -3
View File
@@ -63,7 +63,9 @@ struct OnboardingView: View {
viewModel: viewModel,
tags: tags,
onFinish: {
_ = viewModel.completeOnboarding(context: context)
// Commit the data but leave onboarding on screen: the flag
// that dismisses it is written when the paywall step exits.
_ = viewModel.completeOnboarding(context: context, markFinished: false)
viewModel.autoFillFirstWeek(context: context)
viewModel.nextStep()
}
@@ -79,8 +81,8 @@ struct OnboardingView: View {
.tag(5)
PaywallStepView(
onSkip: { onComplete() },
onConverted: { onComplete() }
onSkip: { finishOnboarding() },
onConverted: { finishOnboarding() }
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.tag(6)
@@ -100,6 +102,11 @@ struct OnboardingView: View {
}
}
private func finishOnboarding() {
viewModel.finishOnboarding(context: context)
onComplete()
}
private func ensureDefaultTagsIfNeeded() {
let descriptor = FetchDescriptor<Tag>()
if (try? context.fetch(descriptor))?.isEmpty ?? true {