reviews: pedir valoracion tras la compra y deep link write-review en ajustes

- Tras una compra exitosa se pide la review nativa (una vez por usuario,
  con 2s de delay para que se vea el estado premium activo)
- El boton de ajustes abre el formulario de resena del App Store via
  ?action=write-review en vez de SKStoreReviewController, que Apple
  limita a 3 prompts/ano y el resto de veces no muestra nada
- Evento analytics review_prompt_post_purchase para medirlo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG
This commit is contained in:
alexandrev-tibco
2026-08-23 13:36:06 +02:00
parent 889a2b2d2f
commit 57c52afc3c
3 changed files with 22 additions and 7 deletions
+20 -4
View File
@@ -7,11 +7,31 @@ final class 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 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 !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()
}
}
func shouldShowFunnelAfterWeekCompletion(completedWeeks: Int) -> Bool {
guard !hasCompletedReviewFlow else { return false }
guard completedWeeks >= minimumCompletedWeeksToStart else { return false }
@@ -29,10 +49,6 @@ final class ReviewPromptService {
UserDefaults.standard.set(true, forKey: reviewCompletedKey)
}
func requestFromSettings() {
requestReview()
}
var hasCompletedReviewFlow: Bool {
UserDefaults.standard.bool(forKey: reviewCompletedKey)
}
+1
View File
@@ -356,6 +356,7 @@ struct PremiumView: View {
AnalyticsService.logFreeTrialStarted(plan: product.id)
}
try? context.save()
ReviewPromptService.shared.requestAfterPurchase()
case .pending:
purchaseStatusMessageKey = "premium_purchase_pending"
case .cancelled:
+1 -3
View File
@@ -270,9 +270,7 @@ struct SettingsView: View {
.listRowBackground(Color.mealMoodSurface)
Section {
Button {
ReviewPromptService.shared.requestFromSettings()
} label: {
Link(destination: ReviewPromptService.writeReviewURL) {
Label("settings_rate_app", systemImage: "star.bubble")
}