Parcourir la Source

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 il y a 1 semaine
Parent
commit
76f157bef6
1 fichiers modifiés avec 96 ajouts et 6 suppressions
  1. 96 6
      meetings_app/ViewController.swift

+ 96 - 6
meetings_app/ViewController.swift

@@ -120,6 +120,15 @@ private final class StoreKitCoordinator {
120 120
                 active.insert(transaction.productID)
121 121
             }
122 122
         }
123
+        // Some StoreKit test timelines can briefly report empty current entitlements
124
+        // even though a latest verified transaction exists for a non-consumable.
125
+        // Merge in latest transactions to keep launch access state accurate.
126
+        for productID in PremiumStoreProduct.allIDs {
127
+            guard let latest = await Transaction.latest(for: productID),
128
+                  case .verified(let transaction) = latest,
129
+                  Self.isTransactionActive(transaction) else { continue }
130
+            active.insert(productID)
131
+        }
123 132
         activeEntitlementProductIDs = active
124 133
     }
125 134
 
@@ -231,6 +240,9 @@ final class ViewController: NSViewController {
231 240
     private weak var paywallOfferLabel: NSTextField?
232 241
     private weak var paywallContinueLabel: NSTextField?
233 242
     private weak var paywallContinueButton: NSView?
243
+    private weak var sidebarPremiumTitleLabel: NSTextField?
244
+    private weak var sidebarPremiumIconView: NSImageView?
245
+    private weak var sidebarPremiumButtonView: HoverTrackingView?
234 246
     private weak var meetLinkField: NSTextField?
235 247
     private weak var browseAddressField: NSTextField?
236 248
     private var inAppBrowserWindowController: InAppBrowserWindowController?
@@ -242,6 +254,9 @@ final class ViewController: NSViewController {
242 254
     private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
243 255
     private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
244 256
     private var paywallContinueEnabled = true
257
+    private var hasCompletedInitialStoreKitSync = false
258
+    private var hasPresentedLaunchPaywall = false
259
+    private var hasViewAppearedOnce = false
245 260
 
246 261
     private enum ScheduleFilter: Int {
247 262
         case all = 0
@@ -315,6 +330,8 @@ final class ViewController: NSViewController {
315 330
 
316 331
     override func viewDidAppear() {
317 332
         super.viewDidAppear()
333
+        hasViewAppearedOnce = true
334
+        presentLaunchPaywallIfNeeded()
318 335
         applyWindowTitle(for: selectedSidebarPage)
319 336
         guard let window = view.window else { return }
320 337
 
@@ -412,7 +429,11 @@ private extension ViewController {
412 429
     }
413 430
 
414 431
     @objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
415
-        showPaywall()
432
+        if storeKitCoordinator.hasPremiumAccess {
433
+            openManageSubscriptions()
434
+        } else {
435
+            showPaywall()
436
+        }
416 437
     }
417 438
 
418 439
     @objc private func sidebarButtonClicked(_ sender: NSButton) {
@@ -570,6 +591,14 @@ private extension ViewController {
570 591
         }
571 592
     }
572 593
 
594
+    private func openManageSubscriptions() {
595
+        guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else {
596
+            showSimpleAlert(title: "Unable to Open Subscriptions", message: "The subscriptions URL is invalid.")
597
+            return
598
+        }
599
+        openInDefaultBrowser(url: url)
600
+    }
601
+
573 602
     private func showSidebarPage(_ page: SidebarPage) {
574 603
         selectedSidebarPage = page
575 604
         updateSidebarAppearance()
@@ -827,7 +856,9 @@ private extension ViewController {
827 856
         storeKitStartupTask = Task { [weak self] in
828 857
             guard let self else { return }
829 858
             await self.storeKitCoordinator.start()
859
+            self.hasCompletedInitialStoreKitSync = true
830 860
             self.refreshPaywallStoreUI()
861
+            self.presentLaunchPaywallIfNeeded()
831 862
         }
832 863
     }
833 864
 
@@ -844,10 +875,38 @@ private extension ViewController {
844 875
                   let period = product.subscription?.subscriptionPeriod else { continue }
845 876
             label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
846 877
         }
878
+        refreshSidebarPremiumButton()
847 879
         updatePaywallPlanSelection()
848 880
         updatePaywallContinueState(isLoading: false)
849 881
     }
850 882
 
883
+    private func refreshSidebarPremiumButton() {
884
+        let isPremium = storeKitCoordinator.hasPremiumAccess
885
+        if isPremium {
886
+            sidebarPremiumTitleLabel?.stringValue = "Manage Subscription"
887
+            sidebarPremiumIconView?.image = premiumButtonSymbolImage(named: "crown.fill")
888
+        } else {
889
+            sidebarPremiumTitleLabel?.stringValue = "Get Premium"
890
+            sidebarPremiumIconView?.image = premiumButtonSymbolImage(named: "star.fill")
891
+        }
892
+        sidebarPremiumIconView?.contentTintColor = .white
893
+        sidebarPremiumButtonView?.onHoverChanged?(false)
894
+    }
895
+
896
+    private func premiumButtonSymbolImage(named symbolName: String) -> NSImage? {
897
+        let configuration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
898
+        return NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)?
899
+            .withSymbolConfiguration(configuration)
900
+    }
901
+
902
+    private func presentLaunchPaywallIfNeeded() {
903
+        guard hasCompletedInitialStoreKitSync, hasViewAppearedOnce, !hasPresentedLaunchPaywall else { return }
904
+        hasPresentedLaunchPaywall = true
905
+        if !storeKitCoordinator.hasPremiumAccess {
906
+            showPaywall()
907
+        }
908
+    }
909
+
851 910
     @objc private func paywallContinueClicked(_ sender: Any?) {
852 911
         startSelectedPlanPurchase()
853 912
     }
@@ -1198,7 +1257,11 @@ private extension ViewController {
1198 1257
         button.heightAnchor.constraint(equalToConstant: 34).isActive = true
1199 1258
         styleSurface(button, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: false)
1200 1259
 
1201
-        let icon = textLabel("★", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .white)
1260
+        let icon = NSImageView()
1261
+        icon.translatesAutoresizingMaskIntoConstraints = false
1262
+        icon.imageScaling = .scaleProportionallyUpOrDown
1263
+        icon.contentTintColor = .white
1264
+        icon.image = premiumButtonSymbolImage(named: "star.fill")
1202 1265
         let title = textLabel("Get Premium", font: NSFont.systemFont(ofSize: 14, weight: .semibold), color: .white)
1203 1266
         button.addSubview(icon)
1204 1267
         button.addSubview(title)
@@ -1206,18 +1269,45 @@ private extension ViewController {
1206 1269
         NSLayoutConstraint.activate([
1207 1270
             icon.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 12),
1208 1271
             icon.centerYAnchor.constraint(equalTo: button.centerYAnchor),
1272
+            icon.widthAnchor.constraint(equalToConstant: 14),
1273
+            icon.heightAnchor.constraint(equalToConstant: 14),
1209 1274
             title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 8),
1210 1275
             title.centerYAnchor.constraint(equalTo: button.centerYAnchor),
1211 1276
             title.trailingAnchor.constraint(lessThanOrEqualTo: button.trailingAnchor, constant: -12)
1212 1277
         ])
1213 1278
 
1214
-        let baseColor = palette.primaryBlue
1215
-        let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
1216
-        let hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor
1217
-        button.onHoverChanged = { hovering in
1279
+        button.onHoverChanged = { [weak self, weak button] hovering in
1280
+            guard let self, let button else { return }
1281
+            let isPremium = self.storeKitCoordinator.hasPremiumAccess
1282
+            let baseColor = isPremium
1283
+                ? NSColor(calibratedRed: 0.93, green: 0.73, blue: 0.16, alpha: 1.0)
1284
+                : self.palette.primaryBlue
1285
+            let borderColor = isPremium
1286
+                ? NSColor(calibratedRed: 0.78, green: 0.56, blue: 0.10, alpha: 1.0)
1287
+                : self.palette.primaryBlueBorder
1288
+            let hoverColor: NSColor
1289
+            let hoverBorderColor: NSColor
1290
+            if isPremium {
1291
+                // Darker rich-gold hover for stronger premium feedback.
1292
+                hoverColor = NSColor(calibratedRed: 0.84, green: 0.62, blue: 0.11, alpha: 1.0)
1293
+                hoverBorderColor = NSColor(calibratedRed: 0.67, green: 0.46, blue: 0.07, alpha: 1.0)
1294
+            } else {
1295
+                let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
1296
+                hoverColor = baseColor.blended(withFraction: 0.10, of: hoverBlend) ?? baseColor
1297
+                hoverBorderColor = borderColor
1298
+            }
1218 1299
             button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
1300
+            button.layer?.shadowColor = NSColor.black.cgColor
1301
+            button.layer?.shadowOpacity = hovering ? (isPremium ? 0.30 : 0.20) : 0.14
1302
+            button.layer?.shadowOffset = CGSize(width: 0, height: -1)
1303
+            button.layer?.shadowRadius = hovering ? (isPremium ? 8 : 6) : 4
1304
+            self.styleSurface(button, borderColor: (hovering ? hoverBorderColor : borderColor), borderWidth: hovering ? 1.5 : 1, shadow: false)
1219 1305
         }
1220 1306
         button.onHoverChanged?(false)
1307
+        sidebarPremiumTitleLabel = title
1308
+        sidebarPremiumIconView = icon
1309
+        sidebarPremiumButtonView = button
1310
+        refreshSidebarPremiumButton()
1221 1311
 
1222 1312
         let click = NSClickGestureRecognizer(target: self, action: #selector(premiumButtonClicked(_:)))
1223 1313
         button.addGestureRecognizer(click)