Преглед изворни кода

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 пре 1 недеља
родитељ
комит
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 {
120
                 active.insert(transaction.productID)
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
         activeEntitlementProductIDs = active
132
         activeEntitlementProductIDs = active
124
     }
133
     }
125
 
134
 
@@ -231,6 +240,9 @@ final class ViewController: NSViewController {
231
     private weak var paywallOfferLabel: NSTextField?
240
     private weak var paywallOfferLabel: NSTextField?
232
     private weak var paywallContinueLabel: NSTextField?
241
     private weak var paywallContinueLabel: NSTextField?
233
     private weak var paywallContinueButton: NSView?
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
     private weak var meetLinkField: NSTextField?
246
     private weak var meetLinkField: NSTextField?
235
     private weak var browseAddressField: NSTextField?
247
     private weak var browseAddressField: NSTextField?
236
     private var inAppBrowserWindowController: InAppBrowserWindowController?
248
     private var inAppBrowserWindowController: InAppBrowserWindowController?
@@ -242,6 +254,9 @@ final class ViewController: NSViewController {
242
     private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
254
     private var paywallPriceLabels: [PremiumPlan: NSTextField] = [:]
243
     private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
255
     private var paywallSubtitleLabels: [PremiumPlan: NSTextField] = [:]
244
     private var paywallContinueEnabled = true
256
     private var paywallContinueEnabled = true
257
+    private var hasCompletedInitialStoreKitSync = false
258
+    private var hasPresentedLaunchPaywall = false
259
+    private var hasViewAppearedOnce = false
245
 
260
 
246
     private enum ScheduleFilter: Int {
261
     private enum ScheduleFilter: Int {
247
         case all = 0
262
         case all = 0
@@ -315,6 +330,8 @@ final class ViewController: NSViewController {
315
 
330
 
316
     override func viewDidAppear() {
331
     override func viewDidAppear() {
317
         super.viewDidAppear()
332
         super.viewDidAppear()
333
+        hasViewAppearedOnce = true
334
+        presentLaunchPaywallIfNeeded()
318
         applyWindowTitle(for: selectedSidebarPage)
335
         applyWindowTitle(for: selectedSidebarPage)
319
         guard let window = view.window else { return }
336
         guard let window = view.window else { return }
320
 
337
 
@@ -412,7 +429,11 @@ private extension ViewController {
412
     }
429
     }
413
 
430
 
414
     @objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
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
     @objc private func sidebarButtonClicked(_ sender: NSButton) {
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
     private func showSidebarPage(_ page: SidebarPage) {
602
     private func showSidebarPage(_ page: SidebarPage) {
574
         selectedSidebarPage = page
603
         selectedSidebarPage = page
575
         updateSidebarAppearance()
604
         updateSidebarAppearance()
@@ -827,7 +856,9 @@ private extension ViewController {
827
         storeKitStartupTask = Task { [weak self] in
856
         storeKitStartupTask = Task { [weak self] in
828
             guard let self else { return }
857
             guard let self else { return }
829
             await self.storeKitCoordinator.start()
858
             await self.storeKitCoordinator.start()
859
+            self.hasCompletedInitialStoreKitSync = true
830
             self.refreshPaywallStoreUI()
860
             self.refreshPaywallStoreUI()
861
+            self.presentLaunchPaywallIfNeeded()
831
         }
862
         }
832
     }
863
     }
833
 
864
 
@@ -844,10 +875,38 @@ private extension ViewController {
844
                   let period = product.subscription?.subscriptionPeriod else { continue }
875
                   let period = product.subscription?.subscriptionPeriod else { continue }
845
             label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
876
             label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
846
         }
877
         }
878
+        refreshSidebarPremiumButton()
847
         updatePaywallPlanSelection()
879
         updatePaywallPlanSelection()
848
         updatePaywallContinueState(isLoading: false)
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
     @objc private func paywallContinueClicked(_ sender: Any?) {
910
     @objc private func paywallContinueClicked(_ sender: Any?) {
852
         startSelectedPlanPurchase()
911
         startSelectedPlanPurchase()
853
     }
912
     }
@@ -1198,7 +1257,11 @@ private extension ViewController {
1198
         button.heightAnchor.constraint(equalToConstant: 34).isActive = true
1257
         button.heightAnchor.constraint(equalToConstant: 34).isActive = true
1199
         styleSurface(button, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: false)
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
         let title = textLabel("Get Premium", font: NSFont.systemFont(ofSize: 14, weight: .semibold), color: .white)
1265
         let title = textLabel("Get Premium", font: NSFont.systemFont(ofSize: 14, weight: .semibold), color: .white)
1203
         button.addSubview(icon)
1266
         button.addSubview(icon)
1204
         button.addSubview(title)
1267
         button.addSubview(title)
@@ -1206,18 +1269,45 @@ private extension ViewController {
1206
         NSLayoutConstraint.activate([
1269
         NSLayoutConstraint.activate([
1207
             icon.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 12),
1270
             icon.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 12),
1208
             icon.centerYAnchor.constraint(equalTo: button.centerYAnchor),
1271
             icon.centerYAnchor.constraint(equalTo: button.centerYAnchor),
1272
+            icon.widthAnchor.constraint(equalToConstant: 14),
1273
+            icon.heightAnchor.constraint(equalToConstant: 14),
1209
             title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 8),
1274
             title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 8),
1210
             title.centerYAnchor.constraint(equalTo: button.centerYAnchor),
1275
             title.centerYAnchor.constraint(equalTo: button.centerYAnchor),
1211
             title.trailingAnchor.constraint(lessThanOrEqualTo: button.trailingAnchor, constant: -12)
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
             button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
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
         button.onHoverChanged?(false)
1306
         button.onHoverChanged?(false)
1307
+        sidebarPremiumTitleLabel = title
1308
+        sidebarPremiumIconView = icon
1309
+        sidebarPremiumButtonView = button
1310
+        refreshSidebarPremiumButton()
1221
 
1311
 
1222
         let click = NSClickGestureRecognizer(target: self, action: #selector(premiumButtonClicked(_:)))
1312
         let click = NSClickGestureRecognizer(target: self, action: #selector(premiumButtonClicked(_:)))
1223
         button.addGestureRecognizer(click)
1313
         button.addGestureRecognizer(click)