Parcourir la Source

Add premium plans sheet from dashboard upgrade action.

Replace external upgrade link with an in-app premium plans sheet and align footer links in equal-width columns for consistent layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 il y a 3 semaines
Parent
commit
f279c6805a

+ 519 - 0
App for Indeed/Controllers/PremiumPlansWindowController.swift

@@ -0,0 +1,519 @@
1
+import Cocoa
2
+
3
+final class PremiumPlansWindowController: NSWindowController {
4
+    init() {
5
+        let viewController = PremiumPlansViewController()
6
+        let window = NSWindow(contentViewController: viewController)
7
+        window.title = "Premium Plans"
8
+        window.styleMask = [.titled, .closable, .miniaturizable, .resizable]
9
+        window.setContentSize(NSSize(width: 1160, height: 760))
10
+        window.minSize = NSSize(width: 980, height: 680)
11
+        window.center()
12
+        super.init(window: window)
13
+    }
14
+
15
+    @available(*, unavailable)
16
+    required init?(coder: NSCoder) {
17
+        nil
18
+    }
19
+}
20
+
21
+private final class PremiumPlansViewController: NSViewController {
22
+    private struct Plan {
23
+        let id: String
24
+        let title: String
25
+        let subtitle: String
26
+        let price: String
27
+        let period: String
28
+        let billedPill: String
29
+        let billedLine: String
30
+        let crossedPrice: String?
31
+        let savingsText: String?
32
+        let features: [String]
33
+        let iconName: String
34
+        let iconTint: NSColor
35
+        let highlight: Bool
36
+    }
37
+
38
+    private enum Theme {
39
+        static let pageStart = NSColor(srgbRed: 249 / 255, green: 252 / 255, blue: 255 / 255, alpha: 1)
40
+        static let pageEnd = NSColor(srgbRed: 238 / 255, green: 244 / 255, blue: 255 / 255, alpha: 1)
41
+        static let cardBackground = NSColor.white
42
+        static let primaryText = NSColor(srgbRed: 27 / 255, green: 38 / 255, blue: 79 / 255, alpha: 1)
43
+        static let secondaryText = NSColor(srgbRed: 108 / 255, green: 120 / 255, blue: 157 / 255, alpha: 1)
44
+        static let cardBorder = NSColor(srgbRed: 198 / 255, green: 216 / 255, blue: 255 / 255, alpha: 1)
45
+        static let accent = NSColor(srgbRed: 55 / 255, green: 128 / 255, blue: 255 / 255, alpha: 1)
46
+        static let accentHover = NSColor(srgbRed: 38 / 255, green: 108 / 255, blue: 232 / 255, alpha: 1)
47
+        static let mutedButtonFill = NSColor(srgbRed: 238 / 255, green: 243 / 255, blue: 252 / 255, alpha: 1)
48
+        static let bottomStrip = NSColor(srgbRed: 244 / 255, green: 248 / 255, blue: 255 / 255, alpha: 1)
49
+        static let divider = NSColor(srgbRed: 218 / 255, green: 228 / 255, blue: 247 / 255, alpha: 1)
50
+        static let successText = NSColor(srgbRed: 21 / 255, green: 154 / 255, blue: 220 / 255, alpha: 1)
51
+        static let iconTint = NSColor(srgbRed: 47 / 255, green: 136 / 255, blue: 255 / 255, alpha: 1)
52
+    }
53
+
54
+    private let plans: [Plan] = [
55
+        Plan(
56
+            id: "weekly",
57
+            title: "Weekly",
58
+            subtitle: "Flexible and commitment-free",
59
+            price: "$9.99",
60
+            period: "/ week",
61
+            billedPill: "",
62
+            billedLine: "",
63
+            crossedPrice: nil,
64
+            savingsText: nil,
65
+            features: [
66
+                "All premium features",
67
+                "Perfect for short-term goals",
68
+                "Cancel anytime"
69
+            ],
70
+            iconName: "paperplane.fill",
71
+            iconTint: Theme.iconTint,
72
+            highlight: false
73
+        ),
74
+        Plan(
75
+            id: "monthly",
76
+            title: "Monthly",
77
+            subtitle: "Balanced for regular productivity",
78
+            price: "$19.99",
79
+            period: "/ month",
80
+            billedPill: "Most Popular",
81
+            billedLine: "",
82
+            crossedPrice: nil,
83
+            savingsText: nil,
84
+            features: [
85
+                "All premium features",
86
+                "Best value for regular users",
87
+                "Priority support"
88
+            ],
89
+            iconName: "bolt.fill",
90
+            iconTint: Theme.accent,
91
+            highlight: true
92
+        ),
93
+        Plan(
94
+            id: "yearly",
95
+            title: "Yearly",
96
+            subtitle: "Best value for long-term users",
97
+            price: "$39.99",
98
+            period: "/ year",
99
+            billedPill: "3-day free trial",
100
+            billedLine: "",
101
+            crossedPrice: nil,
102
+            savingsText: nil,
103
+            features: [
104
+                "All premium features",
105
+                "Lowest effective monthly cost",
106
+                "Ideal for long-term use"
107
+            ],
108
+            iconName: "crown.fill",
109
+            iconTint: Theme.successText,
110
+            highlight: false
111
+        )
112
+    ]
113
+
114
+    private let pageGradient = CAGradientLayer()
115
+    private lazy var popularGradient: CAGradientLayer = {
116
+        let layer = CAGradientLayer()
117
+        layer.colors = [
118
+            NSColor(srgbRed: 189 / 255, green: 52 / 255, blue: 255 / 255, alpha: 1).cgColor,
119
+            NSColor(srgbRed: 73 / 255, green: 153 / 255, blue: 255 / 255, alpha: 1).cgColor
120
+        ]
121
+        layer.startPoint = CGPoint(x: 0, y: 0.5)
122
+        layer.endPoint = CGPoint(x: 1, y: 0.5)
123
+        return layer
124
+    }()
125
+
126
+    override func viewDidLayout() {
127
+        super.viewDidLayout()
128
+        pageGradient.frame = view.bounds
129
+        popularGradient.frame = CGRect(x: 0, y: 0, width: 160, height: 22)
130
+    }
131
+
132
+    override func loadView() {
133
+        view = NSView()
134
+        view.wantsLayer = true
135
+        pageGradient.colors = [Theme.pageStart.cgColor, Theme.pageEnd.cgColor]
136
+        pageGradient.startPoint = CGPoint(x: 0, y: 1)
137
+        pageGradient.endPoint = CGPoint(x: 1, y: 0)
138
+        view.layer?.addSublayer(pageGradient)
139
+        setupLayout()
140
+    }
141
+
142
+    private func setupLayout() {
143
+        let crownIcon = NSImageView()
144
+        crownIcon.translatesAutoresizingMaskIntoConstraints = false
145
+        crownIcon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 18, weight: .semibold)
146
+        crownIcon.image = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: nil)
147
+        crownIcon.contentTintColor = NSColor(srgbRed: 254 / 255, green: 214 / 255, blue: 92 / 255, alpha: 1)
148
+
149
+        let title = NSTextField(labelWithString: "Upgrade to Pro")
150
+        title.font = .systemFont(ofSize: 52, weight: .bold)
151
+        title.textColor = Theme.primaryText
152
+        title.alignment = .center
153
+
154
+        let subtitle = NSTextField(labelWithString: "Unlock unlimited access to premium tools and boost your productivity.")
155
+        subtitle.font = .systemFont(ofSize: 15, weight: .medium)
156
+        subtitle.textColor = Theme.secondaryText
157
+        subtitle.alignment = .center
158
+
159
+        let cardsRow = NSStackView(views: plans.map(makePricingCard(_:)))
160
+        cardsRow.orientation = .horizontal
161
+        cardsRow.spacing = 14
162
+        cardsRow.alignment = .top
163
+        cardsRow.distribution = .fillEqually
164
+        cardsRow.translatesAutoresizingMaskIntoConstraints = false
165
+
166
+        let trustRow = makeTrustRow()
167
+        let footerRow = makeFooterRow()
168
+
169
+        let root = NSStackView(views: [crownIcon, title, subtitle, cardsRow, trustRow, footerRow])
170
+        root.orientation = .vertical
171
+        root.spacing = 18
172
+        root.alignment = .centerX
173
+        root.translatesAutoresizingMaskIntoConstraints = false
174
+
175
+        view.addSubview(root)
176
+        NSLayoutConstraint.activate([
177
+            root.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
178
+            root.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
179
+            root.topAnchor.constraint(equalTo: view.topAnchor, constant: 20),
180
+            root.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
181
+            cardsRow.widthAnchor.constraint(equalTo: root.widthAnchor),
182
+            cardsRow.heightAnchor.constraint(equalToConstant: 420),
183
+            trustRow.widthAnchor.constraint(equalTo: root.widthAnchor),
184
+            footerRow.widthAnchor.constraint(equalTo: root.widthAnchor),
185
+            crownIcon.heightAnchor.constraint(equalToConstant: 20)
186
+        ])
187
+    }
188
+
189
+    private func makePricingCard(_ plan: Plan) -> NSView {
190
+        let card = NSView()
191
+        card.translatesAutoresizingMaskIntoConstraints = false
192
+        card.wantsLayer = true
193
+        card.layer?.backgroundColor = Theme.cardBackground.cgColor
194
+        card.layer?.cornerRadius = 16
195
+        card.layer?.borderWidth = plan.highlight ? 2 : 1
196
+        card.layer?.borderColor = (plan.highlight ? Theme.accent : Theme.cardBorder).cgColor
197
+
198
+        let iconWell = NSView()
199
+        iconWell.translatesAutoresizingMaskIntoConstraints = false
200
+        iconWell.wantsLayer = true
201
+        iconWell.layer?.cornerRadius = 10
202
+        iconWell.layer?.backgroundColor = Theme.bottomStrip.cgColor
203
+        iconWell.widthAnchor.constraint(equalToConstant: 24).isActive = true
204
+        iconWell.heightAnchor.constraint(equalToConstant: 24).isActive = true
205
+
206
+        let icon = NSImageView()
207
+        icon.translatesAutoresizingMaskIntoConstraints = false
208
+        icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
209
+        icon.image = NSImage(systemSymbolName: plan.iconName, accessibilityDescription: nil)
210
+        icon.contentTintColor = plan.iconTint
211
+        iconWell.addSubview(icon)
212
+        NSLayoutConstraint.activate([
213
+            icon.centerXAnchor.constraint(equalTo: iconWell.centerXAnchor),
214
+            icon.centerYAnchor.constraint(equalTo: iconWell.centerYAnchor)
215
+        ])
216
+
217
+        let titleLabel = NSTextField(labelWithString: plan.title)
218
+        titleLabel.font = .systemFont(ofSize: 44, weight: .bold)
219
+        titleLabel.textColor = Theme.primaryText
220
+        titleLabel.alignment = .center
221
+
222
+        let subtitleLabel = NSTextField(labelWithString: plan.subtitle)
223
+        subtitleLabel.font = .systemFont(ofSize: 13, weight: .medium)
224
+        subtitleLabel.textColor = Theme.secondaryText
225
+        subtitleLabel.alignment = .center
226
+
227
+        let billingPill = pillLabel(text: plan.billedPill, tint: Theme.bottomStrip, textColor: Theme.iconTint)
228
+        billingPill.isHidden = plan.billedPill.isEmpty
229
+
230
+        let priceLabel = NSTextField(labelWithString: plan.price)
231
+        priceLabel.font = .systemFont(ofSize: 40, weight: .bold)
232
+        priceLabel.textColor = Theme.primaryText
233
+
234
+        let periodLabel = NSTextField(labelWithString: plan.period)
235
+        periodLabel.font = .systemFont(ofSize: 30, weight: .semibold)
236
+        periodLabel.textColor = Theme.secondaryText
237
+
238
+        let priceRow = NSStackView(views: [priceLabel, periodLabel])
239
+        priceRow.orientation = .horizontal
240
+        priceRow.spacing = 4
241
+        priceRow.alignment = .firstBaseline
242
+
243
+        let billingLabel = NSTextField(labelWithString: plan.billedLine)
244
+        billingLabel.font = .systemFont(ofSize: 13, weight: .medium)
245
+        billingLabel.textColor = Theme.secondaryText
246
+        billingLabel.isHidden = plan.billedLine.isEmpty
247
+
248
+        let inlinePriceInfo = inlinePriceInfoLabel(oldPrice: plan.crossedPrice, newPrice: plan.savingsText)
249
+        inlinePriceInfo.isHidden = (plan.crossedPrice == nil || plan.savingsText == nil)
250
+
251
+        let divider = NSBox()
252
+        divider.boxType = .separator
253
+        divider.translatesAutoresizingMaskIntoConstraints = false
254
+        divider.borderColor = Theme.divider
255
+
256
+        let featuresStack = NSStackView(views: plan.features.map(makeFeatureRow(_:)))
257
+        featuresStack.orientation = .vertical
258
+        featuresStack.spacing = 9
259
+        featuresStack.alignment = .leading
260
+
261
+        let selectButton = NSButton(title: "Get \(plan.title)", target: self, action: #selector(didTapSelectPlan))
262
+        selectButton.identifier = NSUserInterfaceItemIdentifier(plan.id)
263
+        selectButton.isBordered = false
264
+        selectButton.bezelStyle = .rounded
265
+        selectButton.font = .systemFont(ofSize: 15, weight: .bold)
266
+        selectButton.contentTintColor = plan.highlight ? .white : Theme.primaryText
267
+        selectButton.wantsLayer = true
268
+        selectButton.layer?.cornerRadius = 12
269
+        selectButton.layer?.borderWidth = 1
270
+        selectButton.layer?.borderColor = (plan.highlight ? Theme.accent : Theme.divider).cgColor
271
+        selectButton.layer?.backgroundColor = (plan.highlight
272
+            ? NSColor(srgbRed: 189 / 255, green: 52 / 255, blue: 255 / 255, alpha: 1)
273
+            : Theme.mutedButtonFill).cgColor
274
+        selectButton.translatesAutoresizingMaskIntoConstraints = false
275
+        selectButton.heightAnchor.constraint(equalToConstant: 58).isActive = true
276
+
277
+        let spacer = NSView()
278
+        spacer.setContentHuggingPriority(.defaultLow, for: .vertical)
279
+
280
+        let content = NSStackView(views: [iconWell, titleLabel, subtitleLabel, billingPill, priceRow, billingLabel, inlinePriceInfo, divider, featuresStack, spacer, selectButton])
281
+        content.orientation = .vertical
282
+        content.spacing = 10
283
+        content.alignment = .centerX
284
+        content.translatesAutoresizingMaskIntoConstraints = false
285
+        card.addSubview(content)
286
+
287
+        NSLayoutConstraint.activate([
288
+            divider.widthAnchor.constraint(equalTo: content.widthAnchor),
289
+            content.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 18),
290
+            content.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -18),
291
+            content.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
292
+            content.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -16),
293
+            selectButton.widthAnchor.constraint(equalTo: content.widthAnchor)
294
+        ])
295
+
296
+        if plan.highlight {
297
+            let badgeHost = NSView()
298
+            badgeHost.translatesAutoresizingMaskIntoConstraints = false
299
+            badgeHost.wantsLayer = true
300
+            badgeHost.layer?.cornerRadius = 14
301
+            badgeHost.layer?.masksToBounds = true
302
+            badgeHost.layer?.addSublayer(popularGradient)
303
+
304
+            let sparkle = NSImageView()
305
+            sparkle.translatesAutoresizingMaskIntoConstraints = false
306
+            sparkle.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)
307
+            sparkle.image = NSImage(systemSymbolName: "sparkles", accessibilityDescription: nil)
308
+            sparkle.contentTintColor = .white
309
+
310
+            let badge = NSTextField(labelWithString: "Most Popular")
311
+            badge.font = .systemFont(ofSize: 12, weight: .bold)
312
+            badge.textColor = .white
313
+
314
+            badgeHost.addSubview(sparkle)
315
+            badgeHost.addSubview(badge)
316
+            card.addSubview(badgeHost)
317
+            NSLayoutConstraint.activate([
318
+                badgeHost.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
319
+                badgeHost.topAnchor.constraint(equalTo: card.topAnchor, constant: 10),
320
+                badgeHost.widthAnchor.constraint(equalToConstant: 160),
321
+                badgeHost.heightAnchor.constraint(equalToConstant: 22),
322
+                sparkle.leadingAnchor.constraint(equalTo: badgeHost.leadingAnchor, constant: 14),
323
+                sparkle.centerYAnchor.constraint(equalTo: badgeHost.centerYAnchor),
324
+                badge.leadingAnchor.constraint(equalTo: sparkle.trailingAnchor, constant: 6),
325
+                badge.centerYAnchor.constraint(equalTo: badgeHost.centerYAnchor)
326
+            ])
327
+        }
328
+
329
+        return card
330
+    }
331
+
332
+    private func makeFeatureRow(_ text: String) -> NSView {
333
+        let icon = NSImageView()
334
+        icon.translatesAutoresizingMaskIntoConstraints = false
335
+        icon.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 11, weight: .bold)
336
+        icon.image = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: nil)
337
+        icon.contentTintColor = Theme.iconTint
338
+        icon.widthAnchor.constraint(equalToConstant: 14).isActive = true
339
+
340
+        let label = NSTextField(labelWithString: text)
341
+        label.font = .systemFont(ofSize: 16, weight: .semibold)
342
+        label.textColor = Theme.primaryText
343
+
344
+        let row = NSStackView(views: [icon, label])
345
+        row.orientation = .horizontal
346
+        row.spacing = 8
347
+        row.alignment = .centerY
348
+        row.distribution = .fill
349
+        return row
350
+    }
351
+
352
+    private func inlinePriceInfoLabel(oldPrice: String?, newPrice: String?) -> NSTextField {
353
+        guard let oldPrice, let newPrice else {
354
+            return NSTextField(labelWithString: "")
355
+        }
356
+        let full = NSMutableAttributedString()
357
+        let oldAttributes: [NSAttributedString.Key: Any] = [
358
+            .font: NSFont.systemFont(ofSize: 12, weight: .semibold),
359
+            .foregroundColor: Theme.secondaryText,
360
+            .strikethroughStyle: NSUnderlineStyle.single.rawValue
361
+        ]
362
+        let newAttributes: [NSAttributedString.Key: Any] = [
363
+            .font: NSFont.systemFont(ofSize: 12, weight: .bold),
364
+            .foregroundColor: Theme.successText
365
+        ]
366
+        full.append(NSAttributedString(string: "\(oldPrice)  ", attributes: oldAttributes))
367
+        full.append(NSAttributedString(string: newPrice, attributes: newAttributes))
368
+        let label = NSTextField(labelWithAttributedString: full)
369
+        return label
370
+    }
371
+
372
+    private func pillLabel(text: String, tint: NSColor, textColor: NSColor) -> NSTextField {
373
+        let pill = NSTextField(labelWithString: text)
374
+        pill.font = .systemFont(ofSize: 10, weight: .semibold)
375
+        pill.textColor = textColor
376
+        pill.alignment = .center
377
+        pill.wantsLayer = true
378
+        pill.layer?.backgroundColor = tint.cgColor
379
+        pill.layer?.cornerRadius = 9
380
+        pill.translatesAutoresizingMaskIntoConstraints = false
381
+        pill.heightAnchor.constraint(equalToConstant: 18).isActive = true
382
+        pill.widthAnchor.constraint(greaterThanOrEqualToConstant: 95).isActive = true
383
+        return pill
384
+    }
385
+
386
+    private func makeTrustRow() -> NSView {
387
+        let badges = NSStackView(views: [
388
+            trustBadge(icon: "shield.fill", title: "Secure Payments", subtitle: "Your payment is 100% secure."),
389
+            trustBadge(icon: "arrow.counterclockwise", title: "Cancel Anytime", subtitle: "No commitment, cancel anytime."),
390
+            trustBadge(icon: "headphones", title: "24/7 Support", subtitle: "We're here to help you anytime."),
391
+            trustBadge(icon: "lock.fill", title: "Privacy First", subtitle: "Your data is safe with us.")
392
+        ])
393
+        badges.orientation = .horizontal
394
+        badges.alignment = .centerY
395
+        badges.distribution = .fillEqually
396
+        badges.spacing = 12
397
+        badges.edgeInsets = NSEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
398
+        badges.translatesAutoresizingMaskIntoConstraints = false
399
+        badges.wantsLayer = true
400
+        badges.layer?.backgroundColor = Theme.bottomStrip.cgColor
401
+        badges.layer?.borderColor = Theme.divider.cgColor
402
+        badges.layer?.borderWidth = 1
403
+        badges.layer?.cornerRadius = 10
404
+        badges.setHuggingPriority(.defaultLow, for: .horizontal)
405
+        badges.heightAnchor.constraint(equalToConstant: 72).isActive = true
406
+        return badges
407
+    }
408
+
409
+    private func makeFooterRow() -> NSView {
410
+        let items = [
411
+            "Manage Subscription",
412
+            "Restore Purchase",
413
+            "Privacy Policy",
414
+            "Terms of Services",
415
+            "Support"
416
+        ]
417
+
418
+        let cells = items.enumerated().map { index, text in
419
+            footerCell(text: text, showsTrailingDivider: index < items.count - 1)
420
+        }
421
+
422
+        let links = NSStackView(views: cells)
423
+        links.orientation = .horizontal
424
+        links.distribution = .fillEqually
425
+        links.spacing = 0
426
+        links.alignment = .centerY
427
+        links.translatesAutoresizingMaskIntoConstraints = false
428
+        return links
429
+    }
430
+
431
+    private func footerCell(text: String, showsTrailingDivider: Bool) -> NSView {
432
+        let container = NSView()
433
+        container.translatesAutoresizingMaskIntoConstraints = false
434
+
435
+        let label = footerLink(text)
436
+        label.translatesAutoresizingMaskIntoConstraints = false
437
+        label.alignment = .center
438
+        container.addSubview(label)
439
+
440
+        var constraints = [
441
+            label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
442
+            label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
443
+        ]
444
+
445
+        if showsTrailingDivider {
446
+            let divider = footerDivider()
447
+            container.addSubview(divider)
448
+            constraints.append(contentsOf: [
449
+                divider.trailingAnchor.constraint(equalTo: container.trailingAnchor),
450
+                divider.centerYAnchor.constraint(equalTo: container.centerYAnchor)
451
+            ])
452
+        }
453
+
454
+        NSLayoutConstraint.activate(constraints)
455
+        return container
456
+    }
457
+
458
+    private func trustBadge(icon: String, title: String, subtitle: String) -> NSView {
459
+        let image = NSImageView()
460
+        image.translatesAutoresizingMaskIntoConstraints = false
461
+        image.symbolConfiguration = NSImage.SymbolConfiguration(pointSize: 12, weight: .semibold)
462
+        image.image = NSImage(systemSymbolName: icon, accessibilityDescription: nil)
463
+        image.contentTintColor = Theme.primaryText
464
+
465
+        let titleLabel = NSTextField(labelWithString: title)
466
+        titleLabel.font = .systemFont(ofSize: 12, weight: .bold)
467
+        titleLabel.textColor = Theme.primaryText
468
+        let subtitleLabel = NSTextField(labelWithString: subtitle)
469
+        subtitleLabel.font = .systemFont(ofSize: 10, weight: .medium)
470
+        subtitleLabel.textColor = Theme.secondaryText
471
+
472
+        let textStack = NSStackView(views: [titleLabel, subtitleLabel])
473
+        textStack.orientation = .vertical
474
+        textStack.spacing = 2
475
+        textStack.alignment = .leading
476
+
477
+        let stack = NSStackView(views: [image, textStack])
478
+        stack.orientation = .horizontal
479
+        stack.spacing = 8
480
+        stack.alignment = .leading
481
+        stack.edgeInsets = NSEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
482
+        stack.wantsLayer = true
483
+        stack.layer?.backgroundColor = NSColor.clear.cgColor
484
+        return stack
485
+    }
486
+
487
+    private func footerLink(_ text: String) -> NSTextField {
488
+        let label = NSTextField(labelWithString: text)
489
+        label.font = .systemFont(ofSize: 12, weight: .medium)
490
+        label.textColor = Theme.secondaryText
491
+        return label
492
+    }
493
+
494
+    private func footerDivider() -> NSBox {
495
+        let divider = NSBox()
496
+        divider.boxType = .separator
497
+        divider.borderColor = Theme.divider
498
+        divider.translatesAutoresizingMaskIntoConstraints = false
499
+        divider.widthAnchor.constraint(equalToConstant: 1).isActive = true
500
+        divider.heightAnchor.constraint(equalToConstant: 14).isActive = true
501
+        return divider
502
+    }
503
+
504
+    @objc private func didTapSelectPlan(_ sender: NSButton) {
505
+        sender.layer?.backgroundColor = Theme.accentHover.cgColor
506
+        let selectedPlan = sender.identifier?.rawValue ?? sender.title
507
+
508
+        let alert = NSAlert()
509
+        alert.messageText = "Premium checkout coming soon"
510
+        alert.informativeText = "Plan selected: \(selectedPlan.capitalized). Payment flow can be connected next."
511
+        alert.alertStyle = .informational
512
+        alert.addButton(withTitle: "OK")
513
+        if let window = view.window {
514
+            alert.beginSheetModal(for: window)
515
+        } else {
516
+            alert.runModal()
517
+        }
518
+    }
519
+}

