1.1.5: conversion funnel overhaul

Paywall:
- Redesigned PremiumView with yearly/monthly/lifetime side by side
- 7-day free trial badge with savings % vs monthly
- Social proof line, BEST VALUE highlight on yearly
- StoreManager: new yearlyProduct, lifetimeProduct, yearlySavingsPercent, hasTrialAvailable

Funnel:
- Paywall step added to onboarding (PaywallStepView)
- Contextual modal at dish limit (replaces silent block)
- Visible "X/15 dishes" counter in DishListView
- BottomPromoBanner rotates AdMob with internal "Remove ads" CTA (1 in 4)
- Free dish limit lowered 20 → 15
- Review prompt earlier: 2 weeks → 1 week

Analytics:
- paywall_viewed/dismissed with source + seconds_on_screen
- dish_limit_hit, week_limit_hit, free_trial_started events
- Every PremiumView call site now passes its source

StoreKit:
- MealMood.storekit: added yearly $19.99 + 7d trial, monthly trial, lifetime $39.99
- AppStoreConnect-1.1.5-setup.md with full manual setup instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alexandrev-tibco
2026-06-11 22:21:36 +02:00
parent 72770801d3
commit b45cb2a530
31 changed files with 1067 additions and 104 deletions
+10 -2
View File
@@ -13,6 +13,7 @@ struct HomeView: View {
@StateObject private var viewModel = HomeViewModel()
@State private var editingDish: Dish?
@State private var showPremiumFromExport: Bool = false
@State private var paywallSource: String = "unknown"
@State private var showMonthlyHistory: Bool = false
@State private var showStats: Bool = false
@State private var showWeekLimitUpsell: Bool = false
@@ -111,6 +112,8 @@ struct HomeView: View {
withAnimation(.easeInOut(duration: 0.2)) {
showWeekLimitUpsell = true
}
AnalyticsService.logWeekLimitHit()
paywallSource = "week_limit"
showPremiumFromExport = true
HapticManager.shared.notification(type: .warning)
}
@@ -218,6 +221,7 @@ struct HomeView: View {
messageKey: "premium_limit_future_weeks",
actionTitleKey: "premium_subscribe"
) {
paywallSource = "week_limit_banner"
showPremiumFromExport = true
}
.padding(.horizontal, 16)
@@ -267,7 +271,7 @@ struct HomeView: View {
.frame(maxHeight: .infinity, alignment: .top)
.safeAreaInset(edge: .bottom, spacing: 0) {
if !settings.isPremium && !isRunningOnMac {
AdBannerView()
BottomPromoBanner(settings: settings)
}
}
.alert("reset_title", isPresented: $viewModel.showResetAlert) {
@@ -312,6 +316,7 @@ struct HomeView: View {
.alert("onboarding_premium_prompt_title", isPresented: $showPostOnboardingPremiumPrompt) {
Button("onboarding_premium_prompt_cta") {
UserDefaults.standard.set(false, forKey: OnboardingViewModel.pendingPremiumPromptKey)
paywallSource = "post_onboarding"
showPremiumFromExport = true
}
Button("reset_cancel", role: .cancel) {
@@ -459,7 +464,7 @@ struct HomeView: View {
}
.sheet(isPresented: $showPremiumFromExport) {
NavigationStack {
PremiumView(settings: settings)
PremiumView(settings: settings, source: paywallSource)
}
}
.sheet(isPresented: $showExportStylePicker) {
@@ -727,6 +732,8 @@ struct HomeView: View {
withAnimation(.easeInOut(duration: 0.2)) {
showWeekLimitUpsell = true
}
AnalyticsService.logWeekLimitHit()
paywallSource = "week_picker"
showPremiumFromExport = true
HapticManager.shared.notification(type: .warning)
}
@@ -819,6 +826,7 @@ struct HomeView: View {
.buttonStyle(.plain)
} else {
Button {
paywallSource = "share_week"
showPremiumFromExport = true
} label: {
Label("share_week_button", systemImage: "star.fill")