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
+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 }