|
|
@@ -188,6 +188,8 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
private let subtitleLabel = NSTextField(labelWithString: "")
|
|
|
private let priceLabel = NSTextField(labelWithString: "")
|
|
|
private var badgeView: PaywallBadgeView?
|
|
|
+ private var hoverTracker: HoverTracker?
|
|
|
+ private var isHovered = false
|
|
|
|
|
|
var isChosen: Bool = false {
|
|
|
didSet { updateAppearance() }
|
|
|
@@ -262,7 +264,12 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
])
|
|
|
}
|
|
|
|
|
|
+ applyCardShadow()
|
|
|
updateAppearance()
|
|
|
+
|
|
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
+ self?.setHovered(hovering)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@available(*, unavailable)
|
|
|
@@ -271,6 +278,15 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
func refreshAppearance() {
|
|
|
updateAppearance()
|
|
|
subtitleLabel.refreshThemeLabelColor()
|
|
|
+ if isHovered {
|
|
|
+ applyHoverLift(true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
+ isHovered = hovering
|
|
|
+ applyHoverLift(hovering)
|
|
|
+ updateAppearance()
|
|
|
}
|
|
|
|
|
|
private func updateAppearance() {
|
|
|
@@ -279,8 +295,19 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
priceLabel.textColor = titleColor
|
|
|
|
|
|
layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
|
|
|
- layer?.borderWidth = isChosen ? 2 : 1
|
|
|
- layer?.borderColor = (isChosen ? AppTheme.green : AppTheme.paywallBorder).cgColor
|
|
|
+
|
|
|
+ if isChosen {
|
|
|
+ layer?.borderWidth = 2
|
|
|
+ layer?.borderColor = AppTheme.green.cgColor
|
|
|
+ } else if isHovered {
|
|
|
+ layer?.borderWidth = 1.5
|
|
|
+ let hoverBorder = AppTheme.paywallBorder.blended(withFraction: 0.35, of: AppTheme.paywallAccent)
|
|
|
+ ?? AppTheme.paywallBorder
|
|
|
+ layer?.borderColor = hoverBorder.cgColor
|
|
|
+ } else {
|
|
|
+ layer?.borderWidth = 1
|
|
|
+ layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
|
@@ -296,6 +323,9 @@ private final class PaywallPlanCard: NSControl, AppearanceRefreshable {
|
|
|
// MARK: - Footer Link
|
|
|
|
|
|
private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
|
|
|
+ private var hoverTracker: HoverTracker?
|
|
|
+ private var isHovered = false
|
|
|
+
|
|
|
init(title: String) {
|
|
|
super.init(frame: .zero)
|
|
|
self.title = title
|
|
|
@@ -303,13 +333,18 @@ private final class PaywallFooterLink: NSButton, AppearanceRefreshable {
|
|
|
font = AppTheme.regularFont(size: 11)
|
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
|
refreshAppearance()
|
|
|
+
|
|
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
+ self?.isHovered = hovering
|
|
|
+ self?.refreshAppearance()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@available(*, unavailable)
|
|
|
required init?(coder: NSCoder) { nil }
|
|
|
|
|
|
func refreshAppearance() {
|
|
|
- contentTintColor = AppTheme.textSecondary
|
|
|
+ contentTintColor = isHovered ? AppTheme.textPrimary : AppTheme.textSecondary
|
|
|
}
|
|
|
|
|
|
override func resetCursorRects() {
|
|
|
@@ -392,6 +427,49 @@ private final class PaywallTrustItemView: NSView, AppearanceRefreshable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// MARK: - CTA Button
|
|
|
+
|
|
|
+private final class PaywallCTAButton: NSButton, AppearanceRefreshable {
|
|
|
+ private var hoverTracker: HoverTracker?
|
|
|
+
|
|
|
+ init() {
|
|
|
+ super.init(frame: .zero)
|
|
|
+ isBordered = false
|
|
|
+ wantsLayer = true
|
|
|
+ layer?.cornerRadius = 12
|
|
|
+ font = AppTheme.semiboldFont(size: 15)
|
|
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ refreshAppearance()
|
|
|
+
|
|
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
+ self?.setHovered(hovering)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @available(*, unavailable)
|
|
|
+ required init?(coder: NSCoder) { nil }
|
|
|
+
|
|
|
+ func refreshAppearance() {
|
|
|
+ layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
|
|
|
+ contentTintColor = AppTheme.paywallCTAForeground
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
+ let base = AppTheme.paywallCTABackground
|
|
|
+ let color = hovering ? base.blended(withFraction: 0.12, of: .black) ?? base : base
|
|
|
+ animateHover {
|
|
|
+ layer?.backgroundColor = color.cgColor
|
|
|
+ layer?.transform = hovering
|
|
|
+ ? CATransform3DMakeScale(1.02, 1.02, 1)
|
|
|
+ : CATransform3DIdentity
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override func resetCursorRects() {
|
|
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// MARK: - Main Paywall Card
|
|
|
|
|
|
final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
@@ -401,7 +479,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
|
|
|
private var selectedPlan: PaywallPlan = .yearly
|
|
|
private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
|
|
|
- private let ctaButton = NSButton()
|
|
|
+ private let ctaButton = PaywallCTAButton()
|
|
|
private var leftPanelTitle: NSTextField!
|
|
|
private var rightTitle: NSTextField!
|
|
|
private var rightSubtitle: NSTextField!
|
|
|
@@ -423,8 +501,7 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
rightSubtitle?.refreshThemeLabelColor()
|
|
|
trustStack?.layer?.backgroundColor = AppTheme.paywallTrustBackground.cgColor
|
|
|
trustStack?.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
- ctaButton.layer?.backgroundColor = AppTheme.paywallCTABackground.cgColor
|
|
|
- ctaButton.contentTintColor = AppTheme.paywallCTAForeground
|
|
|
+ ctaButton.refreshAppearance()
|
|
|
subviews.forEach { $0.refreshAppearanceRecursively() }
|
|
|
}
|
|
|
|
|
|
@@ -531,10 +608,6 @@ final class PaywallView: NSView, AppearanceRefreshable {
|
|
|
}
|
|
|
|
|
|
ctaButton.title = selectedPlan.ctaTitle
|
|
|
- ctaButton.isBordered = false
|
|
|
- ctaButton.wantsLayer = true
|
|
|
- ctaButton.layer?.cornerRadius = 12
|
|
|
- ctaButton.font = AppTheme.semiboldFont(size: 15)
|
|
|
ctaButton.target = self
|
|
|
ctaButton.action = #selector(purchaseTapped)
|
|
|
ctaButton.translatesAutoresizingMaskIntoConstraints = false
|