From 57c52afc3ccb07e04676cd973df0cfba19b4cf2c Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Sun, 23 Aug 2026 13:36:06 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_013H6bXqGX1ygwib1Dm3n3UG --- MealMood/Services/ReviewPromptService.swift | 24 +++++++++++++++++---- MealMood/Views/Premium/PremiumView.swift | 1 + MealMood/Views/Settings/SettingsView.swift | 4 +--- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/MealMood/Services/ReviewPromptService.swift b/MealMood/Services/ReviewPromptService.swift index 9711a21..bced2b1 100644 --- a/MealMood/Services/ReviewPromptService.swift +++ b/MealMood/Services/ReviewPromptService.swift @@ -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) } diff --git a/MealMood/Views/Premium/PremiumView.swift b/MealMood/Views/Premium/PremiumView.swift index 9d8b03d..707a44b 100644 --- a/MealMood/Views/Premium/PremiumView.swift +++ b/MealMood/Views/Premium/PremiumView.swift @@ -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: diff --git a/MealMood/Views/Settings/SettingsView.swift b/MealMood/Views/Settings/SettingsView.swift index 5a23911..c2aafa7 100644 --- a/MealMood/Views/Settings/SettingsView.swift +++ b/MealMood/Views/Settings/SettingsView.swift @@ -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") }