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
+31
View File
@@ -35,6 +35,13 @@ enum AnalyticsEvent {
// Feedback loop
static let weekRated = "week_rated"
// Conversion funnel (1.1.5)
static let paywallViewed = "paywall_viewed"
static let paywallDismissed = "paywall_dismissed"
static let dishLimitHit = "dish_limit_hit"
static let weekLimitHit = "week_limit_hit"
static let freeTrialStarted = "free_trial_started"
}
enum AnalyticsService {
@@ -118,4 +125,28 @@ enum AnalyticsService {
let value = rating == 1 ? "liked" : rating == -1 ? "disliked" : "cleared"
logEvent(AnalyticsEvent.weekRated, parameters: ["rating": value])
}
static func logPaywallViewed(source: String) {
logEvent(AnalyticsEvent.paywallViewed, parameters: ["source": source])
}
static func logPaywallDismissed(source: String, secondsOnScreen: Int, didConvert: Bool) {
logEvent(AnalyticsEvent.paywallDismissed, parameters: [
"source": source,
"seconds_on_screen": secondsOnScreen,
"did_convert": didConvert
])
}
static func logDishLimitHit() {
logEvent(AnalyticsEvent.dishLimitHit)
}
static func logWeekLimitHit() {
logEvent(AnalyticsEvent.weekLimitHit)
}
static func logFreeTrialStarted(plan: String) {
logEvent(AnalyticsEvent.freeTrialStarted, parameters: ["plan": plan])
}
}