+ 21 - 2
App for Indeed/Views/DashboardView.swift

@@ -125,6 +125,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
125 125
     /// Shown under the latest user message while a job search request is in flight.
126 126
     private var chatThinkingRowHost: NSView?
127 127
     private let jobSearchService = OpenAIJobSearchService()
128
+    private var premiumPlansWindowController: PremiumPlansWindowController?
128 129
 
129 130
     /// Upper bound sent to the model per request (was fixed at 8 in the prompt). Clamped when calling the API.
130 131
     private static let jobsPerSearchDefault = 15
@@ -2050,8 +2051,26 @@ final class DashboardView: NSView, NSTextFieldDelegate {
2050 2051
     }
2051 2052
 
2052 2053
     @objc private func didTapUpgradeToPro() {
2053
-        guard let url = URL(string: "https://www.indeed.com") else { return }
2054
-        NSWorkspace.shared.open(url)
2054
+        guard let hostWindow = window else { return }
2055
+
2056
+        if premiumPlansWindowController == nil {
2057
+            premiumPlansWindowController = PremiumPlansWindowController()
2058
+        }
2059
+        guard let paywallWindow = premiumPlansWindowController?.window else { return }
2060
+
2061
+        if hostWindow.attachedSheet === paywallWindow {
2062
+            return
2063
+        }
2064
+
2065
+        let hostContentSize = hostWindow.contentView?.bounds.size ?? hostWindow.frame.size
2066
+        paywallWindow.setContentSize(hostContentSize)
2067
+        paywallWindow.minSize = hostContentSize
2068
+        paywallWindow.maxSize = hostContentSize
2069
+        paywallWindow.styleMask.insert(.fullSizeContentView)
2070
+        paywallWindow.titlebarAppearsTransparent = true
2071
+        paywallWindow.titleVisibility = .hidden
2072
+
2073
+        hostWindow.beginSheet(paywallWindow)
2055 2074
     }
2056 2075
 
2057 2076
     @objc private func didChangeThemeSelection(_ sender: NSSegmentedControl) {