Преглед на файлове

Gate join and schedule interactions for non-premium users.

Show paywall immediately on locked actions, disable premium-only join flows, and add locked event card overlays with a premium upsell message.

Made-with: Cursor
huzaifahayat12 преди 3 месеца
родител
ревизия
2d8c7b28ba
променени са 1 файла, в които са добавени 129 реда и са изтрити 7 реда
  1. 129 7
      meetings_app/ViewController.swift

+ 129 - 7
meetings_app/ViewController.swift

@@ -81,6 +81,7 @@ private final class StoreKitCoordinator {
     private(set) var productsByID: [String: Product] = [:]
     private(set) var activeEntitlementProductIDs: Set<String> = []
     private(set) var lastProductLoadError: String?
+    var onEntitlementsChanged: ((Bool) -> Void)?
 
     var hasPremiumAccess: Bool { !activeEntitlementProductIDs.isEmpty }
 
@@ -112,6 +113,7 @@ private final class StoreKitCoordinator {
     }
 
     func refreshEntitlements() async {
+        let previousHasPremiumAccess = hasPremiumAccess
         var active = Set<String>()
         for await entitlement in Transaction.currentEntitlements {
             guard case .verified(let transaction) = entitlement else { continue }
@@ -130,6 +132,10 @@ private final class StoreKitCoordinator {
             active.insert(productID)
         }
         activeEntitlementProductIDs = active
+        let newHasPremiumAccess = hasPremiumAccess
+        if newHasPremiumAccess != previousHasPremiumAccess {
+            onEntitlementsChanged?(newHasPremiumAccess)
+        }
     }
 
     func purchase(plan: PremiumPlan) async -> PurchaseOutcome {
@@ -243,6 +249,12 @@ final class ViewController: NSViewController {
     private weak var sidebarPremiumTitleLabel: NSTextField?
     private weak var sidebarPremiumIconView: NSImageView?
     private weak var sidebarPremiumButtonView: HoverTrackingView?
+    private weak var instantMeetCardView: HoverSurfaceView?
+    private weak var instantMeetTitleLabel: NSTextField?
+    private weak var instantMeetSubtitleLabel: NSTextField?
+    private weak var joinWithLinkCardView: HoverSurfaceView?
+    private weak var joinWithLinkTitleLabel: NSTextField?
+    private weak var joinMeetPrimaryButton: NSButton?
     private weak var meetLinkField: NSTextField?
     private weak var browseAddressField: NSTextField?
     private var inAppBrowserWindowController: InAppBrowserWindowController?
@@ -257,6 +269,7 @@ final class ViewController: NSViewController {
     private var hasCompletedInitialStoreKitSync = false
     private var hasPresentedLaunchPaywall = false
     private var hasViewAppearedOnce = false
+    private var lastKnownPremiumAccess = false
 
     private enum ScheduleFilter: Int {
         case all = 0
@@ -323,6 +336,10 @@ final class ViewController: NSViewController {
         // Sync toggle + palette with current macOS appearance on launch.
         darkModeEnabled = systemPrefersDarkMode()
         palette = Palette(isDarkMode: darkModeEnabled)
+        storeKitCoordinator.onEntitlementsChanged = { [weak self] hasPremiumAccess in
+            guard let self else { return }
+            self.handlePremiumAccessChanged(hasPremiumAccess)
+        }
         setupRootView()
         buildMainLayout()
         startStoreKit()
@@ -449,6 +466,10 @@ private extension ViewController {
     }
 
     @objc private func joinMeetClicked(_ sender: Any?) {
+        guard storeKitCoordinator.hasPremiumAccess else {
+            showPaywall()
+            return
+        }
         let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
 
         guard let url = normalizedMeetJoinURL(from: rawInput) else {
@@ -462,6 +483,14 @@ private extension ViewController {
         openInDefaultBrowser(url: url)
     }
 
+    @objc private func joinWithLinkCardClicked(_ sender: NSClickGestureRecognizer) {
+        guard storeKitCoordinator.hasPremiumAccess else {
+            showPaywall()
+            return
+        }
+        meetLinkField?.window?.makeFirstResponder(meetLinkField)
+    }
+
     @objc private func cancelMeetJoinClicked(_ sender: Any?) {
         meetLinkField?.stringValue = ""
     }
@@ -496,6 +525,10 @@ private extension ViewController {
     }
 
     @objc private func instantMeetClicked(_ sender: NSClickGestureRecognizer) {
+        guard storeKitCoordinator.hasPremiumAccess else {
+            showPaywall()
+            return
+        }
         guard let url = URL(string: "https://meet.google.com/new") else { return }
         openInDefaultBrowser(url: url)
     }
@@ -876,10 +909,39 @@ private extension ViewController {
             label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
         }
         refreshSidebarPremiumButton()
+        refreshInstantMeetPremiumState()
         updatePaywallPlanSelection()
         updatePaywallContinueState(isLoading: false)
     }
 
+    private func refreshInstantMeetPremiumState() {
+        let isPremium = storeKitCoordinator.hasPremiumAccess
+        instantMeetCardView?.alphaValue = isPremium ? 1.0 : 0.65
+        instantMeetTitleLabel?.alphaValue = isPremium ? 1.0 : 0.75
+        instantMeetSubtitleLabel?.alphaValue = isPremium ? 1.0 : 0.75
+        instantMeetCardView?.toolTip = isPremium ? nil : "Premium required. Click to open paywall."
+        instantMeetCardView?.onHoverChanged?(false)
+
+        joinWithLinkCardView?.alphaValue = isPremium ? 1.0 : 0.65
+        joinWithLinkTitleLabel?.alphaValue = isPremium ? 1.0 : 0.75
+        meetLinkField?.isEditable = isPremium
+        meetLinkField?.isSelectable = isPremium
+        meetLinkField?.alphaValue = isPremium ? 1.0 : 0.75
+        // Keep button enabled so non-premium taps can still trigger paywall.
+        joinMeetPrimaryButton?.isEnabled = true
+        joinMeetPrimaryButton?.alphaValue = isPremium ? 1.0 : 0.80
+        joinWithLinkCardView?.toolTip = isPremium ? nil : "Premium required. Click to open paywall."
+    }
+
+    private func handlePremiumAccessChanged(_ hasPremiumAccess: Bool) {
+        let hadPremiumAccess = lastKnownPremiumAccess
+        lastKnownPremiumAccess = hasPremiumAccess
+        refreshPaywallStoreUI()
+        if hadPremiumAccess && !hasPremiumAccess {
+            showPaywall()
+        }
+    }
+
     private func refreshSidebarPremiumButton() {
         let isPremium = storeKitCoordinator.hasPremiumAccess
         if isPremium {
@@ -1476,17 +1538,37 @@ private extension ViewController {
         let baseColor = palette.sectionCard
         let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
         let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor
-        instant.onHoverChanged = { hovering in
-            instant.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
+        instant.onHoverChanged = { [weak self] hovering in
+            guard let self else { return }
+            if self.storeKitCoordinator.hasPremiumAccess {
+                instant.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
+            } else {
+                let disabledColor = self.palette.sectionCard.blended(withFraction: 0.22, of: self.palette.pageBackground) ?? self.palette.sectionCard
+                instant.layer?.backgroundColor = disabledColor.cgColor
+            }
         }
-        codeCard.onHoverChanged = { hovering in
-            codeCard.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
+        codeCard.onHoverChanged = { [weak self] hovering in
+            guard let self else { return }
+            if self.storeKitCoordinator.hasPremiumAccess {
+                codeCard.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
+            } else {
+                let disabledColor = self.palette.sectionCard.blended(withFraction: 0.22, of: self.palette.pageBackground) ?? self.palette.sectionCard
+                codeCard.layer?.backgroundColor = disabledColor.cgColor
+            }
         }
         instant.onHoverChanged?(false)
         codeCard.onHoverChanged?(false)
 
         let instantClick = NSClickGestureRecognizer(target: self, action: #selector(instantMeetClicked(_:)))
         instant.addGestureRecognizer(instantClick)
+        let joinWithLinkClick = NSClickGestureRecognizer(target: self, action: #selector(joinWithLinkCardClicked(_:)))
+        codeCard.addGestureRecognizer(joinWithLinkClick)
+        instantMeetCardView = instant
+        instantMeetTitleLabel = instantTitle
+        instantMeetSubtitleLabel = instantSub
+        joinWithLinkCardView = codeCard
+        joinWithLinkTitleLabel = codeTitle
+        refreshInstantMeetPremiumState()
 
         row.addArrangedSubview(instant)
         row.addArrangedSubview(codeCard)
@@ -1511,13 +1593,16 @@ private extension ViewController {
             width: 110,
             action: #selector(cancelMeetJoinClicked(_:))
         ))
-        row.addArrangedSubview(meetActionButton(
+        let joinButton = meetActionButton(
             title: "Join",
             color: palette.primaryBlue,
             textColor: .white,
             width: 116,
             action: #selector(joinMeetClicked(_:))
-        ))
+        )
+        joinMeetPrimaryButton = joinButton
+        row.addArrangedSubview(joinButton)
+        refreshInstantMeetPremiumState()
         return row
     }
 
@@ -2552,10 +2637,43 @@ private extension ViewController {
             let base = self.palette.sectionCard
             let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
             let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
-            card.layer?.backgroundColor = (hovering ? hover : base).cgColor
+            if self.storeKitCoordinator.hasPremiumAccess {
+                card.layer?.backgroundColor = (hovering ? hover : base).cgColor
+            } else {
+                card.layer?.backgroundColor = base.cgColor
+            }
         }
         hit.onHoverChanged?(false)
 
+        if !storeKitCoordinator.hasPremiumAccess {
+            let lockOverlay = NSVisualEffectView()
+            lockOverlay.translatesAutoresizingMaskIntoConstraints = false
+            lockOverlay.material = darkModeEnabled ? .hudWindow : .popover
+            lockOverlay.blendingMode = .withinWindow
+            lockOverlay.state = .active
+            lockOverlay.wantsLayer = true
+            lockOverlay.layer?.cornerRadius = 12
+            lockOverlay.layer?.masksToBounds = true
+            lockOverlay.layer?.backgroundColor = NSColor.black.withAlphaComponent(darkModeEnabled ? 0.28 : 0.12).cgColor
+
+            let lockLabel = textLabel("Get Premium to see events", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .white)
+            lockLabel.alignment = .center
+
+            card.addSubview(lockOverlay)
+            lockOverlay.addSubview(lockLabel)
+            NSLayoutConstraint.activate([
+                lockOverlay.leadingAnchor.constraint(equalTo: card.leadingAnchor),
+                lockOverlay.trailingAnchor.constraint(equalTo: card.trailingAnchor),
+                lockOverlay.topAnchor.constraint(equalTo: card.topAnchor),
+                lockOverlay.bottomAnchor.constraint(equalTo: card.bottomAnchor),
+                lockLabel.centerXAnchor.constraint(equalTo: lockOverlay.centerXAnchor),
+                lockLabel.centerYAnchor.constraint(equalTo: lockOverlay.centerYAnchor),
+                lockLabel.leadingAnchor.constraint(greaterThanOrEqualTo: lockOverlay.leadingAnchor, constant: 10),
+                lockLabel.trailingAnchor.constraint(lessThanOrEqualTo: lockOverlay.trailingAnchor, constant: -10)
+            ])
+            hit.toolTip = "Premium required. Click to open paywall."
+        }
+
         return hit
     }
 
@@ -3489,6 +3607,10 @@ private extension ViewController {
     }
 
     @objc func scheduleCardButtonPressed(_ sender: NSButton) {
+        guard storeKitCoordinator.hasPremiumAccess else {
+            showPaywall()
+            return
+        }
         guard let raw = sender.identifier?.rawValue,
               let url = URL(string: raw) else { return }
         openMeetingURL(url)