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" private let minCheckInsBetweenPrompts = 3 private let minDaysBetweenPrompts = 90 private let userDefaults: UserDefaults private let dateProvider: () -> Date private let reviewRequestHandler: () -> Void private init() { userDefaults = .standard dateProvider = Date.init reviewRequestHandler = ReviewPromptService.defaultReviewRequestHandler } init( userDefaults: UserDefaults, dateProvider: @escaping () -> Date, reviewRequestHandler: @escaping () -> 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) -> 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) } 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) } } }