|
|
@@ -335,6 +335,76 @@ private final class PaywallFooterLink: NSButton {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// MARK: - Footer Trust Item
|
|
|
+
|
|
|
+private final class PaywallTrustItemView: NSView {
|
|
|
+ init(iconName: String, title: String, subtitle: String) {
|
|
|
+ super.init(frame: .zero)
|
|
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let iconContainer = NSView()
|
|
|
+ iconContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ iconContainer.wantsLayer = true
|
|
|
+ iconContainer.layer?.backgroundColor = AppTheme.blueLight.cgColor
|
|
|
+ iconContainer.layer?.cornerRadius = 10
|
|
|
+ iconContainer.layer?.masksToBounds = true
|
|
|
+
|
|
|
+ let icon = NSImageView()
|
|
|
+ icon.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ if let image = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) {
|
|
|
+ let config = NSImage.SymbolConfiguration(pointSize: 11, weight: .semibold)
|
|
|
+ icon.image = image.withSymbolConfiguration(config)
|
|
|
+ }
|
|
|
+ icon.contentTintColor = AppTheme.navy
|
|
|
+
|
|
|
+ let titleLabel = NSTextField(labelWithString: title)
|
|
|
+ titleLabel.font = AppTheme.semiboldFont(size: 13)
|
|
|
+ titleLabel.textColor = AppTheme.navy
|
|
|
+ titleLabel.lineBreakMode = .byTruncatingTail
|
|
|
+ titleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ let subtitleLabel = NSTextField(labelWithString: subtitle)
|
|
|
+ subtitleLabel.font = AppTheme.regularFont(size: 12)
|
|
|
+ subtitleLabel.textColor = AppTheme.textSecondary
|
|
|
+ subtitleLabel.lineBreakMode = .byTruncatingTail
|
|
|
+ subtitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
+ subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ addSubview(iconContainer)
|
|
|
+ iconContainer.addSubview(icon)
|
|
|
+ addSubview(titleLabel)
|
|
|
+ addSubview(subtitleLabel)
|
|
|
+
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ iconContainer.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
|
+ iconContainer.topAnchor.constraint(equalTo: topAnchor),
|
|
|
+ iconContainer.widthAnchor.constraint(equalToConstant: 20),
|
|
|
+ iconContainer.heightAnchor.constraint(equalToConstant: 20),
|
|
|
+
|
|
|
+ icon.centerXAnchor.constraint(equalTo: iconContainer.centerXAnchor),
|
|
|
+ icon.centerYAnchor.constraint(equalTo: iconContainer.centerYAnchor),
|
|
|
+ icon.widthAnchor.constraint(equalToConstant: 12),
|
|
|
+ icon.heightAnchor.constraint(equalToConstant: 12),
|
|
|
+
|
|
|
+ titleLabel.leadingAnchor.constraint(equalTo: iconContainer.trailingAnchor, constant: 8),
|
|
|
+ titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 1),
|
|
|
+ titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
+
|
|
|
+ subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
|
|
+ subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2),
|
|
|
+ subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
+ subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
+ ])
|
|
|
+
|
|
|
+ setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
+ setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
|
+ }
|
|
|
+
|
|
|
+ @available(*, unavailable)
|
|
|
+ required init?(coder: NSCoder) { nil }
|
|
|
+}
|
|
|
+
|
|
|
// MARK: - Main Paywall Card
|
|
|
|
|
|
final class PaywallView: NSView {
|
|
|
@@ -464,14 +534,16 @@ final class PaywallView: NSView {
|
|
|
ctaButton.action = #selector(purchaseTapped)
|
|
|
ctaButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
- let footer = makeFooter()
|
|
|
+ let trustRow = makeTrustRow()
|
|
|
+ let footerLinks = makeFooterLinks()
|
|
|
|
|
|
panel.addSubview(closeButton)
|
|
|
panel.addSubview(title)
|
|
|
panel.addSubview(subtitle)
|
|
|
panel.addSubview(plansStack)
|
|
|
+ panel.addSubview(trustRow)
|
|
|
panel.addSubview(ctaButton)
|
|
|
- panel.addSubview(footer)
|
|
|
+ panel.addSubview(footerLinks)
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
closeButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -20),
|
|
|
@@ -491,60 +563,112 @@ final class PaywallView: NSView {
|
|
|
plansStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
|
|
|
plansStack.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 24),
|
|
|
|
|
|
+ trustRow.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 22),
|
|
|
+ trustRow.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -22),
|
|
|
+ trustRow.topAnchor.constraint(equalTo: plansStack.bottomAnchor, constant: 18),
|
|
|
+
|
|
|
ctaButton.leadingAnchor.constraint(equalTo: title.leadingAnchor),
|
|
|
ctaButton.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
|
|
|
- ctaButton.topAnchor.constraint(equalTo: plansStack.bottomAnchor, constant: 20),
|
|
|
+ ctaButton.topAnchor.constraint(equalTo: trustRow.bottomAnchor, constant: 16),
|
|
|
ctaButton.heightAnchor.constraint(equalToConstant: 48),
|
|
|
+ ctaButton.bottomAnchor.constraint(lessThanOrEqualTo: footerLinks.topAnchor, constant: -14),
|
|
|
|
|
|
- footer.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
|
|
|
- footer.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -20),
|
|
|
+ footerLinks.centerXAnchor.constraint(equalTo: panel.centerXAnchor),
|
|
|
+ footerLinks.bottomAnchor.constraint(equalTo: panel.bottomAnchor, constant: -20),
|
|
|
])
|
|
|
|
|
|
return panel
|
|
|
}
|
|
|
|
|
|
- private func makeFooter() -> NSView {
|
|
|
+ private func makeTrustRow() -> NSView {
|
|
|
+ let securePayments = PaywallTrustItemView(
|
|
|
+ iconName: "shield.fill",
|
|
|
+ title: "Secure Payments",
|
|
|
+ subtitle: "Your payment is 100% secure"
|
|
|
+ )
|
|
|
+ let cancelAnytime = PaywallTrustItemView(
|
|
|
+ iconName: "arrow.counterclockwise",
|
|
|
+ title: "Cancel Anytime",
|
|
|
+ subtitle: "No commitment, cancel anytime."
|
|
|
+ )
|
|
|
+ let support = PaywallTrustItemView(
|
|
|
+ iconName: "headphones",
|
|
|
+ title: "24/7 Support",
|
|
|
+ subtitle: "We're here to help you anytime."
|
|
|
+ )
|
|
|
+ let privacyFirst = PaywallTrustItemView(
|
|
|
+ iconName: "lock.fill",
|
|
|
+ title: "Privacy First",
|
|
|
+ subtitle: "Your data is safe with us."
|
|
|
+ )
|
|
|
+
|
|
|
+ let trustStack = NSStackView(views: [securePayments, cancelAnytime, support, privacyFirst])
|
|
|
+ trustStack.orientation = .horizontal
|
|
|
+ trustStack.distribution = .fillEqually
|
|
|
+ trustStack.spacing = 16
|
|
|
+ trustStack.alignment = .top
|
|
|
+ trustStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ trustStack.wantsLayer = true
|
|
|
+ trustStack.layer?.backgroundColor = NSColor.white.cgColor
|
|
|
+ trustStack.layer?.cornerRadius = 12
|
|
|
+ trustStack.layer?.borderWidth = 1
|
|
|
+ trustStack.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
+ trustStack.layer?.masksToBounds = true
|
|
|
+ trustStack.edgeInsets = NSEdgeInsets(top: 12, left: 14, bottom: 12, right: 14)
|
|
|
+ trustStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ return trustStack
|
|
|
+ }
|
|
|
+
|
|
|
+ private func makeFooterLinks() -> NSView {
|
|
|
let container = NSView()
|
|
|
container.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
- let restoreLink = PaywallFooterLink(title: "Restore Purchases")
|
|
|
+ let manageSubscriptionLink = PaywallFooterLink(title: "Manage Subscription")
|
|
|
+ let restoreLink = PaywallFooterLink(title: "Restore Purchase")
|
|
|
restoreLink.target = self
|
|
|
restoreLink.action = #selector(restoreTapped)
|
|
|
|
|
|
let privacyLink = PaywallFooterLink(title: "Privacy Policy")
|
|
|
let termsLink = PaywallFooterLink(title: "Terms of Service")
|
|
|
+ let supportLink = PaywallFooterLink(title: "Support")
|
|
|
|
|
|
- let dot1 = makeFooterDot()
|
|
|
- let dot2 = makeFooterDot()
|
|
|
+ let separator1 = makeFooterSeparator()
|
|
|
+ let separator2 = makeFooterSeparator()
|
|
|
+ let separator3 = makeFooterSeparator()
|
|
|
+ let separator4 = makeFooterSeparator()
|
|
|
|
|
|
- let stack = NSStackView(views: [restoreLink, dot1, privacyLink, dot2, termsLink])
|
|
|
- stack.orientation = .horizontal
|
|
|
- stack.spacing = 6
|
|
|
- stack.alignment = .centerY
|
|
|
- stack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ let linksStack = NSStackView(views: [
|
|
|
+ manageSubscriptionLink, separator1, restoreLink, separator2, privacyLink, separator3, termsLink, separator4, supportLink,
|
|
|
+ ])
|
|
|
+ linksStack.orientation = .horizontal
|
|
|
+ linksStack.spacing = 8
|
|
|
+ linksStack.alignment = .centerY
|
|
|
+ linksStack.distribution = .fillProportionally
|
|
|
+ linksStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
- container.addSubview(stack)
|
|
|
+ container.addSubview(linksStack)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- stack.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
|
- stack.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
|
- stack.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
- stack.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
|
+ linksStack.centerXAnchor.constraint(equalTo: container.centerXAnchor),
|
|
|
+ linksStack.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor),
|
|
|
+ linksStack.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor),
|
|
|
+ linksStack.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
+ linksStack.bottomAnchor.constraint(equalTo: container.bottomAnchor)
|
|
|
])
|
|
|
|
|
|
return container
|
|
|
}
|
|
|
|
|
|
- private func makeFooterDot() -> NSView {
|
|
|
- let dot = NSView()
|
|
|
- dot.translatesAutoresizingMaskIntoConstraints = false
|
|
|
- dot.wantsLayer = true
|
|
|
- dot.layer?.backgroundColor = AppTheme.textSecondary.cgColor
|
|
|
- dot.layer?.cornerRadius = 1.5
|
|
|
+ private func makeFooterSeparator() -> NSView {
|
|
|
+ let separator = NSView()
|
|
|
+ separator.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ separator.wantsLayer = true
|
|
|
+ separator.layer?.backgroundColor = AppTheme.paywallBorder.cgColor
|
|
|
NSLayoutConstraint.activate([
|
|
|
- dot.widthAnchor.constraint(equalToConstant: 3),
|
|
|
- dot.heightAnchor.constraint(equalToConstant: 3),
|
|
|
+ separator.widthAnchor.constraint(equalToConstant: 1),
|
|
|
+ separator.heightAnchor.constraint(equalToConstant: 12),
|
|
|
])
|
|
|
- return dot
|
|
|
+ return separator
|
|
|
}
|
|
|
|
|
|
private func selectPlan(_ plan: PaywallPlan) {
|