Parcourir la Source

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 il y a 11 heures
Parent
commit
cfb57d51e2
1 fichiers modifiés avec 152 ajouts et 28 suppressions
  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) {