Quellcode durchsuchen

Apply dark theme to the premium subscription paywall.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 vor 2 Wochen
Ursprung
Commit
c57ef41737
1 geänderte Dateien mit 221 neuen und 21 gelöschten Zeilen
  1. 221 21
      App for Indeed/Controllers/PremiumPlansWindowController.swift

+ 221 - 21
App for Indeed/Controllers/PremiumPlansWindowController.swift

@@ -3,7 +3,7 @@ import StoreKit
3 3
 
4 4
 final class PremiumPlansWindowController: NSWindowController {
5 5
     /// Matches `PremiumPlansViewController.Theme.pageStart` so the window backing fills sheet corners.
6
-    static let paywallSheetBackground = NSColor(srgbRed: 249 / 255, green: 252 / 255, blue: 255 / 255, alpha: 1)
6
+    static var paywallSheetBackground: NSColor { PremiumPlansViewController.Theme.pageStart }
7 7
 
8 8
     init() {
9 9
         let viewController = PremiumPlansViewController()
@@ -29,9 +29,10 @@ final class PremiumPlansWindowController: NSWindowController {
29 29
 
30 30
 private final class PremiumPlansViewController: NSViewController {
31 31
     private final class HoverPricingCardView: NSView {
32
-        private let baseBorderColor: NSColor
33
-        private let hoverBorderColor: NSColor
32
+        private var baseBorderColor: NSColor
33
+        private var hoverBorderColor: NSColor
34 34
         private var trackingAreaRef: NSTrackingArea?
35
+        private var isHovered = false
35 36
 
36 37
         init(baseBorderColor: NSColor, hoverBorderColor: NSColor) {
37 38
             self.baseBorderColor = baseBorderColor
@@ -60,14 +61,22 @@ private final class PremiumPlansViewController: NSViewController {
60 61
 
61 62
         override func mouseEntered(with event: NSEvent) {
62 63
             super.mouseEntered(with: event)
64
+            isHovered = true
63 65
             applyHoverStyle(isHovered: true, animated: true)
64 66
         }
65 67
 
66 68
         override func mouseExited(with event: NSEvent) {
67 69
             super.mouseExited(with: event)
70
+            isHovered = false
68 71
             applyHoverStyle(isHovered: false, animated: true)
69 72
         }
70 73
 
74
+        func updateBorderColors(base: NSColor, hover: NSColor) {
75
+            baseBorderColor = base
76
+            hoverBorderColor = hover
77
+            applyHoverStyle(isHovered: isHovered, animated: false)
78
+        }
79
+
71 80
         private func applyHoverStyle(isHovered: Bool, animated: Bool) {
72 81
             guard let layer else { return }
73 82
             let updates = {
@@ -232,6 +241,10 @@ private final class PremiumPlansViewController: NSViewController {
232 241
             }
233 242
         }
234 243
 
244
+        func refreshAppearance() {
245
+            applyBaseStyle(hovered: false, animated: false)
246
+        }
247
+
235 248
         private func applyBaseStyle(hovered: Bool, animated: Bool = true) {
236 249
             let updates = {
237 250
                 if self.isPrimaryStyle {
@@ -332,11 +345,15 @@ private final class PremiumPlansViewController: NSViewController {
332 345
             }
333 346
         }
334 347
 
348
+        func refreshAppearance(hovered: Bool) {
349
+            applyStyle(hovered: hovered, animated: false)
350
+        }
351
+
335 352
         private func applyStyle(hovered: Bool, animated: Bool) {
336 353
             let updates = {
337 354
                 self.layer?.backgroundColor = (hovered
338
-                    ? NSColor.white.withAlphaComponent(0.98)
339
-                    : NSColor.white.withAlphaComponent(0.92)).cgColor
355
+                    ? Theme.closeButtonBackgroundHover
356
+                    : Theme.closeButtonBackground).cgColor
340 357
                 self.layer?.borderColor = (hovered ? Theme.accent.withAlphaComponent(0.45) : Theme.divider).cgColor
341 358
                 self.layer?.borderWidth = hovered ? 1.5 : 1
342 359
                 self.contentTintColor = hovered ? Theme.accent : Theme.secondaryText
@@ -374,20 +391,95 @@ private final class PremiumPlansViewController: NSViewController {
374 391
         let highlight: Bool
375 392
     }
376 393
 
377
-    private enum Theme {
378
-        static let pageStart = NSColor(srgbRed: 249 / 255, green: 252 / 255, blue: 255 / 255, alpha: 1)
379
-        static let pageEnd = NSColor(srgbRed: 238 / 255, green: 244 / 255, blue: 255 / 255, alpha: 1)
380
-        static let cardBackground = NSColor.white
381
-        static let primaryText = NSColor(srgbRed: 27 / 255, green: 38 / 255, blue: 79 / 255, alpha: 1)
382
-        static let secondaryText = NSColor(srgbRed: 108 / 255, green: 120 / 255, blue: 157 / 255, alpha: 1)
383
-        static let cardBorder = NSColor(srgbRed: 198 / 255, green: 216 / 255, blue: 255 / 255, alpha: 1)
384
-        static let accent = NSColor(srgbRed: 55 / 255, green: 128 / 255, blue: 255 / 255, alpha: 1)
385
-        static let accentHover = NSColor(srgbRed: 38 / 255, green: 108 / 255, blue: 232 / 255, alpha: 1)
386
-        static let mutedButtonFill = NSColor(srgbRed: 238 / 255, green: 243 / 255, blue: 252 / 255, alpha: 1)
387
-        static let bottomStrip = NSColor(srgbRed: 244 / 255, green: 248 / 255, blue: 255 / 255, alpha: 1)
388
-        static let divider = NSColor(srgbRed: 218 / 255, green: 228 / 255, blue: 247 / 255, alpha: 1)
389
-        static let successText = NSColor(srgbRed: 21 / 255, green: 154 / 255, blue: 220 / 255, alpha: 1)
390
-        static let iconTint = NSColor(srgbRed: 47 / 255, green: 136 / 255, blue: 255 / 255, alpha: 1)
394
+    fileprivate enum Theme {
395
+        static var pageStart: NSColor {
396
+            AppDashboardTheme.isDark
397
+                ? AppDashboardTheme.pageBackground
398
+                : NSColor(srgbRed: 249 / 255, green: 252 / 255, blue: 255 / 255, alpha: 1)
399
+        }
400
+
401
+        static var pageEnd: NSColor {
402
+            AppDashboardTheme.isDark
403
+                ? AppDashboardTheme.loadingPageBackgroundBottom
404
+                : NSColor(srgbRed: 238 / 255, green: 244 / 255, blue: 255 / 255, alpha: 1)
405
+        }
406
+
407
+        static var cardBackground: NSColor { AppDashboardTheme.cardBackground }
408
+
409
+        static var primaryText: NSColor {
410
+            AppDashboardTheme.isDark
411
+                ? AppDashboardTheme.primaryText
412
+                : NSColor(srgbRed: 27 / 255, green: 38 / 255, blue: 79 / 255, alpha: 1)
413
+        }
414
+
415
+        static var secondaryText: NSColor {
416
+            AppDashboardTheme.isDark
417
+                ? AppDashboardTheme.secondaryText
418
+                : NSColor(srgbRed: 108 / 255, green: 120 / 255, blue: 157 / 255, alpha: 1)
419
+        }
420
+
421
+        static var cardBorder: NSColor {
422
+            AppDashboardTheme.isDark
423
+                ? AppDashboardTheme.border
424
+                : NSColor(srgbRed: 198 / 255, green: 216 / 255, blue: 255 / 255, alpha: 1)
425
+        }
426
+
427
+        static var accent: NSColor { AppDashboardTheme.brandBlue }
428
+        static var accentHover: NSColor { AppDashboardTheme.brandBlueHover }
429
+
430
+        static var mutedButtonFill: NSColor {
431
+            AppDashboardTheme.isDark
432
+                ? AppDashboardTheme.profileFieldFill
433
+                : NSColor(srgbRed: 238 / 255, green: 243 / 255, blue: 252 / 255, alpha: 1)
434
+        }
435
+
436
+        static var bottomStrip: NSColor {
437
+            AppDashboardTheme.isDark
438
+                ? AppDashboardTheme.proCardFill
439
+                : NSColor(srgbRed: 244 / 255, green: 248 / 255, blue: 255 / 255, alpha: 1)
440
+        }
441
+
442
+        static var divider: NSColor {
443
+            AppDashboardTheme.isDark
444
+                ? AppDashboardTheme.border
445
+                : NSColor(srgbRed: 218 / 255, green: 228 / 255, blue: 247 / 255, alpha: 1)
446
+        }
447
+
448
+        static var successText: NSColor {
449
+            AppDashboardTheme.isDark
450
+                ? AppDashboardTheme.welcomeHeroHeadingBlue
451
+                : NSColor(srgbRed: 21 / 255, green: 154 / 255, blue: 220 / 255, alpha: 1)
452
+        }
453
+
454
+        static var iconTint: NSColor { AppDashboardTheme.brandBlue }
455
+
456
+        static var closeButtonBackground: NSColor {
457
+            AppDashboardTheme.isDark
458
+                ? AppDashboardTheme.cardBackground
459
+                : NSColor.white.withAlphaComponent(0.92)
460
+        }
461
+
462
+        static var closeButtonBackgroundHover: NSColor {
463
+            AppDashboardTheme.isDark
464
+                ? AppDashboardTheme.neutralHoverFill
465
+                : NSColor.white.withAlphaComponent(0.98)
466
+        }
467
+    }
468
+
469
+    private struct PricingCardAppearanceTarget {
470
+        let card: HoverPricingCardView
471
+        let iconWell: NSView
472
+        let planIconView: NSImageView
473
+        let planId: String
474
+        let titleLabel: NSTextField
475
+        let subtitleLabel: NSTextField
476
+        let priceLabel: NSTextField
477
+        let periodLabel: NSTextField
478
+        let billingLabel: NSTextField?
479
+        let divider: NSBox
480
+        let featureLabels: [NSTextField]
481
+        let featureIcons: [NSImageView]
482
+        let purchaseButton: PlanPurchaseHoverButton
391 483
     }
392 484
 
393 485
     private enum FeatureListMetrics {
@@ -399,8 +491,9 @@ private final class PremiumPlansViewController: NSViewController {
399 491
     private var planPriceFields: [String: (price: NSTextField, period: NSTextField)] = [:]
400 492
     private var planPurchaseButtons: [String: NSButton] = [:]
401 493
     private var subscriptionPrimaryFooterButton: NSButton?
402
-    private var premiumCloseButton: NSButton?
494
+    private var premiumCloseButton: PremiumCloseHoverButton?
403 495
     private var subscriptionStatusObservation: NSObjectProtocol?
496
+    private var appearanceObserver: NSObjectProtocol?
404 497
 
405 498
     private let plans: [Plan] = [
406 499
         Plan(
@@ -460,15 +553,30 @@ private final class PremiumPlansViewController: NSViewController {
460 553
     ]
461 554
 
462 555
     private let pageGradient = CAGradientLayer()
556
+    private var premiumTitleLabel: NSTextField?
557
+    private var premiumSubtitleLabel: NSTextField?
558
+    private var pricingCardTargets: [PricingCardAppearanceTarget] = []
559
+    private weak var trustBadgesRow: NSStackView?
560
+    private var footerLinkButtons: [FooterLinkButton] = []
463 561
 
464 562
     deinit {
465 563
         if let subscriptionStatusObservation {
466 564
             NotificationCenter.default.removeObserver(subscriptionStatusObservation)
467 565
         }
566
+        if let appearanceObserver {
567
+            NotificationCenter.default.removeObserver(appearanceObserver)
568
+        }
468 569
     }
469 570
 
470 571
     override func viewDidLoad() {
471 572
         super.viewDidLoad()
573
+        appearanceObserver = NotificationCenter.default.addObserver(
574
+            forName: AppAppearanceManager.didChangeNotification,
575
+            object: nil,
576
+            queue: .main
577
+        ) { [weak self] _ in
578
+            self?.applyCurrentAppearance()
579
+        }
472 580
         subscriptionStatusObservation = NotificationCenter.default.addObserver(
473 581
             forName: .subscriptionStatusDidChange,
474 582
             object: nil,
@@ -499,6 +607,7 @@ private final class PremiumPlansViewController: NSViewController {
499 607
         pageGradient.endPoint = CGPoint(x: 1, y: 0)
500 608
         view.layer?.addSublayer(pageGradient)
501 609
         setupLayout()
610
+        applyCurrentAppearance()
502 611
     }
503 612
 
504 613
     private func setupLayout() {
@@ -514,12 +623,15 @@ private final class PremiumPlansViewController: NSViewController {
514 623
         title.font = .systemFont(ofSize: 40, weight: .semibold)
515 624
         title.textColor = Theme.primaryText
516 625
         title.alignment = .center
626
+        premiumTitleLabel = title
517 627
 
518 628
         let subtitle = NSTextField(labelWithString: "Unlock unlimited access to premium tools and boost your productivity.")
519 629
         subtitle.font = .systemFont(ofSize: 14, weight: .regular)
520 630
         subtitle.textColor = Theme.secondaryText
521 631
         subtitle.alignment = .center
632
+        premiumSubtitleLabel = subtitle
522 633
 
634
+        pricingCardTargets = []
523 635
         let cardsRow = NSStackView(views: plans.map(makePricingCard(_:)))
524 636
         cardsRow.orientation = .horizontal
525 637
         cardsRow.spacing = 14
@@ -626,7 +738,8 @@ private final class PremiumPlansViewController: NSViewController {
626 738
         divider.translatesAutoresizingMaskIntoConstraints = false
627 739
         divider.borderColor = Theme.divider
628 740
 
629
-        let featuresStack = NSStackView(views: plan.features.map(makeFeatureRow(_:)))
741
+        let featureRows = plan.features.map(makeFeatureRow(_:))
742
+        let featuresStack = NSStackView(views: featureRows)
630 743
         featuresStack.orientation = .vertical
631 744
         featuresStack.spacing = FeatureListMetrics.spacing
632 745
         featuresStack.alignment = .leading
@@ -643,6 +756,31 @@ private final class PremiumPlansViewController: NSViewController {
643 756
         planPriceFields[plan.id] = (priceLabel, periodLabel)
644 757
         selectButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
645 758
 
759
+        let featureLabels = featureRows.compactMap { row -> NSTextField? in
760
+            (row as? NSStackView)?.arrangedSubviews.compactMap { $0 as? NSTextField }.first
761
+        }
762
+        let featureIcons = featureRows.compactMap { row -> NSImageView? in
763
+            (row as? NSStackView)?.arrangedSubviews.compactMap { $0 as? NSImageView }.first
764
+        }
765
+
766
+        pricingCardTargets.append(
767
+            PricingCardAppearanceTarget(
768
+                card: card,
769
+                iconWell: iconWell,
770
+                planIconView: icon,
771
+                planId: plan.id,
772
+                titleLabel: titleLabel,
773
+                subtitleLabel: subtitleLabel,
774
+                priceLabel: priceLabel,
775
+                periodLabel: periodLabel,
776
+                billingLabel: plan.billedLine.isEmpty ? nil : billingLabel,
777
+                divider: divider,
778
+                featureLabels: featureLabels,
779
+                featureIcons: featureIcons,
780
+                purchaseButton: selectButton
781
+            )
782
+        )
783
+
646 784
         var contentViews: [NSView] = [iconWell, titleLabel, subtitleLabel, priceRow]
647 785
         if !plan.billedLine.isEmpty {
648 786
             contentViews.append(billingLabel)
@@ -755,10 +893,12 @@ private final class PremiumPlansViewController: NSViewController {
755 893
         badges.layer?.cornerRadius = 10
756 894
         badges.setHuggingPriority(.defaultLow, for: .horizontal)
757 895
         badges.heightAnchor.constraint(equalToConstant: 72).isActive = true
896
+        trustBadgesRow = badges
758 897
         return badges
759 898
     }
760 899
 
761 900
     private func makeFooterRow() -> NSView {
901
+        footerLinkButtons = []
762 902
         let primary = footerActionCell(
763 903
             title: subscriptionPrimaryFooterTitle(),
764 904
             action: #selector(didTapPrimaryFooterSubscriptionAction),
@@ -797,6 +937,7 @@ private final class PremiumPlansViewController: NSViewController {
797 937
         button.contentTintColor = Theme.secondaryText
798 938
         button.focusRingType = .none
799 939
         button.translatesAutoresizingMaskIntoConstraints = false
940
+        footerLinkButtons.append(button)
800 941
         container.addSubview(button)
801 942
 
802 943
         var constraints: [NSLayoutConstraint] = [
@@ -1045,4 +1186,63 @@ private final class PremiumPlansViewController: NSViewController {
1045 1186
         }
1046 1187
         window.close()
1047 1188
     }
1189
+
1190
+    private func applyCurrentAppearance() {
1191
+        view.window?.backgroundColor = PremiumPlansWindowController.paywallSheetBackground
1192
+        pageGradient.colors = [Theme.pageStart.cgColor, Theme.pageEnd.cgColor]
1193
+
1194
+        premiumTitleLabel?.textColor = Theme.primaryText
1195
+        premiumSubtitleLabel?.textColor = Theme.secondaryText
1196
+
1197
+        for target in pricingCardTargets {
1198
+            target.card.updateBorderColors(base: Theme.cardBorder, hover: Theme.accent)
1199
+            target.card.layer?.backgroundColor = Theme.cardBackground.cgColor
1200
+            target.iconWell.layer?.backgroundColor = Theme.bottomStrip.cgColor
1201
+            target.planIconView.contentTintColor = planIconTint(planId: target.planId)
1202
+            target.titleLabel.textColor = Theme.primaryText
1203
+            target.subtitleLabel.textColor = Theme.secondaryText
1204
+            target.priceLabel.textColor = Theme.primaryText
1205
+            target.periodLabel.textColor = Theme.secondaryText
1206
+            target.billingLabel?.textColor = Theme.secondaryText
1207
+            target.divider.borderColor = Theme.divider
1208
+            for label in target.featureLabels {
1209
+                label.textColor = Theme.primaryText
1210
+            }
1211
+            for icon in target.featureIcons {
1212
+                icon.contentTintColor = Theme.iconTint
1213
+            }
1214
+            target.purchaseButton.refreshAppearance()
1215
+        }
1216
+
1217
+        if let trustBadgesRow {
1218
+            trustBadgesRow.layer?.backgroundColor = Theme.bottomStrip.cgColor
1219
+            trustBadgesRow.layer?.borderColor = Theme.divider.cgColor
1220
+            for case let badge as NSStackView in trustBadgesRow.arrangedSubviews {
1221
+                for case let image as NSImageView in badge.arrangedSubviews {
1222
+                    image.contentTintColor = Theme.primaryText
1223
+                }
1224
+                for case let textStack as NSStackView in badge.arrangedSubviews {
1225
+                    let labels = textStack.arrangedSubviews.compactMap { $0 as? NSTextField }
1226
+                    if labels.count >= 2 {
1227
+                        labels[0].textColor = Theme.primaryText
1228
+                        labels[1].textColor = Theme.secondaryText
1229
+                    }
1230
+                }
1231
+            }
1232
+        }
1233
+
1234
+        for button in footerLinkButtons {
1235
+            button.contentTintColor = Theme.secondaryText
1236
+        }
1237
+
1238
+        premiumCloseButton?.refreshAppearance(hovered: false)
1239
+    }
1240
+
1241
+    private func planIconTint(planId: String) -> NSColor {
1242
+        switch planId {
1243
+        case "monthly": Theme.accent
1244
+        case "yearly": Theme.successText
1245
+        default: Theme.iconTint
1246
+        }
1247
+    }
1048 1248
 }