浏览代码

Gate desktop widgets behind premium access

Disable widget previews for non-premium users, route locked widget clicks to the paywall, and remove persisted desktop widgets when premium access is lost.

Co-authored-by: Cursor <cursoragent@cursor.com>
huzaifahayat12 2 月之前
父节点
当前提交
71961a89fd
共有 1 个文件被更改,包括 88 次插入10 次删除
  1. 88 10
      zoom_app/ViewController.swift

+ 88 - 10
zoom_app/ViewController.swift

@@ -601,7 +601,9 @@ class ViewController: NSViewController {
         showHomeView(profile: nil)
         syncBrandPositionForCurrentWindowState(animated: false)
         scheduleLaunchPaywallIfNeeded()
-        DesktopWidgetWindowManager.shared.restore()
+        if storeKitCoordinator.hasPremiumAccess {
+            DesktopWidgetWindowManager.shared.restore()
+        }
     }
 
     override func viewDidLayout() {
@@ -3575,6 +3577,7 @@ class ViewController: NSViewController {
                 let wasPremium = self.lastKnownPremiumAccess
                 self.lastKnownPremiumAccess = newIsPremium
                 self.updatePremiumButtons()
+                self.reconcileDesktopWidgetsForPremiumAccess()
                 if (wasPremium == false) && newIsPremium {
                     self.startMeetingsAutoRefresh()
                     self.triggerMeetingsRefresh(force: true)
@@ -3598,6 +3601,7 @@ class ViewController: NSViewController {
             await self?.storeKitCoordinator.start()
             await MainActor.run {
                 self?.updatePremiumButtons()
+                self?.reconcileDesktopWidgetsForPremiumAccess()
                 self?.scheduleLaunchPaywallIfNeeded()
             }
         }
@@ -5662,6 +5666,7 @@ class ViewController: NSViewController {
     @MainActor
     private func updatePremiumLockedSections() {
         let shouldLock = isUserLoggedIn() && (storeKitCoordinator.hasPremiumAccess == false)
+        refreshWidgetsPagePremiumState()
         homePremiumLockOverlay = updatePremiumLockOverlay(
             existing: homePremiumLockOverlay,
             in: homeMeetingsPanel,
@@ -5754,7 +5759,7 @@ class ViewController: NSViewController {
     }
 
     private func makeWidgetsPageView() -> NSView {
-        let canAdd = isUserLoggedIn() && storeKitCoordinator.hasPremiumAccess
+        let canAdd = storeKitCoordinator.hasPremiumAccess
         let host = NSHostingView(
             rootView: WidgetsRootView(
                 canAddWidgets: canAdd,
@@ -5768,6 +5773,27 @@ class ViewController: NSViewController {
         return host
     }
 
+    @MainActor
+    private func refreshWidgetsPagePremiumState() {
+        guard let host = widgetsPageRootView as? NSHostingView<WidgetsRootView> else { return }
+        host.rootView = WidgetsRootView(
+            canAddWidgets: storeKitCoordinator.hasPremiumAccess,
+            onAddBlocked: { [weak self] in
+                self?.showPaywall()
+            }
+        )
+    }
+
+    @MainActor
+    private func reconcileDesktopWidgetsForPremiumAccess() {
+        refreshWidgetsPagePremiumState()
+        if storeKitCoordinator.hasPremiumAccess {
+            DesktopWidgetWindowManager.shared.restore()
+        } else {
+            DesktopWidgetWindowManager.shared.removeAllWidgets()
+        }
+    }
+
     @MainActor
     private func updateWidgetMeetingStoreFromCurrentMeetings() {
         let upcoming = allScheduledMeetings
@@ -7915,6 +7941,18 @@ private final class DesktopWidgetWindowManager: ObservableObject {
         return removed.count
     }
 
+    @discardableResult
+    func removeAllWidgets() -> Int {
+        let removedCount = loadInstances().count
+        for panel in windowsByInstanceID.values {
+            panel.close()
+        }
+        windowsByInstanceID.removeAll()
+        saveInstances([])
+        instancesChangeToken += 1
+        return removedCount
+    }
+
     func restore() {
         for instance in loadInstances() where windowsByInstanceID[instance.id] == nil {
             show(instance: instance)
@@ -8174,6 +8212,7 @@ private struct WidgetsRootView: View {
                             variant: variant,
                             layoutScale: scale,
                             isAdded: desktopWidgetManager.hasInstance(variantID: variant.id),
+                            isLocked: canAddWidgets == false,
                             onToggle: { toggleWidget(variant: variant) }
                         )
                     }
@@ -8225,16 +8264,16 @@ private struct WidgetsRootView: View {
     }
 
     private func toggleWidget(variant: WidgetVariant) {
+        guard canAddWidgets else {
+            showToast("Widgets are Premium only. Upgrade to add widgets.")
+            onAddBlocked?()
+            return
+        }
         if desktopWidgetManager.hasInstance(variantID: variant.id) {
             let removed = desktopWidgetManager.removeInstances(variantID: variant.id)
             guard removed > 0 else { return }
             showToast("Removed \(variant.size.title) widget from desktop.")
         } else {
-            guard canAddWidgets else {
-                showToast("Widgets are Premium only. Upgrade to add widgets.")
-                onAddBlocked?()
-                return
-            }
             desktopWidgetManager.show(
                 instance: WidgetInstance(variantID: variant.id, size: variant.size, origin: nil)
             )
@@ -8257,6 +8296,7 @@ private struct WidgetPreviewAddCard: View {
     let variant: WidgetVariant
     var layoutScale: CGFloat = 1
     let isAdded: Bool
+    let isLocked: Bool
     let onToggle: () -> Void
     @State private var hovering = false
 
@@ -8264,21 +8304,59 @@ private struct WidgetPreviewAddCard: View {
         ZStack(alignment: .topTrailing) {
             Button(action: onToggle) {
                 WidgetPreviewCard(variant: variant, layoutScale: layoutScale)
+                    .opacity(isLocked ? 0.52 : 1)
                     .contentShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
+                    .overlay {
+                        if isLocked {
+                            RoundedRectangle(cornerRadius: 18, style: .continuous)
+                                .fill(Color.black.opacity(0.28))
+                                .frame(
+                                    width: variant.previewCardSize.width * max(0.01, layoutScale),
+                                    height: variant.previewCardSize.height * max(0.01, layoutScale)
+                                )
+                        }
+                    }
+                    .overlay {
+                        if isLocked {
+                            VStack(spacing: 6) {
+                                Image(systemName: "lock.fill")
+                                    .font(.system(size: 18, weight: .bold))
+                                Text("Premium")
+                                    .font(.system(size: 13, weight: .bold))
+                            }
+                            .foregroundStyle(.white.opacity(0.96))
+                            .padding(.horizontal, 14)
+                            .padding(.vertical, 10)
+                            .background(
+                                RoundedRectangle(cornerRadius: 12, style: .continuous)
+                                    .fill(Color.black.opacity(0.46))
+                            )
+                            .frame(
+                                width: variant.previewCardSize.width * max(0.01, layoutScale),
+                                height: variant.previewCardSize.height * max(0.01, layoutScale)
+                            )
+                        }
+                    }
             }
             .buttonStyle(.plain)
 
             Button(action: onToggle) {
-                Image(systemName: isAdded ? "minus" : "plus")
+                Image(systemName: isLocked ? "lock.fill" : (isAdded ? "minus" : "plus"))
                     .font(.system(size: 12, weight: .bold))
                     .foregroundStyle(.white.opacity(0.95))
                     .frame(width: 26, height: 26)
-                    .background(Circle().fill(isAdded ? Color.red.opacity(0.92) : Color.green.opacity(0.95)))
+                    .background(
+                        Circle().fill(
+                            isLocked
+                                ? Color.orange.opacity(0.95)
+                                : (isAdded ? Color.red.opacity(0.92) : Color.green.opacity(0.95))
+                        )
+                    )
                     .shadow(color: .black.opacity(0.28), radius: 10, x: 0, y: 6)
             }
             .buttonStyle(.plain)
             .offset(x: 10 * layoutScale, y: -10 * layoutScale)
-            .opacity(isAdded ? 1 : (hovering ? 1 : 0))
+            .opacity((isAdded || isLocked) ? 1 : (hovering ? 1 : 0))
             .animation(.easeOut(duration: 0.12), value: hovering)
         }
         .onHover { hovering = $0 }