b45cb2a530
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>
48 lines
1.5 KiB
Swift
48 lines
1.5 KiB
Swift
import UIKit
|
|
import StoreKit
|
|
|
|
@MainActor
|
|
final class ReviewPromptService {
|
|
static let shared = ReviewPromptService()
|
|
|
|
private let reviewCompletedKey = "review_funnel_completed"
|
|
private let lastPromptedCompletedWeeksKey = "review_funnel_last_prompted_completed_weeks"
|
|
private let minimumCompletedWeeksToStart = 1
|
|
private let promptIntervalWeeks = 2
|
|
|
|
private init() {}
|
|
|
|
func shouldShowFunnelAfterWeekCompletion(completedWeeks: Int) -> Bool {
|
|
guard !hasCompletedReviewFlow else { return false }
|
|
guard completedWeeks >= minimumCompletedWeeksToStart else { return false }
|
|
guard completedWeeks % promptIntervalWeeks == 0 else { return false }
|
|
|
|
let lastPrompted = UserDefaults.standard.integer(forKey: lastPromptedCompletedWeeksKey)
|
|
return completedWeeks > lastPrompted
|
|
}
|
|
|
|
func markFunnelShown(completedWeeks: Int) {
|
|
UserDefaults.standard.set(completedWeeks, forKey: lastPromptedCompletedWeeksKey)
|
|
}
|
|
|
|
func markReviewCompleted() {
|
|
UserDefaults.standard.set(true, forKey: reviewCompletedKey)
|
|
}
|
|
|
|
func requestFromSettings() {
|
|
requestReview()
|
|
}
|
|
|
|
var hasCompletedReviewFlow: Bool {
|
|
UserDefaults.standard.bool(forKey: reviewCompletedKey)
|
|
}
|
|
|
|
func requestReview() {
|
|
guard let scene = UIApplication.shared.connectedScenes
|
|
.compactMap({ $0 as? UIWindowScene })
|
|
.first(where: { $0.activationState == .foregroundActive }) else { return }
|
|
|
|
SKStoreReviewController.requestReview(in: scene)
|
|
}
|
|
}
|