浏览代码

Improve premium CTA behavior and launch entitlement handling.

Show Manage Subscription for active premium users, open Apple subscription management directly, and keep launch paywall/CTA state in sync with StoreKit entitlement refresh.

Made-with: Cursor
huzaifahayat12 3 月之前
父节点
当前提交
76f157bef6
共有 1 个文件被更改,包括 96 次插入6 次删除
  1. 96 6
      meetings_app/ViewController.swift

+ 96 - 6
meetings_app/ViewController.swift

@@ -120,6 +120,15 @@ private final class StoreKitCoordinator {
                 active.insert(transaction.productID)
             }
         }
+        // Some StoreKit test timelines can briefly report empty current entitlements
+        // even though a latest verified transaction exists for a non-consumable.
+        // Merge in latest transactions to keep launch access state accurate.
+        for productID in PremiumStoreProduct.allIDs {
+            guard let latest = await Transaction.latest(for: productID),
+                  case .verified(let transaction) = latest,
+                  Self.isTransactionActive(transaction) else { continue }
+            active.insert(productID)
+        }
         activeEntitlementProductIDs = active
     }
 
@@ -231,6 +240,9 @@ final class ViewController: NSViewController {
     private weak var paywallOfferLabel: NSTextField?
     private weak var paywallContinueLabel: NSTextField?
     private weak var paywallContinueButton: NSView?
+    private weak var sidebarPremiumTitleLabel: NSTextField?
+    private weak var sidebarPremiumIconView: NSImageView?
+    private weak var sidebarPremiumButtonView: HoverTrackingView?
     private weak var meetLinkField: NSTextField?
     private weak var browseAddressField: NSTextField?
     private var inAppBrowserWindowController: InAppBrowserWindowController?
@@ -242,6 +254,9 @@ final class ViewController: NSViewController {
     private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
     private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
     private var paywallContinueEnabled = true
+    private var hasCompletedInitialStoreKitSync = false
+    private var hasPresentedLaunchPaywall = false
+    private var hasViewAppearedOnce = false
 
     private enum ScheduleFilter: Int {
         case all = 0
@@ -315,6 +330,8 @@ final class ViewController: NSViewController {
 
     override func viewDidAppear() {
         super.viewDidAppear()
+        hasViewAppearedOnce = true
+        presentLaunchPaywallIfNeeded()
         applyWindowTitle(for: selectedSidebarPage)
         guard let window = view.window else { return }
 
@@ -412,7 +429,11 @@ private extension ViewController {
     }
 
     @objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
-        showPaywall()
+        if storeKitCoordinator.hasPremiumAccess {
+            openManageSubscriptions()
+        } else {
+            showPaywall()
+        }
     }
 
     @objc private func sidebarButtonClicked(_ sender: NSButton) {
@@ -570,6 +591,14 @@ private extension ViewController {
         }
     }
 
+    private func openManageSubscriptions() {
+        guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else {
+            showSimpleAlert(title: "Unable to Open Subscriptions", message: "The subscriptions URL is invalid.")
+            return
+        }
+        openInDefaultBrowser(url: url)
+    }
+
     private func showSidebarPage(_ page: SidebarPage) {
         selectedSidebarPage = page
         updateSidebarAppearance()
@@ -827,7 +856,9 @@ private extension ViewController {
         storeKitStartupTask = Task { [weak self] in
             guard let self else { return }
             await self.storeKitCoordinator.start()
+            self.hasCompletedInitialStoreKitSync = true
             self.refreshPaywallStoreUI()
+            self.presentLaunchPaywallIfNeeded()
         }
     }
 
@@ -844,10 +875,38 @@ private extension ViewController {
                   let period = product.subscription?.subscriptionPeriod else { continue }
             label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
         }
+        refreshSidebarPremiumButton()
         updatePaywallPlanSelection()
         updatePaywallContinueState(isLoading: false)
     }
 
+    private func refreshSidebarPremiumButton() {
+        let isPremium = storeKitCoordinator.hasPremiumAccess
+        if isPremium {
+            sidebarPremiumTitleLabel?.stringValue = "Manage Subscription"
+            sidebarPremiumIconView?.image = premiumButtonSymbolImage(named: "crown.fill")
+        } else {
+            sidebarPremiumTitleLabel?.stringValue = "Get Premium"
+            sidebarPremiumIconView?.image = premiumButtonSymbolImage(named: "star.fill")
+        }
+        sidebarPremiumIconView?.contentTintColor = .white
+        sidebarPremiumButtonView?.onHoverChanged?(false)
+    }
+
+    private func premiumButtonSymbolImage(named symbolName: String) -> NSImage? {
+        let configuration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
+        return NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)?
+            .withSymbolConfiguration(configuration)
+    }
+
+    private func presentLaunchPaywallIfNeeded() {
+        guard hasCompletedInitialStoreKitSync, hasViewAppearedOnce, !hasPresentedLaunchPaywall else { return }
+        hasPresentedLaunchPaywall = true
+        if !storeKitCoordinator.hasPremiumAccess {
+            showPaywall()
+        }
+    }
+
     @objc private func paywallContinueClicked(_ sender: Any?) {
         startSelectedPlanPurchase()
     }
@@ -1198,7 +1257,11 @@ private extension ViewController {
         button.heightAnchor.constraint(equalToConstant: 34).isActive = true
         styleSurface(button, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: false)
 
-        let icon = textLabel("★", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .white)
+        let icon = NSImageView()
+        icon.translatesAutoresizingMaskIntoConstraints = false
+        icon.imageScaling = .scaleProportionallyUpOrDown
+        icon.contentTintColor = .white
+        icon.image = premiumButtonSymbolImage(named: "star.fill")
         let title = textLabel("Get Premium", font: NSFont.systemFont(ofSize: 14, weight: .semibold), color: .white)
         button.addSubview(icon)
         button.addSubview(title)
@@ -1206,18 +1269,45 @@ private extension ViewController {
         NSLayoutConstraint.activate([
             icon.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 12),
             icon.centerYAnchor.constraint(equalTo: button.centerYAnchor),
+            icon.widthAnchor.constraint(equalToConstant: 14),
+            icon.heightAnchor.constraint(equalToConstant: 14),
             title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 8),
             title.centerYAnchor.constraint(equalTo: button.centerYAnchor),
             title.trailingAnchor.constraint(lessThanOrEqualTo: button.trailingAnchor, constant: -12)
         ])
 
-        let baseColor = palette.primaryBlue
-        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
-        let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor
-        button.onHoverChanged = { hovering in
+        button.onHoverChanged = { [weak self, weak button] hovering in
+            guard let self, let button else { return }
+            let isPremium = self.storeKitCoordinator.hasPremiumAccess
+            let baseColor = isPremium
+                ? NSColor(calibratedRed: 0.93, green: 0.73, blue: 0.16, alpha: 1.0)
+                : self.palette.primaryBlue
+            let borderColor = isPremium
+                ? NSColor(calibratedRed: 0.78, green: 0.56, blue: 0.10, alpha: 1.0)
+                : self.palette.primaryBlueBorder
+            let hoverColor: NSColor
+            let hoverBorderColor: NSColor
+            if isPremium {
+                // Darker rich-gold hover for stronger premium feedback.
+                hoverColor = NSColor(calibratedRed: 0.84, green: 0.62, blue: 0.11, alpha: 1.0)
+                hoverBorderColor = NSColor(calibratedRed: 0.67, green: 0.46, blue: 0.07, alpha: 1.0)
+            } else {
+                let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
+                hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor
+                hoverBorderColor = borderColor
+            }
             button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
+            button.layer?.shadowColor = NSColor.black.cgColor
+            button.layer?.shadowOpacity = hovering ? (isPremium ? 0.30 : 0.20) : 0.14
+            button.layer?.shadowOffset = CGSize(width: 0, height: -1)
+            button.layer?.shadowRadius = hovering ? (isPremium ? 8 : 6) : 4
+            self.styleSurface(button, borderColor: (hovering ? hoverBorderColor : borderColor), borderWidth: hovering ? 1.5 : 1, shadow: false)
         }
         button.onHoverChanged?(false)
+        sidebarPremiumTitleLabel = title
+        sidebarPremiumIconView = icon
+        sidebarPremiumButtonView = button
+        refreshSidebarPremiumButton()
 
         let click = NSClickGestureRecognizer(target: self, action: #selector(premiumButtonClicked(_:)))
         button.addGestureRecognizer(click)