|
|
@@ -259,6 +259,8 @@ class ViewController: NSViewController {
|
|
|
private weak var paywallOverlayView: NSView?
|
|
|
private weak var paywallPanelView: NSView?
|
|
|
private weak var paywallBodyStack: NSStackView?
|
|
|
+ private weak var paywallPlansRowWide: NSStackView?
|
|
|
+ private weak var paywallPlansGridNarrow: NSStackView?
|
|
|
private var paywallIsDesktopLayout = false
|
|
|
private var paywallPanelWidthConstraint: NSLayoutConstraint?
|
|
|
private var paywallPanelHeightConstraint: NSLayoutConstraint?
|
|
|
@@ -608,7 +610,6 @@ class ViewController: NSViewController {
|
|
|
super.viewDidLayout()
|
|
|
alignNativeTrafficLights()
|
|
|
if paywallOverlayView != nil {
|
|
|
- updatePaywallPanelSize()
|
|
|
updatePaywallResponsiveLayout()
|
|
|
}
|
|
|
}
|
|
|
@@ -3544,34 +3545,16 @@ class ViewController: NSViewController {
|
|
|
overlay.translatesAutoresizingMaskIntoConstraints = false
|
|
|
overlay.wantsLayer = true
|
|
|
overlay.alphaValue = 0
|
|
|
-
|
|
|
- // Full-app dim + blur background.
|
|
|
- let blur = NSVisualEffectView()
|
|
|
- blur.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- blur.material = palette.isDarkMode ? .hudWindow : .popover
|
|
|
- blur.blendingMode = .withinWindow
|
|
|
- blur.state = .active
|
|
|
- overlay.addSubview(blur)
|
|
|
+ overlay.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
|
|
|
let dim = NSView()
|
|
|
dim.translatesAutoresizingMaskIntoConstraints = false
|
|
|
dim.wantsLayer = true
|
|
|
dim.layer?.backgroundColor = palette.isDarkMode
|
|
|
- ? NSColor.black.withAlphaComponent(0.50).cgColor
|
|
|
- : NSColor.white.withAlphaComponent(0.22).cgColor
|
|
|
+ ? NSColor.black.withAlphaComponent(0.62).cgColor
|
|
|
+ : NSColor.black.withAlphaComponent(0.28).cgColor
|
|
|
overlay.addSubview(dim)
|
|
|
|
|
|
- NSLayoutConstraint.activate([
|
|
|
- blur.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
|
|
|
- blur.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
|
|
|
- blur.topAnchor.constraint(equalTo: overlay.topAnchor),
|
|
|
- blur.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
|
|
|
- dim.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
|
|
|
- dim.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
|
|
|
- dim.topAnchor.constraint(equalTo: overlay.topAnchor),
|
|
|
- dim.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
|
|
|
- ])
|
|
|
-
|
|
|
// Clicking outside the paywall dismisses it.
|
|
|
let outsideButton = NSButton(title: "", target: self, action: #selector(closePaywallClicked(_:)))
|
|
|
outsideButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -3583,7 +3566,6 @@ class ViewController: NSViewController {
|
|
|
overlay.addSubview(outsideButton)
|
|
|
|
|
|
let content = makePaywallContent()
|
|
|
- paywallPanelView = content
|
|
|
overlay.addSubview(content)
|
|
|
|
|
|
view.addSubview(overlay)
|
|
|
@@ -3592,26 +3574,21 @@ class ViewController: NSViewController {
|
|
|
overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
overlay.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
|
+ dim.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
|
|
|
+ dim.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
|
|
|
+ dim.topAnchor.constraint(equalTo: overlay.topAnchor),
|
|
|
+ dim.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
|
|
|
outsideButton.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
|
|
|
outsideButton.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
|
|
|
outsideButton.topAnchor.constraint(equalTo: overlay.topAnchor),
|
|
|
outsideButton.bottomAnchor.constraint(equalTo: overlay.bottomAnchor),
|
|
|
- content.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
|
|
|
- content.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
|
|
|
+ content.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
|
|
|
+ content.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
|
|
|
+ content.topAnchor.constraint(equalTo: overlay.topAnchor),
|
|
|
+ content.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
|
|
|
])
|
|
|
-
|
|
|
- // Choose an initial panel size for the current window size.
|
|
|
- let safeInsetX: CGFloat = 80
|
|
|
- let safeInsetY: CGFloat = 110
|
|
|
- let maxWidth = max(640, view.bounds.width - safeInsetX)
|
|
|
- let maxHeight = max(560, view.bounds.height - safeInsetY)
|
|
|
- let panelWidth = min(1040, maxWidth)
|
|
|
- let panelHeight = min(860, maxHeight)
|
|
|
-
|
|
|
- paywallPanelWidthConstraint = content.widthAnchor.constraint(equalToConstant: panelWidth)
|
|
|
- paywallPanelHeightConstraint = content.heightAnchor.constraint(equalToConstant: panelHeight)
|
|
|
- paywallPanelWidthConstraint?.isActive = true
|
|
|
- paywallPanelHeightConstraint?.isActive = true
|
|
|
+ paywallPanelWidthConstraint = nil
|
|
|
+ paywallPanelHeightConstraint = nil
|
|
|
|
|
|
paywallOverlayView = overlay
|
|
|
|
|
|
@@ -3687,6 +3664,10 @@ class ViewController: NSViewController {
|
|
|
bodyStack.spacing = 18
|
|
|
}
|
|
|
|
|
|
+ let useWidePlans = width >= 880
|
|
|
+ paywallPlansRowWide?.isHidden = useWidePlans == false
|
|
|
+ paywallPlansGridNarrow?.isHidden = useWidePlans
|
|
|
+
|
|
|
bodyStack.layoutSubtreeIfNeeded()
|
|
|
}
|
|
|
|
|
|
@@ -3700,16 +3681,39 @@ class ViewController: NSViewController {
|
|
|
paywallContinueLabel = nil
|
|
|
paywallContinueButton = nil
|
|
|
paywallContinueEnabled = true
|
|
|
+ paywallPlansRowWide = nil
|
|
|
+ paywallPlansGridNarrow = nil
|
|
|
+
|
|
|
+ let root = NSView()
|
|
|
+ root.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ root.wantsLayer = true
|
|
|
+ root.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
|
|
|
let panel = NSView()
|
|
|
panel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
panel.wantsLayer = true
|
|
|
- panel.layer?.backgroundColor = appBackground.cgColor
|
|
|
+ panel.layer?.backgroundColor = palette.sectionCard.cgColor
|
|
|
+ panel.layer?.cornerRadius = 0
|
|
|
+ styleSurface(panel, borderColor: NSColor.clear, borderWidth: 0, shadow: false)
|
|
|
+ root.addSubview(panel)
|
|
|
+
|
|
|
+ // Full-screen: fill the entire app content area.
|
|
|
+ let insetX: CGFloat = 0
|
|
|
+ let insetY: CGFloat = 0
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ panel.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: insetX),
|
|
|
+ panel.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -insetX),
|
|
|
+ panel.topAnchor.constraint(equalTo: root.topAnchor, constant: insetY),
|
|
|
+ panel.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -insetY)
|
|
|
+ ])
|
|
|
+
|
|
|
+ // Responsive logic should measure the panel size (not the full-screen root).
|
|
|
+ self.paywallPanelView = panel
|
|
|
|
|
|
let contentStack = NSStackView()
|
|
|
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
contentStack.orientation = .vertical
|
|
|
- contentStack.spacing = 12
|
|
|
+ contentStack.spacing = 14
|
|
|
contentStack.alignment = .leading
|
|
|
panel.addSubview(contentStack)
|
|
|
|
|
|
@@ -3742,19 +3746,25 @@ class ViewController: NSViewController {
|
|
|
topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
contentStack.addArrangedSubview(topRow)
|
|
|
|
|
|
- contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: secondaryText))
|
|
|
+ let hero = paywallHeroCard(
|
|
|
+ eyebrow: "PREMIUM EXPERIENCE",
|
|
|
+ title: "Elevate your classroom experience",
|
|
|
+ subtitle: "Unlock intelligent automation, deep analytics, and premium support for modern educators."
|
|
|
+ )
|
|
|
+ contentStack.addArrangedSubview(hero)
|
|
|
+
|
|
|
+ let features = paywallFeaturesRow()
|
|
|
+ contentStack.addArrangedSubview(features)
|
|
|
+
|
|
|
let bodyStack = NSStackView()
|
|
|
bodyStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
bodyStack.orientation = .vertical
|
|
|
- bodyStack.spacing = 18
|
|
|
+ bodyStack.spacing = 16
|
|
|
bodyStack.alignment = .leading
|
|
|
bodyStack.distribution = .fill
|
|
|
contentStack.addArrangedSubview(bodyStack)
|
|
|
paywallBodyStack = bodyStack
|
|
|
|
|
|
- let benefits = paywallBenefitsSection()
|
|
|
- bodyStack.addArrangedSubview(benefits)
|
|
|
-
|
|
|
let planColumn = NSStackView()
|
|
|
planColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
|
planColumn.orientation = .vertical
|
|
|
@@ -3772,8 +3782,6 @@ class ViewController: NSViewController {
|
|
|
plan: .weekly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- planColumn.addArrangedSubview(weeklyCard)
|
|
|
-
|
|
|
let monthlyCard = paywallPlanCard(
|
|
|
title: "Monthly",
|
|
|
price: paywallDisplayPrice(for: .monthly),
|
|
|
@@ -3783,8 +3791,6 @@ class ViewController: NSViewController {
|
|
|
plan: .monthly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- planColumn.addArrangedSubview(monthlyCard)
|
|
|
-
|
|
|
let yearlyCard = paywallPlanCard(
|
|
|
title: "Yearly",
|
|
|
price: paywallDisplayPrice(for: .yearly),
|
|
|
@@ -3794,8 +3800,6 @@ class ViewController: NSViewController {
|
|
|
plan: .yearly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- planColumn.addArrangedSubview(yearlyCard)
|
|
|
-
|
|
|
let lifetimeCard = paywallPlanCard(
|
|
|
title: "Lifetime",
|
|
|
price: paywallDisplayPrice(for: .lifetime),
|
|
|
@@ -3805,9 +3809,85 @@ class ViewController: NSViewController {
|
|
|
plan: .lifetime,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- planColumn.addArrangedSubview(lifetimeCard)
|
|
|
+
|
|
|
+ let plansRowWide = NSStackView()
|
|
|
+ plansRowWide.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ plansRowWide.orientation = .horizontal
|
|
|
+ plansRowWide.spacing = 12
|
|
|
+ plansRowWide.alignment = .centerY
|
|
|
+ plansRowWide.distribution = .fillEqually
|
|
|
+ plansRowWide.addArrangedSubview(weeklyCard)
|
|
|
+ plansRowWide.addArrangedSubview(monthlyCard)
|
|
|
+ plansRowWide.addArrangedSubview(yearlyCard)
|
|
|
+ plansRowWide.addArrangedSubview(lifetimeCard)
|
|
|
+ paywallPlansRowWide = plansRowWide
|
|
|
+ planColumn.addArrangedSubview(plansRowWide)
|
|
|
+
|
|
|
+ let plansGridNarrow = NSStackView()
|
|
|
+ plansGridNarrow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ plansGridNarrow.orientation = .vertical
|
|
|
+ plansGridNarrow.spacing = 12
|
|
|
+ plansGridNarrow.alignment = .leading
|
|
|
+ plansGridNarrow.distribution = .fillEqually
|
|
|
+
|
|
|
+ let plansRow1 = NSStackView()
|
|
|
+ plansRow1.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ plansRow1.orientation = .horizontal
|
|
|
+ plansRow1.spacing = 12
|
|
|
+ plansRow1.alignment = .centerY
|
|
|
+ plansRow1.distribution = .fillEqually
|
|
|
+ plansRow1.addArrangedSubview(paywallPlanCard(
|
|
|
+ title: "Weekly",
|
|
|
+ price: paywallDisplayPrice(for: .weekly),
|
|
|
+ badge: "Basic Deal",
|
|
|
+ badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
|
|
|
+ subtitle: "Billed weekly",
|
|
|
+ plan: .weekly,
|
|
|
+ strikePrice: nil
|
|
|
+ ))
|
|
|
+ plansRow1.addArrangedSubview(paywallPlanCard(
|
|
|
+ title: "Monthly",
|
|
|
+ price: paywallDisplayPrice(for: .monthly),
|
|
|
+ badge: "Free Trial",
|
|
|
+ badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
|
|
|
+ subtitle: "7-day free trial, then monthly",
|
|
|
+ plan: .monthly,
|
|
|
+ strikePrice: nil
|
|
|
+ ))
|
|
|
+
|
|
|
+ let plansRow2 = NSStackView()
|
|
|
+ plansRow2.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ plansRow2.orientation = .horizontal
|
|
|
+ plansRow2.spacing = 12
|
|
|
+ plansRow2.alignment = .centerY
|
|
|
+ plansRow2.distribution = .fillEqually
|
|
|
+ plansRow2.addArrangedSubview(paywallPlanCard(
|
|
|
+ title: "Yearly",
|
|
|
+ price: paywallDisplayPrice(for: .yearly),
|
|
|
+ badge: "Best Deal",
|
|
|
+ badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
|
|
|
+ subtitle: "Billed yearly",
|
|
|
+ plan: .yearly,
|
|
|
+ strikePrice: nil
|
|
|
+ ))
|
|
|
+ plansRow2.addArrangedSubview(paywallPlanCard(
|
|
|
+ title: "Lifetime",
|
|
|
+ price: paywallDisplayPrice(for: .lifetime),
|
|
|
+ badge: "Save 50%",
|
|
|
+ badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
|
|
|
+ subtitle: "One-time purchase",
|
|
|
+ plan: .lifetime,
|
|
|
+ strikePrice: nil
|
|
|
+ ))
|
|
|
+
|
|
|
+ plansGridNarrow.addArrangedSubview(plansRow1)
|
|
|
+ plansGridNarrow.addArrangedSubview(plansRow2)
|
|
|
+ plansGridNarrow.isHidden = true
|
|
|
+ paywallPlansGridNarrow = plansGridNarrow
|
|
|
+ planColumn.addArrangedSubview(plansGridNarrow)
|
|
|
+
|
|
|
updatePaywallPlanSelection()
|
|
|
- planColumn.setCustomSpacing(20, after: lifetimeCard)
|
|
|
+ planColumn.setCustomSpacing(16, after: plansGridNarrow)
|
|
|
|
|
|
let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: primaryText)
|
|
|
offer.alignment = .center
|
|
|
@@ -3822,7 +3902,11 @@ class ViewController: NSViewController {
|
|
|
offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
|
|
|
])
|
|
|
planColumn.addArrangedSubview(offerWrap)
|
|
|
- planColumn.setCustomSpacing(18, after: offerWrap)
|
|
|
+ planColumn.setCustomSpacing(14, after: offerWrap)
|
|
|
+
|
|
|
+ let trustRow = paywallTrustRow()
|
|
|
+ planColumn.addArrangedSubview(trustRow)
|
|
|
+ planColumn.setCustomSpacing(14, after: trustRow)
|
|
|
|
|
|
let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
|
|
|
continueButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -3830,11 +3914,11 @@ class ViewController: NSViewController {
|
|
|
continueButton.bezelStyle = .regularSquare
|
|
|
continueButton.wantsLayer = true
|
|
|
continueButton.layer?.cornerRadius = 14
|
|
|
- continueButton.normalColor = accentBlue
|
|
|
- continueButton.hoverColor = accentBlue.withAlphaComponent(0.92)
|
|
|
+ continueButton.normalColor = NSColor(calibratedRed: 0.0, green: 0.70, blue: 0.42, alpha: 1)
|
|
|
+ continueButton.hoverColor = NSColor(calibratedRed: 0.0, green: 0.70, blue: 0.42, alpha: 0.92)
|
|
|
continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
|
continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
- styleSurface(continueButton, borderColor: accentBlue.withAlphaComponent(0.85), borderWidth: 1, shadow: true)
|
|
|
+ styleSurface(continueButton, borderColor: NSColor.black.withAlphaComponent(0.12), borderWidth: 1, shadow: true)
|
|
|
let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
|
|
|
continueButton.addSubview(continueLabel)
|
|
|
NSLayoutConstraint.activate([
|
|
|
@@ -3864,10 +3948,30 @@ class ViewController: NSViewController {
|
|
|
footerButtons.translatesAutoresizingMaskIntoConstraints = false
|
|
|
footerButtons.orientation = .horizontal
|
|
|
footerButtons.alignment = .centerY
|
|
|
- footerButtons.distribution = .fillEqually
|
|
|
+ footerButtons.distribution = .fill
|
|
|
footerButtons.spacing = 8
|
|
|
footerButtons.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
|
|
|
+ let leftFooter = NSStackView()
|
|
|
+ leftFooter.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ leftFooter.orientation = .horizontal
|
|
|
+ leftFooter.alignment = .centerY
|
|
|
+ leftFooter.spacing = 14
|
|
|
+
|
|
|
+ let manageButton = paywallFooterButton(
|
|
|
+ title: "Manage Subscription",
|
|
|
+ action: #selector(paywallManageSubscriptionClicked(_:))
|
|
|
+ )
|
|
|
+ let restoreButton = paywallFooterButton(
|
|
|
+ title: "Restore Purchase",
|
|
|
+ action: #selector(paywallRestorePurchaseClicked(_:))
|
|
|
+ )
|
|
|
+ leftFooter.addArrangedSubview(manageButton)
|
|
|
+ leftFooter.addArrangedSubview(restoreButton)
|
|
|
+
|
|
|
+ let footerSpacer = NSView()
|
|
|
+ footerSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
let privacyButton = paywallFooterButton(
|
|
|
title: "Privacy Policy",
|
|
|
action: #selector(paywallPrivacyPolicyClicked(_:))
|
|
|
@@ -3880,6 +3984,8 @@ class ViewController: NSViewController {
|
|
|
title: "Terms of Services",
|
|
|
action: #selector(paywallTermsOfServicesClicked(_:))
|
|
|
)
|
|
|
+ footerButtons.addArrangedSubview(leftFooter)
|
|
|
+ footerButtons.addArrangedSubview(footerSpacer)
|
|
|
footerButtons.addArrangedSubview(privacyButton)
|
|
|
footerButtons.addArrangedSubview(supportButton)
|
|
|
footerButtons.addArrangedSubview(termsButton)
|
|
|
@@ -3889,11 +3995,12 @@ class ViewController: NSViewController {
|
|
|
contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
|
|
|
contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
|
|
|
contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
|
|
|
- contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12)
|
|
|
+ contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -16)
|
|
|
])
|
|
|
|
|
|
refreshPaywallStoreUI()
|
|
|
- return panel
|
|
|
+ updatePaywallResponsiveLayout()
|
|
|
+ return root
|
|
|
}
|
|
|
|
|
|
private func paywallBenefitsSection() -> NSView {
|
|
|
@@ -3927,6 +4034,103 @@ class ViewController: NSViewController {
|
|
|
return stack
|
|
|
}
|
|
|
|
|
|
+ private func paywallHeroCard(eyebrow: String, title: String, subtitle: String) -> NSView {
|
|
|
+ let card = NSView()
|
|
|
+ card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ card.wantsLayer = true
|
|
|
+ card.layer?.cornerRadius = 14
|
|
|
+ card.layer?.backgroundColor = palette.secondaryCardBackground.cgColor
|
|
|
+ styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
+
|
|
|
+ let eyebrowLabel = textLabel(eyebrow, font: NSFont.systemFont(ofSize: 10, weight: .bold), color: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1))
|
|
|
+ eyebrowLabel.alignment = .left
|
|
|
+
|
|
|
+ let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 16, weight: .bold), color: primaryText)
|
|
|
+ titleLabel.alignment = .left
|
|
|
+
|
|
|
+ let subtitleLabel = textLabel(subtitle, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: secondaryText)
|
|
|
+ subtitleLabel.maximumNumberOfLines = 2
|
|
|
+ subtitleLabel.lineBreakMode = .byTruncatingTail
|
|
|
+ subtitleLabel.alignment = .left
|
|
|
+
|
|
|
+ let stack = NSStackView()
|
|
|
+ stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ stack.orientation = .vertical
|
|
|
+ stack.alignment = .leading
|
|
|
+ stack.spacing = 6
|
|
|
+ stack.addArrangedSubview(eyebrowLabel)
|
|
|
+ stack.addArrangedSubview(titleLabel)
|
|
|
+ stack.addArrangedSubview(subtitleLabel)
|
|
|
+ card.addSubview(stack)
|
|
|
+
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
|
|
+ stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
|
|
+ stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 12),
|
|
|
+ stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -12)
|
|
|
+ ])
|
|
|
+
|
|
|
+ return card
|
|
|
+ }
|
|
|
+
|
|
|
+ private func paywallFeaturesRow() -> NSView {
|
|
|
+ let row = NSStackView()
|
|
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ row.orientation = .horizontal
|
|
|
+ row.spacing = 10
|
|
|
+ row.alignment = .centerY
|
|
|
+ row.distribution = .fillEqually
|
|
|
+
|
|
|
+ row.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage classes"))
|
|
|
+ row.addArrangedSubview(paywallBenefitItem(icon: "💬", text: "Smart to-dos"))
|
|
|
+ row.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Faster workflow"))
|
|
|
+ row.addArrangedSubview(paywallBenefitItem(icon: "🔔", text: "Reminder notifications"))
|
|
|
+ return row
|
|
|
+ }
|
|
|
+
|
|
|
+ private func paywallTrustRow() -> NSView {
|
|
|
+ let row = NSStackView()
|
|
|
+ row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ row.orientation = .horizontal
|
|
|
+ row.spacing = 10
|
|
|
+ row.alignment = .centerY
|
|
|
+ row.distribution = .fillEqually
|
|
|
+
|
|
|
+ row.addArrangedSubview(paywallTrustItem(title: "Cancel anytime", subtitle: "No lock-in"))
|
|
|
+ row.addArrangedSubview(paywallTrustItem(title: "Instant access", subtitle: "Unlock all tools"))
|
|
|
+ row.addArrangedSubview(paywallTrustItem(title: "Secure billing", subtitle: "Handled by Apple"))
|
|
|
+ return row
|
|
|
+ }
|
|
|
+
|
|
|
+ private func paywallTrustItem(title: String, subtitle: String) -> NSView {
|
|
|
+ let card = NSView()
|
|
|
+ card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ card.wantsLayer = true
|
|
|
+ card.layer?.cornerRadius = 12
|
|
|
+ card.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
+ styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
+
|
|
|
+ let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 11, weight: .semibold), color: primaryText)
|
|
|
+ let subtitleLabel = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .medium), color: secondaryText)
|
|
|
+
|
|
|
+ let stack = NSStackView()
|
|
|
+ stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ stack.orientation = .vertical
|
|
|
+ stack.alignment = .centerX
|
|
|
+ stack.spacing = 2
|
|
|
+ stack.addArrangedSubview(titleLabel)
|
|
|
+ stack.addArrangedSubview(subtitleLabel)
|
|
|
+ card.addSubview(stack)
|
|
|
+
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ card.heightAnchor.constraint(equalToConstant: 44),
|
|
|
+ stack.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
+ stack.centerYAnchor.constraint(equalTo: card.centerYAnchor)
|
|
|
+ ])
|
|
|
+
|
|
|
+ return card
|
|
|
+ }
|
|
|
+
|
|
|
private func paywallBenefitItem(icon: String, text: String) -> NSView {
|
|
|
let card = NSView()
|
|
|
card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -3979,8 +4183,7 @@ class ViewController: NSViewController {
|
|
|
wrapper.bezelStyle = .regularSquare
|
|
|
wrapper.wantsLayer = true
|
|
|
wrapper.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
- wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
- wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
|
|
|
+ wrapper.heightAnchor.constraint(equalToConstant: 122).isActive = true
|
|
|
wrapper.tag = PremiumPlan.allCases.firstIndex(of: plan) ?? 0
|
|
|
|
|
|
let card = NSView()
|
|
|
@@ -3988,7 +4191,6 @@ class ViewController: NSViewController {
|
|
|
card.wantsLayer = true
|
|
|
card.layer?.cornerRadius = 16
|
|
|
card.layer?.backgroundColor = palette.sectionCard.cgColor
|
|
|
- card.heightAnchor.constraint(equalToConstant: 82).isActive = true
|
|
|
wrapper.addSubview(card)
|
|
|
NSLayoutConstraint.activate([
|
|
|
card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
|
|
@@ -4019,19 +4221,19 @@ class ViewController: NSViewController {
|
|
|
|
|
|
let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 15, weight: .bold), color: accentBlue)
|
|
|
card.addSubview(titleLabel)
|
|
|
- let priceLabel = textLabel(price, font: NSFont.systemFont(ofSize: 12, weight: .bold), color: primaryText)
|
|
|
+ let priceLabel = textLabel(price, font: NSFont.systemFont(ofSize: 13, weight: .bold), color: primaryText)
|
|
|
card.addSubview(priceLabel)
|
|
|
paywallPlanPriceLabels[plan] = priceLabel
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
- badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
+ badgeWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
|
|
|
badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
|
|
|
|
|
|
titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
|
|
- titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
|
|
|
+ titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 22),
|
|
|
|
|
|
- priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
|
|
|
- priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32)
|
|
|
+ priceLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
|
|
+ priceLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8)
|
|
|
])
|
|
|
|
|
|
if let subtitle {
|
|
|
@@ -4039,8 +4241,9 @@ class ViewController: NSViewController {
|
|
|
card.addSubview(sub)
|
|
|
paywallPlanSubtitleLabels[plan] = sub
|
|
|
NSLayoutConstraint.activate([
|
|
|
- sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
|
|
|
- sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0)
|
|
|
+ sub.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
|
|
|
+ sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 2),
|
|
|
+ sub.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
|
|
|
])
|
|
|
}
|
|
|
|
|
|
@@ -4152,6 +4355,22 @@ class ViewController: NSViewController {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+ @objc private func paywallManageSubscriptionClicked(_ sender: Any?) {
|
|
|
+ openManageSubscriptions()
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func paywallRestorePurchaseClicked(_ sender: Any?) {
|
|
|
+ Task { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ let message = await self.storeKitCoordinator.restorePurchases()
|
|
|
+ await MainActor.run {
|
|
|
+ self.refreshPaywallStoreUI()
|
|
|
+ self.updatePremiumButtons()
|
|
|
+ self.showSimpleAlert(title: "Restore Purchase", message: message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private func startSelectedPlanPurchase() {
|
|
|
guard paywallContinueEnabled else { return }
|
|
|
paywallPurchaseTask?.cancel()
|
|
|
@@ -4215,7 +4434,11 @@ class ViewController: NSViewController {
|
|
|
paywallContinueButton?.alphaValue = 0.75
|
|
|
} else {
|
|
|
paywallContinueEnabled = true
|
|
|
- paywallContinueLabel?.stringValue = "Continue"
|
|
|
+ if selectedPremiumPlan == .monthly {
|
|
|
+ paywallContinueLabel?.stringValue = "Start with free trial"
|
|
|
+ } else {
|
|
|
+ paywallContinueLabel?.stringValue = "Continue"
|
|
|
+ }
|
|
|
paywallContinueButton?.alphaValue = 1.0
|
|
|
}
|
|
|
}
|