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:
@@ -7,11 +7,31 @@ final class ReviewPromptService {
|
|||||||
|
|
||||||
private let reviewCompletedKey = "review_funnel_completed"
|
private let reviewCompletedKey = "review_funnel_completed"
|
||||||
private let lastPromptedCompletedWeeksKey = "review_funnel_last_prompted_completed_weeks"
|
private let lastPromptedCompletedWeeksKey = "review_funnel_last_prompted_completed_weeks"
|
||||||
|
private let postPurchaseAskedKey = "review_post_purchase_asked"
|
||||||
private let minimumCompletedWeeksToStart = 1
|
private let minimumCompletedWeeksToStart = 1
|
||||||
private let promptIntervalWeeks = 2
|
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() {}
|
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 {
|
func shouldShowFunnelAfterWeekCompletion(completedWeeks: Int) -> Bool {
|
||||||
guard !hasCompletedReviewFlow else { return false }
|
guard !hasCompletedReviewFlow else { return false }
|
||||||
guard completedWeeks >= minimumCompletedWeeksToStart else { return false }
|
guard completedWeeks >= minimumCompletedWeeksToStart else { return false }
|
||||||
@@ -29,10 +49,6 @@ final class ReviewPromptService {
|
|||||||
UserDefaults.standard.set(true, forKey: reviewCompletedKey)
|
UserDefaults.standard.set(true, forKey: reviewCompletedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestFromSettings() {
|
|
||||||
requestReview()
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasCompletedReviewFlow: Bool {
|
var hasCompletedReviewFlow: Bool {
|
||||||
UserDefaults.standard.bool(forKey: reviewCompletedKey)
|
UserDefaults.standard.bool(forKey: reviewCompletedKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ struct PremiumView: View {
|
|||||||
AnalyticsService.logFreeTrialStarted(plan: product.id)
|
AnalyticsService.logFreeTrialStarted(plan: product.id)
|
||||||
}
|
}
|
||||||
try? context.save()
|
try? context.save()
|
||||||
|
ReviewPromptService.shared.requestAfterPurchase()
|
||||||
case .pending:
|
case .pending:
|
||||||
purchaseStatusMessageKey = "premium_purchase_pending"
|
purchaseStatusMessageKey = "premium_purchase_pending"
|
||||||
case .cancelled:
|
case .cancelled:
|
||||||
|
|||||||
@@ -270,9 +270,7 @@ struct SettingsView: View {
|
|||||||
.listRowBackground(Color.mealMoodSurface)
|
.listRowBackground(Color.mealMoodSurface)
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
Button {
|
Link(destination: ReviewPromptService.writeReviewURL) {
|
||||||
ReviewPromptService.shared.requestFromSettings()
|
|
||||||
} label: {
|
|
||||||
Label("settings_rate_app", systemImage: "star.bubble")
|
Label("settings_rate_app", systemImage: "star.bubble")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user