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
This commit is contained in:
@@ -362,3 +362,17 @@
|
||||
/* Week rating */
|
||||
"week_rating_prompt" = "Wie war diese Woche?";
|
||||
|
||||
|
||||
/* Week ready onboarding step (1.2.0) */
|
||||
"onboarding_week_ready_title" = "Deine Woche ist fertig!";
|
||||
"onboarding_week_ready_subtitle" = "%d Mahlzeiten automatisch geplant. Du kannst später alles anpassen.";
|
||||
"onboarding_week_ready_subtitle_empty" = "Dein Wochenplan ist eingerichtet. Füge jederzeit Gerichte hinzu und fülle ihn mit einem Tipp.";
|
||||
"onboarding_reminder_pitch" = "Erhalte jeden Sonntag eine Erinnerung, damit die Woche immer geplant beginnt.";
|
||||
"onboarding_reminder_allow" = "Sonntags erinnern";
|
||||
"onboarding_reminder_skip" = "Jetzt nicht";
|
||||
|
||||
/* Notification settings (1.2.0) */
|
||||
"settings_notifications_header" = "Mitteilungen";
|
||||
"settings_weekly_reminder" = "Wöchentliche Planungserinnerung";
|
||||
"settings_notifications_denied_hint" = "Mitteilungen sind in den Systemeinstellungen deaktiviert.";
|
||||
"settings_open_system_settings" = "Einstellungen öffnen";
|
||||
|
||||
@@ -362,3 +362,17 @@
|
||||
/* Week rating */
|
||||
"week_rating_prompt" = "How was this week?";
|
||||
|
||||
|
||||
/* Week ready onboarding step (1.2.0) */
|
||||
"onboarding_week_ready_title" = "Your week is ready!";
|
||||
"onboarding_week_ready_subtitle" = "%d meals planned automatically. You can tweak anything later.";
|
||||
"onboarding_week_ready_subtitle_empty" = "Your weekly plan is set up. Add dishes anytime and fill it in one tap.";
|
||||
"onboarding_reminder_pitch" = "Get a gentle reminder every Sunday so next week is always planned before it starts.";
|
||||
"onboarding_reminder_allow" = "Remind me on Sundays";
|
||||
"onboarding_reminder_skip" = "Not now";
|
||||
|
||||
/* Notification settings (1.2.0) */
|
||||
"settings_notifications_header" = "Notifications";
|
||||
"settings_weekly_reminder" = "Weekly planning reminder";
|
||||
"settings_notifications_denied_hint" = "Notifications are disabled in system settings.";
|
||||
"settings_open_system_settings" = "Open Settings";
|
||||
|
||||
@@ -362,3 +362,17 @@
|
||||
/* Week rating */
|
||||
"week_rating_prompt" = "¿Qué tal fue la semana?";
|
||||
|
||||
|
||||
/* Week ready onboarding step (1.2.0) */
|
||||
"onboarding_week_ready_title" = "¡Tu semana está lista!";
|
||||
"onboarding_week_ready_subtitle" = "%d comidas planificadas automáticamente. Podrás ajustarlo todo después.";
|
||||
"onboarding_week_ready_subtitle_empty" = "Tu plan semanal está preparado. Añade platos cuando quieras y complétalo con un toque.";
|
||||
"onboarding_reminder_pitch" = "Recibe un recordatorio cada domingo para que la semana siempre empiece planificada.";
|
||||
"onboarding_reminder_allow" = "Recuérdamelo los domingos";
|
||||
"onboarding_reminder_skip" = "Ahora no";
|
||||
|
||||
/* Notification settings (1.2.0) */
|
||||
"settings_notifications_header" = "Notificaciones";
|
||||
"settings_weekly_reminder" = "Recordatorio semanal de planificación";
|
||||
"settings_notifications_denied_hint" = "Las notificaciones están desactivadas en los ajustes del sistema.";
|
||||
"settings_open_system_settings" = "Abrir Ajustes";
|
||||
|
||||
@@ -362,3 +362,17 @@
|
||||
/* Week rating */
|
||||
"week_rating_prompt" = "Comment s'est passée cette semaine ?";
|
||||
|
||||
|
||||
/* Week ready onboarding step (1.2.0) */
|
||||
"onboarding_week_ready_title" = "Votre semaine est prête !";
|
||||
"onboarding_week_ready_subtitle" = "%d repas planifiés automatiquement. Vous pourrez tout ajuster ensuite.";
|
||||
"onboarding_week_ready_subtitle_empty" = "Votre planning hebdomadaire est prêt. Ajoutez des plats quand vous voulez et remplissez-le en un geste.";
|
||||
"onboarding_reminder_pitch" = "Recevez un rappel chaque dimanche pour que la semaine commence toujours planifiée.";
|
||||
"onboarding_reminder_allow" = "Rappelez-le-moi le dimanche";
|
||||
"onboarding_reminder_skip" = "Pas maintenant";
|
||||
|
||||
/* Notification settings (1.2.0) */
|
||||
"settings_notifications_header" = "Notifications";
|
||||
"settings_weekly_reminder" = "Rappel hebdomadaire de planification";
|
||||
"settings_notifications_denied_hint" = "Les notifications sont désactivées dans les réglages du système.";
|
||||
"settings_open_system_settings" = "Ouvrir Réglages";
|
||||
|
||||
@@ -362,3 +362,17 @@
|
||||
/* Week rating */
|
||||
"week_rating_prompt" = "Com'è andata questa settimana?";
|
||||
|
||||
|
||||
/* Week ready onboarding step (1.2.0) */
|
||||
"onboarding_week_ready_title" = "La tua settimana è pronta!";
|
||||
"onboarding_week_ready_subtitle" = "%d pasti pianificati automaticamente. Potrai modificare tutto in seguito.";
|
||||
"onboarding_week_ready_subtitle_empty" = "Il tuo piano settimanale è pronto. Aggiungi piatti quando vuoi e completalo con un tocco.";
|
||||
"onboarding_reminder_pitch" = "Ricevi un promemoria ogni domenica così la settimana inizia sempre pianificata.";
|
||||
"onboarding_reminder_allow" = "Ricordamelo la domenica";
|
||||
"onboarding_reminder_skip" = "Non ora";
|
||||
|
||||
/* Notification settings (1.2.0) */
|
||||
"settings_notifications_header" = "Notifiche";
|
||||
"settings_weekly_reminder" = "Promemoria settimanale di pianificazione";
|
||||
"settings_notifications_denied_hint" = "Le notifiche sono disattivate nelle impostazioni di sistema.";
|
||||
"settings_open_system_settings" = "Apri Impostazioni";
|
||||
|
||||
@@ -362,3 +362,17 @@
|
||||
/* Week rating */
|
||||
"week_rating_prompt" = "Como foi esta semana?";
|
||||
|
||||
|
||||
/* Week ready onboarding step (1.2.0) */
|
||||
"onboarding_week_ready_title" = "Sua semana está pronta!";
|
||||
"onboarding_week_ready_subtitle" = "%d refeições planejadas automaticamente. Você pode ajustar tudo depois.";
|
||||
"onboarding_week_ready_subtitle_empty" = "Seu plano semanal está pronto. Adicione pratos quando quiser e preencha com um toque.";
|
||||
"onboarding_reminder_pitch" = "Receba um lembrete todo domingo para que a semana sempre comece planejada.";
|
||||
"onboarding_reminder_allow" = "Lembrar aos domingos";
|
||||
"onboarding_reminder_skip" = "Agora não";
|
||||
|
||||
/* Notification settings (1.2.0) */
|
||||
"settings_notifications_header" = "Notificações";
|
||||
"settings_weekly_reminder" = "Lembrete semanal de planejamento";
|
||||
"settings_notifications_denied_hint" = "As notificações estão desativadas nos ajustes do sistema.";
|
||||
"settings_open_system_settings" = "Abrir Ajustes";
|
||||
|
||||
@@ -4,9 +4,46 @@ import UserNotifications
|
||||
@MainActor
|
||||
final class NotificationService {
|
||||
static let shared = NotificationService()
|
||||
static let remindersEnabledKey = "notifications_reminders_enabled"
|
||||
|
||||
private static let allReminderIdentifiers = [
|
||||
"mealmood.next-week-planning",
|
||||
"mealmood.sunday-planning"
|
||||
]
|
||||
|
||||
private init() {}
|
||||
|
||||
/// User-level preference for weekly planning reminders (independent of OS permission).
|
||||
var remindersEnabled: Bool {
|
||||
get {
|
||||
UserDefaults.standard.object(forKey: Self.remindersEnabledKey) as? Bool ?? true
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: Self.remindersEnabledKey)
|
||||
if !newValue { cancelAllReminders() }
|
||||
}
|
||||
}
|
||||
|
||||
func authorizationStatus() async -> UNAuthorizationStatus {
|
||||
await UNUserNotificationCenter.current().notificationSettings().authorizationStatus
|
||||
}
|
||||
|
||||
/// Explicit permission request for contextual prompts (onboarding, settings toggle).
|
||||
/// Returns whether notifications are authorized after the request.
|
||||
@discardableResult
|
||||
func requestPermission() async -> Bool {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let settings = await center.notificationSettings()
|
||||
switch settings.authorizationStatus {
|
||||
case .notDetermined:
|
||||
return (try? await center.requestAuthorization(options: [.alert, .sound, .badge])) ?? false
|
||||
case .authorized, .provisional, .ephemeral:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func requestPermissionIfNeeded() async {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let settings = await center.notificationSettings()
|
||||
@@ -14,7 +51,16 @@ final class NotificationService {
|
||||
_ = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
func cancelAllReminders() {
|
||||
UNUserNotificationCenter.current()
|
||||
.removePendingNotificationRequests(withIdentifiers: Self.allReminderIdentifiers)
|
||||
}
|
||||
|
||||
func schedulePlanningReminderIfNeeded(nextWeekPlan: WeekPlan?, language: AppLanguage) {
|
||||
guard remindersEnabled else {
|
||||
cancelAllReminders()
|
||||
return
|
||||
}
|
||||
let center = UNUserNotificationCenter.current()
|
||||
let identifier = "mealmood.next-week-planning"
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ final class OnboardingViewModel: ObservableObject {
|
||||
@Published var addedDishes: [(name: String, tagIds: [UUID])] = []
|
||||
let maxOnboardingDishes = 10
|
||||
|
||||
let totalSteps = 6
|
||||
let totalSteps = 7
|
||||
|
||||
/// Slots filled by the in-onboarding auto-fill (aha moment step).
|
||||
@Published var autoFilledCount: Int = 0
|
||||
|
||||
var canContinue: Bool {
|
||||
switch currentStep {
|
||||
@@ -37,7 +40,8 @@ final class OnboardingViewModel: ObservableObject {
|
||||
case 2: return true // Weekends (toggle)
|
||||
case 3: return true // Calendar (optional)
|
||||
case 4: return true // Dishes are optional
|
||||
case 5: return true // Paywall (always skippable)
|
||||
case 5: return true // Week ready (aha moment + reminder opt-in)
|
||||
case 6: return true // Paywall (always skippable)
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@@ -161,10 +165,9 @@ final class OnboardingViewModel: ObservableObject {
|
||||
AnalyticsService.logDishAdded(tagCount: dishData.tagIds.count, source: "onboarding")
|
||||
}
|
||||
AnalyticsService.logOnboardingCompleted(dishCount: onboardingDishCount)
|
||||
// If the user added dishes, auto-fill their first week on launch so they
|
||||
// land on a ready plan instead of an empty one. Otherwise fall back to the
|
||||
// home auto-assign prompt.
|
||||
UserDefaults.standard.set(createdDishes, forKey: Self.pendingAutoFillOnLaunchKey)
|
||||
// Auto-fill now happens inline in the Week Ready onboarding step, so the
|
||||
// launch flag stays off. Users without dishes still get the home prompt.
|
||||
UserDefaults.standard.set(false, forKey: Self.pendingAutoFillOnLaunchKey)
|
||||
UserDefaults.standard.set(!createdDishes, forKey: Self.pendingAutoAssignPromptKey)
|
||||
UserDefaults.standard.set(true, forKey: Self.pendingPremiumPromptKey)
|
||||
return true
|
||||
@@ -173,4 +176,39 @@ final class OnboardingViewModel: ObservableObject {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
func autoFillFirstWeek(context: ModelContext) -> Int {
|
||||
autoFilledCount = 0
|
||||
guard !addedDishes.isEmpty else { return 0 }
|
||||
|
||||
let dishes = (try? context.fetch(FetchDescriptor<Dish>())) ?? []
|
||||
let tags = (try? context.fetch(FetchDescriptor<Tag>())) ?? []
|
||||
let weekStart = Date().startOfWeek()
|
||||
let plans = (try? context.fetch(FetchDescriptor<WeekPlan>())) ?? []
|
||||
guard let plan = plans.first(where: { $0.weekStartDate == weekStart }) else { return 0 }
|
||||
|
||||
let emptySlots = plan.slots.filter { $0.dishId == nil && !$0.isEatingOut }
|
||||
guard !emptySlots.isEmpty else { return 0 }
|
||||
|
||||
let result = AutocompleteEngine.autocomplete(
|
||||
emptySlots: emptySlots,
|
||||
currentPlan: plan,
|
||||
allDishes: dishes,
|
||||
allTags: tags,
|
||||
recentPlans: [],
|
||||
rejectionCounts: FeedbackStore.rejectionCounts()
|
||||
)
|
||||
plan.updatedAt = Date()
|
||||
try? context.save()
|
||||
|
||||
AnalyticsService.logAutoAssignUsed(
|
||||
filledSlots: result.filledSlots.count,
|
||||
unfilledSlots: result.unfilledCount
|
||||
)
|
||||
autoFilledCount = result.filledSlots.count
|
||||
return result.filledSlots.count
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,7 +515,8 @@ struct HomeView: View {
|
||||
StatsView(weekPlans: weekPlans, allDishes: dishes, allTags: tags, language: settings.languageEnum.resolved())
|
||||
}
|
||||
.task {
|
||||
await NotificationService.shared.requestPermissionIfNeeded()
|
||||
// Permission is now requested contextually (onboarding Week Ready step or
|
||||
// the Settings toggle) — no cold OS prompt on first Home load.
|
||||
let nextPlan = fetchWeekPlan(for: Date().startOfWeek().addingDays(7))
|
||||
NotificationService.shared.schedulePlanningReminderIfNeeded(
|
||||
nextWeekPlan: nextPlan,
|
||||
@@ -986,6 +987,13 @@ struct HomeView: View {
|
||||
let shouldShowPremiumPrompt = UserDefaults.standard.bool(forKey: OnboardingViewModel.pendingPremiumPromptKey)
|
||||
guard shouldShowPremiumPrompt else { return }
|
||||
|
||||
// The paywall was just shown as the final onboarding step — prompting again
|
||||
// 2s later is prompt fatigue. Defer to the second session instead.
|
||||
let sessionCountKey = "post_onboarding_session_count"
|
||||
let sessions = UserDefaults.standard.integer(forKey: sessionCountKey) + 1
|
||||
UserDefaults.standard.set(sessions, forKey: sessionCountKey)
|
||||
guard sessions >= 2 else { return }
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
showPostOnboardingPremiumPrompt = true
|
||||
}
|
||||
|
||||
@@ -64,18 +64,26 @@ struct OnboardingView: View {
|
||||
tags: tags,
|
||||
onFinish: {
|
||||
_ = viewModel.completeOnboarding(context: context)
|
||||
viewModel.autoFillFirstWeek(context: context)
|
||||
viewModel.nextStep()
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.tag(4)
|
||||
|
||||
WeekReadyStepView(
|
||||
filledCount: viewModel.autoFilledCount,
|
||||
onContinue: { viewModel.nextStep() }
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.tag(5)
|
||||
|
||||
PaywallStepView(
|
||||
onSkip: { onComplete() },
|
||||
onConverted: { onComplete() }
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.tag(5)
|
||||
.tag(6)
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.animation(.easeInOut(duration: 0.3), value: viewModel.currentStep)
|
||||
@@ -126,7 +134,9 @@ struct OnboardingView: View {
|
||||
private var onboardingHeader: some View {
|
||||
VStack(spacing: 6) {
|
||||
HStack {
|
||||
if viewModel.currentStep > 0 {
|
||||
// No back navigation once onboarding data is committed (step 4 finish):
|
||||
// re-finishing would duplicate dishes and the week plan.
|
||||
if viewModel.currentStep > 0 && viewModel.currentStep < 5 {
|
||||
Button {
|
||||
viewModel.previousStep()
|
||||
} label: {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,45 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import EventKit
|
||||
import UserNotifications
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.modelContext) private var context
|
||||
@Query private var allSettings: [AppSettings]
|
||||
@StateObject private var viewModel = SettingsViewModel()
|
||||
@State private var showResetAllDataAlert = false
|
||||
@State private var notificationAuthStatus: UNAuthorizationStatus = .notDetermined
|
||||
@State private var remindersEnabled = NotificationService.shared.remindersEnabled
|
||||
|
||||
private var settings: AppSettings? { allSettings.first }
|
||||
|
||||
private func refreshNotificationStatus() async {
|
||||
notificationAuthStatus = await NotificationService.shared.authorizationStatus()
|
||||
remindersEnabled = NotificationService.shared.remindersEnabled
|
||||
}
|
||||
|
||||
private func handleReminderToggle(_ enabled: Bool, settings: AppSettings) {
|
||||
Task {
|
||||
if enabled {
|
||||
let granted = await NotificationService.shared.requestPermission()
|
||||
NotificationService.shared.remindersEnabled = true
|
||||
remindersEnabled = true
|
||||
notificationAuthStatus = await NotificationService.shared.authorizationStatus()
|
||||
AnalyticsService.logNotificationsToggled(enabled: granted)
|
||||
if granted {
|
||||
NotificationService.shared.schedulePlanningReminderIfNeeded(
|
||||
nextWeekPlan: nil,
|
||||
language: settings.languageEnum.resolved()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
NotificationService.shared.remindersEnabled = false
|
||||
remindersEnabled = false
|
||||
AnalyticsService.logNotificationsToggled(enabled: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.mealMoodBackground.ignoresSafeArea()
|
||||
@@ -147,6 +177,37 @@ struct SettingsView: View {
|
||||
}
|
||||
.listRowBackground(Color.mealMoodSurface)
|
||||
|
||||
// Notifications section
|
||||
Section {
|
||||
Toggle("settings_weekly_reminder", isOn: Binding(
|
||||
get: { remindersEnabled && notificationAuthStatus != .denied },
|
||||
set: { newValue in handleReminderToggle(newValue, settings: settings) }
|
||||
))
|
||||
.tint(.mealMoodCoral)
|
||||
.disabled(notificationAuthStatus == .denied)
|
||||
|
||||
if notificationAuthStatus == .denied {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("settings_notifications_denied_hint")
|
||||
.font(.mealMoodSmall)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
Button {
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
} label: {
|
||||
Text("settings_open_system_settings")
|
||||
.font(.mealMoodSmall)
|
||||
.foregroundColor(.mealMoodCoral)
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Label("settings_notifications_header", systemImage: "bell")
|
||||
}
|
||||
.listRowBackground(Color.mealMoodSurface)
|
||||
.task { await refreshNotificationStatus() }
|
||||
|
||||
// Tags section
|
||||
Section {
|
||||
NavigationLink(destination: TagListView()) {
|
||||
|
||||
Reference in New Issue
Block a user