|
|
@@ -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
|