Quellcode durchsuchen

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 vor 1 Woche
Ursprung
Commit
2d8c7b28ba
1 geänderte Dateien mit 129 neuen und 7 gelöschten Zeilen
  1. 129 7
      meetings_app/ViewController.swift

+ 129 - 7
meetings_app/ViewController.swift

@@ -81,6 +81,7 @@ private final class StoreKitCoordinator {
81 81
     private(set) var productsByID: [String: Product] = [:]
82 82
     private(set) var activeEntitlementProductIDs: Set<String> = []
83 83
     private(set) var lastProductLoadError: String?
84
+    var onEntitlementsChanged: ((Bool) -> Void)?
84 85
 
85 86
     var hasPremiumAccess: Bool { !activeEntitlementProductIDs.isEmpty }
86 87
 
@@ -112,6 +113,7 @@ private final class StoreKitCoordinator {
112 113
     }
113 114
 
114 115
     func refreshEntitlements() async {
116
+        let previousHasPremiumAccess = hasPremiumAccess
115 117
         var active = Set<String>()
116 118
         for await entitlement in Transaction.currentEntitlements {
117 119
             guard case .verified(let transaction) = entitlement else { continue }
@@ -130,6 +132,10 @@ private final class StoreKitCoordinator {
130 132
             active.insert(productID)
131 133
         }
132 134
         activeEntitlementProductIDs = active
135
+        let newHasPremiumAccess = hasPremiumAccess
136
+        if newHasPremiumAccess != previousHasPremiumAccess {
137
+            onEntitlementsChanged?(newHasPremiumAccess)
138
+        }
133 139
     }
134 140
 
135 141
     func purchase(plan: PremiumPlan) async -> PurchaseOutcome {
@@ -243,6 +249,12 @@ final class ViewController: NSViewController {
243 249
     private weak var sidebarPremiumTitleLabel: NSTextField?
244 250
     private weak var sidebarPremiumIconView: NSImageView?
245 251
     private weak var sidebarPremiumButtonView: HoverTrackingView?
252
+    private weak var instantMeetCardView: HoverSurfaceView?
253
+    private weak var instantMeetTitleLabel: NSTextField?
254
+    private weak var instantMeetSubtitleLabel: NSTextField?
255
+    private weak var joinWithLinkCardView: HoverSurfaceView?
256
+    private weak var joinWithLinkTitleLabel: NSTextField?
257
+    private weak var joinMeetPrimaryButton: NSButton?
246 258
     private weak var meetLinkField: NSTextField?
247 259
     private weak var browseAddressField: NSTextField?
248 260
     private var inAppBrowserWindowController: InAppBrowserWindowController?
@@ -257,6 +269,7 @@ final class ViewController: NSViewController {
257 269
     private var hasCompletedInitialStoreKitSync = false
258 270
     private var hasPresentedLaunchPaywall = false
259 271
     private var hasViewAppearedOnce = false
272
+    private var lastKnownPremiumAccess = false
260 273
 
261 274
     private enum ScheduleFilter: Int {
262 275
         case all = 0
@@ -323,6 +336,10 @@ final class ViewController: NSViewController {
323 336
         // Sync toggle + palette with current macOS appearance on launch.
324 337
         darkModeEnabled = systemPrefersDarkMode()
325 338
         palette = Palette(isDarkMode: darkModeEnabled)
339
+        storeKitCoordinator.onEntitlementsChanged = { [weak self] hasPremiumAccess in
340
+            guard let self else { return }
341
+            self.handlePremiumAccessChanged(hasPremiumAccess)
342
+        }
326 343
         setupRootView()
327 344
         buildMainLayout()
328 345
         startStoreKit()
@@ -449,6 +466,10 @@ private extension ViewController {
449 466
     }
450 467
 
451 468
     @objc private func joinMeetClicked(_ sender: Any?) {
469
+        guard storeKitCoordinator.hasPremiumAccess else {
470
+            showPaywall()
471
+            return
472
+        }
452 473
         let rawInput = meetLinkField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
453 474
 
454 475
         guard let url = normalizedMeetJoinURL(from: rawInput) else {
@@ -462,6 +483,14 @@ private extension ViewController {
462 483
         openInDefaultBrowser(url: url)
463 484
     }
464 485
 
486
+    @objc private func joinWithLinkCardClicked(_ sender: NSClickGestureRecognizer) {
487
+        guard storeKitCoordinator.hasPremiumAccess else {
488
+            showPaywall()
489
+            return
490
+        }
491
+        meetLinkField?.window?.makeFirstResponder(meetLinkField)
492
+    }
493
+
465 494
     @objc private func cancelMeetJoinClicked(_ sender: Any?) {
466 495
         meetLinkField?.stringValue = ""
467 496
     }
@@ -496,6 +525,10 @@ private extension ViewController {
496 525
     }
497 526
 
498 527
     @objc private func instantMeetClicked(_ sender: NSClickGestureRecognizer) {
528
+        guard storeKitCoordinator.hasPremiumAccess else {
529
+            showPaywall()
530
+            return
531
+        }
499 532
         guard let url = URL(string: "https://meet.google.com/new") else { return }
500 533
         openInDefaultBrowser(url: url)
501 534
     }
@@ -876,10 +909,39 @@ private extension ViewController {
876 909
             label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
877 910
         }
878 911
         refreshSidebarPremiumButton()
912
+        refreshInstantMeetPremiumState()
879 913
         updatePaywallPlanSelection()
880 914
         updatePaywallContinueState(isLoading: false)
881 915
     }
882 916
 
917
+    private func refreshInstantMeetPremiumState() {
918
+        let isPremium = storeKitCoordinator.hasPremiumAccess
919
+        instantMeetCardView?.alphaValue = isPremium ? 1.0 : 0.65
920
+        instantMeetTitleLabel?.alphaValue = isPremium ? 1.0 : 0.75
921
+        instantMeetSubtitleLabel?.alphaValue = isPremium ? 1.0 : 0.75
922
+        instantMeetCardView?.toolTip = isPremium ? nil : "Premium required. Click to open paywall."
923
+        instantMeetCardView?.onHoverChanged?(false)
924
+
925
+        joinWithLinkCardView?.alphaValue = isPremium ? 1.0 : 0.65
926
+        joinWithLinkTitleLabel?.alphaValue = isPremium ? 1.0 : 0.75
927
+        meetLinkField?.isEditable = isPremium
928
+        meetLinkField?.isSelectable = isPremium
929
+        meetLinkField?.alphaValue = isPremium ? 1.0 : 0.75
930
+        // Keep button enabled so non-premium taps can still trigger paywall.
931
+        joinMeetPrimaryButton?.isEnabled = true
932
+        joinMeetPrimaryButton?.alphaValue = isPremium ? 1.0 : 0.80
933
+        joinWithLinkCardView?.toolTip = isPremium ? nil : "Premium required. Click to open paywall."
934
+    }
935
+
936
+    private func handlePremiumAccessChanged(_ hasPremiumAccess: Bool) {
937
+        let hadPremiumAccess = lastKnownPremiumAccess
938
+        lastKnownPremiumAccess = hasPremiumAccess
939
+        refreshPaywallStoreUI()
940
+        if hadPremiumAccess && !hasPremiumAccess {
941
+            showPaywall()
942
+        }
943
+    }
944
+
883 945
     private func refreshSidebarPremiumButton() {
884 946
         let isPremium = storeKitCoordinator.hasPremiumAccess
885 947
         if isPremium {
@@ -1476,17 +1538,37 @@ private extension ViewController {
1476 1538
         let baseColor = palette.sectionCard
1477 1539
         let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
1478 1540
         let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor
1479
-        instant.onHoverChanged = { hovering in
1480
-            instant.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
1541
+        instant.onHoverChanged = { [weak self] hovering in
1542
+            guard let self else { return }
1543
+            if self.storeKitCoordinator.hasPremiumAccess {
1544
+                instant.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
1545
+            } else {
1546
+                let disabledColor = self.palette.sectionCard.blended(withFraction: 0.22, of: self.palette.pageBackground) ?? self.palette.sectionCard
1547
+                instant.layer?.backgroundColor = disabledColor.cgColor
1548
+            }
1481 1549
         }
1482
-        codeCard.onHoverChanged = { hovering in
1483
-            codeCard.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
1550
+        codeCard.onHoverChanged = { [weak self] hovering in
1551
+            guard let self else { return }
1552
+            if self.storeKitCoordinator.hasPremiumAccess {
1553
+                codeCard.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
1554
+            } else {
1555
+                let disabledColor = self.palette.sectionCard.blended(withFraction: 0.22, of: self.palette.pageBackground) ?? self.palette.sectionCard
1556
+                codeCard.layer?.backgroundColor = disabledColor.cgColor
1557
+            }
1484 1558
         }
1485 1559
         instant.onHoverChanged?(false)
1486 1560
         codeCard.onHoverChanged?(false)
1487 1561
 
1488 1562
         let instantClick = NSClickGestureRecognizer(target: self, action: #selector(instantMeetClicked(_:)))
1489 1563
         instant.addGestureRecognizer(instantClick)
1564
+        let joinWithLinkClick = NSClickGestureRecognizer(target: self, action: #selector(joinWithLinkCardClicked(_:)))
1565
+        codeCard.addGestureRecognizer(joinWithLinkClick)
1566
+        instantMeetCardView = instant
1567
+        instantMeetTitleLabel = instantTitle
1568
+        instantMeetSubtitleLabel = instantSub
1569
+        joinWithLinkCardView = codeCard
1570
+        joinWithLinkTitleLabel = codeTitle
1571
+        refreshInstantMeetPremiumState()
1490 1572
 
1491 1573
         row.addArrangedSubview(instant)
1492 1574
         row.addArrangedSubview(codeCard)
@@ -1511,13 +1593,16 @@ private extension ViewController {
1511 1593
             width: 110,
1512 1594
             action: #selector(cancelMeetJoinClicked(_:))
1513 1595
         ))
1514
-        row.addArrangedSubview(meetActionButton(
1596
+        let joinButton = meetActionButton(
1515 1597
             title: "Join",
1516 1598
             color: palette.primaryBlue,
1517 1599
             textColor: .white,
1518 1600
             width: 116,
1519 1601
             action: #selector(joinMeetClicked(_:))
1520
-        ))
1602
+        )
1603
+        joinMeetPrimaryButton = joinButton
1604
+        row.addArrangedSubview(joinButton)
1605
+        refreshInstantMeetPremiumState()
1521 1606
         return row
1522 1607
     }
1523 1608
 
@@ -2552,10 +2637,43 @@ private extension ViewController {
2552 2637
             let base = self.palette.sectionCard
2553 2638
             let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
2554 2639
             let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
2555
-            card.layer?.backgroundColor = (hovering ? hover : base).cgColor
2640
+            if self.storeKitCoordinator.hasPremiumAccess {
2641
+                card.layer?.backgroundColor = (hovering ? hover : base).cgColor
2642
+            } else {
2643
+                card.layer?.backgroundColor = base.cgColor
2644
+            }
2556 2645
         }
2557 2646
         hit.onHoverChanged?(false)
2558 2647
 
2648
+        if !storeKitCoordinator.hasPremiumAccess {
2649
+            let lockOverlay = NSVisualEffectView()
2650
+            lockOverlay.translatesAutoresizingMaskIntoConstraints = false
2651
+            lockOverlay.material = darkModeEnabled ? .hudWindow : .popover
2652
+            lockOverlay.blendingMode = .withinWindow
2653
+            lockOverlay.state = .active
2654
+            lockOverlay.wantsLayer = true
2655
+            lockOverlay.layer?.cornerRadius = 12
2656
+            lockOverlay.layer?.masksToBounds = true
2657
+            lockOverlay.layer?.backgroundColor = NSColor.black.withAlphaComponent(darkModeEnabled ? 0.28 : 0.12).cgColor
2658
+
2659
+            let lockLabel = textLabel("Get Premium to see events", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .white)
2660
+            lockLabel.alignment = .center
2661
+
2662
+            card.addSubview(lockOverlay)
2663
+            lockOverlay.addSubview(lockLabel)
2664
+            NSLayoutConstraint.activate([
2665
+                lockOverlay.leadingAnchor.constraint(equalTo: card.leadingAnchor),
2666
+                lockOverlay.trailingAnchor.constraint(equalTo: card.trailingAnchor),
2667
+                lockOverlay.topAnchor.constraint(equalTo: card.topAnchor),
2668
+                lockOverlay.bottomAnchor.constraint(equalTo: card.bottomAnchor),
2669
+                lockLabel.centerXAnchor.constraint(equalTo: lockOverlay.centerXAnchor),
2670
+                lockLabel.centerYAnchor.constraint(equalTo: lockOverlay.centerYAnchor),
2671
+                lockLabel.leadingAnchor.constraint(greaterThanOrEqualTo: lockOverlay.leadingAnchor, constant: 10),
2672
+                lockLabel.trailingAnchor.constraint(lessThanOrEqualTo: lockOverlay.trailingAnchor, constant: -10)
2673
+            ])
2674
+            hit.toolTip = "Premium required. Click to open paywall."
2675
+        }
2676
+
2559 2677
         return hit
2560 2678
     }
2561 2679
 
@@ -3489,6 +3607,10 @@ private extension ViewController {
3489 3607
     }
3490 3608
 
3491 3609
     @objc func scheduleCardButtonPressed(_ sender: NSButton) {
3610
+        guard storeKitCoordinator.hasPremiumAccess else {
3611
+            showPaywall()
3612
+            return
3613
+        }
3492 3614
         guard let raw = sender.identifier?.rawValue,
3493 3615
               let url = URL(string: raw) else { return }
3494 3616
         openMeetingURL(url)