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:
@@ -9,7 +9,7 @@ struct DishFormView: View {
|
||||
@Query private var allSettings: [AppSettings]
|
||||
@StateObject private var viewModel = DishViewModel()
|
||||
@State private var showPremium = false
|
||||
@State private var showDishLimitUpsell = false
|
||||
@State private var showDishLimitModal = false
|
||||
@State private var showSavedFeedback = false
|
||||
|
||||
var dish: Dish?
|
||||
@@ -154,14 +154,6 @@ struct DishFormView: View {
|
||||
.background(Color.mealMoodSurface)
|
||||
.cornerRadius(14)
|
||||
|
||||
if showDishLimitUpsell && reachedFreeDishLimit {
|
||||
PremiumUpsellBanner(
|
||||
messageKey: "premium_limit_dishes",
|
||||
actionTitleKey: "premium_subscribe"
|
||||
) {
|
||||
showPremium = true
|
||||
}
|
||||
}
|
||||
|
||||
// Delete button (edit mode only)
|
||||
if viewModel.isEditing {
|
||||
@@ -195,10 +187,8 @@ struct DishFormView: View {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
guard !reachedFreeDishLimit else {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
showDishLimitUpsell = true
|
||||
}
|
||||
showPremium = true
|
||||
AnalyticsService.logDishLimitHit()
|
||||
showDishLimitModal = true
|
||||
HapticManager.shared.notification(type: .warning)
|
||||
return
|
||||
}
|
||||
@@ -226,9 +216,22 @@ struct DishFormView: View {
|
||||
}
|
||||
.sheet(isPresented: $showPremium) {
|
||||
if let settings = allSettings.first {
|
||||
NavigationStack { PremiumView(settings: settings) }
|
||||
NavigationStack { PremiumView(settings: settings, source: "dish_limit") }
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showDishLimitModal) {
|
||||
DishLimitModal(
|
||||
onSeePremium: {
|
||||
showDishLimitModal = false
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
showPremium = true
|
||||
}
|
||||
},
|
||||
onDismiss: { showDishLimitModal = false }
|
||||
)
|
||||
.presentationDetents([.medium])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.alert("dish_delete_title", isPresented: $viewModel.showDeleteAlert) {
|
||||
Button("dish_cancel", role: .cancel) {}
|
||||
Button("dish_delete", role: .destructive) {
|
||||
@@ -255,6 +258,47 @@ struct DishFormView: View {
|
||||
}
|
||||
|
||||
// Simple flow layout for tags
|
||||
private struct DishLimitModal: View {
|
||||
let onSeePremium: () -> Void
|
||||
let onDismiss: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "sparkles")
|
||||
.font(.system(size: 44))
|
||||
.foregroundColor(.mealMoodCoral)
|
||||
.padding(.top, 24)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("dish_limit_modal_title")
|
||||
.font(.mealMoodH2)
|
||||
.foregroundColor(.mealMoodTextPrimary)
|
||||
.multilineTextAlignment(.center)
|
||||
Text("dish_limit_modal_message")
|
||||
.font(.mealMoodBody)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 10) {
|
||||
PrimaryButton(title: String(localized: "dish_limit_modal_cta"), action: onSeePremium)
|
||||
Button(action: onDismiss) {
|
||||
Text("dish_limit_modal_dismiss")
|
||||
.font(.mealMoodSmall)
|
||||
.foregroundColor(.mealMoodTextSecondary)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.bottom, 24)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.mealMoodBackground.ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
|
||||
struct FlowLayout: Layout {
|
||||
var spacing: CGFloat = 8
|
||||
|
||||
|
||||
Reference in New Issue
Block a user