|
|
@@ -256,7 +256,12 @@ class ViewController: NSViewController {
|
|
|
private weak var topBarAuthButton: NSButton?
|
|
|
private var topBarAuthButtonWidthConstraint: NSLayoutConstraint?
|
|
|
private var profileMenuPopover: NSPopover?
|
|
|
- private var paywallWindow: NSWindow?
|
|
|
+ private weak var paywallOverlayView: NSView?
|
|
|
+ private weak var paywallPanelView: NSView?
|
|
|
+ private weak var paywallBodyStack: NSStackView?
|
|
|
+ private var paywallIsDesktopLayout = false
|
|
|
+ private var paywallPanelWidthConstraint: NSLayoutConstraint?
|
|
|
+ private var paywallPanelHeightConstraint: NSLayoutConstraint?
|
|
|
private var joinMeetingWindow: NSWindow?
|
|
|
private var scheduleMeetingWindow: NSWindow?
|
|
|
private weak var scheduleTopicField: NSTextField?
|
|
|
@@ -602,6 +607,10 @@ class ViewController: NSViewController {
|
|
|
override func viewDidLayout() {
|
|
|
super.viewDidLayout()
|
|
|
alignNativeTrafficLights()
|
|
|
+ if paywallOverlayView != nil {
|
|
|
+ updatePaywallPanelSize()
|
|
|
+ updatePaywallResponsiveLayout()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private func setupUI() {
|
|
|
@@ -3521,49 +3530,164 @@ class ViewController: NSViewController {
|
|
|
// MARK: - Paywall (ported from meetings_app)
|
|
|
|
|
|
private func showPaywall() {
|
|
|
- if let existing = paywallWindow {
|
|
|
+ if let existing = paywallOverlayView {
|
|
|
refreshPaywallStoreUI()
|
|
|
- existing.makeKeyAndOrderFront(nil)
|
|
|
+ if let superview = existing.superview {
|
|
|
+ superview.addSubview(existing)
|
|
|
+ }
|
|
|
+ updatePaywallResponsiveLayout()
|
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ let overlay = NSView()
|
|
|
+ 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)
|
|
|
+
|
|
|
+ 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
|
|
|
+ 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
|
|
|
+ outsideButton.isBordered = false
|
|
|
+ outsideButton.bezelStyle = .regularSquare
|
|
|
+ outsideButton.contentTintColor = .clear
|
|
|
+ outsideButton.wantsLayer = true
|
|
|
+ outsideButton.layer?.backgroundColor = NSColor.clear.cgColor
|
|
|
+ overlay.addSubview(outsideButton)
|
|
|
+
|
|
|
let content = makePaywallContent()
|
|
|
- let controller = NSViewController()
|
|
|
- controller.view = content
|
|
|
+ paywallPanelView = content
|
|
|
+ overlay.addSubview(content)
|
|
|
|
|
|
- let panel = NSPanel(
|
|
|
- contentRect: NSRect(x: 0, y: 0, width: 640, height: 820),
|
|
|
- styleMask: [.titled, .closable, .fullSizeContentView],
|
|
|
- backing: .buffered,
|
|
|
- defer: false
|
|
|
- )
|
|
|
- panel.title = "Get Premium"
|
|
|
- panel.titleVisibility = .hidden
|
|
|
- panel.titlebarAppearsTransparent = true
|
|
|
- panel.hidesOnDeactivate = true
|
|
|
- panel.isReleasedWhenClosed = false
|
|
|
- panel.standardWindowButton(.closeButton)?.isHidden = true
|
|
|
- panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
|
|
- panel.standardWindowButton(.zoomButton)?.isHidden = true
|
|
|
- panel.center()
|
|
|
- panel.contentViewController = controller
|
|
|
- panel.makeKeyAndOrderFront(nil)
|
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
|
- paywallWindow = panel
|
|
|
+ view.addSubview(overlay)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ overlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
+ overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
+ overlay.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
+ overlay.bottomAnchor.constraint(equalTo: view.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)
|
|
|
+ ])
|
|
|
+
|
|
|
+ // 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
|
|
|
+
|
|
|
+ paywallOverlayView = overlay
|
|
|
+
|
|
|
+ NSAnimationContext.runAnimationGroup { context in
|
|
|
+ context.duration = 0.18
|
|
|
+ overlay.animator().alphaValue = 1
|
|
|
+ }
|
|
|
|
|
|
Task { [weak self] in
|
|
|
guard let self else { return }
|
|
|
await self.storeKitCoordinator.refreshProducts()
|
|
|
await MainActor.run {
|
|
|
self.refreshPaywallStoreUI()
|
|
|
+ self.updatePaywallResponsiveLayout()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@objc private func closePaywallClicked(_ sender: Any?) {
|
|
|
- paywallWindow?.performClose(nil)
|
|
|
- paywallWindow = nil
|
|
|
+ hidePaywall()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func hidePaywall() {
|
|
|
+ guard let overlay = paywallOverlayView else { return }
|
|
|
+
|
|
|
+ NSAnimationContext.runAnimationGroup { context in
|
|
|
+ context.duration = 0.16
|
|
|
+ overlay.animator().alphaValue = 0
|
|
|
+ } completionHandler: { [weak self] in
|
|
|
+ overlay.removeFromSuperview()
|
|
|
+ self?.paywallPanelWidthConstraint = nil
|
|
|
+ self?.paywallPanelHeightConstraint = nil
|
|
|
+ self?.paywallPanelView = nil
|
|
|
+ self?.paywallBodyStack = nil
|
|
|
+ self?.paywallIsDesktopLayout = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updatePaywallPanelSize() {
|
|
|
+ guard paywallPanelWidthConstraint != nil, paywallPanelHeightConstraint != nil else { return }
|
|
|
+
|
|
|
+ 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)
|
|
|
+
|
|
|
+ if abs(paywallPanelWidthConstraint?.constant ?? 0 - panelWidth) > 1 {
|
|
|
+ paywallPanelWidthConstraint?.constant = panelWidth
|
|
|
+ }
|
|
|
+ if abs(paywallPanelHeightConstraint?.constant ?? 0 - panelHeight) > 1 {
|
|
|
+ paywallPanelHeightConstraint?.constant = panelHeight
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updatePaywallResponsiveLayout() {
|
|
|
+ guard let bodyStack = paywallBodyStack, let panel = paywallPanelView else { return }
|
|
|
+ let width = panel.bounds.width
|
|
|
+ // Two-column desktop layout needs room for the plan column (520+) + a slimmer benefits column.
|
|
|
+ let shouldUseDesktop = width >= 900
|
|
|
+ guard shouldUseDesktop != paywallIsDesktopLayout else { return }
|
|
|
+ paywallIsDesktopLayout = shouldUseDesktop
|
|
|
+
|
|
|
+ if shouldUseDesktop {
|
|
|
+ bodyStack.orientation = .horizontal
|
|
|
+ bodyStack.alignment = .top
|
|
|
+ bodyStack.spacing = 26
|
|
|
+ } else {
|
|
|
+ bodyStack.orientation = .vertical
|
|
|
+ bodyStack.alignment = .leading
|
|
|
+ bodyStack.spacing = 18
|
|
|
+ }
|
|
|
+
|
|
|
+ bodyStack.layoutSubtreeIfNeeded()
|
|
|
}
|
|
|
|
|
|
private func makePaywallContent() -> NSView {
|
|
|
@@ -3619,9 +3743,25 @@ class ViewController: NSViewController {
|
|
|
contentStack.addArrangedSubview(topRow)
|
|
|
|
|
|
contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: secondaryText))
|
|
|
+ let bodyStack = NSStackView()
|
|
|
+ bodyStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ bodyStack.orientation = .vertical
|
|
|
+ bodyStack.spacing = 18
|
|
|
+ bodyStack.alignment = .leading
|
|
|
+ bodyStack.distribution = .fill
|
|
|
+ contentStack.addArrangedSubview(bodyStack)
|
|
|
+ paywallBodyStack = bodyStack
|
|
|
+
|
|
|
let benefits = paywallBenefitsSection()
|
|
|
- contentStack.addArrangedSubview(benefits)
|
|
|
- contentStack.setCustomSpacing(18, after: benefits)
|
|
|
+ bodyStack.addArrangedSubview(benefits)
|
|
|
+
|
|
|
+ let planColumn = NSStackView()
|
|
|
+ planColumn.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ planColumn.orientation = .vertical
|
|
|
+ planColumn.spacing = 12
|
|
|
+ planColumn.alignment = .leading
|
|
|
+ planColumn.distribution = .fill
|
|
|
+ bodyStack.addArrangedSubview(planColumn)
|
|
|
|
|
|
let weeklyCard = paywallPlanCard(
|
|
|
title: "Weekly",
|
|
|
@@ -3632,7 +3772,7 @@ class ViewController: NSViewController {
|
|
|
plan: .weekly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- contentStack.addArrangedSubview(weeklyCard)
|
|
|
+ planColumn.addArrangedSubview(weeklyCard)
|
|
|
|
|
|
let monthlyCard = paywallPlanCard(
|
|
|
title: "Monthly",
|
|
|
@@ -3643,7 +3783,7 @@ class ViewController: NSViewController {
|
|
|
plan: .monthly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- contentStack.addArrangedSubview(monthlyCard)
|
|
|
+ planColumn.addArrangedSubview(monthlyCard)
|
|
|
|
|
|
let yearlyCard = paywallPlanCard(
|
|
|
title: "Yearly",
|
|
|
@@ -3654,7 +3794,7 @@ class ViewController: NSViewController {
|
|
|
plan: .yearly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- contentStack.addArrangedSubview(yearlyCard)
|
|
|
+ planColumn.addArrangedSubview(yearlyCard)
|
|
|
|
|
|
let lifetimeCard = paywallPlanCard(
|
|
|
title: "Lifetime",
|
|
|
@@ -3665,9 +3805,9 @@ class ViewController: NSViewController {
|
|
|
plan: .lifetime,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- contentStack.addArrangedSubview(lifetimeCard)
|
|
|
+ planColumn.addArrangedSubview(lifetimeCard)
|
|
|
updatePaywallPlanSelection()
|
|
|
- contentStack.setCustomSpacing(20, after: lifetimeCard)
|
|
|
+ planColumn.setCustomSpacing(20, after: lifetimeCard)
|
|
|
|
|
|
let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: primaryText)
|
|
|
offer.alignment = .center
|
|
|
@@ -3681,8 +3821,8 @@ class ViewController: NSViewController {
|
|
|
offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
|
|
|
offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
|
|
|
])
|
|
|
- contentStack.addArrangedSubview(offerWrap)
|
|
|
- contentStack.setCustomSpacing(18, after: offerWrap)
|
|
|
+ planColumn.addArrangedSubview(offerWrap)
|
|
|
+ planColumn.setCustomSpacing(18, after: offerWrap)
|
|
|
|
|
|
let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
|
|
|
continueButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -3703,8 +3843,8 @@ class ViewController: NSViewController {
|
|
|
])
|
|
|
paywallContinueButton = continueButton
|
|
|
paywallContinueLabel = continueLabel
|
|
|
- contentStack.addArrangedSubview(continueButton)
|
|
|
- contentStack.setCustomSpacing(16, after: continueButton)
|
|
|
+ planColumn.addArrangedSubview(continueButton)
|
|
|
+ planColumn.setCustomSpacing(16, after: continueButton)
|
|
|
|
|
|
let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: secondaryText)
|
|
|
secure.alignment = .center
|
|
|
@@ -3717,8 +3857,8 @@ class ViewController: NSViewController {
|
|
|
secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
|
|
|
secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
|
|
|
])
|
|
|
- contentStack.addArrangedSubview(secureWrap)
|
|
|
- contentStack.setCustomSpacing(8, after: secureWrap)
|
|
|
+ planColumn.addArrangedSubview(secureWrap)
|
|
|
+ planColumn.setCustomSpacing(8, after: secureWrap)
|
|
|
|
|
|
let footerButtons = NSStackView()
|
|
|
footerButtons.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -3743,7 +3883,7 @@ class ViewController: NSViewController {
|
|
|
footerButtons.addArrangedSubview(privacyButton)
|
|
|
footerButtons.addArrangedSubview(supportButton)
|
|
|
footerButtons.addArrangedSubview(termsButton)
|
|
|
- contentStack.addArrangedSubview(footerButtons)
|
|
|
+ planColumn.addArrangedSubview(footerButtons)
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
|
|
|
@@ -3762,7 +3902,7 @@ class ViewController: NSViewController {
|
|
|
stack.orientation = .vertical
|
|
|
stack.spacing = 8
|
|
|
stack.alignment = .leading
|
|
|
- stack.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
+ stack.widthAnchor.constraint(greaterThanOrEqualToConstant: 320).isActive = true
|
|
|
|
|
|
let rowOne = NSStackView()
|
|
|
rowOne.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -4036,8 +4176,7 @@ class ViewController: NSViewController {
|
|
|
self.schedulePremiumRatingPromptIfNeeded()
|
|
|
}
|
|
|
self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
|
|
|
- self.paywallWindow?.performClose(nil)
|
|
|
- self.paywallWindow = nil
|
|
|
+ self.hidePaywall()
|
|
|
if self.storeKitCoordinator.hasPremiumAccess {
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { [weak self] in
|
|
|
self?.presentRatingPrompt(source: "premium")
|