Files
InvestmentTrackerApp/PortfolioJournal/Services/ReviewPromptService.swift
T
alexandrev-tibco a8a9ad64f3 Xcode 27: compila sin warnings y migra API deprecadas de SwiftUI
- Imports de CoreData que faltaban (MemberImportVisibility) en AccountsView,
  ImportDataView, ChartsContainerView y AddSourceView.
- Aislamiento MainActor: IsolatedDefaultValues en el target de la app,
  LinearGradient @MainActor, WatchSyncMessage nonisolated, closures de
  ReviewPromptService/MonthlyCheckInStore/AppIntents.
- Warnings de valores sin usar (SnapshotGapDetector, DashboardLayoutStore,
  OnboardingView, GoalEditorView, CoreDataStack, SettingsViewModel).
- foregroundColor→foregroundStyle, cornerRadius→clipShape,
  navigationBarLeading/Trailing→topBarLeading/Trailing,
  Task.sleep(nanoseconds:)→sleep(for:), NavigationLink(isActive:)→
  navigationDestination(isPresented:).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F1u4K16xy7eQVtgsYNZ9Vn
2026-09-16 11:44:49 +02:00

106 lines
4.0 KiB
Swift

import StoreKit
import UIKit
final class ReviewPromptService {
static let shared = ReviewPromptService()
private let lastPromptKey = "reviewPromptLastDate"
private let checkInCountKey = "reviewPromptCheckinCount"
private let hasCompletedStoreReviewKey = "reviewPromptHasCompletedStoreReview"
private let promptedAchievementKeysKey = "reviewPromptedAchievementKeys"
// Rating velocity: with a monthly-cadence app, 3 check-ins + 90 days meant 3+ months
// before the first review could even be requested. 2 check-ins + 30 days keeps the
// prompt tied to a good moment (a completed check-in) while building ratings sooner.
private let minCheckInsBetweenPrompts = 2
private let minDaysBetweenPrompts = 30
private let userDefaults: UserDefaults
private let dateProvider: () -> Date
private let reviewRequestHandler: @MainActor () -> Void
private init() {
userDefaults = .standard
dateProvider = Date.init
reviewRequestHandler = { ReviewPromptService.defaultReviewRequestHandler() }
}
init(
userDefaults: UserDefaults,
dateProvider: @escaping () -> Date,
reviewRequestHandler: @escaping @MainActor () -> Void
) {
self.userDefaults = userDefaults
self.dateProvider = dateProvider
self.reviewRequestHandler = reviewRequestHandler
}
func recordMonthlyCheckInCompleted() {
let currentCount = userDefaults.integer(forKey: checkInCountKey)
userDefaults.set(currentCount + 1, forKey: checkInCountKey)
requestReviewIfEligible()
}
func shouldAskForAchievementSatisfaction(newlyUnlockedAchievementKeys: Set<String>) -> Bool {
guard !newlyUnlockedAchievementKeys.isEmpty else { return false }
guard !hasCompletedStoreReview else { return false }
let promptedKeys = Set(userDefaults.stringArray(forKey: promptedAchievementKeysKey) ?? [])
let unpromptedKeys = newlyUnlockedAchievementKeys.subtracting(promptedKeys)
guard !unpromptedKeys.isEmpty else { return false }
let mergedKeys = promptedKeys.union(unpromptedKeys).sorted()
userDefaults.set(mergedKeys, forKey: promptedAchievementKeysKey)
return true
}
var hasCompletedStoreReview: Bool {
userDefaults.bool(forKey: hasCompletedStoreReviewKey)
}
func markStoreReviewCompleted() {
userDefaults.set(true, forKey: hasCompletedStoreReviewKey)
}
static func appStoreWriteReviewURL() -> URL {
guard var components = URLComponents(url: GoalShareService.appStoreURL, resolvingAgainstBaseURL: false) else {
return GoalShareService.appStoreURL
}
var queryItems = components.queryItems ?? []
queryItems.removeAll { $0.name == "action" }
queryItems.append(URLQueryItem(name: "action", value: "write-review"))
components.queryItems = queryItems
return components.url ?? GoalShareService.appStoreURL
}
private func requestReviewIfEligible() {
guard !hasCompletedStoreReview else { return }
let now = dateProvider()
if let lastPrompt = userDefaults.object(forKey: lastPromptKey) as? Date {
let daysSince = now.timeIntervalSince(lastPrompt) / 86_400
if daysSince < Double(minDaysBetweenPrompts) {
return
}
}
let count = userDefaults.integer(forKey: checkInCountKey)
guard count >= minCheckInsBetweenPrompts else { return }
requestReview()
}
private func requestReview() {
reviewRequestHandler()
userDefaults.set(dateProvider(), forKey: lastPromptKey)
userDefaults.set(0, forKey: checkInCountKey)
}
@MainActor
private static func defaultReviewRequestHandler() {
guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
if #available(iOS 18.0, *) {
AppStore.requestReview(in: scene)
} else {
SKStoreReviewController.requestReview(in: scene)
}
}
}