浏览代码

Add trust footer strip and links to the paywall.

Moves the trust row above the CTA and ensures the footer content compresses to avoid widening the window.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 9 小时之前
父节点
当前提交
cfb57d51e2
共有 1 个文件被更改,包括 152 次插入28 次删除
  1. 152 28
      smart_printer/PaywallView.swift

+ 152 - 28
smart_printer/PaywallView.swift

@@ -335,6 +335,76 @@ private final class PaywallFooterLink: NSButton {
335 335
     }
336 336
 }
337 337
 
338
+// MARK: - Footer Trust Item
339
+
340
+private final class PaywallTrustItemView: NSView {
341
+    init(iconName: String, title: String, subtitle: String) {
342
+        super.init(frame: .zero)
343
+        translatesAutoresizingMaskIntoConstraints = false
344
+
345
+        let iconContainer = NSView()
346
+        iconContainer.translatesAutoresizingMaskIntoConstraints = false
347
+        iconContainer.wantsLayer = true
348
+        iconContainer.layer?.backgroundColor = AppTheme.blueLight.cgColor
349
+        iconContainer.layer?.cornerRadius = 10
350
+        iconContainer.layer?.masksToBounds = true
351
+
352
+        let icon = NSImageView()
353
+        icon.translatesAutoresizingMaskIntoConstraints = false
354
+        if let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) {
355
+            let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
356
+            icon.image = image.withSymbolConfiguration(config)
357
+        }
358
+        icon.contentTintColor = AppTheme.navy
359
+
360
+        let titleLabel = NSTextField(labelWithString: title)
361
+        titleLabel.font = AppTheme.semiboldFont(size: 13)
362
+        titleLabel.textColor = AppTheme.navy
363
+        titleLabel.lineBreakMode = .byTruncatingTail
364
+        titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
365
+        titleLabel.translatesAutoresizingMaskIntoConstraints = false
366
+
367
+        let subtitleLabel = NSTextField(labelWithString: subtitle)
368
+        subtitleLabel.font = AppTheme.regularFont(size: 12)
369
+        subtitleLabel.textColor = AppTheme.textSecondary
370
+        subtitleLabel.lineBreakMode = .byTruncatingTail
371
+        subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
372
+        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
373
+
374
+        addSubview(iconContainer)
375
+        iconContainer.addSubview(icon)
376
+        addSubview(titleLabel)
377
+        addSubview(subtitleLabel)
378
+
379
+        NSLayoutConstraint.activate([
380
+            iconContainer.leadingAnchor.constraint(equalTo: leadingAnchor),
381
+            iconContainer.topAnchor.constraint(equalTo: topAnchor),
382
+            iconContainer.widthAnchor.constraint(equalToConstant: 20),
383
+            iconContainer.heightAnchor.constraint(equalToConstant: 20),
384
+
385
+            icon.centerXAnchor.constraint(equalTo: iconContainer.centerXAnchor),
386
+            icon.centerYAnchor.constraint(equalTo: iconContainer.centerYAnchor),
387
+            icon.widthAnchor.constraint(equalToConstant: 12),
388
+            icon.heightAnchor.constraint(equalToConstant: 12),
389
+
390
+            titleLabel.leadingAnchor.constraint(equalTo: iconContainer.trailingAnchor, constant: 8),
391
+            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 1),
392
+            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
393
+
394
+            subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
395
+            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2),
396
+            subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
397
+            subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
398
+        ])
399
+
400
+        setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
401
+        setContentHuggingPriority(.defaultLow, for: .horizontal)
402
+    }
403
+
404
+    @available(*, unavailable)
405
+    required init?(coder: NSCoder) { nil }
406
+}
407
+
338 408
 // MARK: - Main Paywall Card
339 409
 
340 410
 final class PaywallView: NSView {
@@ -464,14 +534,16 @@ final class PaywallView: NSView {
464 534
         ctaButton.action = #selector(purchaseTapped)
465 535
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
466 536
 
467
-        let footer = makeFooter()
537
+        let trustRow = makeTrustRow()
538
+        let footerLinks = makeFooterLinks()
468 539
 
469 540
         panel.addSubview(closeButton)
470 541
         panel.addSubview(title)
471 542
         panel.addSubview(subtitle)
472 543
         panel.addSubview(plansStack)
544
+        panel.addSubview(trustRow)
473 545
         panel.addSubview(ctaButton)
474
-        panel.addSubview(footer)
546
+        panel.addSubview(footerLinks)
475 547
 
476 548
         NSLayoutConstraint.activate([
477 549
             closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
@@ -491,60 +563,112 @@ final class PaywallView: NSView {
491 563
             plansStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
492 564
             plansStack.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
493 565
 
566
+            trustRow.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 22),
567
+            trustRow.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -22),
568
+            trustRow.topAnchor.constraint(equalTo: plansStack.bottomAnchor, constant: 18),
569
+
494 570
             ctaButton.leadingAnchor.constraint(equalTo: title.leadingAnchor),
495 571
             ctaButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
496
-            ctaButton.topAnchor.constraint(equalTo: plansStack.bottomAnchor, constant: 20),
572
+            ctaButton.topAnchor.constraint(equalTo: trustRow.bottomAnchor, constant: 16),
497 573
             ctaButton.heightAnchor.constraint(equalToConstant: 48),
574
+            ctaButton.bottomAnchor.constraint(lessThanOrEqualTo: footerLinks.topAnchor, constant: -14),
498 575
 
499
-            footer.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
500
-            footer.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -20),
576
+            footerLinks.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
577
+            footerLinks.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -20),
501 578
         ])
502 579
 
503 580
         return panel
504 581
     }
