Ver código fonte

Refine premium rating prompts by gating Rate Us behind premium usage eligibility and scheduling a delayed review request after successful upgrades.

This keeps rating visibility aligned with business rules while avoiding stale legacy rating flags that blocked retesting.

Made-with: Cursor
huzaifahayat12 3 meses atrás
pai
commit
b75ddc0f46
1 arquivos alterados com 139 adições e 7 exclusões
  1. 139 7
      meetings_app/ViewController.swift

+ 139 - 7
meetings_app/ViewController.swift

@@ -283,6 +283,9 @@ final class ViewController: NSViewController {
     private var hasViewAppearedOnce = false
     private var lastKnownPremiumAccess = false
     private var displayedScheduleMeetings: [ScheduledMeeting] = []
+    private var appUsageSessionStartDate: Date?
+    private var hasObservedAppLifecycleForUsage = false
+    private var premiumUpgradeRatingPromptWorkItem: DispatchWorkItem?
 
     private enum ScheduleFilter: Int {
         case all = 0
@@ -316,6 +319,10 @@ final class ViewController: NSViewController {
     private let inAppBrowserDefaultPolicy: InAppBrowserURLPolicy = .allowAll
 
     private let darkModeDefaultsKey = "settings.darkModeEnabled"
+    private let appUsageAccumulatedSecondsDefaultsKey = "rating.appUsageAccumulatedSeconds"
+    private let userHasRatedDefaultsKey = "rating.userHasRated"
+    private let ratingStateMigrationV2DoneDefaultsKey = "rating.stateMigrationV2Done"
+    private let ratingEligibleUsageSeconds: TimeInterval = 30 * 60
     private var darkModeEnabled: Bool {
         get {
             let hasValue = UserDefaults.standard.object(forKey: darkModeDefaultsKey) != nil
@@ -329,10 +336,12 @@ final class ViewController: NSViewController {
         popover.behavior = .transient
         popover.animates = true
         let showUpgradeInSettings = storeKitCoordinator.hasPremiumAccess && !storeKitCoordinator.hasLifetimeAccess
+        let showRateUsInSettings = shouldShowRateUsInSettings
         popover.contentViewController = SettingsMenuViewController(
             palette: palette,
             typography: typography,
             darkModeEnabled: darkModeEnabled,
+            showRateUsInSettings: showRateUsInSettings,
             showUpgradeInSettings: showUpgradeInSettings,
             onToggleDarkMode: { [weak self] enabled in
                 self?.setDarkMode(enabled)
@@ -355,6 +364,9 @@ final class ViewController: NSViewController {
             guard let self else { return }
             self.handlePremiumAccessChanged(hasPremiumAccess)
         }
+        migrateLegacyRatingStateIfNeeded()
+        beginUsageTrackingSessionIfNeeded()
+        observeAppLifecycleForUsageTrackingIfNeeded()
         setupRootView()
         buildMainLayout()
         startStoreKit()
@@ -397,6 +409,11 @@ final class ViewController: NSViewController {
     }
 
     deinit {
+        premiumUpgradeRatingPromptWorkItem?.cancel()
+        endUsageTrackingSession()
+        if hasObservedAppLifecycleForUsage {
+            NotificationCenter.default.removeObserver(self)
+        }
         storeKitStartupTask?.cancel()
         paywallPurchaseTask?.cancel()
         launchPaywallWorkItem?.cancel()
@@ -764,10 +781,7 @@ private extension ViewController {
         case .rateUs:
             settingsPopover?.performClose(nil)
             settingsPopover = nil
-            // Replace with your App Store product URL when the app is listed.
-            if let url = URL(string: "https://apps.apple.com/app/id0000000000") {
-                openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
-            }
+            requestAppRatingIfEligible(markAsRated: true)
         case .support:
             settingsPopover?.performClose(nil)
             settingsPopover = nil
@@ -1038,18 +1052,124 @@ private extension ViewController {
     private func handlePremiumAccessChanged(_ hasPremiumAccess: Bool) {
         let hadPremiumAccess = lastKnownPremiumAccess
         lastKnownPremiumAccess = hasPremiumAccess
+        premiumUpgradeRatingPromptWorkItem?.cancel()
         refreshPaywallStoreUI()
         refreshScheduleCardsForPremiumStateChange()
         if !hadPremiumAccess && hasPremiumAccess {
             Task { [weak self] in
                 await self?.loadSchedule()
             }
+            scheduleRatingPromptAfterPremiumUpgrade()
         }
         if hadPremiumAccess && !hasPremiumAccess {
             showPaywall()
         }
     }
 
+    private var userHasRated: Bool {
+        UserDefaults.standard.bool(forKey: userHasRatedDefaultsKey)
+    }
+
+    private var accumulatedAppUsageSeconds: TimeInterval {
+        get {
+            UserDefaults.standard.double(forKey: appUsageAccumulatedSecondsDefaultsKey)
+        }
+        set {
+            UserDefaults.standard.set(newValue, forKey: appUsageAccumulatedSecondsDefaultsKey)
+        }
+    }
+
+    private var totalTrackedUsageSeconds: TimeInterval {
+        let liveSessionSeconds: TimeInterval
+        if let start = appUsageSessionStartDate {
+            liveSessionSeconds = max(0, Date().timeIntervalSince(start))
+        } else {
+            liveSessionSeconds = 0
+        }
+        return accumulatedAppUsageSeconds + liveSessionSeconds
+    }
+
+    private var hasReachedRatingUsageThreshold: Bool {
+        totalTrackedUsageSeconds >= ratingEligibleUsageSeconds
+    }
+
+    private var shouldShowRateUsInSettings: Bool {
+        storeKitCoordinator.hasPremiumAccess && !userHasRated && hasReachedRatingUsageThreshold
+    }
+
+    private func migrateLegacyRatingStateIfNeeded() {
+        let defaults = UserDefaults.standard
+        guard !defaults.bool(forKey: ratingStateMigrationV2DoneDefaultsKey) else { return }
+        // Legacy behavior marked "rated" immediately after requesting review.
+        // Clear once so testing and new logic can run correctly.
+        defaults.set(false, forKey: userHasRatedDefaultsKey)
+        defaults.set(true, forKey: ratingStateMigrationV2DoneDefaultsKey)
+    }
+
+    private func beginUsageTrackingSessionIfNeeded() {
+        guard appUsageSessionStartDate == nil else { return }
+        appUsageSessionStartDate = Date()
+    }
+
+    private func endUsageTrackingSession() {
+        guard let start = appUsageSessionStartDate else { return }
+        let sessionElapsedSeconds = max(0, Date().timeIntervalSince(start))
+        accumulatedAppUsageSeconds += sessionElapsedSeconds
+        appUsageSessionStartDate = nil
+    }
+
+    private func observeAppLifecycleForUsageTrackingIfNeeded() {
+        guard !hasObservedAppLifecycleForUsage else { return }
+        hasObservedAppLifecycleForUsage = true
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(applicationDidBecomeActiveForUsageTracking),
+            name: NSApplication.didBecomeActiveNotification,
+            object: nil
+        )
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(applicationWillResignActiveForUsageTracking),
+            name: NSApplication.willResignActiveNotification,
+            object: nil
+        )
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(applicationWillTerminateForUsageTracking),
+            name: NSApplication.willTerminateNotification,
+            object: nil
+        )
+    }
+
+    @objc private func applicationDidBecomeActiveForUsageTracking() {
+        beginUsageTrackingSessionIfNeeded()
+    }
+
+    @objc private func applicationWillResignActiveForUsageTracking() {
+        endUsageTrackingSession()
+    }
+
+    @objc private func applicationWillTerminateForUsageTracking() {
+        endUsageTrackingSession()
+    }
+
+    private func scheduleRatingPromptAfterPremiumUpgrade() {
+        guard !userHasRated else { return }
+        let workItem = DispatchWorkItem { [weak self] in
+            self?.requestAppRatingIfEligible(markAsRated: false)
+        }
+        premiumUpgradeRatingPromptWorkItem = workItem
+        DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: workItem)
+    }
+
+    private func requestAppRatingIfEligible(markAsRated: Bool) {
+        guard storeKitCoordinator.hasPremiumAccess, !userHasRated else { return }
+        SKStoreReviewController.requestReview()
+        if markAsRated {
+            UserDefaults.standard.set(true, forKey: userHasRatedDefaultsKey)
+        }
+    }
+
     private func refreshScheduleCardsForPremiumStateChange() {
         guard let stack = scheduleCardsStack else { return }
         renderScheduleCards(into: stack, meetings: displayedScheduleMeetings)
@@ -1118,6 +1238,7 @@ private extension ViewController {
             case .success:
                 self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
                 self.paywallWindow?.performClose(nil)
+                self.scheduleRatingPromptAfterPremiumUpgrade()
             case .cancelled:
                 break
             case .pending:
@@ -3439,6 +3560,7 @@ private final class SettingsMenuViewController: NSViewController {
         palette: Palette,
         typography: Typography,
         darkModeEnabled: Bool,
+        showRateUsInSettings: Bool,
         showUpgradeInSettings: Bool,
         onToggleDarkMode: @escaping (Bool) -> Void,
         onAction: @escaping (SettingsAction) -> Void
@@ -3448,7 +3570,11 @@ private final class SettingsMenuViewController: NSViewController {
         self.onToggleDarkMode = onToggleDarkMode
         self.onAction = onAction
         super.init(nibName: nil, bundle: nil)
-        self.view = makeView(darkModeEnabled: darkModeEnabled, showUpgradeInSettings: showUpgradeInSettings)
+        self.view = makeView(
+            darkModeEnabled: darkModeEnabled,
+            showRateUsInSettings: showRateUsInSettings,
+            showUpgradeInSettings: showUpgradeInSettings
+        )
     }
 
     @available(*, unavailable)
@@ -3460,7 +3586,11 @@ private final class SettingsMenuViewController: NSViewController {
         darkToggle?.state = enabled ? .on : .off
     }
 
-    private func makeView(darkModeEnabled: Bool, showUpgradeInSettings: Bool) -> NSView {
+    private func makeView(
+        darkModeEnabled: Bool,
+        showRateUsInSettings: Bool,
+        showUpgradeInSettings: Bool
+    ) -> NSView {
         let root = NSView()
         root.translatesAutoresizingMaskIntoConstraints = false
 
@@ -3489,7 +3619,9 @@ private final class SettingsMenuViewController: NSViewController {
         ])
 
         stack.addArrangedSubview(settingsDarkModeRow(enabled: darkModeEnabled))
-        stack.addArrangedSubview(settingsActionRow(icon: "★", title: "Rate Us", action: .rateUs))
+        if showRateUsInSettings {
+            stack.addArrangedSubview(settingsActionRow(icon: "★", title: "Rate Us", action: .rateUs))
+        }
         stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support))
         stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp))
         if showUpgradeInSettings {