Files
FamilyMealPlanner/MealMood/Services/ReviewPromptService.swift
T
alexandrev-tibco 26d00f65e0 reviews: ask unico a premium existentes, funnel instrumentado y fix source unknown del paywall
- requestForExistingPremiumUser(): prompt nativo una sola vez para quien
  ya era premium antes del ask post-compra (evento review_prompt_premium_user)
- Funnel de semanas completadas emite review_funnel_shown/positive/negative/dismissed
- Paywall de Home via sheet(item:) con PaywallPresentation: el source viaja
  con la presentacion; antes dos @State separados hacian que a veces se
  registrara paywall_viewed con source=unknown (post_onboarding nunca llegaba)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
2026-08-26 14:33:18 +02:00

81 lines
3.3 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 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)
}
}