mirror of
https://github.com/confirmedcode/Lockdown-iOS.git
synced 2025-12-21 12:14:02 +01:00
411 lines
17 KiB
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")
|
|
}
|