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:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user