Files
FamilyMealPlanner/MealMood/Components/BottomPromoBanner.swift
T
alexandrev-tibco b45cb2a530 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>
2026-06-11 22:21:36 +02:00

71 lines
2.5 KiB
Swift

import SwiftUI
struct BottomPromoBanner: View {
let settings: AppSettings
@State private var showRemoveAdsPromo: Bool = false
@State private var showPremium = false
private static let counterKey = "com.mealmood.bottomBannerRotation"
private static let rotationInterval = 4
var body: some View {
Group {
if showRemoveAdsPromo {
removeAdsPromo
} else {
AdBannerView()
}
}
.onAppear(perform: decideRotation)
.sheet(isPresented: $showPremium) {
NavigationStack { PremiumView(settings: settings, source: "ad_replacement") }
}
}
private var removeAdsPromo: some View {
VStack(spacing: 0) {
Rectangle()
.fill(Color.mealMoodTextSecondary.opacity(0.22))
.frame(height: 1)
Button {
showPremium = true
} label: {
HStack(spacing: 10) {
Image(systemName: "star.fill")
.foregroundColor(.mealMoodCoral)
VStack(alignment: .leading, spacing: 1) {
Text("remove_ads_promo_title")
.font(.mealMoodSmall.weight(.semibold))
.foregroundColor(.mealMoodTextPrimary)
Text("remove_ads_promo_subtitle")
.font(.mealMoodCaption)
.foregroundColor(.mealMoodTextSecondary)
}
Spacer()
Text("remove_ads_promo_cta")
.font(.mealMoodCaption.weight(.semibold))
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.mealMoodCoral)
.clipShape(Capsule())
}
.padding(.horizontal, 14)
}
.buttonStyle(.plain)
.frame(maxWidth: .infinity, minHeight: 56)
.background(Color.mealMoodSurface)
}
.frame(maxWidth: .infinity)
.background(Color.mealMoodSurface)
}
private func decideRotation() {
let current = UserDefaults.standard.integer(forKey: Self.counterKey)
let next = current + 1
UserDefaults.standard.set(next, forKey: Self.counterKey)
showRemoveAdsPromo = (next % Self.rotationInterval == 0)
}
}