Ei kuvausta

PaywallView.swift 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. import Cocoa
  2. // MARK: - Plan Model
  3. enum PaywallPlan: CaseIterable {
  4. case monthly
  5. case yearly
  6. case lifetime
  7. var title: String {
  8. switch self {
  9. case .monthly: "Monthly"
  10. case .yearly: "Yearly"
  11. case .lifetime: "Lifetime"
  12. }
  13. }
  14. var subtitle: String {
  15. switch self {
  16. case .monthly: "$4.99 / month, cancel anytime"
  17. case .yearly: "Eligible new subscribers get 7 days free, then $29.99 / year"
  18. case .lifetime: "$99.99 once, lifetime access"
  19. }
  20. }
  21. var price: String {
  22. switch self {
  23. case .monthly: "$4.99"
  24. case .yearly: "$29.99"
  25. case .lifetime: "$99.99"
  26. }
  27. }
  28. var ctaTitle: String {
  29. switch self {
  30. case .monthly: "Subscribe for $4.99 / Month"
  31. case .yearly: "Start 7-Day Free Trial"
  32. case .lifetime: "Buy Lifetime Access"
  33. }
  34. }
  35. }
  36. // MARK: - Left Panel
  37. private final class PaywallLeftPanelView: NSView {
  38. private let gradientLayer = CAGradientLayer()
  39. override init(frame frameRect: NSRect) {
  40. super.init(frame: frameRect)
  41. wantsLayer = true
  42. gradientLayer.colors = [
  43. NSColor(red: 0.88, green: 0.94, blue: 1.0, alpha: 1).cgColor,
  44. NSColor(red: 0.95, green: 0.97, blue: 1.0, alpha: 1).cgColor,
  45. ]
  46. gradientLayer.startPoint = CGPoint(x: 0.5, y: 1)
  47. gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
  48. layer?.insertSublayer(gradientLayer, at: 0)
  49. }
  50. @available(*, unavailable)
  51. required init?(coder: NSCoder) { nil }
  52. override func layout() {
  53. super.layout()
  54. gradientLayer.frame = bounds
  55. let mask = CAShapeLayer()
  56. mask.path = CGPath(
  57. roundedRect: bounds,
  58. cornerWidth: 20,
  59. cornerHeight: 20,
  60. transform: nil
  61. )
  62. layer?.mask = mask
  63. }
  64. }
  65. // MARK: - Badge
  66. private final class PaywallBadgeView: NSView {
  67. init(text: String, iconName: String, background: NSColor, foreground: NSColor) {
  68. super.init(frame: .zero)
  69. translatesAutoresizingMaskIntoConstraints = false
  70. wantsLayer = true
  71. layer?.backgroundColor = background.cgColor
  72. layer?.cornerRadius = 10
  73. layer?.masksToBounds = true
  74. let icon = NSImageView()
  75. icon.translatesAutoresizingMaskIntoConstraints = false
  76. if let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) {
  77. let config = NSImage.SymbolConfiguration(pointSize: 9, weight: .semibold)
  78. icon.image = image.withSymbolConfiguration(config)
  79. }
  80. icon.contentTintColor = foreground
  81. let label = NSTextField(labelWithString: text)
  82. label.font = AppTheme.semiboldFont(size: 10)
  83. label.textColor = foreground
  84. label.translatesAutoresizingMaskIntoConstraints = false
  85. addSubview(icon)
  86. addSubview(label)
  87. NSLayoutConstraint.activate([
  88. heightAnchor.constraint(equalToConstant: 20),
  89. icon.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
  90. icon.centerYAnchor.constraint(equalTo: centerYAnchor),
  91. icon.widthAnchor.constraint(equalToConstant: 12),
  92. icon.heightAnchor.constraint(equalToConstant: 12),
  93. label.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 4),
  94. label.centerYAnchor.constraint(equalTo: centerYAnchor),
  95. label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
  96. ])
  97. }
  98. @available(*, unavailable)
  99. required init?(coder: NSCoder) { nil }
  100. }
  101. // MARK: - Feature Row
  102. private final class PaywallFeatureRow: NSView {
  103. init(text: String) {
  104. super.init(frame: .zero)
  105. translatesAutoresizingMaskIntoConstraints = false
  106. let checkContainer = NSView()
  107. checkContainer.translatesAutoresizingMaskIntoConstraints = false
  108. checkContainer.wantsLayer = true
  109. checkContainer.layer?.backgroundColor = AppTheme.green.cgColor
  110. checkContainer.layer?.cornerRadius = 10
  111. checkContainer.layer?.masksToBounds = true
  112. let checkIcon = NSImageView()
  113. checkIcon.translatesAutoresizingMaskIntoConstraints = false
  114. if let image = NSImage(systemSymbolName: "checkmark", accessibilityDescription: nil) {
  115. let config = NSImage.SymbolConfiguration(pointSize: 9, weight: .bold)
  116. checkIcon.image = image.withSymbolConfiguration(config)
  117. }
  118. checkIcon.contentTintColor = .white
  119. let label = NSTextField(labelWithString: text)
  120. label.font = AppTheme.regularFont(size: 14)
  121. label.textColor = AppTheme.navy
  122. label.translatesAutoresizingMaskIntoConstraints = false
  123. addSubview(checkContainer)
  124. checkContainer.addSubview(checkIcon)
  125. addSubview(label)
  126. NSLayoutConstraint.activate([
  127. heightAnchor.constraint(equalToConstant: 28),
  128. checkContainer.leadingAnchor.constraint(equalTo: leadingAnchor),
  129. checkContainer.centerYAnchor.constraint(equalTo: centerYAnchor),
  130. checkContainer.widthAnchor.constraint(equalToConstant: 20),
  131. checkContainer.heightAnchor.constraint(equalToConstant: 20),
  132. checkIcon.centerXAnchor.constraint(equalTo: checkContainer.centerXAnchor),
  133. checkIcon.centerYAnchor.constraint(equalTo: checkContainer.centerYAnchor),
  134. checkIcon.widthAnchor.constraint(equalToConstant: 12),
  135. checkIcon.heightAnchor.constraint(equalToConstant: 12),
  136. label.leadingAnchor.constraint(equalTo: checkContainer.trailingAnchor, constant: 12),
  137. label.centerYAnchor.constraint(equalTo: centerYAnchor),
  138. label.trailingAnchor.constraint(equalTo: trailingAnchor),
  139. ])
  140. }
  141. @available(*, unavailable)
  142. required init?(coder: NSCoder) { nil }
  143. }
  144. // MARK: - Plan Card
  145. private final class PaywallPlanCard: NSControl {
  146. var onSelect: (() -> Void)?
  147. private let plan: PaywallPlan
  148. private let titleLabel = NSTextField(labelWithString: "")
  149. private let subtitleLabel = NSTextField(labelWithString: "")
  150. private let priceLabel = NSTextField(labelWithString: "")
  151. private var badgeView: PaywallBadgeView?
  152. var isChosen: Bool = false {
  153. didSet { updateAppearance() }
  154. }
  155. init(plan: PaywallPlan) {
  156. self.plan = plan
  157. super.init(frame: .zero)
  158. translatesAutoresizingMaskIntoConstraints = false
  159. wantsLayer = true
  160. layer?.cornerRadius = 12
  161. layer?.backgroundColor = NSColor.white.cgColor
  162. layer?.masksToBounds = false
  163. titleLabel.stringValue = plan.title
  164. titleLabel.font = AppTheme.semiboldFont(size: 15)
  165. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  166. subtitleLabel.stringValue = plan.subtitle
  167. subtitleLabel.font = AppTheme.regularFont(size: 11)
  168. subtitleLabel.textColor = AppTheme.textSecondary
  169. subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
  170. priceLabel.stringValue = plan.price
  171. priceLabel.font = AppTheme.semiboldFont(size: 15)
  172. priceLabel.alignment = .right
  173. priceLabel.translatesAutoresizingMaskIntoConstraints = false
  174. addSubview(titleLabel)
  175. addSubview(subtitleLabel)
  176. addSubview(priceLabel)
  177. NSLayoutConstraint.activate([
  178. heightAnchor.constraint(equalToConstant: 86),
  179. titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
  180. titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 24),
  181. subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  182. subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3),
  183. subtitleLabel.trailingAnchor.constraint(lessThanOrEqualTo: priceLabel.leadingAnchor, constant: -12),
  184. priceLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
  185. priceLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
  186. ])
  187. if plan == .yearly {
  188. let badge = PaywallBadgeView(
  189. text: "7 Days Free Trial",
  190. iconName: "calendar",
  191. background: AppTheme.paywallPink,
  192. foreground: AppTheme.paywallPinkText
  193. )
  194. badgeView = badge
  195. addSubview(badge)
  196. NSLayoutConstraint.activate([
  197. badge.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
  198. badge.topAnchor.constraint(equalTo: topAnchor, constant: 8),
  199. ])
  200. } else if plan == .lifetime {
  201. let badge = PaywallBadgeView(
  202. text: "Best Value",
  203. iconName: "star.fill",
  204. background: AppTheme.paywallGold,
  205. foreground: AppTheme.paywallGoldText
  206. )
  207. badgeView = badge
  208. addSubview(badge)
  209. NSLayoutConstraint.activate([
  210. badge.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
  211. badge.topAnchor.constraint(equalTo: topAnchor, constant: 8),
  212. ])
  213. }
  214. updateAppearance()
  215. }
  216. @available(*, unavailable)
  217. required init?(coder: NSCoder) { nil }
  218. private func updateAppearance() {
  219. let titleColor = isChosen && plan == .yearly ? AppTheme.green : AppTheme.navy
  220. titleLabel.textColor = titleColor
  221. priceLabel.textColor = titleColor
  222. layer?.borderWidth = isChosen ? 2 : 1
  223. layer?.borderColor = (isChosen ? AppTheme.green : AppTheme.paywallBorder).cgColor
  224. }
  225. override func mouseUp(with event: NSEvent) {
  226. guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
  227. onSelect?()
  228. }
  229. override func resetCursorRects() {
  230. addCursorRect(bounds, cursor: .pointingHand)
  231. }
  232. }
  233. // MARK: - Close Button
  234. private final class PaywallCloseButton: NSButton {
  235. var onClose: (() -> Void)?
  236. init() {
  237. super.init(frame: .zero)
  238. isBordered = false
  239. translatesAutoresizingMaskIntoConstraints = false
  240. wantsLayer = true
  241. layer?.backgroundColor = AppTheme.blueLight.cgColor
  242. layer?.cornerRadius = 14
  243. if let image = NSImage(systemSymbolName: "xmark", accessibilityDescription: "Close") {
  244. let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
  245. self.image = image.withSymbolConfiguration(config)
  246. }
  247. contentTintColor = AppTheme.blue
  248. target = self
  249. action = #selector(tapped)
  250. }
  251. @available(*, unavailable)
  252. required init?(coder: NSCoder) { nil }
  253. @objc private func tapped() {
  254. onClose?()
  255. }
  256. override func resetCursorRects() {
  257. addCursorRect(bounds, cursor: .pointingHand)
  258. }
  259. }
  260. // MARK: - Footer Link
  261. private final class PaywallFooterLink: NSButton {
  262. init(title: String) {
  263. super.init(frame: .zero)
  264. self.title = title
  265. isBordered = false
  266. font = AppTheme.regularFont(size: 11)
  267. contentTintColor = AppTheme.textSecondary
  268. translatesAutoresizingMaskIntoConstraints = false
  269. }
  270. @available(*, unavailable)
  271. required init?(coder: NSCoder) { nil }
  272. override func resetCursorRects() {
  273. addCursorRect(bounds, cursor: .pointingHand)
  274. }
  275. }
  276. // MARK: - Main Paywall Card
  277. final class PaywallView: NSView {
  278. var onClose: (() -> Void)?
  279. var onPurchase: ((PaywallPlan) -> Void)?
  280. var onRestore: (() -> Void)?
  281. private var selectedPlan: PaywallPlan = .yearly
  282. private var planCards: [PaywallPlan: PaywallPlanCard] = [:]
  283. private let ctaButton = NSButton()
  284. init() {
  285. super.init(frame: .zero)
  286. translatesAutoresizingMaskIntoConstraints = false
  287. wantsLayer = true
  288. layer?.backgroundColor = NSColor.white.cgColor
  289. layer?.cornerRadius = 0
  290. setup()
  291. }
  292. @available(*, unavailable)
  293. required init?(coder: NSCoder) { nil }
  294. private func setup() {
  295. let leftPanel = makeLeftPanel()
  296. let rightPanel = makeRightPanel()
  297. addSubview(leftPanel)
  298. addSubview(rightPanel)
  299. NSLayoutConstraint.activate([
  300. leftPanel.leadingAnchor.constraint(equalTo: leadingAnchor),
  301. leftPanel.topAnchor.constraint(equalTo: topAnchor),
  302. leftPanel.bottomAnchor.constraint(equalTo: bottomAnchor),
  303. leftPanel.widthAnchor.constraint(equalToConstant: 320),
  304. rightPanel.leadingAnchor.constraint(equalTo: leftPanel.trailingAnchor),
  305. rightPanel.trailingAnchor.constraint(equalTo: trailingAnchor),
  306. rightPanel.topAnchor.constraint(equalTo: topAnchor),
  307. rightPanel.bottomAnchor.constraint(equalTo: bottomAnchor),
  308. ])
  309. }
  310. private func makeLeftPanel() -> NSView {
  311. let panel = PaywallLeftPanelView()
  312. panel.translatesAutoresizingMaskIntoConstraints = false
  313. let title = NSTextField(labelWithString: "Unlock Your Full\nPrinting Potential")
  314. title.font = AppTheme.semiboldFont(size: 22)
  315. title.textColor = AppTheme.navy
  316. title.maximumNumberOfLines = 2
  317. title.translatesAutoresizingMaskIntoConstraints = false
  318. let featuresStack = NSStackView()
  319. featuresStack.orientation = .vertical
  320. featuresStack.spacing = 6
  321. featuresStack.alignment = .leading
  322. featuresStack.translatesAutoresizingMaskIntoConstraints = false
  323. let features = [
  324. "Unlimited high-quality scans",
  325. "Advanced OCR technology",
  326. "Direct cloud printing",
  327. "Ad-free experience",
  328. "Priority support",
  329. "Secure storage",
  330. ]
  331. for feature in features {
  332. featuresStack.addArrangedSubview(PaywallFeatureRow(text: feature))
  333. }
  334. panel.addSubview(title)
  335. panel.addSubview(featuresStack)
  336. NSLayoutConstraint.activate([
  337. title.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
  338. title.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
  339. title.topAnchor.constraint(equalTo: panel.topAnchor, constant: 48),
  340. featuresStack.leadingAnchor.constraint(equalTo: title.leadingAnchor),
  341. featuresStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
  342. featuresStack.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 28),
  343. ])
  344. return panel
  345. }
  346. private func makeRightPanel() -> NSView {
  347. let panel = NSView()
  348. panel.translatesAutoresizingMaskIntoConstraints = false
  349. let closeButton = PaywallCloseButton()
  350. closeButton.onClose = { [weak self] in self?.onClose?() }
  351. let title = NSTextField(labelWithString: "Go Premium")
  352. title.font = AppTheme.semiboldFont(size: 26)
  353. title.textColor = AppTheme.navy
  354. title.translatesAutoresizingMaskIntoConstraints = false
  355. let subtitle = NSTextField(labelWithString: "Experience professional quality printing and scanning without limits.")
  356. subtitle.font = AppTheme.regularFont(size: 13)
  357. subtitle.textColor = AppTheme.textSecondary
  358. subtitle.maximumNumberOfLines = 2
  359. subtitle.translatesAutoresizingMaskIntoConstraints = false
  360. let plansStack = NSStackView()
  361. plansStack.orientation = .vertical
  362. plansStack.spacing = 12
  363. plansStack.translatesAutoresizingMaskIntoConstraints = false
  364. for plan in PaywallPlan.allCases {
  365. let card = PaywallPlanCard(plan: plan)
  366. card.isChosen = plan == selectedPlan
  367. card.onSelect = { [weak self] in self?.selectPlan(plan) }
  368. planCards[plan] = card
  369. plansStack.addArrangedSubview(card)
  370. }
  371. ctaButton.title = selectedPlan.ctaTitle
  372. ctaButton.isBordered = false
  373. ctaButton.wantsLayer = true
  374. ctaButton.layer?.backgroundColor = AppTheme.navy.cgColor
  375. ctaButton.layer?.cornerRadius = 12
  376. ctaButton.font = AppTheme.semiboldFont(size: 15)
  377. ctaButton.contentTintColor = .white
  378. ctaButton.target = self
  379. ctaButton.action = #selector(purchaseTapped)
  380. ctaButton.translatesAutoresizingMaskIntoConstraints = false
  381. let footer = makeFooter()
  382. panel.addSubview(closeButton)
  383. panel.addSubview(title)
  384. panel.addSubview(subtitle)
  385. panel.addSubview(plansStack)
  386. panel.addSubview(ctaButton)
  387. panel.addSubview(footer)
  388. NSLayoutConstraint.activate([
  389. closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
  390. closeButton.topAnchor.constraint(equalTo: panel.topAnchor, constant: 20),
  391. closeButton.widthAnchor.constraint(equalToConstant: 28),
  392. closeButton.heightAnchor.constraint(equalToConstant: 28),
  393. title.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
  394. title.topAnchor.constraint(equalTo: panel.topAnchor, constant: 40),
  395. title.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -12),
  396. subtitle.leadingAnchor.constraint(equalTo: title.leadingAnchor),
  397. subtitle.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
  398. subtitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6),
  399. plansStack.leadingAnchor.constraint(equalTo: title.leadingAnchor),
  400. plansStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
  401. plansStack.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
  402. ctaButton.leadingAnchor.constraint(equalTo: title.leadingAnchor),
  403. ctaButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
  404. ctaButton.topAnchor.constraint(equalTo: plansStack.bottomAnchor, constant: 20),
  405. ctaButton.heightAnchor.constraint(equalToConstant: 48),
  406. footer.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
  407. footer.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -20),
  408. ])
  409. return panel
  410. }
  411. private func makeFooter() -> NSView {
  412. let container = NSView()
  413. container.translatesAutoresizingMaskIntoConstraints = false
  414. let restoreLink = PaywallFooterLink(title: "Restore Purchases")
  415. restoreLink.target = self
  416. restoreLink.action = #selector(restoreTapped)
  417. let privacyLink = PaywallFooterLink(title: "Privacy Policy")
  418. let termsLink = PaywallFooterLink(title: "Terms of Service")
  419. let dot1 = makeFooterDot()
  420. let dot2 = makeFooterDot()
  421. let stack = NSStackView(views: [restoreLink, dot1, privacyLink, dot2, termsLink])
  422. stack.orientation = .horizontal
  423. stack.spacing = 6
  424. stack.alignment = .centerY
  425. stack.translatesAutoresizingMaskIntoConstraints = false
  426. container.addSubview(stack)
  427. NSLayoutConstraint.activate([
  428. stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  429. stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
  430. stack.topAnchor.constraint(equalTo: container.topAnchor),
  431. stack.bottomAnchor.constraint(equalTo: container.bottomAnchor),
  432. ])
  433. return container
  434. }
  435. private func makeFooterDot() -> NSView {
  436. let dot = NSView()
  437. dot.translatesAutoresizingMaskIntoConstraints = false
  438. dot.wantsLayer = true
  439. dot.layer?.backgroundColor = AppTheme.textSecondary.cgColor
  440. dot.layer?.cornerRadius = 1.5
  441. NSLayoutConstraint.activate([
  442. dot.widthAnchor.constraint(equalToConstant: 3),
  443. dot.heightAnchor.constraint(equalToConstant: 3),
  444. ])
  445. return dot
  446. }
  447. private func selectPlan(_ plan: PaywallPlan) {
  448. selectedPlan = plan
  449. for (key, card) in planCards {
  450. card.isChosen = key == plan
  451. }
  452. ctaButton.title = plan.ctaTitle
  453. }
  454. @objc private func purchaseTapped() {
  455. onPurchase?(selectedPlan)
  456. }
  457. @objc private func restoreTapped() {
  458. onRestore?()
  459. }
  460. }
  461. // MARK: - Overlay Presenter
  462. final class PaywallOverlayView: NSView {
  463. var onDismiss: (() -> Void)?
  464. private let paywallView: PaywallView
  465. private let blurView = NSVisualEffectView()
  466. private let backdrop = NSView()
  467. init() {
  468. paywallView = PaywallView()
  469. super.init(frame: .zero)
  470. translatesAutoresizingMaskIntoConstraints = false
  471. setup()
  472. }
  473. @available(*, unavailable)
  474. required init?(coder: NSCoder) { nil }
  475. private func setup() {
  476. blurView.translatesAutoresizingMaskIntoConstraints = false
  477. blurView.material = .underWindowBackground
  478. blurView.blendingMode = .withinWindow
  479. blurView.state = .active
  480. backdrop.translatesAutoresizingMaskIntoConstraints = false
  481. backdrop.wantsLayer = true
  482. backdrop.layer?.backgroundColor = NSColor(calibratedWhite: 0.15, alpha: 0.22).cgColor
  483. let pattern = WavePatternView()
  484. pattern.translatesAutoresizingMaskIntoConstraints = false
  485. pattern.alphaValue = 0.35
  486. paywallView.translatesAutoresizingMaskIntoConstraints = false
  487. paywallView.onClose = { [weak self] in self?.dismiss() }
  488. paywallView.onPurchase = { plan in
  489. NSLog("Purchase tapped: \(plan.title)")
  490. }
  491. paywallView.onRestore = {
  492. NSLog("Restore purchases tapped")
  493. }
  494. addSubview(blurView)
  495. addSubview(backdrop)
  496. backdrop.addSubview(pattern)
  497. addSubview(paywallView)
  498. NSLayoutConstraint.activate([
  499. blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
  500. blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
  501. blurView.topAnchor.constraint(equalTo: topAnchor),
  502. blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
  503. backdrop.leadingAnchor.constraint(equalTo: leadingAnchor),
  504. backdrop.trailingAnchor.constraint(equalTo: trailingAnchor),
  505. backdrop.topAnchor.constraint(equalTo: topAnchor),
  506. backdrop.bottomAnchor.constraint(equalTo: bottomAnchor),
  507. pattern.leadingAnchor.constraint(equalTo: backdrop.leadingAnchor),
  508. pattern.trailingAnchor.constraint(equalTo: backdrop.trailingAnchor),
  509. pattern.topAnchor.constraint(equalTo: backdrop.topAnchor),
  510. pattern.bottomAnchor.constraint(equalTo: backdrop.bottomAnchor),
  511. paywallView.leadingAnchor.constraint(equalTo: leadingAnchor),
  512. paywallView.trailingAnchor.constraint(equalTo: trailingAnchor),
  513. paywallView.topAnchor.constraint(equalTo: topAnchor),
  514. paywallView.bottomAnchor.constraint(equalTo: bottomAnchor),
  515. ])
  516. }
  517. func present(in parent: NSView) {
  518. guard superview == nil else { return }
  519. if let window = parent.window,
  520. let windowFrameView = window.contentView?.superview {
  521. windowFrameView.addSubview(self)
  522. NSLayoutConstraint.activate([
  523. leadingAnchor.constraint(equalTo: windowFrameView.leadingAnchor),
  524. trailingAnchor.constraint(equalTo: windowFrameView.trailingAnchor),
  525. topAnchor.constraint(equalTo: windowFrameView.topAnchor),
  526. bottomAnchor.constraint(equalTo: windowFrameView.bottomAnchor),
  527. ])
  528. } else {
  529. parent.addSubview(self)
  530. NSLayoutConstraint.activate([
  531. leadingAnchor.constraint(equalTo: parent.leadingAnchor),
  532. trailingAnchor.constraint(equalTo: parent.trailingAnchor),
  533. topAnchor.constraint(equalTo: parent.topAnchor),
  534. bottomAnchor.constraint(equalTo: parent.bottomAnchor),
  535. ])
  536. }
  537. alphaValue = 0
  538. NSAnimationContext.runAnimationGroup { context in
  539. context.duration = 0.2
  540. animator().alphaValue = 1
  541. }
  542. }
  543. func dismiss() {
  544. NSAnimationContext.runAnimationGroup({ context in
  545. context.duration = 0.15
  546. animator().alphaValue = 0
  547. }, completionHandler: { [weak self] in
  548. self?.removeFromSuperview()
  549. self?.onDismiss?()
  550. })
  551. }
  552. }