diff --git a/MealMood/Resources/Info.plist b/MealMood/Resources/Info.plist index 1faf265..8163334 100644 --- a/MealMood/Resources/Info.plist +++ b/MealMood/Resources/Info.plist @@ -16,6 +16,17 @@ APPL CFBundleShortVersionString 1.1.5 + CFBundleURLTypes + + + CFBundleURLName + com.alexandrevazquez.mealmood + CFBundleURLSchemes + + mealmood + + + CFBundleVersion 54 GADApplicationIdentifier diff --git a/MealMood/Resources/de.lproj/Localizable.strings b/MealMood/Resources/de.lproj/Localizable.strings index ea97000..fd411db 100644 --- a/MealMood/Resources/de.lproj/Localizable.strings +++ b/MealMood/Resources/de.lproj/Localizable.strings @@ -376,3 +376,7 @@ "settings_weekly_reminder" = "Wöchentliche Planungserinnerung"; "settings_notifications_denied_hint" = "Mitteilungen sind in den Systemeinstellungen deaktiviert."; "settings_open_system_settings" = "Einstellungen öffnen"; + +/* Widget promo (1.2.0) */ +"widget_promo_title" = "Die heutigen Mahlzeiten auf deinem Home-Bildschirm"; +"widget_promo_body" = "Halte den Home-Bildschirm gedrückt, tippe auf + und suche nach MealMood, um das Widget hinzuzufügen."; diff --git a/MealMood/Resources/en.lproj/Localizable.strings b/MealMood/Resources/en.lproj/Localizable.strings index 4e74bce..96b3f26 100644 --- a/MealMood/Resources/en.lproj/Localizable.strings +++ b/MealMood/Resources/en.lproj/Localizable.strings @@ -376,3 +376,7 @@ "settings_weekly_reminder" = "Weekly planning reminder"; "settings_notifications_denied_hint" = "Notifications are disabled in system settings."; "settings_open_system_settings" = "Open Settings"; + +/* Widget promo (1.2.0) */ +"widget_promo_title" = "Today's meals on your Home Screen"; +"widget_promo_body" = "Long-press your Home Screen, tap +, and search for MealMood to add the widget."; diff --git a/MealMood/Resources/es.lproj/Localizable.strings b/MealMood/Resources/es.lproj/Localizable.strings index 20578c6..18b8a53 100644 --- a/MealMood/Resources/es.lproj/Localizable.strings +++ b/MealMood/Resources/es.lproj/Localizable.strings @@ -376,3 +376,7 @@ "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"; + +/* Widget promo (1.2.0) */ +"widget_promo_title" = "Las comidas de hoy en tu pantalla de inicio"; +"widget_promo_body" = "Mantén pulsada la pantalla de inicio, toca + y busca MealMood para añadir el widget."; diff --git a/MealMood/Resources/fr.lproj/Localizable.strings b/MealMood/Resources/fr.lproj/Localizable.strings index 41399b2..381ca0a 100644 --- a/MealMood/Resources/fr.lproj/Localizable.strings +++ b/MealMood/Resources/fr.lproj/Localizable.strings @@ -376,3 +376,7 @@ "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"; + +/* Widget promo (1.2.0) */ +"widget_promo_title" = "Les repas du jour sur votre écran d'accueil"; +"widget_promo_body" = "Appuyez longuement sur l'écran d'accueil, touchez + et cherchez MealMood pour ajouter le widget."; diff --git a/MealMood/Resources/it.lproj/Localizable.strings b/MealMood/Resources/it.lproj/Localizable.strings index 99c165c..349db56 100644 --- a/MealMood/Resources/it.lproj/Localizable.strings +++ b/MealMood/Resources/it.lproj/Localizable.strings @@ -376,3 +376,7 @@ "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"; + +/* Widget promo (1.2.0) */ +"widget_promo_title" = "I pasti di oggi sulla schermata Home"; +"widget_promo_body" = "Tieni premuta la schermata Home, tocca + e cerca MealMood per aggiungere il widget."; diff --git a/MealMood/Resources/pt-BR.lproj/Localizable.strings b/MealMood/Resources/pt-BR.lproj/Localizable.strings index 1e02143..1449a81 100644 --- a/MealMood/Resources/pt-BR.lproj/Localizable.strings +++ b/MealMood/Resources/pt-BR.lproj/Localizable.strings @@ -376,3 +376,7 @@ "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"; + +/* Widget promo (1.2.0) */ +"widget_promo_title" = "As refeições de hoje na sua tela de início"; +"widget_promo_body" = "Pressione a tela de início, toque em + e procure MealMood para adicionar o widget."; diff --git a/MealMood/Views/Home/HomeView.swift b/MealMood/Views/Home/HomeView.swift index a690590..6c24314 100644 --- a/MealMood/Views/Home/HomeView.swift +++ b/MealMood/Views/Home/HomeView.swift @@ -28,6 +28,10 @@ struct HomeView: View { @State private var showPostOnboardingAutoAssignPrompt: Bool = false @State private var showPostOnboardingPremiumPrompt: Bool = false @State private var hasEvaluatedPostOnboardingPrompts: Bool = false + @State private var showWidgetPromo: Bool = false + + private static let widgetPromoDismissedKey = "widget_promo_dismissed" + private static let homeSessionCountKey = "home_session_count" @State private var showExportStylePicker: Bool = false @State private var pendingExportPlan: WeekPlan? @State private var shareImageURL: URL? @@ -228,6 +232,12 @@ struct HomeView: View { .padding(.bottom, 8) } + if showWidgetPromo { + widgetPromoCard + .padding(.horizontal, 16) + .padding(.bottom, 8) + } + VStack(spacing: 10) { WeekCalendarView( plan: plan, @@ -524,8 +534,14 @@ struct HomeView: View { ) wasWeekComplete = isWeekComplete(plan: plan) evaluatePostOnboardingPromptsIfNeeded(plan: plan, settings: settings) + evaluateWidgetPromo() updateWidget(settings: settings) } + .onOpenURL { url in + // mealmood://today — widget deep-link back to the current week. + guard url.scheme == "mealmood", url.host == "today" else { return } + viewModel.jumpToWeek(startDate: Date().startOfWeek()) + } .onChange(of: plan.updatedAt) { _, _ in let nowComplete = isWeekComplete(plan: plan) if nowComplete && !wasWeekComplete { @@ -944,6 +960,65 @@ struct HomeView: View { return DefaultDataService.createWeekPlan(for: viewModel.currentWeekStart, settings: settings, context: context) } + private var widgetPromoCard: some View { + HStack(spacing: 12) { + Image(systemName: "apps.iphone.badge.plus") + .font(.system(size: 24)) + .foregroundColor(.mealMoodCoral) + + VStack(alignment: .leading, spacing: 2) { + Text("widget_promo_title") + .font(.mealMoodBodyBold) + .foregroundColor(.mealMoodTextPrimary) + Text("widget_promo_body") + .font(.mealMoodSmall) + .foregroundColor(.mealMoodTextSecondary) + .fixedSize(horizontal: false, vertical: true) + } + + Spacer() + + Button { + dismissWidgetPromo() + } label: { + Image(systemName: "xmark") + .font(.system(size: 12, weight: .semibold)) + .foregroundColor(.mealMoodTextSecondary) + .frame(width: 28, height: 28) + .background(Color.mealMoodBackground) + .clipShape(Circle()) + } + .buttonStyle(.plain) + } + .padding(14) + .background(Color.mealMoodSurface) + .cornerRadius(14) + .overlay( + RoundedRectangle(cornerRadius: 14) + .stroke(Color(hex: "#F0F0F0"), lineWidth: 1) + ) + } + + private func evaluateWidgetPromo() { + guard !UserDefaults.standard.bool(forKey: Self.widgetPromoDismissedKey) else { return } + let sessions = UserDefaults.standard.integer(forKey: Self.homeSessionCountKey) + 1 + UserDefaults.standard.set(sessions, forKey: Self.homeSessionCountKey) + // From the 2nd session on: the user has seen the app's value, now make it sticky. + guard sessions >= 2 else { return } + if !showWidgetPromo { + showWidgetPromo = true + AnalyticsService.logEvent("widget_promo_shown") + } + } + + private func dismissWidgetPromo() { + UserDefaults.standard.set(true, forKey: Self.widgetPromoDismissedKey) + withAnimation(.easeOut(duration: 0.2)) { + showWidgetPromo = false + } + AnalyticsService.logEvent("widget_promo_dismissed") + } + private func evaluateReviewPrompt() { let descriptor = FetchDescriptor() guard let plans = try? context.fetch(descriptor) else { return } diff --git a/MealMoodWidget/MealMoodWidget.swift b/MealMoodWidget/MealMoodWidget.swift index bd813f3..0279d21 100644 --- a/MealMoodWidget/MealMoodWidget.swift +++ b/MealMoodWidget/MealMoodWidget.swift @@ -199,6 +199,51 @@ private struct MediumWidgetView: View { } } +// MARK: - Lock Screen widgets + +private struct AccessoryRectangularView: View { + let entry: TodayMealEntry + + var body: some View { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 3) { + Image(systemName: "fork.knife") + .font(.system(size: 10, weight: .bold)) + Text(entry.weekdayName.isEmpty ? "MealMood" : entry.weekdayName) + .font(.system(size: 12, weight: .bold)) + .lineLimit(1) + } + .widgetAccentable() + + if let lunch = entry.lunch { + Text("\(Image(systemName: "sun.max.fill")) \(lunch)") + .font(.system(size: 12)) + .lineLimit(1) + } + if let dinner = entry.dinner { + Text("\(Image(systemName: "moon.stars.fill")) \(dinner)") + .font(.system(size: 12)) + .lineLimit(1) + } + if entry.lunch == nil && entry.dinner == nil { + Text("—") + .font(.system(size: 12)) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +private struct AccessoryInlineView: View { + let entry: TodayMealEntry + + var body: some View { + // Inline is a single line: show the next relevant meal. + let name = entry.dinner ?? entry.lunch + Label(name ?? "MealMood", systemImage: "fork.knife") + } +} + // MARK: - Widget entry view struct MealMoodWidgetView: View { @@ -206,12 +251,24 @@ struct MealMoodWidgetView: View { @Environment(\.widgetFamily) var family var body: some View { - switch family { - case .systemMedium: - MediumWidgetView(entry: entry) - default: - SmallWidgetView(entry: entry) + Group { + switch family { + case .systemMedium: + MediumWidgetView(entry: entry) + .containerBackground(for: .widget) { Color.white } + case .accessoryRectangular: + AccessoryRectangularView(entry: entry) + .containerBackground(for: .widget) { Color.clear } + case .accessoryInline: + AccessoryInlineView(entry: entry) + .containerBackground(for: .widget) { Color.clear } + default: + SmallWidgetView(entry: entry) + .containerBackground(for: .widget) { Color.white } + } } + // Deep-link into the app's current week ("today") when tapped. + .widgetURL(URL(string: "mealmood://today")) } } @@ -224,10 +281,9 @@ struct MealMoodWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: Provider()) { entry in MealMoodWidgetView(entry: entry) - .containerBackground(for: .widget) { Color.white } } .configurationDisplayName("MealMood") .description("Today's lunch and dinner at a glance.") - .supportedFamilies([.systemSmall, .systemMedium]) + .supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular, .accessoryInline]) } }