Files
FamilyMealPlanner/MealMood/Views/Onboarding/WeekReadyStepView.swift
T
alexandrev-tibco 73c2039338 1.2.0: contextual notifications + in-onboarding auto-fill (aha moment)
- New "Week Ready" onboarding step: auto-fills the first week inline right
  after dish creation, then pitches the Sunday reminder with context before
  requesting OS notification permission (no more cold prompt on Home)
- Notifications section in Settings: weekly reminder toggle, deep-link to
  system settings when permission is denied
- NotificationService: remindersEnabled preference, requestPermission(),
  authorizationStatus(), cancelAllReminders()
- Post-onboarding premium prompt deferred to 2nd session (paywall was just
  shown as the final onboarding step — 2s-later alert was prompt fatigue)
- Back navigation blocked after onboarding commit to avoid duplicate dishes
- 10 new strings × 6 languages

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtWT52pAE91D7mbDda1cAM
2026-07-03 10:08:04 +02:00

116 lines
4.2 KiB
Swift

import SwiftUI
/// Onboarding "aha moment" step: shows the freshly auto-filled first week and
/// asks for the weekly reminder with context (instead of a cold OS prompt on Home).
struct WeekReadyStepView: View {
let filledCount: Int
var onContinue: () -> Void
@State private var isRequestingPermission = false
@State private var reminderGranted = false
var body: some View {
VStack(spacing: 24) {
VStack(spacing: 16) {
ZStack {
Circle()
.fill(Color.mealMoodCoral.opacity(0.12))
.frame(width: 88, height: 88)
Image(systemName: filledCount > 0 ? "checkmark.circle.fill" : "calendar.badge.checkmark")
.font(.system(size: 44, weight: .semibold))
.foregroundColor(.mealMoodCoral)
}
.padding(.top, 24)
Text("onboarding_week_ready_title")
.font(.mealMoodH1)
.foregroundColor(.mealMoodTextPrimary)
.multilineTextAlignment(.center)
Text(subtitleText)
.font(.mealMoodBody)
.foregroundColor(.mealMoodTextSecondary)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 24)
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 12) {
Image(systemName: "bell.badge.fill")
.font(.system(size: 22))
.foregroundColor(.mealMoodCoral)
Text("onboarding_reminder_pitch")
.font(.mealMoodSmall)
.foregroundColor(.mealMoodTextPrimary)
.fixedSize(horizontal: false, vertical: true)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.mealMoodSurface)
.cornerRadius(14)
.overlay(
RoundedRectangle(cornerRadius: 14)
.stroke(Color(hex: "#F0F0F0"), lineWidth: 1)
)
.padding(.horizontal, 24)
Spacer(minLength: 0)
VStack(spacing: 12) {
PrimaryButton(
title: reminderGranted
? String(localized: "onboarding_continue")
: String(localized: "onboarding_reminder_allow"),
action: {
if reminderGranted {
onContinue()
} else {
enableReminders()
}
}
)
.disabled(isRequestingPermission)
if !reminderGranted {
Button {
onContinue()
} label: {
Text("onboarding_reminder_skip")
.font(.mealMoodSmall)
.foregroundColor(.mealMoodTextSecondary)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 24)
.padding(.bottom, 40)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
}
private var subtitleText: String {
if filledCount > 0 {
return String(format: String(localized: "onboarding_week_ready_subtitle"), filledCount)
}
return String(localized: "onboarding_week_ready_subtitle_empty")
}
private func enableReminders() {
isRequestingPermission = true
Task {
let granted = await NotificationService.shared.requestPermission()
NotificationService.shared.remindersEnabled = true
AnalyticsService.logNotificationsToggled(enabled: granted)
isRequestingPermission = false
if granted {
reminderGranted = true
HapticManager.shared.notification(type: .success)
// Small beat so the user sees the button flip before moving on.
try? await Task.sleep(nanoseconds: 600_000_000)
}
onContinue()
}
}
}