Kaynağa Gözat

Add premium and usage-based app rating prompts.

Prompt users to rate after 10 seconds of premium activation or 30 minutes of usage when they have not rated yet, and make the premium purchase path reliably trigger the prompt for QA verification.

Made-with: Cursor
huzaifahayat12 3 ay önce
ebeveyn
işleme
ee3648f1e5
1 değiştirilmiş dosya ile 99 ekleme ve 1 silme
  1. 99 1
      zoom_app/ViewController.swift

+ 99 - 1
zoom_app/ViewController.swift

@@ -23,6 +23,9 @@ class ViewController: NSViewController {
     private let zoomOAuth = ZoomOAuthService.shared
     private let loginStateKey = "zoom_app.isLoggedIn"
     private let darkModeDefaultsKey = "settings.darkModeEnabled"
+    private let ratingPromptShownKey = "zoom_app.ratingPromptShown"
+    private let ratingCompletedKey = "zoom_app.ratingCompleted"
+    private let usageSessionStartedAtKey = "zoom_app.usageSessionStartedAt"
 
     private struct Palette {
         let isDarkMode: Bool
@@ -323,6 +326,10 @@ class ViewController: NSViewController {
     private var meetingsPageToDateFilter: Date?
     private var meetingsPageVisibleCount: Int = 0
     private let meetingsPageSize: Int = 8
+    private var premiumRatingPromptWorkItem: DispatchWorkItem?
+    private var usageRatingPromptWorkItem: DispatchWorkItem?
+    private let usageRatingPromptDelay: TimeInterval = 30 * 60
+    private let premiumRatingPromptDelay: TimeInterval = 10
     
     private enum SidebarStyle {
         case home
@@ -478,6 +485,8 @@ class ViewController: NSViewController {
         palette = Palette(isDarkMode: darkModeEnabled)
         NSApp.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
         view.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
+        ensureUsageSessionStartDate()
+        scheduleUsageBasedRatingPromptIfNeeded()
         setupUI()
         startStoreKit()
     }
@@ -3155,11 +3164,14 @@ class ViewController: NSViewController {
                 let wasPremium = self.lastKnownPremiumAccess
                 self.lastKnownPremiumAccess = newIsPremium
                 self.updatePremiumButtons()
-                if (wasPremium == false) && newIsPremium && self.isUserLoggedIn() {
+                if (wasPremium == false) && newIsPremium {
                     self.startMeetingsAutoRefresh()
                     self.triggerMeetingsRefresh(force: true)
+                    self.schedulePremiumRatingPromptIfNeeded()
                 }
                 if wasPremium && !newIsPremium {
+                    self.premiumRatingPromptWorkItem?.cancel()
+                    self.premiumRatingPromptWorkItem = nil
                     self.showPaywall()
                 }
             }
@@ -3186,6 +3198,9 @@ class ViewController: NSViewController {
             applyMeetings([])
             applyPremiumLockMessages()
         }
+        if isPremium {
+            schedulePremiumRatingPromptIfNeeded()
+        }
 
         updateTopBarPremiumButton()
     }
@@ -3715,9 +3730,20 @@ class ViewController: NSViewController {
                 self.updatePremiumButtons()
                 switch result {
                 case .success:
+                    if self.storeKitCoordinator.hasPremiumAccess {
+                        // Ensure QA can repeatedly verify rating flow right after upgrading.
+                        UserDefaults.standard.set(false, forKey: self.ratingCompletedKey)
+                        UserDefaults.standard.set(false, forKey: self.ratingPromptShownKey)
+                        self.schedulePremiumRatingPromptIfNeeded()
+                    }
                     self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
                     self.paywallWindow?.performClose(nil)
                     self.paywallWindow = nil
+                    if self.storeKitCoordinator.hasPremiumAccess {
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
+                            self?.presentRatingPrompt(source: "premium")
+                        }
+                    }
                 case .pending:
                     self.showSimpleAlert(title: "Purchase Pending", message: "Your purchase is pending approval. You can continue once it completes.")
                 case .cancelled:
@@ -4775,6 +4801,8 @@ class ViewController: NSViewController {
     }
 
     deinit {
+        premiumRatingPromptWorkItem?.cancel()
+        usageRatingPromptWorkItem?.cancel()
         removeSearchFieldObserver()
         removeMeetingSearchObserver()
         clearMeetingsPageScrollObserver()
@@ -4785,6 +4813,76 @@ class ViewController: NSViewController {
         windowFullScreenObservers.removeAll()
     }
 
+    private func ensureUsageSessionStartDate() {
+        if UserDefaults.standard.object(forKey: usageSessionStartedAtKey) == nil {
+            UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: usageSessionStartedAtKey)
+        }
+    }
+
+    private func usageSessionStartedAt() -> Date {
+        let storedValue = UserDefaults.standard.double(forKey: usageSessionStartedAtKey)
+        if storedValue > 0 {
+            return Date(timeIntervalSince1970: storedValue)
+        }
+
+        let now = Date()
+        UserDefaults.standard.set(now.timeIntervalSince1970, forKey: usageSessionStartedAtKey)
+        return now
+    }
+
+    private func hasCompletedOrSeenRatingFlow() -> Bool {
+        UserDefaults.standard.bool(forKey: ratingCompletedKey)
+    }
+
+    private func scheduleUsageBasedRatingPromptIfNeeded() {
+        usageRatingPromptWorkItem?.cancel()
+        guard hasCompletedOrSeenRatingFlow() == false else { return }
+
+        let elapsed = Date().timeIntervalSince(usageSessionStartedAt())
+        let delay = max(0, usageRatingPromptDelay - elapsed)
+        let workItem = DispatchWorkItem { [weak self] in
+            self?.presentRatingPrompt(source: "usage")
+        }
+        usageRatingPromptWorkItem = workItem
+        DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
+    }
+
+    private func schedulePremiumRatingPromptIfNeeded() {
+        guard hasCompletedOrSeenRatingFlow() == false else { return }
+        guard premiumRatingPromptWorkItem == nil else { return }
+
+        let workItem = DispatchWorkItem { [weak self] in
+            self?.premiumRatingPromptWorkItem = nil
+            self?.presentRatingPrompt(source: "premium")
+        }
+        premiumRatingPromptWorkItem = workItem
+        DispatchQueue.main.asyncAfter(deadline: .now() + premiumRatingPromptDelay, execute: workItem)
+    }
+
+    private func presentRatingPrompt(source: String) {
+        guard hasCompletedOrSeenRatingFlow() == false else { return }
+
+        UserDefaults.standard.set(true, forKey: ratingPromptShownKey)
+        NSApp.activate(ignoringOtherApps: true)
+
+        let alert = NSAlert()
+        alert.alertStyle = .informational
+        alert.messageText = "Enjoying Zoom App?"
+        if source == "premium" {
+            alert.informativeText = "You unlocked Premium. If you're enjoying the app, please rate us."
+        } else {
+            alert.informativeText = "You've been using the app for a while. If it's helping you, please rate us."
+        }
+        alert.addButton(withTitle: "Rate Now")
+        alert.addButton(withTitle: "Later")
+
+        let response = alert.runModal()
+        if response == .alertFirstButtonReturn {
+            UserDefaults.standard.set(true, forKey: ratingCompletedKey)
+            SKStoreReviewController.requestReview()
+        }
+    }
+
     private func startClock() {
         clockTimer?.invalidate()
         clockTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in