|
|
@@ -226,6 +226,15 @@ private final class StoreKitCoordinator {
|
|
|
}
|
|
|
|
|
|
final class ViewController: NSViewController {
|
|
|
+ private enum PaywallFooterAction {
|
|
|
+ case manageSubscription
|
|
|
+ case restorePurchase
|
|
|
+ case continueWithFreePlan
|
|
|
+ case privacyPolicy
|
|
|
+ case support
|
|
|
+ case termsOfServices
|
|
|
+ }
|
|
|
+
|
|
|
private struct GoogleProfileDisplay {
|
|
|
let name: String
|
|
|
let email: String
|
|
|
@@ -249,11 +258,13 @@ final class ViewController: NSViewController {
|
|
|
private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
|
|
|
private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
|
|
|
private var paywallWindow: NSWindow?
|
|
|
+ private weak var paywallOverlayView: NSView?
|
|
|
private let paywallContentWidth: CGFloat = 520
|
|
|
private let launchWindowLeftOffset: CGFloat = 80
|
|
|
private var selectedPremiumPlan: PremiumPlan = .monthly
|
|
|
private var paywallPlanViews: [PremiumPlan: NSView] = [:]
|
|
|
private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
|
|
|
+ private var paywallFooterActionByView = [ObjectIdentifier: PaywallFooterAction]()
|
|
|
private weak var paywallOfferLabel: NSTextField?
|
|
|
private weak var paywallContinueLabel: NSTextField?
|
|
|
private weak var paywallContinueButton: NSView?
|
|
|
@@ -539,8 +550,10 @@ private extension ViewController {
|
|
|
}
|
|
|
|
|
|
@objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
- if storeKitCoordinator.hasPremiumAccess {
|
|
|
+ if storeKitCoordinator.hasLifetimeAccess {
|
|
|
openManageSubscriptions()
|
|
|
+ } else if storeKitCoordinator.hasPremiumAccess {
|
|
|
+ showPaywall(upgradeFlow: true, preferredPlan: .lifetime)
|
|
|
} else {
|
|
|
showPaywall()
|
|
|
}
|
|
|
@@ -745,6 +758,10 @@ private extension ViewController {
|
|
|
}
|
|
|
|
|
|
private func openManageSubscriptions() {
|
|
|
+ if let appStoreURL = URL(string: "macappstore://apps.apple.com/account/subscriptions"),
|
|
|
+ NSWorkspace.shared.open(appStoreURL) {
|
|
|
+ return
|
|
|
+ }
|
|
|
guard let url = URL(string: "https://apps.apple.com/account/subscriptions") else {
|
|
|
showSimpleAlert(title: "Unable to Open Subscriptions", message: "The subscriptions URL is invalid.")
|
|
|
return
|
|
|
@@ -752,6 +769,27 @@ private extension ViewController {
|
|
|
openInDefaultBrowser(url: url)
|
|
|
}
|
|
|
|
|
|
+ private func openRestoreSubscriptionPage() {
|
|
|
+ let fallbackURL = "https://support.apple.com/en-us/108096"
|
|
|
+ let restoreURL = (Bundle.main.object(forInfoDictionaryKey: "RestoreSubscriptionURL") as? String) ?? fallbackURL
|
|
|
+ guard let url = URL(string: restoreURL) else {
|
|
|
+ if let fallback = URL(string: fallbackURL) {
|
|
|
+ NSWorkspace.shared.open(fallback)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ NSWorkspace.shared.open(url)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func performRestorePurchases() {
|
|
|
+ Task { [weak self] in
|
|
|
+ guard let self else { return }
|
|
|
+ let message = await self.storeKitCoordinator.restorePurchases()
|
|
|
+ self.refreshPaywallStoreUI()
|
|
|
+ self.showSimpleAlert(title: "Restore Purchases", message: message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private func shareAppURL() -> URL? {
|
|
|
if let configured = Bundle.main.object(forInfoDictionaryKey: "AppShareURL") as? String {
|
|
|
let trimmed = configured.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
@@ -839,6 +877,7 @@ private extension ViewController {
|
|
|
settingsActionByView.removeAll()
|
|
|
paywallPlanViews.removeAll()
|
|
|
premiumPlanByView.removeAll()
|
|
|
+ paywallFooterActionByView.removeAll()
|
|
|
paywallPriceLabels.removeAll()
|
|
|
paywallSubtitleLabels.removeAll()
|
|
|
paywallContinueLabel = nil
|
|
|
@@ -860,12 +899,7 @@ private extension ViewController {
|
|
|
private func handleSettingsAction(_ action: SettingsAction, sourceView: NSView? = nil, clickLocationInSourceView: NSPoint? = nil) {
|
|
|
switch action {
|
|
|
case .restore:
|
|
|
- Task { [weak self] in
|
|
|
- guard let self else { return }
|
|
|
- let message = await self.storeKitCoordinator.restorePurchases()
|
|
|
- self.refreshPaywallStoreUI()
|
|
|
- self.showSimpleAlert(title: "Restore Purchases", message: message)
|
|
|
- }
|
|
|
+ performRestorePurchases()
|
|
|
case .rateUs:
|
|
|
openRateUsDestination()
|
|
|
case .support:
|
|
|
@@ -990,20 +1024,37 @@ private extension ViewController {
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0, execute: hideWorkItem)
|
|
|
}
|
|
|
|
|
|
- private func confirmPremiumUpgrade() -> Bool {
|
|
|
+ private func confirmPremiumUpgrade(for targetPlan: PremiumPlan) -> Bool {
|
|
|
let alert = NSAlert()
|
|
|
- alert.messageText = "Already Premium"
|
|
|
- alert.informativeText = "You are already premium. Do you want to continue with this purchase?"
|
|
|
+ if targetPlan == .lifetime, storeKitCoordinator.hasPremiumAccess, !storeKitCoordinator.hasLifetimeAccess {
|
|
|
+ alert.messageText = "Switching to Lifetime"
|
|
|
+ alert.informativeText = "You already have an active subscription. If you buy Lifetime, cancel your current subscription in App Store Subscriptions to avoid future renewal charges."
|
|
|
+ } else {
|
|
|
+ alert.messageText = "Already Premium"
|
|
|
+ alert.informativeText = "You are already premium. Do you want to continue with this purchase?"
|
|
|
+ }
|
|
|
alert.addButton(withTitle: "Continue")
|
|
|
alert.addButton(withTitle: "Cancel")
|
|
|
return alert.runModal() == .alertFirstButtonReturn
|
|
|
}
|
|
|
|
|
|
private func showPaywall(upgradeFlow: Bool = false, preferredPlan: PremiumPlan? = nil) {
|
|
|
+ if !Thread.isMainThread {
|
|
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
+ self?.showPaywall(upgradeFlow: upgradeFlow, preferredPlan: preferredPlan)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
paywallUpgradeFlowEnabled = upgradeFlow
|
|
|
if let preferredPlan {
|
|
|
selectedPremiumPlan = preferredPlan
|
|
|
}
|
|
|
+ if let existingOverlay = paywallOverlayView {
|
|
|
+ refreshPaywallStoreUI()
|
|
|
+ existingOverlay.alphaValue = 1
|
|
|
+ view.addSubview(existingOverlay, positioned: .above, relativeTo: nil)
|
|
|
+ return
|
|
|
+ }
|
|
|
if let existing = paywallWindow {
|
|
|
refreshPaywallStoreUI()
|
|
|
animatePaywallPresentation(existing)
|
|
|
@@ -1013,33 +1064,30 @@ private extension ViewController {
|
|
|
}
|
|
|
|
|
|
let content = makePaywallContent()
|
|
|
- let controller = NSViewController()
|
|
|
- controller.view = 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.isFloatingPanel = false
|
|
|
- panel.level = .normal
|
|
|
- panel.hidesOnDeactivate = true
|
|
|
- panel.isReleasedWhenClosed = false
|
|
|
- panel.delegate = self
|
|
|
- panel.standardWindowButton(.closeButton)?.isHidden = true
|
|
|
- panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
|
|
- panel.standardWindowButton(.zoomButton)?.isHidden = true
|
|
|
- panel.center()
|
|
|
- panel.contentViewController = controller
|
|
|
- panel.alphaValue = 0
|
|
|
- panel.makeKeyAndOrderFront(nil)
|
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
|
- paywallWindow = panel
|
|
|
- animatePaywallPresentation(panel)
|
|
|
+ let overlay = NSView()
|
|
|
+ overlay.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ overlay.wantsLayer = true
|
|
|
+ overlay.layer?.backgroundColor = palette.pageBackground.withAlphaComponent(0.98).cgColor
|
|
|
+ overlay.alphaValue = 0
|
|
|
+ overlay.addSubview(content)
|
|
|
+ view.addSubview(overlay, positioned: .above, relativeTo: nil)
|
|
|
+ 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),
|
|
|
+
|
|
|
+ content.leadingAnchor.constraint(equalTo: overlay.leadingAnchor),
|
|
|
+ content.trailingAnchor.constraint(equalTo: overlay.trailingAnchor),
|
|
|
+ content.topAnchor.constraint(equalTo: overlay.topAnchor),
|
|
|
+ content.bottomAnchor.constraint(equalTo: overlay.bottomAnchor)
|
|
|
+ ])
|
|
|
+ paywallOverlayView = overlay
|
|
|
+ NSAnimationContext.runAnimationGroup { context in
|
|
|
+ context.duration = 0.20
|
|
|
+ context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
|
|
|
+ overlay.animator().alphaValue = 1
|
|
|
+ }
|
|
|
|
|
|
Task { [weak self] in
|
|
|
guard let self else { return }
|
|
|
@@ -1071,6 +1119,12 @@ private extension ViewController {
|
|
|
}
|
|
|
|
|
|
@objc private func closePaywallClicked(_ sender: Any?) {
|
|
|
+ if let overlay = paywallOverlayView {
|
|
|
+ paywallOverlayView = nil
|
|
|
+ paywallUpgradeFlowEnabled = false
|
|
|
+ overlay.removeFromSuperview()
|
|
|
+ return
|
|
|
+ }
|
|
|
if let win = paywallWindow {
|
|
|
win.performClose(nil)
|
|
|
return
|
|
|
@@ -1085,20 +1139,47 @@ private extension ViewController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private func dismissPaywallIfPresented() {
|
|
|
+ if !Thread.isMainThread {
|
|
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
+ self?.dismissPaywallIfPresented()
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ closePaywallClicked(nil)
|
|
|
+ }
|
|
|
+
|
|
|
@objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
guard let view = sender.view else { return }
|
|
|
+ let action = paywallFooterActionByView[ObjectIdentifier(view)]
|
|
|
let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
|
|
|
- let defaultURL = (Bundle.main.object(forInfoDictionaryKey: "AppLaunchPlaceholderURL") as? String) ?? "https://example.com/app-link-coming-soon"
|
|
|
- let map: [String: String] = [
|
|
|
- "Privacy Policy": (Bundle.main.object(forInfoDictionaryKey: "PrivacyPolicyURL") as? String) ?? defaultURL,
|
|
|
- "Support": (Bundle.main.object(forInfoDictionaryKey: "SupportURL") as? String) ?? defaultURL,
|
|
|
- "Terms of Services": (Bundle.main.object(forInfoDictionaryKey: "TermsOfServiceURL") as? String) ?? defaultURL
|
|
|
- ]
|
|
|
- if let urlString = map[text], let url = URL(string: urlString) {
|
|
|
- openInAppBrowser(with: url, policy: inAppBrowserDefaultPolicy)
|
|
|
+ guard let action else {
|
|
|
+ showSimpleAlert(title: text, message: "\(text) tapped.")
|
|
|
return
|
|
|
}
|
|
|
- showSimpleAlert(title: text, message: "\(text) tapped.")
|
|
|
+ handlePaywallFooterAction(action)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func handlePaywallFooterAction(_ action: PaywallFooterAction) {
|
|
|
+ switch action {
|
|
|
+ case .manageSubscription:
|
|
|
+ openManageSubscriptions()
|
|
|
+ case .restorePurchase:
|
|
|
+ openRestoreSubscriptionPage()
|
|
|
+ case .continueWithFreePlan:
|
|
|
+ closePaywallClicked(nil)
|
|
|
+ case .privacyPolicy:
|
|
|
+ openSettingsLink(infoKey: "PrivacyPolicyURL")
|
|
|
+ case .support:
|
|
|
+ openSettingsLink(infoKey: "SupportURL")
|
|
|
+ case .termsOfServices:
|
|
|
+ openSettingsLink(infoKey: "TermsOfServiceURL")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func paywallFooterButtonPressed(_ sender: NSButton) {
|
|
|
+ guard let action = paywallFooterActionByView[ObjectIdentifier(sender)] else { return }
|
|
|
+ handlePaywallFooterAction(action)
|
|
|
}
|
|
|
|
|
|
@objc private func paywallPlanClicked(_ sender: NSClickGestureRecognizer) {
|
|
|
@@ -1141,8 +1222,13 @@ private extension ViewController {
|
|
|
if product.type == .nonConsumable {
|
|
|
return "\(pkrPrice) one-time purchase"
|
|
|
}
|
|
|
- if let period = product.subscription?.subscriptionPeriod {
|
|
|
- return "\(pkrPrice)/\(subscriptionUnitText(period.unit))"
|
|
|
+ if let subscription = product.subscription {
|
|
|
+ let billingText = "\(pkrPrice)/\(subscriptionUnitText(subscription.subscriptionPeriod.unit))"
|
|
|
+ if let introOffer = subscription.introductoryOffer,
|
|
|
+ introOffer.paymentMode == .freeTrial {
|
|
|
+ return "Free for \(subscriptionPeriodText(introOffer.period)), then \(billingText)"
|
|
|
+ }
|
|
|
+ return billingText
|
|
|
}
|
|
|
return pkrPrice
|
|
|
}
|
|
|
@@ -1150,7 +1236,7 @@ private extension ViewController {
|
|
|
case .weekly:
|
|
|
return "PKR 1,100.00/week"
|
|
|
case .monthly:
|
|
|
- return "Free for 3 Days then PKR 2,500.00/month"
|
|
|
+ return "PKR 2,500.00/month (3 days free trial)"
|
|
|
case .yearly:
|
|
|
return "PKR 9,900.00/year (about 190.38/week)"
|
|
|
case .lifetime:
|
|
|
@@ -1177,6 +1263,22 @@ private extension ViewController {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private func subscriptionPeriodText(_ period: Product.SubscriptionPeriod) -> String {
|
|
|
+ let unit = subscriptionUnitText(period.unit)
|
|
|
+ if period.value == 1 {
|
|
|
+ return "1 \(unit)"
|
|
|
+ }
|
|
|
+ return "\(period.value) \(unit)s"
|
|
|
+ }
|
|
|
+
|
|
|
+ private func freeTrialPackageText(for product: Product) -> String? {
|
|
|
+ guard let introOffer = product.subscription?.introductoryOffer,
|
|
|
+ introOffer.paymentMode == .freeTrial else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ return "\(subscriptionPeriodText(introOffer.period)) free trial"
|
|
|
+ }
|
|
|
+
|
|
|
private func startStoreKit() {
|
|
|
storeKitStartupTask?.cancel()
|
|
|
storeKitStartupTask = Task { [weak self] in
|
|
|
@@ -1198,8 +1300,17 @@ private extension ViewController {
|
|
|
for (plan, label) in paywallSubtitleLabels {
|
|
|
let productID = PremiumStoreProduct.productID(for: plan)
|
|
|
guard let product = storeKitCoordinator.productsByID[productID],
|
|
|
- let period = product.subscription?.subscriptionPeriod else { continue }
|
|
|
- label.stringValue = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
|
|
|
+ let period = product.subscription?.subscriptionPeriod else {
|
|
|
+ // Show neutral fallback text when subscription metadata isn't available.
|
|
|
+ label.stringValue = "Billed via App Store"
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ let recurringText = "\(pkrDisplayPrice(product.displayPrice))/\(subscriptionUnitText(period.unit))"
|
|
|
+ if let trialText = freeTrialPackageText(for: product) {
|
|
|
+ label.stringValue = "\(recurringText) • \(trialText)"
|
|
|
+ } else {
|
|
|
+ label.stringValue = recurringText
|
|
|
+ }
|
|
|
}
|
|
|
refreshSidebarPremiumButton()
|
|
|
refreshInstantMeetPremiumState()
|
|
|
@@ -1236,6 +1347,7 @@ private extension ViewController {
|
|
|
}
|
|
|
|
|
|
if !hadPremiumAccess && hasPremiumAccess {
|
|
|
+ dismissPaywallIfPresented()
|
|
|
if selectedSidebarPage != .joinMeetings {
|
|
|
Task { [weak self] in
|
|
|
await self?.loadSchedule()
|
|
|
@@ -1419,7 +1531,7 @@ private extension ViewController {
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
- if paywallUpgradeFlowEnabled && storeKitCoordinator.hasPremiumAccess && !confirmPremiumUpgrade() {
|
|
|
+ if paywallUpgradeFlowEnabled && storeKitCoordinator.hasPremiumAccess && !confirmPremiumUpgrade(for: selectedPremiumPlan) {
|
|
|
return
|
|
|
}
|
|
|
paywallPurchaseTask?.cancel()
|
|
|
@@ -1437,7 +1549,7 @@ private extension ViewController {
|
|
|
await self?.loadSchedule()
|
|
|
}
|
|
|
self.showSimpleAlert(title: "Purchase Complete", message: "Premium has been unlocked successfully.")
|
|
|
- self.paywallWindow?.performClose(nil)
|
|
|
+ self.dismissPaywallIfPresented()
|
|
|
self.scheduleRatingPromptAfterPremiumUpgrade()
|
|
|
case .cancelled:
|
|
|
break
|
|
|
@@ -2568,8 +2680,10 @@ private extension ViewController {
|
|
|
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
contentStack.orientation = .vertical
|
|
|
contentStack.spacing = 12
|
|
|
- contentStack.alignment = .leading
|
|
|
+ contentStack.distribution = .fill
|
|
|
+ contentStack.alignment = .centerX
|
|
|
panel.addSubview(contentStack)
|
|
|
+ let paywallLayoutWidth: CGFloat = 980
|
|
|
|
|
|
let topRow = NSStackView()
|
|
|
topRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -2577,73 +2691,109 @@ private extension ViewController {
|
|
|
topRow.alignment = .centerY
|
|
|
topRow.distribution = .fill
|
|
|
topRow.spacing = 10
|
|
|
- topRow.addArrangedSubview(textLabel("Get Premium", font: NSFont.systemFont(ofSize: 24, weight: .bold), color: palette.textPrimary))
|
|
|
- let topSpacer = NSView()
|
|
|
- topSpacer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- topRow.addArrangedSubview(topSpacer)
|
|
|
- let closeButton = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
|
|
|
- closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- closeButton.isBordered = false
|
|
|
- closeButton.bezelStyle = .regularSquare
|
|
|
- closeButton.wantsLayer = true
|
|
|
- closeButton.layer?.cornerRadius = 14
|
|
|
- closeButton.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
- closeButton.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
- closeButton.layer?.borderWidth = 1
|
|
|
- closeButton.font = typography.iconButton
|
|
|
- closeButton.contentTintColor = palette.textSecondary
|
|
|
- closeButton.widthAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
- closeButton.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
- closeButton.onHoverChanged = { [weak closeButton, weak self] hovering in
|
|
|
- guard let closeButton, let self else { return }
|
|
|
- let base = self.palette.inputBackground
|
|
|
- let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
- let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
|
- closeButton.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
|
- closeButton.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
+ topRow.addArrangedSubview(textLabel("Meetings Pro", font: NSFont.systemFont(ofSize: 27, weight: .bold), color: palette.textPrimary))
|
|
|
+ var closeButton: HoverButton?
|
|
|
+ if storeKitCoordinator.hasPremiumAccess {
|
|
|
+ let button = HoverButton(title: "✕", target: self, action: #selector(closePaywallClicked(_:)))
|
|
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ button.isBordered = false
|
|
|
+ button.bezelStyle = .regularSquare
|
|
|
+ button.wantsLayer = true
|
|
|
+ button.layer?.cornerRadius = 14
|
|
|
+ button.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
+ button.layer?.borderColor = palette.inputBorder.cgColor
|
|
|
+ button.layer?.borderWidth = 1
|
|
|
+ button.font = typography.iconButton
|
|
|
+ button.contentTintColor = palette.textSecondary
|
|
|
+ button.widthAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
+ button.heightAnchor.constraint(equalToConstant: 28).isActive = true
|
|
|
+ button.onHoverChanged = { [weak button, weak self] hovering in
|
|
|
+ guard let button, let self else { return }
|
|
|
+ let base = self.palette.inputBackground
|
|
|
+ let hoverBlend = self.darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
+ let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
|
+ button.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
|
+ button.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
+ }
|
|
|
+ panel.addSubview(button)
|
|
|
+ closeButton = button
|
|
|
}
|
|
|
- topRow.addArrangedSubview(closeButton)
|
|
|
- topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
contentStack.addArrangedSubview(topRow)
|
|
|
+ topRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
+
|
|
|
+ let hero = roundedContainer(cornerRadius: 16, color: palette.sectionCard)
|
|
|
+ hero.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ styleSurface(hero, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
+ contentStack.addArrangedSubview(hero)
|
|
|
+ hero.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
+
|
|
|
+ let heroBadge = textLabel("PREMIUM EXPERIENCE", font: NSFont.systemFont(ofSize: 10, weight: .bold), color: NSColor(calibratedRed: 0.42, green: 0.93, blue: 0.70, alpha: 1))
|
|
|
+ hero.addSubview(heroBadge)
|
|
|
+ let heroTitle = textLabel("Elevate your meetings workflow", font: NSFont.systemFont(ofSize: 22, weight: .bold), color: palette.textPrimary)
|
|
|
+ let heroSubtitle = textLabel("Unlock automation, premium meeting tools, and priority support for every session.", font: NSFont.systemFont(ofSize: 13, weight: .medium), color: palette.textSecondary)
|
|
|
+ heroTitle.maximumNumberOfLines = 2
|
|
|
+ heroSubtitle.maximumNumberOfLines = 3
|
|
|
+ hero.addSubview(heroTitle)
|
|
|
+ hero.addSubview(heroSubtitle)
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ heroBadge.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
|
|
|
+ heroBadge.topAnchor.constraint(equalTo: hero.topAnchor, constant: 14),
|
|
|
+ heroTitle.leadingAnchor.constraint(equalTo: hero.leadingAnchor, constant: 16),
|
|
|
+ heroTitle.trailingAnchor.constraint(lessThanOrEqualTo: hero.trailingAnchor, constant: -16),
|
|
|
+ heroTitle.topAnchor.constraint(equalTo: heroBadge.bottomAnchor, constant: 10),
|
|
|
+ heroSubtitle.leadingAnchor.constraint(equalTo: heroTitle.leadingAnchor),
|
|
|
+ heroSubtitle.trailingAnchor.constraint(equalTo: hero.trailingAnchor, constant: -16),
|
|
|
+ heroSubtitle.topAnchor.constraint(equalTo: heroTitle.bottomAnchor, constant: 8),
|
|
|
+ heroSubtitle.bottomAnchor.constraint(equalTo: hero.bottomAnchor, constant: -16)
|
|
|
+ ])
|
|
|
|
|
|
- contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textSecondary))
|
|
|
- let benefits = paywallBenefitsSection()
|
|
|
- contentStack.addArrangedSubview(benefits)
|
|
|
- contentStack.setCustomSpacing(18, after: benefits)
|
|
|
+ let benefitsRow = NSStackView()
|
|
|
+ benefitsRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ benefitsRow.orientation = .horizontal
|
|
|
+ benefitsRow.spacing = 8
|
|
|
+ benefitsRow.distribution = .fillEqually
|
|
|
+ benefitsRow.alignment = .centerY
|
|
|
+ benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage meetings"))
|
|
|
+ benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🧠", text: "Smart scheduling"))
|
|
|
+ benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Faster workflow"))
|
|
|
+ benefitsRow.addArrangedSubview(paywallBenefitItem(icon: "🔔", text: "Reminder notifications"))
|
|
|
+ contentStack.addArrangedSubview(benefitsRow)
|
|
|
+ benefitsRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
+
|
|
|
+ let plansRow = NSStackView()
|
|
|
+ plansRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ plansRow.orientation = .horizontal
|
|
|
+ plansRow.spacing = 10
|
|
|
+ plansRow.distribution = .fillEqually
|
|
|
+ plansRow.alignment = .centerY
|
|
|
|
|
|
let weeklyCard = paywallPlanCard(
|
|
|
title: "Weekly",
|
|
|
price: "PKR 1,100.00",
|
|
|
- badge: "Basic Deal",
|
|
|
+ badge: "Basic",
|
|
|
badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
|
|
|
subtitle: nil,
|
|
|
plan: .weekly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- contentStack.addArrangedSubview(weeklyCard)
|
|
|
-
|
|
|
let monthlyCard = paywallPlanCard(
|
|
|
title: "Monthly",
|
|
|
price: "PKR 2,500.00",
|
|
|
- badge: "Free Trial",
|
|
|
+ badge: "Popular",
|
|
|
badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
|
|
|
- subtitle: "625.00/week",
|
|
|
+ subtitle: "3 days free trial",
|
|
|
plan: .monthly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- contentStack.addArrangedSubview(monthlyCard)
|
|
|
-
|
|
|
let yearlyCard = paywallPlanCard(
|
|
|
title: "Yearly",
|
|
|
price: "PKR 9,900.00",
|
|
|
- badge: "Best Deal",
|
|
|
+ badge: "Best Value",
|
|
|
badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
|
|
|
subtitle: "190.38/week",
|
|
|
plan: .yearly,
|
|
|
strikePrice: nil
|
|
|
)
|
|
|
- contentStack.addArrangedSubview(yearlyCard)
|
|
|
-
|
|
|
let lifetimeCard = paywallPlanCard(
|
|
|
title: "Lifetime",
|
|
|
price: "PKR 14,900.00",
|
|
|
@@ -2653,9 +2803,25 @@ private extension ViewController {
|
|
|
plan: .lifetime,
|
|
|
strikePrice: "PKR 29,800.00"
|
|
|
)
|
|
|
- contentStack.addArrangedSubview(lifetimeCard)
|
|
|
+ plansRow.addArrangedSubview(weeklyCard)
|
|
|
+ plansRow.addArrangedSubview(monthlyCard)
|
|
|
+ plansRow.addArrangedSubview(yearlyCard)
|
|
|
+ plansRow.addArrangedSubview(lifetimeCard)
|
|
|
+ contentStack.addArrangedSubview(plansRow)
|
|
|
+ plansRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
updatePaywallPlanSelection()
|
|
|
- contentStack.setCustomSpacing(20, after: lifetimeCard)
|
|
|
+
|
|
|
+ let trustRow = NSStackView()
|
|
|
+ trustRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ trustRow.orientation = .horizontal
|
|
|
+ trustRow.spacing = 8
|
|
|
+ trustRow.distribution = .fillEqually
|
|
|
+ trustRow.alignment = .centerY
|
|
|
+ trustRow.addArrangedSubview(paywallMetaItem(title: "Cancel anytime", subtitle: "No lock-in"))
|
|
|
+ trustRow.addArrangedSubview(paywallMetaItem(title: "Instant access", subtitle: "Unlock all tools"))
|
|
|
+ trustRow.addArrangedSubview(paywallMetaItem(title: "Secure billing", subtitle: "Handled by Apple"))
|
|
|
+ contentStack.addArrangedSubview(trustRow)
|
|
|
+ trustRow.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
|
|
|
let offer = textLabel(paywallOfferText(for: selectedPremiumPlan), font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
|
|
|
offer.alignment = .center
|
|
|
@@ -2663,26 +2829,24 @@ private extension ViewController {
|
|
|
let offerWrap = NSView()
|
|
|
offerWrap.translatesAutoresizingMaskIntoConstraints = false
|
|
|
offerWrap.addSubview(offer)
|
|
|
+ contentStack.addArrangedSubview(offerWrap)
|
|
|
+ offerWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
NSLayoutConstraint.activate([
|
|
|
- offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
|
|
|
offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
|
|
|
- offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
|
|
|
+ offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 4),
|
|
|
offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
|
|
|
])
|
|
|
- contentStack.addArrangedSubview(offerWrap)
|
|
|
- contentStack.setCustomSpacing(18, after: offerWrap)
|
|
|
|
|
|
let continueButton = HoverButton(title: "", target: self, action: #selector(paywallContinueClicked(_:)))
|
|
|
continueButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
continueButton.isBordered = false
|
|
|
continueButton.bezelStyle = .regularSquare
|
|
|
continueButton.wantsLayer = true
|
|
|
- continueButton.layer?.cornerRadius = 14
|
|
|
+ continueButton.layer?.cornerRadius = 12
|
|
|
continueButton.layer?.backgroundColor = palette.primaryBlue.cgColor
|
|
|
- continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
|
- continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
+ continueButton.heightAnchor.constraint(equalToConstant: 36).isActive = true
|
|
|
styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
|
|
|
- let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
|
|
|
+ let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 14, weight: .bold), color: .white)
|
|
|
continueButton.addSubview(continueLabel)
|
|
|
NSLayoutConstraint.activate([
|
|
|
continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
|
|
|
@@ -2698,31 +2862,36 @@ private extension ViewController {
|
|
|
paywallContinueButton = continueButton
|
|
|
paywallContinueLabel = continueLabel
|
|
|
contentStack.addArrangedSubview(continueButton)
|
|
|
- contentStack.setCustomSpacing(16, after: continueButton)
|
|
|
+ continueButton.widthAnchor.constraint(equalToConstant: 360).isActive = true
|
|
|
|
|
|
let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
|
|
|
secure.alignment = .center
|
|
|
let secureWrap = NSView()
|
|
|
secureWrap.translatesAutoresizingMaskIntoConstraints = false
|
|
|
secureWrap.addSubview(secure)
|
|
|
+ contentStack.addArrangedSubview(secureWrap)
|
|
|
+ secureWrap.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
NSLayoutConstraint.activate([
|
|
|
- secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
|
|
|
secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
|
|
|
- secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
|
|
|
- secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
|
|
|
+ secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 2),
|
|
|
+ secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -4)
|
|
|
])
|
|
|
- contentStack.addArrangedSubview(secureWrap)
|
|
|
- contentStack.setCustomSpacing(16, after: secureWrap)
|
|
|
|
|
|
let footer = paywallFooterLinks()
|
|
|
contentStack.addArrangedSubview(footer)
|
|
|
+ footer.widthAnchor.constraint(equalTo: contentStack.widthAnchor).isActive = true
|
|
|
|
|
|
- NSLayoutConstraint.activate([
|
|
|
- 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)
|
|
|
- ])
|
|
|
+ var panelConstraints: [NSLayoutConstraint] = [
|
|
|
+ contentStack.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
|
|
|
+ contentStack.widthAnchor.constraint(equalToConstant: paywallLayoutWidth),
|
|
|
+ contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 80),
|
|
|
+ contentStack.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -12)
|
|
|
+ ]
|
|
|
+ if let closeButton {
|
|
|
+ panelConstraints.append(closeButton.topAnchor.constraint(equalTo: panel.topAnchor, constant: 30))
|
|
|
+ panelConstraints.append(closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -30))
|
|
|
+ }
|
|
|
+ NSLayoutConstraint.activate(panelConstraints)
|
|
|
|
|
|
refreshPaywallStoreUI()
|
|
|
return panel
|
|
|
@@ -2743,8 +2912,8 @@ private extension ViewController {
|
|
|
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.widthAnchor.constraint(greaterThanOrEqualToConstant: 160).isActive = true
|
|
|
+ wrapper.heightAnchor.constraint(equalToConstant: 162).isActive = true
|
|
|
wrapper.tag = plan.rawValue
|
|
|
|
|
|
let card = HoverTrackingView()
|
|
|
@@ -2752,7 +2921,7 @@ private extension ViewController {
|
|
|
card.wantsLayer = true
|
|
|
card.layer?.cornerRadius = 16
|
|
|
card.layer?.backgroundColor = palette.sectionCard.cgColor
|
|
|
- card.heightAnchor.constraint(equalToConstant: 82).isActive = true
|
|
|
+ card.heightAnchor.constraint(equalToConstant: 150).isActive = true
|
|
|
wrapper.addSubview(card)
|
|
|
NSLayoutConstraint.activate([
|
|
|
card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
|
|
|
@@ -2788,14 +2957,15 @@ private extension ViewController {
|
|
|
paywallPriceLabels[plan] = priceLabel
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
- badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
+ badgeWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
|
|
|
badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
|
|
|
|
|
|
- titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
|
|
|
+ titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 12),
|
|
|
titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
|
|
|
|
|
|
- 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: 10),
|
|
|
+ priceLabel.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
|
|
|
])
|
|
|
|
|
|
if let subtitle {
|
|
|
@@ -2803,8 +2973,9 @@ private extension ViewController {
|
|
|
card.addSubview(sub)
|
|
|
paywallSubtitleLabels[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)
|
|
|
])
|
|
|
}
|
|
|
|
|
|
@@ -2812,8 +2983,9 @@ private extension ViewController {
|
|
|
let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
|
|
|
card.addSubview(strike)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- strike.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
|
|
|
- strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4)
|
|
|
+ strike.leadingAnchor.constraint(equalTo: priceLabel.leadingAnchor),
|
|
|
+ strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4),
|
|
|
+ strike.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -12)
|
|
|
])
|
|
|
}
|
|
|
|
|
|
@@ -2830,8 +3002,8 @@ private extension ViewController {
|
|
|
func paywallFooterLinks() -> NSView {
|
|
|
let wrap = NSView()
|
|
|
wrap.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
|
|
|
- wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
|
|
|
+ wrap.heightAnchor.constraint(equalToConstant: 40).isActive = true
|
|
|
+ paywallFooterActionByView.removeAll()
|
|
|
|
|
|
let row = NSStackView()
|
|
|
row.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -2841,9 +3013,15 @@ private extension ViewController {
|
|
|
row.spacing = 0
|
|
|
wrap.addSubview(row)
|
|
|
|
|
|
- row.addArrangedSubview(footerLink("Privacy Policy"))
|
|
|
- row.addArrangedSubview(footerLink("Support"))
|
|
|
- row.addArrangedSubview(footerLink("Terms of Services"))
|
|
|
+ if storeKitCoordinator.hasPremiumAccess {
|
|
|
+ row.addArrangedSubview(footerLink("Manage Subscription", action: .manageSubscription))
|
|
|
+ row.addArrangedSubview(footerLink("Restore Purchase", action: .restorePurchase))
|
|
|
+ } else {
|
|
|
+ row.addArrangedSubview(footerLink("Continue with free plan", action: .continueWithFreePlan))
|
|
|
+ }
|
|
|
+ row.addArrangedSubview(footerLink("Privacy Policy", action: .privacyPolicy))
|
|
|
+ row.addArrangedSubview(footerLink("Support", action: .support))
|
|
|
+ row.addArrangedSubview(footerLink("Terms of Services", action: .termsOfServices))
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
row.leadingAnchor.constraint(equalTo: wrap.leadingAnchor),
|
|
|
@@ -2855,25 +3033,54 @@ private extension ViewController {
|
|
|
return wrap
|
|
|
}
|
|
|
|
|
|
- func footerLink(_ title: String) -> NSView {
|
|
|
- let container = HoverTrackingView()
|
|
|
- container.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- let label = textLabel(title, font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
|
|
|
- label.alignment = .center
|
|
|
- container.addSubview(label)
|
|
|
+ private func footerLink(_ title: String, action: PaywallFooterAction) -> NSView {
|
|
|
+ let button = HoverButton(title: title, target: self, action: #selector(paywallFooterButtonPressed(_:)))
|
|
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ button.isBordered = false
|
|
|
+ button.bezelStyle = .regularSquare
|
|
|
+ button.focusRingType = .none
|
|
|
+ button.font = NSFont.systemFont(ofSize: 12, weight: .semibold)
|
|
|
+ button.alignment = .center
|
|
|
+ button.setButtonType(.momentaryChange)
|
|
|
+ button.contentTintColor = palette.textSecondary
|
|
|
+ paywallFooterActionByView[ObjectIdentifier(button)] = action
|
|
|
+ button.onHoverChanged = { [weak self, weak button] hovering in
|
|
|
+ guard let self, let button else { return }
|
|
|
+ button.contentTintColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
+ }
|
|
|
+ button.onHoverChanged?(false)
|
|
|
+ return button
|
|
|
+ }
|
|
|
+
|
|
|
+ func paywallMetaItem(title: String, subtitle: String) -> NSView {
|
|
|
+ let card = HoverTrackingView()
|
|
|
+ card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ card.wantsLayer = true
|
|
|
+ card.layer?.cornerRadius = 10
|
|
|
+ card.layer?.backgroundColor = palette.inputBackground.cgColor
|
|
|
+ card.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
|
|
+ styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
|
|
|
|
+ let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 11, weight: .semibold), color: palette.textPrimary)
|
|
|
+ let subtitleLabel = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .medium), color: palette.textSecondary)
|
|
|
+ card.addSubview(titleLabel)
|
|
|
+ card.addSubview(subtitleLabel)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
|
- label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
|
|
|
+ titleLabel.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
+ titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 7),
|
|
|
+ subtitleLabel.centerXAnchor.constraint(equalTo: card.centerXAnchor),
|
|
|
+ subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 1),
|
|
|
+ subtitleLabel.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -6)
|
|
|
])
|
|
|
|
|
|
- let click = NSClickGestureRecognizer(target: self, action: #selector(paywallFooterLinkClicked(_:)))
|
|
|
- container.addGestureRecognizer(click)
|
|
|
- container.onHoverChanged = { hovering in
|
|
|
- label.textColor = hovering ? (self.darkModeEnabled ? .white : self.palette.textPrimary) : self.palette.textSecondary
|
|
|
+ let base = palette.inputBackground
|
|
|
+ let hoverBlend = darkModeEnabled ? NSColor.white : NSColor.black
|
|
|
+ let hover = base.blended(withFraction: 0.10, of: hoverBlend) ?? base
|
|
|
+ card.onHoverChanged = { [weak card] hovering in
|
|
|
+ card?.layer?.backgroundColor = (hovering ? hover : base).cgColor
|
|
|
}
|
|
|
- container.onHoverChanged?(false)
|
|
|
- return container
|
|
|
+ card.onHoverChanged?(false)
|
|
|
+ return card
|
|
|
}
|
|
|
|
|
|
func paywallBenefitsSection() -> NSView {
|