|
|
@@ -231,6 +231,9 @@ class ViewController: NSViewController {
|
|
|
private weak var meetingsPageEmptyLabel: NSTextField?
|
|
|
private weak var meetingsPageLoadMoreButton: NSButton?
|
|
|
private weak var meetingsPageScrollView: NSScrollView?
|
|
|
+ private weak var homePremiumLockOverlay: NSButton?
|
|
|
+ private weak var meetingsPremiumLockOverlay: NSButton?
|
|
|
+ private weak var schedulerPremiumLockOverlay: NSButton?
|
|
|
private var schedulerDayButtons: [Date: NSButton] = [:]
|
|
|
private var schedulerDateByButtonTag: [Int: Date] = [:]
|
|
|
private var nextSchedulerButtonTag: Int = 1
|
|
|
@@ -615,6 +618,7 @@ class ViewController: NSViewController {
|
|
|
showSimpleAlert(title: "Sign in required", message: "Please sign in with Zoom to schedule a meeting.")
|
|
|
return
|
|
|
}
|
|
|
+ guard ensurePremiumAccess() else { return }
|
|
|
if let existing = scheduleMeetingWindow {
|
|
|
existing.makeKeyAndOrderFront(nil)
|
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
|
@@ -1451,6 +1455,10 @@ class ViewController: NSViewController {
|
|
|
applyGuestLoginMessages()
|
|
|
return
|
|
|
}
|
|
|
+ guard ensurePremiumAccess() else {
|
|
|
+ applyPremiumLockMessages()
|
|
|
+ return
|
|
|
+ }
|
|
|
Task { @MainActor in
|
|
|
self.animateMeetingsRefresh()
|
|
|
self.setMeetingsLoadingUI(true)
|
|
|
@@ -1498,6 +1506,7 @@ class ViewController: NSViewController {
|
|
|
|
|
|
private func triggerMeetingsRefresh(force: Bool = false) {
|
|
|
guard isUserLoggedIn() else { return }
|
|
|
+ guard storeKitCoordinator.hasPremiumAccess else { return }
|
|
|
let now = Date()
|
|
|
if let until = meetingsRateLimitedUntil, now < until {
|
|
|
return
|
|
|
@@ -1879,20 +1888,45 @@ class ViewController: NSViewController {
|
|
|
schedulerMeetingsEmptyLabel?.stringValue = "Please sign in with Zoom to use Scheduler."
|
|
|
}
|
|
|
|
|
|
+ @MainActor
|
|
|
+ private func applyPremiumLockMessages() {
|
|
|
+ // Keep underlying section labels hidden while locked to avoid duplicate text beneath overlays.
|
|
|
+ meetingsStatusLabel?.stringValue = ""
|
|
|
+ emptyMeetingLabel?.isHidden = true
|
|
|
+ meetingsPageStatusLabel?.stringValue = ""
|
|
|
+ meetingsPageEmptyLabel?.isHidden = true
|
|
|
+ meetingsPageCountLabel?.stringValue = ""
|
|
|
+ schedulerMeetingsEmptyLabel?.isHidden = true
|
|
|
+ }
|
|
|
+
|
|
|
+ private func ensurePremiumAccess() -> Bool {
|
|
|
+ guard storeKitCoordinator.hasPremiumAccess else {
|
|
|
+ showPaywall()
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
@MainActor
|
|
|
private func applyAuthenticationUIState(triggerInitialRefresh: Bool) {
|
|
|
let loggedIn = isUserLoggedIn()
|
|
|
updateTopBarAuthButton()
|
|
|
settingsGoogleActionButton?.title = loggedIn ? "Sign Out" : "Sign in with Zoom"
|
|
|
settingsGoogleActionButton?.isEnabled = true
|
|
|
+ updatePremiumLockedSections()
|
|
|
|
|
|
meetingsRefreshTimer?.invalidate()
|
|
|
meetingsRefreshTimer = nil
|
|
|
meetingsRateLimitedUntil = nil
|
|
|
if loggedIn {
|
|
|
- startMeetingsAutoRefresh()
|
|
|
- if triggerInitialRefresh {
|
|
|
- triggerMeetingsRefresh(force: true)
|
|
|
+ if storeKitCoordinator.hasPremiumAccess {
|
|
|
+ startMeetingsAutoRefresh()
|
|
|
+ if triggerInitialRefresh {
|
|
|
+ triggerMeetingsRefresh(force: true)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ applyMeetings([])
|
|
|
+ applyPremiumLockMessages()
|
|
|
}
|
|
|
} else {
|
|
|
applyGuestLoginMessages()
|
|
|
@@ -1939,6 +1973,21 @@ class ViewController: NSViewController {
|
|
|
@MainActor
|
|
|
private func applyMeetings(_ meetings: [ScheduledMeeting]) {
|
|
|
allScheduledMeetings = meetings
|
|
|
+ if meetings.isEmpty == false {
|
|
|
+ let calendar = Calendar.current
|
|
|
+ let selectedStart = calendar.startOfDay(for: selectedMeetingsDayStart)
|
|
|
+ let selectedEnd = calendar.date(byAdding: .day, value: 1, to: selectedStart) ?? selectedStart.addingTimeInterval(60 * 60 * 24)
|
|
|
+ let selectedDayHasMeetings = meetings.contains { meeting in
|
|
|
+ meeting.start >= selectedStart && meeting.start < selectedEnd
|
|
|
+ }
|
|
|
+ if selectedDayHasMeetings == false,
|
|
|
+ let firstUpcoming = meetings
|
|
|
+ .filter({ $0.start >= Date() })
|
|
|
+ .sorted(by: { $0.start < $1.start })
|
|
|
+ .first {
|
|
|
+ selectedMeetingsDayStart = calendar.startOfDay(for: firstUpcoming.start)
|
|
|
+ }
|
|
|
+ }
|
|
|
applyFilteredMeetings()
|
|
|
applyMeetingsPageFilters(resetVisible: true)
|
|
|
}
|
|
|
@@ -1959,6 +2008,13 @@ class ViewController: NSViewController {
|
|
|
renderSchedulerCalendarGrid()
|
|
|
return
|
|
|
}
|
|
|
+ guard storeKitCoordinator.hasPremiumAccess else {
|
|
|
+ meetingsStatusLabel?.stringValue = ""
|
|
|
+ emptyMeetingLabel?.isHidden = true
|
|
|
+ applySchedulerMeetingsForSelectedDate()
|
|
|
+ renderSchedulerCalendarGrid()
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
let query = (homeSearchField?.stringValue ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
|
let calendar = Calendar.current
|
|
|
@@ -2015,6 +2071,12 @@ class ViewController: NSViewController {
|
|
|
renderMeetingsPageList()
|
|
|
return
|
|
|
}
|
|
|
+ guard storeKitCoordinator.hasPremiumAccess else {
|
|
|
+ meetingsPageFilteredMeetings = []
|
|
|
+ meetingsPageVisibleCount = 0
|
|
|
+ renderMeetingsPageList()
|
|
|
+ return
|
|
|
+ }
|
|
|
let calendar = Calendar.current
|
|
|
let now = Date()
|
|
|
let activePreset = meetingsPageCategoryFilter?.titleOfSelectedItem ?? meetingsPageSelectedRangePreset
|
|
|
@@ -2099,6 +2161,13 @@ class ViewController: NSViewController {
|
|
|
meetingsPageLoadMoreButton?.isHidden = true
|
|
|
return
|
|
|
}
|
|
|
+ guard storeKitCoordinator.hasPremiumAccess else {
|
|
|
+ meetingsPageCountLabel?.stringValue = ""
|
|
|
+ meetingsPageStatusLabel?.stringValue = ""
|
|
|
+ meetingsPageEmptyLabel?.isHidden = true
|
|
|
+ meetingsPageLoadMoreButton?.isHidden = true
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
let totalCount = meetingsPageFilteredMeetings.count
|
|
|
let visibleCount = min(meetingsPageVisibleCount, totalCount)
|
|
|
@@ -2273,6 +2342,10 @@ class ViewController: NSViewController {
|
|
|
schedulerMeetingsEmptyLabel?.stringValue = "Please sign in with Zoom to use Scheduler."
|
|
|
return
|
|
|
}
|
|
|
+ guard storeKitCoordinator.hasPremiumAccess else {
|
|
|
+ schedulerMeetingsEmptyLabel?.isHidden = true
|
|
|
+ return
|
|
|
+ }
|
|
|
|
|
|
let meetings = meetingsForDay(selectedMeetingsDayStart)
|
|
|
if meetings.isEmpty {
|
|
|
@@ -2449,6 +2522,13 @@ class ViewController: NSViewController {
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
+ guard storeKitCoordinator.hasPremiumAccess else {
|
|
|
+ await MainActor.run {
|
|
|
+ self.applyMeetings([])
|
|
|
+ self.applyPremiumLockMessages()
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
if isLoadingMeetings { return }
|
|
|
isLoadingMeetings = true
|
|
|
defer {
|
|
|
@@ -3022,6 +3102,10 @@ class ViewController: NSViewController {
|
|
|
let wasPremium = self.lastKnownPremiumAccess
|
|
|
self.lastKnownPremiumAccess = newIsPremium
|
|
|
self.updatePremiumButtons()
|
|
|
+ if (wasPremium == false) && newIsPremium && self.isUserLoggedIn() {
|
|
|
+ self.startMeetingsAutoRefresh()
|
|
|
+ self.triggerMeetingsRefresh(force: true)
|
|
|
+ }
|
|
|
if wasPremium && !newIsPremium {
|
|
|
self.showPaywall()
|
|
|
}
|
|
|
@@ -3042,6 +3126,11 @@ class ViewController: NSViewController {
|
|
|
settingsUpgradeTitleLabel?.stringValue = isPremium ? "Premium Active" : "Upgrade"
|
|
|
settingsUpgradeButton?.isEnabled = isPremium == false
|
|
|
settingsUpgradeButton?.alphaValue = isPremium ? 0.6 : 1.0
|
|
|
+ updatePremiumLockedSections()
|
|
|
+ if isPremium == false, isUserLoggedIn() {
|
|
|
+ applyMeetings([])
|
|
|
+ applyPremiumLockMessages()
|
|
|
+ }
|
|
|
|
|
|
updateTopBarPremiumButton()
|
|
|
}
|
|
|
@@ -3098,6 +3187,7 @@ class ViewController: NSViewController {
|
|
|
let keepSelected = selectedHomeSidebarItem
|
|
|
selectedHomeSidebarItem = keepSelected
|
|
|
showHomeView(profile: nil)
|
|
|
+ updatePremiumLockedSections()
|
|
|
}
|
|
|
|
|
|
private func applyWindowBackgroundForCurrentTheme(_ window: NSWindow) {
|
|
|
@@ -4332,10 +4422,97 @@ class ViewController: NSViewController {
|
|
|
topBarAuthButtonWidthConstraint?.isActive = true
|
|
|
updateTopBarAuthButton()
|
|
|
applyAuthenticationUIState(triggerInitialRefresh: false)
|
|
|
+ updatePremiumLockedSections()
|
|
|
|
|
|
return root
|
|
|
}
|
|
|
|
|
|
+ @objc private func premiumLockedAreaTapped(_ sender: NSButton) {
|
|
|
+ showPaywall()
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func updatePremiumLockedSections() {
|
|
|
+ let shouldLock = isUserLoggedIn() && (storeKitCoordinator.hasPremiumAccess == false)
|
|
|
+ homePremiumLockOverlay = updatePremiumLockOverlay(
|
|
|
+ existing: homePremiumLockOverlay,
|
|
|
+ in: homeMeetingsPanel,
|
|
|
+ locked: shouldLock,
|
|
|
+ message: "Upgrade to Premium to access meetings"
|
|
|
+ )
|
|
|
+ meetingsPremiumLockOverlay = updatePremiumLockOverlay(
|
|
|
+ existing: meetingsPremiumLockOverlay,
|
|
|
+ in: meetingsPageRootView,
|
|
|
+ locked: shouldLock,
|
|
|
+ message: "Upgrade to Premium to unlock Meetings"
|
|
|
+ )
|
|
|
+ schedulerPremiumLockOverlay = updatePremiumLockOverlay(
|
|
|
+ existing: schedulerPremiumLockOverlay,
|
|
|
+ in: schedulerRootView,
|
|
|
+ locked: shouldLock,
|
|
|
+ message: "Upgrade to Premium to unlock Scheduler"
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ @MainActor
|
|
|
+ private func updatePremiumLockOverlay(
|
|
|
+ existing: NSButton?,
|
|
|
+ in target: NSView?,
|
|
|
+ locked: Bool,
|
|
|
+ message: String
|
|
|
+ ) -> NSButton? {
|
|
|
+ guard let target else { return existing }
|
|
|
+ let button: NSButton
|
|
|
+ if let existing, existing.superview === target {
|
|
|
+ button = existing
|
|
|
+ } else {
|
|
|
+ let created = NSButton(title: message, target: self, action: #selector(premiumLockedAreaTapped(_:)))
|
|
|
+ created.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ created.isBordered = false
|
|
|
+ created.bezelStyle = .shadowlessSquare
|
|
|
+ created.alignment = .center
|
|
|
+ created.wantsLayer = true
|
|
|
+ created.layer?.cornerRadius = 12
|
|
|
+ created.layer?.masksToBounds = true
|
|
|
+ created.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
+ created.font = .systemFont(ofSize: 15, weight: .semibold)
|
|
|
+ created.contentTintColor = .white
|
|
|
+
|
|
|
+ let blur = NSVisualEffectView()
|
|
|
+ blur.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ blur.material = .hudWindow
|
|
|
+ blur.blendingMode = .withinWindow
|
|
|
+ blur.state = .active
|
|
|
+ created.addSubview(blur, positioned: .below, relativeTo: nil)
|
|
|
+
|
|
|
+ let dim = NSView()
|
|
|
+ dim.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ dim.wantsLayer = true
|
|
|
+ dim.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.72).cgColor
|
|
|
+ created.addSubview(dim, positioned: .above, relativeTo: blur)
|
|
|
+
|
|
|
+ target.addSubview(created)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ created.leadingAnchor.constraint(equalTo: target.leadingAnchor),
|
|
|
+ created.trailingAnchor.constraint(equalTo: target.trailingAnchor),
|
|
|
+ created.topAnchor.constraint(equalTo: target.topAnchor),
|
|
|
+ created.bottomAnchor.constraint(equalTo: target.bottomAnchor),
|
|
|
+ blur.leadingAnchor.constraint(equalTo: created.leadingAnchor),
|
|
|
+ blur.trailingAnchor.constraint(equalTo: created.trailingAnchor),
|
|
|
+ blur.topAnchor.constraint(equalTo: created.topAnchor),
|
|
|
+ blur.bottomAnchor.constraint(equalTo: created.bottomAnchor),
|
|
|
+ dim.leadingAnchor.constraint(equalTo: created.leadingAnchor),
|
|
|
+ dim.trailingAnchor.constraint(equalTo: created.trailingAnchor),
|
|
|
+ dim.topAnchor.constraint(equalTo: created.topAnchor),
|
|
|
+ dim.bottomAnchor.constraint(equalTo: created.bottomAnchor)
|
|
|
+ ])
|
|
|
+ button = created
|
|
|
+ }
|
|
|
+ button.title = message
|
|
|
+ button.isHidden = locked == false
|
|
|
+ return button
|
|
|
+ }
|
|
|
+
|
|
|
private func makeMeetingsPageView() -> NSView {
|
|
|
let root = NSView()
|
|
|
root.wantsLayer = true
|