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 postPurchaseAskedKey = "review_post_purchase_asked" private let premiumUserAskedKey = "review_premium_user_asked" private let minimumCompletedWeeksToStart = 1 private let promptIntervalWeeks = 2 /// Write-review deep link: always opens the App Store review form, unlike /// `SKStoreReviewController` which Apple throttles to 3 prompts/year and /// silently no-ops the rest of the time. static let writeReviewURL = URL(string: "https://apps.apple.com/app/id6759255553?action=write-review")! private init() {} /// Asks for a rating right after a successful subscription purchase — the /// user just paid, so sentiment is at its peak. Once per user; the small /// delay lets the "premium active" state render before the system alert. func requestAfterPurchase() { guard !hasCompletedReviewFlow else { return } guard !UserDefaults.standard.bool(forKey: postPurchaseAskedKey) else { return } UserDefaults.standard.set(true, forKey: postPurchaseAskedKey) AnalyticsService.logEvent("review_prompt_post_purchase") Task { @MainActor in try? await Task.sleep(for: .seconds(2)) requestReview() markReviewCompleted() } } /// One-time ask for users who were already premium before the post-purchase /// prompt existed — they never went through it, and a paying user is the /// most likely 5-star reviewer the app has. func requestForExistingPremiumUser() { guard !hasCompletedReviewFlow else { return } guard !UserDefaults.standard.bool(forKey: premiumUserAskedKey) else { return } UserDefaults.standard.set(true, forKey: premiumUserAskedKey) AnalyticsService.logEvent("review_prompt_premium_user") Task { @MainActor in try? await Task.sleep(for: .seconds(2)) requestReview() markReviewCompleted() } } 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) } 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) } }