Files
lockdown-iOS-mirror/LockdowniOS/FeedbackPaywallViewController.swift

411 lines
17 KiB
Swift

//
// FeedbackPaywallViewController.swift
// Lockdown
//
// Created by Fabian Mistoiu on 10.10.2024.
// Copyright © 2024 Confirmed Inc. All rights reserved.
//
import UIKit
import Combine
class FeedbackPaywallViewController: UIViewController {
private let viewModel: FeedbackPaywallViewModel
private var subscriptions = Set<AnyCancellable>()
init(viewModel: FeedbackPaywallViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UI
private lazy var closeButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.tintColor = .feedbackText
button.setTitle(Copy.close, for: .normal)
button.titleLabel?.font = .close
button.addAction(
UIAction { [weak self] _ in
guard let self else { return }
self.viewModel.onCloseHandler?(self)
},
for: .touchUpInside)
return button
}()
private lazy var bannerView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "feedback-paywall-banner"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.addSubview(bannerArrowImageView)
NSLayoutConstraint.activate([
bannerArrowImageView.centerYAnchor.constraint(equalTo: imageView.bottomAnchor),
bannerArrowImageView.centerXAnchor.constraint(equalTo: imageView.rightAnchor, constant: -10),
bannerArrowImageView.widthAnchor.constraint(equalToConstant: 92),
bannerArrowImageView.heightAnchor.constraint(equalToConstant: 92),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1072 / 687),
imageView.widthAnchor.constraint(lessThanOrEqualToConstant: 420)
])
return imageView
}()
private lazy var bannerArrowImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "feedback-paywall-arrow"))
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = .feedbackText
let continueRange = (Copy.title as NSString).range(of: Copy.titleHighlight)
let attributedString = NSMutableAttributedString(
string: Copy.title,
attributes: [.font: UIFont.title as Any])
attributedString.addAttribute(.foregroundColor, value: UIColor.feedbackBlue as Any, range: continueRange)
label.attributedText = attributedString
return label
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.textColor = .feedbackText
label.font = .description
label.text = Copy.description
return label
}()
private lazy var bulletPointContainer: UIView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 5
[Copy.bulletPoint1, Copy.bulletPoint2, Copy.bulletPoint3]
.map { createBulletPointView(copy: $0) }
.forEach { stackView.addArrangedSubview($0) }
return stackView
}()
private lazy var bottomContainer: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [continueButton, linksContainer])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .fill
stackView.spacing = 8
stackView.setCustomSpacing(17, after: continueButton)
return stackView
}()
private lazy var continueButton: UIButton = {
let button = UIButton(type: .system)
button.tintColor = .white
button.setTitle(Copy.continue, for: .normal)
button.titleLabel?.font = .ctaButton
button.backgroundColor = .feedbackBlue
button.layer.cornerRadius = 29
button.anchors.height.equal(58)
button.addAction(
UIAction { [weak self] _ in
guard let self else { return }
let pID = viewModel.paywallPlans[viewModel.selectedPlanIndex].id
viewModel.onPurchaseHandler?(self, pID)
},
for: .touchUpInside)
return button
}()
private lazy var linksContainer: UIView = {
let linkButtons = [
createLinkButton(title: Copy.terms, url: URL(string: "https://lockdownprivacy.com/terms")!),
createLinkButton(title: Copy.privacy, url: URL(string: "https://lockdownprivacy.com/privacy")!)
]
let stackView = UIStackView(arrangedSubviews: linkButtons)
stackView.distribution = .fillEqually
return stackView
}()
private var planButtons: [PlanContainer] = []
// MARK: -
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .background
view.addSubview(closeButton)
NSLayoutConstraint.activate([
closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 5),
closeButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 17)
])
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 28 + 16),
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor),
scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
let bannerImageContainer = UIView()
bannerImageContainer.translatesAutoresizingMaskIntoConstraints = false
bannerImageContainer.addSubview(bannerView)
NSLayoutConstraint.activate([
bannerView.topAnchor.constraint(equalTo: bannerImageContainer.topAnchor),
bannerView.bottomAnchor.constraint(equalTo: bannerImageContainer.bottomAnchor),
bannerView.centerXAnchor.constraint(equalTo: bannerImageContainer.centerXAnchor)
])
let bannerWidthConstraint = bannerView.widthAnchor.constraint(equalTo: bannerImageContainer.widthAnchor, multiplier: 0.8)
bannerWidthConstraint.priority = .init(rawValue: 999)
bannerWidthConstraint.isActive = true
let copyStackView = UIStackView(arrangedSubviews: [bannerImageContainer, titleLabel, descriptionLabel, bulletPointContainer])
copyStackView.translatesAutoresizingMaskIntoConstraints = false
copyStackView.axis = .vertical
copyStackView.alignment = .fill
copyStackView.distribution = .fill
copyStackView.spacing = 0
copyStackView.setCustomSpacing(22, after: bannerImageContainer)
copyStackView.setCustomSpacing(17, after: descriptionLabel)
scrollView.addSubview(copyStackView)
NSLayoutConstraint.activate([
copyStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0),
copyStackView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
copyStackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 39),
copyStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor)
])
view.addSubview(bottomContainer)
NSLayoutConstraint.activate([
bottomContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0),
bottomContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor),
bottomContainer.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 45),
bottomContainer.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 10)
])
viewModel.$paywallPlans.sink(receiveValue: { [weak self] plans in
guard let self else { return }
planButtons.forEach { $0.removeFromSuperview() }
planButtons = plans.map { self.createPlanButton(title: $0.name, price: $0.price, period: $0.pricePeriod, promo: $0.promo) }
planButtons.reversed().forEach { self.bottomContainer.insertArrangedSubview($0, at: 0) }
if let lastButton = planButtons.last {
bottomContainer.setCustomSpacing(30, after: lastButton)
}
selectButton(at: viewModel.selectedPlanIndex)
}).store(in: &subscriptions)
viewModel.$selectedPlanIndex.sink(receiveValue: { [weak self] selectedPlanIndex in
guard let self else { return }
selectButton(at: selectedPlanIndex)
}).store(in: &subscriptions)
}
func selectButton(at selectedIndex: Int) {
for (index, button) in planButtons.map(\.button).enumerated() {
let selected = index == selectedIndex
button.backgroundColor = selected ? .selectedPlanBackground : .clear
button.titleLabel?.font = selected ? .selectedPlanTitle: .unselectedPlanTitle
button.layer.borderColor = selected ? UIColor.feedbackBlue.cgColor : UIColor.smallGrey.cgColor
}
}
// MARK: - UI helper
private func createBulletPointView(copy: String) -> UIView {
let bulletPointImageView = UIImageView(image: UIImage(named: "feedback-checkmark"))
bulletPointImageView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
bulletPointImageView.widthAnchor.constraint(equalToConstant: 9),
bulletPointImageView.heightAnchor.constraint(equalToConstant: 6),
])
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.numberOfLines = 0
label.text = copy
label.textColor = .feedbackText
label.font = .bulletPoint
let spacer = UIView()
spacer.setContentHuggingPriority(.required, for: .horizontal)
NSLayoutConstraint.activate([
spacer.widthAnchor.constraint(equalToConstant: 5),
spacer.heightAnchor.constraint(equalToConstant: 5),
])
let stackView = UIStackView(arrangedSubviews: [spacer, bulletPointImageView, label])
stackView.spacing = 8
stackView.alignment = .center
return stackView
}
private func createPlanButton(title: String, price: String, period: String?, promo: String?) -> PlanContainer {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 54).isActive = true
button.layer.cornerRadius = 27
button.layer.borderWidth = 1
button.tintColor = .feedbackText
button.setTitle(title, for: .normal)
button.contentHorizontalAlignment = .left
button.contentVerticalAlignment = .center
button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 21, bottom: 0, right: 0)
button.addTarget(self, action: #selector(planSelected), for: .touchUpInside)
let priceLabel = UILabel()
priceLabel.translatesAutoresizingMaskIntoConstraints = false
priceLabel.textColor = .feedbackText
priceLabel.font = .planPrice
priceLabel.text = price
let pricePeriodLabel: UILabel? = if period != nil { UILabel() } else { nil }
pricePeriodLabel?.translatesAutoresizingMaskIntoConstraints = false
pricePeriodLabel?.textColor = .feedbackText
pricePeriodLabel?.font = .planPeriod
pricePeriodLabel?.text = period
let stackView = UIStackView(arrangedSubviews: [priceLabel, pricePeriodLabel].compactMap { $0 })
stackView.isUserInteractionEnabled = false
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .trailing
button.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: button.centerYAnchor),
stackView.rightAnchor.constraint(equalTo: button.rightAnchor, constant: -18)
])
var promoView: UIView?
if let promo {
let gradientView = GradientView()
gradientView.translatesAutoresizingMaskIntoConstraints = false
gradientView.gradient = .custom([UIColor.promoGradientStart.cgColor, UIColor.promoGradientStart.cgColor], .horizontal)
gradientView.layer.cornerRadius = 11.5
gradientView.clipsToBounds = true
let promoLabel = UILabel()
promoLabel.translatesAutoresizingMaskIntoConstraints = false
promoLabel.textColor = .feedbackText
promoLabel.font = .selectedPlanTitle
promoLabel.text = promo
gradientView.addSubview(promoLabel)
NSLayoutConstraint.activate([
promoLabel.centerYAnchor.constraint(equalTo: gradientView.centerYAnchor),
promoLabel.centerXAnchor.constraint(equalTo: gradientView.centerXAnchor),
promoLabel.leftAnchor.constraint(equalTo: gradientView.leftAnchor, constant: 7),
gradientView.heightAnchor.constraint(equalToConstant: 23)
])
promoView = gradientView
}
return PlanContainer(button: button, promoLabel: promoView)
}
private func createLinkButton(title: String, url: URL) -> UIButton {
let button = UIButton(type: .system)
button.titleLabel?.font = fontMedium13
button.setTitle(title, for: .normal)
button.tintColor = .feedbackText
button.addAction(
UIAction { _ in
UIApplication.shared.open(url, options: [:], completionHandler: nil)
},
for: .touchUpInside)
return button
}
@objc func planSelected(_ sender: UIButton) {
guard let index = planButtons.map(\.button).firstIndex(of: sender) else { return }
viewModel.selectPlan(at: index)
}
}
private class PlanContainer: UIView {
let button: UIButton
let promoLabel: UIView?
init(button: UIButton, promoLabel: UIView?) {
self.button = button
self.promoLabel = promoLabel
super.init(frame: .zero)
addSubview(button)
NSLayoutConstraint.activate([
button.topAnchor.constraint(equalTo: topAnchor),
button.bottomAnchor.constraint(equalTo: bottomAnchor),
button.leftAnchor.constraint(equalTo: leftAnchor),
button.rightAnchor.constraint(equalTo: rightAnchor)
])
if let promoLabel {
addSubview(promoLabel)
NSLayoutConstraint.activate([
promoLabel.centerYAnchor.constraint(equalTo: button.topAnchor),
promoLabel.rightAnchor.constraint(equalTo: button.rightAnchor, constant: -18)
])
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private enum Copy {
static var close: String = NSLocalizedString("CLOSE", comment: "")
static var title: String = NSLocalizedString("Tap Continue to Activate this ONE TIME Offer", comment: "")
static var titleHighlight: String = NSLocalizedString("Continue", comment: "")
static var `continue`: String = NSLocalizedString("Continue", comment: "")
static let description: String = NSLocalizedString("Private Browsing with Hidden IP and Global Region Switching", comment: "")
static var bulletPoint1: String = NSLocalizedString("Anonymised browsing", comment: "")
static var bulletPoint2: String = NSLocalizedString("Location and IP address hidden", comment: "")
static var bulletPoint3: String = NSLocalizedString("Unlimited bandwidth and data usage & more", comment: "")
static var terms: String = NSLocalizedString("Terms", comment: "")
static var privacy: String = NSLocalizedString("Privacy", comment: "")
}
private extension UIFont {
static let close = UIFont(name: "Montserrat-Bold", size: 13)
static let title = UIFont(name: "SFProRounded-Semibold", size: 28)
static let description = UIFont(name: "Montserrat-Regular", size: 14)
static let bulletPoint = UIFont(name: "Montserrat-SemiBold", size: 12)
static let ctaButton = UIFont(name: "Montserrat-SemiBold", size: 20)
static let selectedPlanTitle = UIFont(name: "Montserrat-Bold", size: 12)
static let unselectedPlanTitle = UIFont(name: "Montserrat-Medium", size: 12)
static let planPrice = UIFont(name: "Montserrat-SemiBold", size: 14)
static let planPeriod = UIFont(name: "Montserrat-Medium", size: 14)
}
private extension UIColor {
static let background = UIColor.panelSecondaryBackground
static let feedbackText = UIColor.label
static let feedbackBlue = UIColor.fromHex("#00ADE7")
static let selectedPlanBackground = feedbackBlue.withAlphaComponent(0.1)
static let unselectedPlanBorder = UIColor.fromHex("#999999")
static let promoGradientStart = UIColor.fromHex("#FB923C")
static let promoGradientEnd = UIColor.fromHex("#EA580C")
}