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 10 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
 // MARK: - Main Paywall Card
408
 // MARK: - Main Paywall Card
339
 
409
 
340
 final class PaywallView: NSView {
410
 final class PaywallView: NSView {
@@ -464,14 +534,16 @@ final class PaywallView: NSView {
464
         ctaButton.action = #selector(purchaseTapped)
534
         ctaButton.action = #selector(purchaseTapped)
465
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
535
         ctaButton.translatesAutoresizingMaskIntoConstraints = false
466
 
536
 
467
-        let footer = makeFooter()
537
+        let trustRow = makeTrustRow()
538
+        let footerLinks = makeFooterLinks()
468
 
539
 
469
         panel.addSubview(closeButton)
540
         panel.addSubview(closeButton)
470
         panel.addSubview(title)
541
         panel.addSubview(title)
471
         panel.addSubview(subtitle)
542
         panel.addSubview(subtitle)
472
         panel.addSubview(plansStack)
543
         panel.addSubview(plansStack)
544
+        panel.addSubview(trustRow)
473
         panel.addSubview(ctaButton)
545
         panel.addSubview(ctaButton)
474
-        panel.addSubview(footer)
546
+        panel.addSubview(footerLinks)
475
 
547
 
476
         NSLayoutConstraint.activate([
548
         NSLayoutConstraint.activate([
477
             closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
549
             closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
@@ -491,60 +563,112 @@ final class PaywallView: NSView {
491
             plansStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
563
             plansStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
492
             plansStack.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
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
             ctaButton.leadingAnchor.constraint(equalTo: title.leadingAnchor),
570
             ctaButton.leadingAnchor.constraint(equalTo: title.leadingAnchor),
495
             ctaButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
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
             ctaButton.heightAnchor.constraint(equalToConstant: 48),
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
         return panel
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
         let container = NSView()
624
         let container = NSView()
508
         container.translatesAutoresizingMaskIntoConstraints = false
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
         restoreLink.target = self
629
         restoreLink.target = self
512
         restoreLink.action = #selector(restoreTapped)
630
         restoreLink.action = #selector(restoreTapped)
513
 
631
 
514
         let privacyLink = PaywallFooterLink(title: "Privacy Policy")
632
         let privacyLink = PaywallFooterLink(title: "Privacy Policy")
515
         let termsLink = PaywallFooterLink(title: "Terms of Service")
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
         NSLayoutConstraint.activate([
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
         return container
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
         NSLayoutConstraint.activate([
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
     private func selectPlan(_ plan: PaywallPlan) {
674
     private func selectPlan(_ plan: PaywallPlan) {