505 582
 
506
-    private func makeFooter() -> NSView {
583
+    private func makeTrustRow() -> NSView {
584
+        let securePayments = PaywallTrustItemView(
585
+            iconName: "shield.fill",
586
+            title: "Secure Payments",
587
+            subtitle: "Your payment is 100% secure"
588
+        )
589
+        let cancelAnytime = PaywallTrustItemView(
590
+            iconName: "arrow.counterclockwise",
591
+            title: "Cancel Anytime",
592
+            subtitle: "No commitment, cancel anytime."
593
+        )
594
+        let support = PaywallTrustItemView(
595
+            iconName: "headphones",
596
+            title: "24/7 Support",
597
+            subtitle: "We're here to help you anytime."
598
+        )
599
+        let privacyFirst = PaywallTrustItemView(
600
+            iconName: "lock.fill",
601
+            title: "Privacy First",
602
+            subtitle: "Your data is safe with us."
603
+        )
604
+
605
+        let trustStack = NSStackView(views: [securePayments, cancelAnytime, support, privacyFirst])
606
+        trustStack.orientation = .horizontal
607
+        trustStack.distribution = .fillEqually
608
+        trustStack.spacing = 16
609
+        trustStack.alignment = .top
610
+        trustStack.translatesAutoresizingMaskIntoConstraints = false
611
+        trustStack.wantsLayer = true
612
+        trustStack.layer?.backgroundColor = NSColor.white.cgColor
613
+        trustStack.layer?.cornerRadius = 12
614
+        trustStack.layer?.borderWidth = 1
615
+        trustStack.layer?.borderColor = AppTheme.paywallBorder.cgColor
616
+        trustStack.layer?.masksToBounds = true
617
+        trustStack.edgeInsets = NSEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
618
+        trustStack.translatesAutoresizingMaskIntoConstraints = false
619
+
620
+        return trustStack
621
+    }
622
+
623
+    private func makeFooterLinks() -> NSView {
507 624
         let container = NSView()
508 625
         container.translatesAutoresizingMaskIntoConstraints = false
509 626
 
510
-        let restoreLink = PaywallFooterLink(title: "Restore Purchases")
627
+        let manageSubscriptionLink = PaywallFooterLink(title: "Manage Subscription")
628
+        let restoreLink = PaywallFooterLink(title: "Restore Purchase")
511 629
         restoreLink.target = self
512 630
         restoreLink.action = #selector(restoreTapped)
513 631
 
514 632
         let privacyLink = PaywallFooterLink(title: "Privacy Policy")
515 633
         let termsLink = PaywallFooterLink(title: "Terms of Service")
634
+        let supportLink = PaywallFooterLink(title: "Support")
516 635
 
517
-        let dot1 = makeFooterDot()
518
-        let dot2 = makeFooterDot()
636
+        let separator1 = makeFooterSeparator()
637
+        let separator2 = makeFooterSeparator()
638
+        let separator3 = makeFooterSeparator()
639
+        let separator4 = makeFooterSeparator()
519 640
 
520
-        let stack = NSStackView(views: [restoreLink, dot1, privacyLink, dot2, termsLink])
521
-        stack.orientation = .horizontal
522
-        stack.spacing = 6
523
-        stack.alignment = .centerY
524
-        stack.translatesAutoresizingMaskIntoConstraints = false
641
+        let linksStack = NSStackView(views: [
642
+            manageSubscriptionLink, separator1, restoreLink, separator2, privacyLink, separator3, termsLink, separator4, supportLink,
643
+        ])
644
+        linksStack.orientation = .horizontal
645
+        linksStack.spacing = 8
646
+        linksStack.alignment = .centerY
647
+        linksStack.distribution = .fillProportionally
648
+        linksStack.translatesAutoresizingMaskIntoConstraints = false
525 649
 
526
-        container.addSubview(stack)
650
+        container.addSubview(linksStack)
527 651
         NSLayoutConstraint.activate([
528
-            stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
529
-            stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
530
-            stack.topAnchor.constraint(equalTo: container.topAnchor),
531
-            stack.bottomAnchor.constraint(equalTo: container.bottomAnchor),
652
+            linksStack.centerXAnchor.constraint(equalTo: container.centerXAnchor),
653
+            linksStack.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor),
654
+            linksStack.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor),
655
+            linksStack.topAnchor.constraint(equalTo: container.topAnchor),
656
+            linksStack.bottomAnchor.constraint(equalTo: container.bottomAnchor)
532 657
         ])
533 658
 
534 659
         return container
535 660
     }
536 661
 
537
-    private func makeFooterDot() -> NSView {
538
-        let dot = NSView()
539
-        dot.translatesAutoresizingMaskIntoConstraints = false
540
-        dot.wantsLayer = true
541
-        dot.layer?.backgroundColor = AppTheme.textSecondary.cgColor
542
-        dot.layer?.cornerRadius = 1.5
662
+    private func makeFooterSeparator() -> NSView {
663
+        let separator = NSView()
664
+        separator.translatesAutoresizingMaskIntoConstraints = false
665
+        separator.wantsLayer = true
666
+        separator.layer?.backgroundColor = AppTheme.paywallBorder.cgColor
543 667
         NSLayoutConstraint.activate([
544
-            dot.widthAnchor.constraint(equalToConstant: 3),
545
-            dot.heightAnchor.constraint(equalToConstant: 3),
668
+            separator.widthAnchor.constraint(equalToConstant: 1),
669
+            separator.heightAnchor.constraint(equalToConstant: 12),
546 670
         ])
547
-        return dot
671
+        return separator
548 672
     }
549 673
 
550 674
     private func selectPlan(_ plan: PaywallPlan) {