|
|
@@ -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) {
|