Просмотр исходного кода

Apply dark theme to the premium subscription paywall.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 недель назад: 2
Родитель
Сommit
c57ef41737
1 измененных файлов с 221 добавлено и 21 удалено
  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
 final class PremiumPlansWindowController: NSWindowController {
4
 final class PremiumPlansWindowController: NSWindowController {
5
     /// Matches `PremiumPlansViewController.Theme.pageStart` so the window backing fills sheet corners.
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
     init() {
8
     init() {
9
         let viewController = PremiumPlansViewController()
9
         let viewController = PremiumPlansViewController()
@@ -29,9 +29,10 @@ final class PremiumPlansWindowController: NSWindowController {
29
 
29
 
30
 private final class PremiumPlansViewController: NSViewController {
30
 private final class PremiumPlansViewController: NSViewController {
31
     private final class HoverPricingCardView: NSView {
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
         private var trackingAreaRef: NSTrackingArea?
34
         private var trackingAreaRef: NSTrackingArea?
35
+        private var isHovered = false
35
 
36
 
36
         init(baseBorderColor: NSColor, hoverBorderColor: NSColor) {
37
         init(baseBorderColor: NSColor, hoverBorderColor: NSColor) {
37
             self.baseBorderColor = baseBorderColor
38
             self.baseBorderColor = baseBorderColor
@@ -60,14 +61,22 @@ private final class PremiumPlansViewController: NSViewController {
60
 
61
 
61
         override func mouseEntered(with event: NSEvent) {
62
         override func mouseEntered(with event: NSEvent) {
62
             super.mouseEntered(with: event)
63
             super.mouseEntered(with: event)
64
+            isHovered = true
63
             applyHoverStyle(isHovered: true, animated: true)
65
             applyHoverStyle(isHovered: true, animated: true)
64
         }
66
         }
65
 
67
 
66
         override func mouseExited(with event: NSEvent) {
68
         override func mouseExited(with event: NSEvent) {
67
             super.mouseExited(with: event)
69
             super.mouseExited(with: event)
70
+            isHovered = false
68
             applyHoverStyle(isHovered: false, animated: true)
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
         private func applyHoverStyle(isHovered: Bool, animated: Bool) {
80
         private func applyHoverStyle(isHovered: Bool, animated: Bool) {
72
             guard let layer else { return }
81
             guard let layer else { return }
73
             let updates = {
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
         private func applyBaseStyle(hovered: Bool, animated: Bool = true) {
248
         private func applyBaseStyle(hovered: Bool, animated: Bool = true) {
236
             let updates = {
249
             let updates = {
237
                 if self.isPrimaryStyle {
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
         private func applyStyle(hovered: Bool, animated: Bool) {
352
         private func applyStyle(hovered: Bool, animated: Bool) {
336
             let updates = {
353
             let updates = {
337
                 self.layer?.backgroundColor = (hovered
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
                 self.layer?.borderColor = (hovered ? Theme.accent.withAlphaComponent(0.45) : Theme.divider).cgColor
357
                 self.layer?.borderColor = (hovered ? Theme.accent.withAlphaComponent(0.45) : Theme.divider).cgColor
341
                 self.layer?.borderWidth = hovered ? 1.5 : 1
358
                 self.layer?.borderWidth = hovered ? 1.5 : 1
342
                 self.contentTintColor = hovered ? Theme.accent : Theme.secondaryText
359
                 self.contentTintColor = hovered ? Theme.accent : Theme.secondaryText
@@ -374,20 +391,95 @@ private final class PremiumPlansViewController: NSViewController {
374
         let highlight: Bool
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
     private enum FeatureListMetrics {
485
     private enum FeatureListMetrics {
@@ -399,8 +491,9 @@ private final class PremiumPlansViewController: NSViewController {
399
     private var planPriceFields: [String: (price: NSTextField, period: NSTextField)] = [:]
491
     private var planPriceFields: [String: (price: NSTextField, period: NSTextField)] = [:]
400
     private var planPurchaseButtons: [String: NSButton] = [:]
492
     private var planPurchaseButtons: [String: NSButton] = [:]
401
     private var subscriptionPrimaryFooterButton: NSButton?
493
     private var subscriptionPrimaryFooterButton: NSButton?
402
-    private var premiumCloseButton: NSButton?
494
+    private var premiumCloseButton: PremiumCloseHoverButton?
403
     private var subscriptionStatusObservation: NSObjectProtocol?
495
     private var subscriptionStatusObservation: NSObjectProtocol?
496
+    private var appearanceObserver: NSObjectProtocol?
404
 
497
 
405
     private let plans: [Plan] = [
498
     private let plans: [Plan] = [
406
         Plan(
499
         Plan(
@@ -460,15 +553,30 @@ private final class PremiumPlansViewController: NSViewController {
460
     ]
553
     ]
461
 
554
 
462
     private let pageGradient = CAGradientLayer()
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
     deinit {
562
     deinit {
465
         if let subscriptionStatusObservation {
563
         if let subscriptionStatusObservation {
466
             NotificationCenter.default.removeObserver(subscriptionStatusObservation)
564
             NotificationCenter.default.removeObserver(subscriptionStatusObservation)
467
         }
565
         }
566
+        if let appearanceObserver {
567
+            NotificationCenter.default.removeObserver(appearanceObserver)
568
+        }
468
     }
569
     }
469
 
570
 
470
     override func viewDidLoad() {
571
     override func viewDidLoad() {
471
         super.viewDidLoad()
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
         subscriptionStatusObservation = NotificationCenter.default.addObserver(
580
         subscriptionStatusObservation = NotificationCenter.default.addObserver(
473
             forName: .subscriptionStatusDidChange,
581
             forName: .subscriptionStatusDidChange,
474
             object: nil,
582
             object: nil,
@@ -499,6 +607,7 @@ private final class PremiumPlansViewController: NSViewController {
499
         pageGradient.endPoint = CGPoint(x: 1, y: 0)
607
         pageGradient.endPoint = CGPoint(x: 1, y: 0)
500
         view.layer?.addSublayer(pageGradient)
608
         view.layer?.addSublayer(pageGradient)
501
         setupLayout()
609
         setupLayout()
610
+        applyCurrentAppearance()
502
     }
611
     }
503
 
612
 
504
     private func setupLayout() {
613
     private func setupLayout() {
@@ -514,12 +623,15 @@ private final class PremiumPlansViewController: NSViewController {
514
         title.font = .systemFont(ofSize: 40, weight: .semibold)
623
         title.font = .systemFont(ofSize: 40, weight: .semibold)
515
         title.textColor = Theme.primaryText
624
         title.textColor = Theme.primaryText
516
         title.alignment = .center
625
         title.alignment = .center
626
+        premiumTitleLabel = title
517
 
627
 
518
         let subtitle = NSTextField(labelWithString: "Unlock unlimited access to premium tools and boost your productivity.")
628
         let subtitle = NSTextField(labelWithString: "Unlock unlimited access to premium tools and boost your productivity.")
519
         subtitle.font = .systemFont(ofSize: 14, weight: .regular)
629
         subtitle.font = .systemFont(ofSize: 14, weight: .regular)
520
         subtitle.textColor = Theme.secondaryText
630
         subtitle.textColor = Theme.secondaryText
521
         subtitle.alignment = .center
631
         subtitle.alignment = .center
632
+        premiumSubtitleLabel = subtitle
522
 
633
 
634
+        pricingCardTargets = []
523
         let cardsRow = NSStackView(views: plans.map(makePricingCard(_:)))
635
         let cardsRow = NSStackView(views: plans.map(makePricingCard(_:)))
524
         cardsRow.orientation = .horizontal
636
         cardsRow.orientation = .horizontal
525
         cardsRow.spacing = 14
637
         cardsRow.spacing = 14
@@ -626,7 +738,8 @@ private final class PremiumPlansViewController: NSViewController {
626
         divider.translatesAutoresizingMaskIntoConstraints = false
738
         divider.translatesAutoresizingMaskIntoConstraints = false
627
         divider.borderColor = Theme.divider
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
         featuresStack.orientation = .vertical
743
         featuresStack.orientation = .vertical
631
         featuresStack.spacing = FeatureListMetrics.spacing
744
         featuresStack.spacing = FeatureListMetrics.spacing
632
         featuresStack.alignment = .leading
745
         featuresStack.alignment = .leading
@@ -643,6 +756,31 @@ private final class PremiumPlansViewController: NSViewController {
643
         planPriceFields[plan.id] = (priceLabel, periodLabel)
756
         planPriceFields[plan.id] = (priceLabel, periodLabel)
644
         selectButton.heightAnchor.constraint(equalToConstant: 50).isActive = true
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
         var contentViews: [NSView] = [iconWell, titleLabel, subtitleLabel, priceRow]
784
         var contentViews: [NSView] = [iconWell, titleLabel, subtitleLabel, priceRow]
647
         if !plan.billedLine.isEmpty {
785
         if !plan.billedLine.isEmpty {
648
             contentViews.append(billingLabel)
786
             contentViews.append(billingLabel)
@@ -755,10 +893,12 @@ private final class PremiumPlansViewController: NSViewController {
755
         badges.layer?.cornerRadius = 10
893
         badges.layer?.cornerRadius = 10
756
         badges.setHuggingPriority(.defaultLow, for: .horizontal)
894
         badges.setHuggingPriority(.defaultLow, for: .horizontal)
757
         badges.heightAnchor.constraint(equalToConstant: 72).isActive = true
895
         badges.heightAnchor.constraint(equalToConstant: 72).isActive = true
896
+        trustBadgesRow = badges
758
         return badges
897
         return badges
759
     }
898
     }
760
 
899
 
761
     private func makeFooterRow() -> NSView {
900
     private func makeFooterRow() -> NSView {
901
+        footerLinkButtons = []
762
         let primary = footerActionCell(
902
         let primary = footerActionCell(
763
             title: subscriptionPrimaryFooterTitle(),
903
             title: subscriptionPrimaryFooterTitle(),
764
             action: #selector(didTapPrimaryFooterSubscriptionAction),
904
             action: #selector(didTapPrimaryFooterSubscriptionAction),
@@ -797,6 +937,7 @@ private final class PremiumPlansViewController: NSViewController {
797
         button.contentTintColor = Theme.secondaryText
937
         button.contentTintColor = Theme.secondaryText
798
         button.focusRingType = .none
938
         button.focusRingType = .none
799
         button.translatesAutoresizingMaskIntoConstraints = false
939
         button.translatesAutoresizingMaskIntoConstraints = false
940
+        footerLinkButtons.append(button)
800
         container.addSubview(button)
941
         container.addSubview(button)
801
 
942
 
802
         var constraints: [NSLayoutConstraint] = [
943
         var constraints: [NSLayoutConstraint] = [
@@ -1045,4 +1186,63 @@ private final class PremiumPlansViewController: NSViewController {
1045
         }
1186
         }
1046
         window.close()
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
 }