26d00f65e0
- 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
81 lines
3.3 KiB
Swift
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)
|
|
}
|
|
}
|