1.2.0: widget deep-link, Lock Screen widgets, in-app widget promo

- Tapping the widget now deep-links via mealmood://today back to the current
  week (URL scheme registered, onOpenURL handler in Home)
- New Lock Screen widget families: accessoryRectangular (weekday + both
  meals) and accessoryInline (next relevant meal)
- One-time dismissable widget promo card on Home from the 2nd session,
  with widget_promo_shown/dismissed analytics
- 2 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:
alexandrev-tibco
2026-07-03 10:11:40 +02:00
parent 73c2039338
commit f5cb67adc0
9 changed files with 173 additions and 7 deletions
+11
View File
@@ -16,6 +16,17 @@
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.1.5</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.alexandrevazquez.mealmood</string>
<key>CFBundleURLSchemes</key>
<array>
<string>mealmood</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>54</string>
<key>GADApplicationIdentifier</key>
@@ -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.";
@@ -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.";
@@ -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.";
@@ -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.";
@@ -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.";
@@ -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.";
+75
View File
@@ -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<WeekPlan>()
guard let plans = try? context.fetch(descriptor) else { return }
+63 -7
View File
@@ -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])
}
}