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