Files
lockdown-iOS-mirror/LockdowniOS/TablePaywallViewController.swift
Alexander Parshakov 2bc6adf847 Release 1.6.1
2023-01-10 21:17:38 +05:00

370 lines
14 KiB
Swift

//
// TablePaywallViewController.swift
// Lockdown
//
// Created by Alexander Parshakov on 8/29/22
// Copyright © 2022 Confirmed Inc. All rights reserved.
//
import CocoaLumberjackSwift
import Foundation
import PromiseKit
import StoreKit
import UIKit
protocol PaywallViewControllerCloseDelegate: AnyObject {
func didClosePaywall()
}
final class TablePaywallViewController: BaseViewController, Loadable {
@IBOutlet private var restorePurchaseLabel: UILabel!
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var featureStackView: UIStackView!
@IBOutlet private var chooseYourPlanLabel: UILabel!
@IBOutlet private var segmentedControl: UISegmentedControl!
@IBOutlet private var annualOfferView: UIView!
@IBOutlet private var annualOfferTitleLabel: UILabel!
@IBOutlet private var annualOfferSubtitleLabel: UILabel!
@IBOutlet private var saveMoneyContainerView: UIView!
@IBOutlet private var saveMoneyLabel: UILabel!
@IBOutlet private var monthlyOfferView: UIView!
@IBOutlet private var monthlyOfferTitleLabel: UILabel!
@IBOutlet private var monthlyOfferSubtitleLabel: UILabel!
@IBOutlet private var shadowView: UIView!
@IBOutlet private var continueButton: UIButton!
@IBOutlet private var privacyPolicyLabel: UILabel!
@IBOutlet private var redeemCodeLabel: UILabel!
@IBOutlet private var termsOfServiceLabel: UILabel!
@IBOutlet private var offerStackViewBottomConstraint: NSLayoutConstraint!
@IBOutlet private var scrollViewBottomConstraint: NSLayoutConstraint!
private var annualOfferGestureRecognizer: UITapGestureRecognizer?
private var monthlyOfferGestureRecognizer: UITapGestureRecognizer?
private var continueButtonGradientLayer: CAGradientLayer?
private let paywallTableFeatures: [PaywallTableFeature]
private let existingSubscription: Subscription?
private let paywallService: PaywallService
weak var delegate: PaywallViewControllerCloseDelegate?
private var currentGroup: AppStoreProductGroup = .pro {
didSet {
updateScreenState()
}
}
private var currentPeriod: SubscriptionOfferPeriodUnit = .year {
didSet {
updateScreenState()
}
}
init(paywallTableFeatures: [PaywallTableFeature] = .allDefaultFeatures,
existingSubscription: Subscription? = BaseUserService.shared.user.currentSubscription,
paywallService: PaywallService = BasePaywallService.shared) {
self.paywallTableFeatures = paywallTableFeatures
self.existingSubscription = existingSubscription
self.paywallService = paywallService
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
VPNSubscription.cacheLocalizedPrices()
setupUI()
updateScreenState()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
saveMoneyContainerView.corners = .continuous(saveMoneyContainerView.bounds.midY)
continueButton.applyGradient(.lightBlue, corners: .continuous(continueButton.bounds.midY))
}
deinit {
delegate?.didClosePaywall()
if UserDefaults.hasSeenLTO == false {
paywallService.context = .followUpLimitedTimeOffer
}
}
@IBAction private func didTapClose(_ sender: Any) {
dismiss(animated: true)
}
@IBAction private func didSelectSegment(_ sender: Any) {
currentGroup = segmentedControl.selectedSegmentIndex == 0 ? .firewallAndVpn : .pro
updateScreenState()
}
@IBAction private func didTapContinue(_ sender: Any) {
continueButton.showAnimatedPress { [weak self] in
self?.updateChosenOffer()
self?.purchaseProduct()
}
}
// MARK: - Private Methods
private func setupUI() {
setupTexts()
updateFeatureStackView()
setupSegmentedControl()
updateOfferViews()
setupShadowFromContinue()
updateViewsForIpad()
setupGestureRecognizers()
}
private func updateFeatureStackView() {
featureStackView.spacing = isPad ? 6 : 3
featureStackView.clear()
featureStackView.addArrangedSubview(TablePaywallHeaderView.make())
paywallTableFeatures.forEach {
featureStackView.addArrangedSubview(Separator(height: isPad ? 2 : 1))
let rowView = TablePaywallFeatureRowView.make(feature: $0, isHighlighted: currentGroup.hasFeature($0))
featureStackView.addArrangedSubview(rowView)
}
}
private func setupSegmentedControl() {
segmentedControl.selectedSegmentIndex = currentGroup == .firewallAndVpn ? 0 : 1
if UIScreen.main.traitCollection.userInterfaceIdiom == .pad {
segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white, .font: UIFont.boldLockdownFont(size: 15)], for: .selected)
segmentedControl.setTitleTextAttributes([.font: UIFont.mediumLockdownFont(size: 16)], for: .normal)
} else {
segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white, .font: UIFont.boldLockdownFont(size: 13)], for: .selected)
segmentedControl.setTitleTextAttributes([.font: UIFont.mediumLockdownFont(size: 13)], for: .normal)
}
segmentedControl.setTitle("VPN", forSegmentAt: 0)
segmentedControl.setTitle("PRO", forSegmentAt: 1)
}
private func setupTexts() {
restorePurchaseLabel.text = .localized("restore_purchase")
titleLabel.text = .localized("start_trial_now_to_get_full_access")
chooseYourPlanLabel.text = .localized("CHOOSE_YOUR_PLAN")
annualOfferTitleLabel.text = .localized("plan_annual")
monthlyOfferTitleLabel.text = .localized("plan_monthly")
continueButton.setTitle(.localized("continue"), for: .normal)
privacyPolicyLabel.text = .localized("Privacy Policy")
if #available(iOS 14.0, *) {
redeemCodeLabel.isHidden = false
redeemCodeLabel.text = .localized("redeem_code")
}
termsOfServiceLabel.text = .localized("terms_of_service")
updateSaveMoneyLabel()
}
private func updateSaveMoneyLabel() {
saveMoneyContainerView.isHidden = false
let localizationKey = currentGroup == .firewallAndVpn ? "save_annual_vpn_subscription" : "save_annual_pro_subscription"
saveMoneyLabel.text = .localized(localizationKey)
}
private func updateOfferViews() {
[annualOfferView, monthlyOfferView].forEach { $0?.recolorToDefault() }
[annualOfferTitleLabel, annualOfferSubtitleLabel, monthlyOfferTitleLabel, monthlyOfferSubtitleLabel].forEach {
$0?.textColor = .confirmedBlue
}
// Highlighting the view for currently selected period
let currentOfferView = currentPeriod == .year ? annualOfferView : monthlyOfferView
let currentTitleLabel = currentPeriod == .year ? annualOfferTitleLabel : monthlyOfferTitleLabel
let currentSubtitleLabel = currentPeriod == .year ? annualOfferSubtitleLabel : monthlyOfferSubtitleLabel
currentOfferView?.backgroundColor = .confirmedBlue
currentTitleLabel?.textColor = .white
currentSubtitleLabel?.textColor = .white
}
private func setupShadowFromContinue() {
shadowView.layer.shadowColor = UIColor.systemBackground.cgColor
shadowView.layer.masksToBounds = false
shadowView.layer.shadowOffset = CGSize(width: 0, height : -20)
shadowView.layer.shadowPath = UIBezierPath(rect: shadowView.bounds).cgPath
shadowView.layer.shadowOpacity = 1
shadowView.layer.shadowRadius = 25
}
private func updateViewsForIpad() {
guard UIScreen.main.traitCollection.userInterfaceIdiom == .pad else { return }
restorePurchaseLabel.font = restorePurchaseLabel.font.withSize(16)
chooseYourPlanLabel.font = chooseYourPlanLabel.font.withSize(24)
annualOfferTitleLabel.font = annualOfferTitleLabel.font.withSize(20)
annualOfferSubtitleLabel.font = annualOfferSubtitleLabel.font.withSize(18)
saveMoneyLabel.font = saveMoneyLabel.font.withSize(17)
monthlyOfferTitleLabel.font = monthlyOfferTitleLabel.font.withSize(20)
monthlyOfferSubtitleLabel.font = monthlyOfferSubtitleLabel.font.withSize(18)
privacyPolicyLabel.font = privacyPolicyLabel.font.withSize(16)
redeemCodeLabel.font = redeemCodeLabel.font.withSize(16)
termsOfServiceLabel.font = termsOfServiceLabel.font.withSize(16)
scrollViewBottomConstraint.constant = 20
offerStackViewBottomConstraint.constant = 50
}
private func setupGestureRecognizers() {
let restorePurchaseTap = UITapGestureRecognizer(target: self, action: #selector(restorePurchase))
restorePurchaseLabel.isUserInteractionEnabled = true
restorePurchaseLabel.addGestureRecognizer(restorePurchaseTap)
let annualTap = UITapGestureRecognizer(target: self, action: #selector(choseAnnualOffer))
annualOfferView.addGestureRecognizer(annualTap)
annualOfferGestureRecognizer = annualTap
let monthlyTap = UITapGestureRecognizer(target: self, action: #selector(choseMonthlyOffer))
monthlyOfferView.addGestureRecognizer(monthlyTap)
monthlyOfferGestureRecognizer = monthlyTap
let privacyPolicyTap = UITapGestureRecognizer(target: self, action: #selector(showPrivacyPolicy))
privacyPolicyLabel.isUserInteractionEnabled = true
privacyPolicyLabel.addGestureRecognizer(privacyPolicyTap)
let redeemCodeTap = UITapGestureRecognizer(target: self, action: #selector(showRedeemCode))
redeemCodeLabel.isUserInteractionEnabled = true
redeemCodeLabel.addGestureRecognizer(redeemCodeTap)
let termsOfServiceTap = UITapGestureRecognizer(target: self, action: #selector(showTermsOfService))
termsOfServiceLabel.isUserInteractionEnabled = true
termsOfServiceLabel.addGestureRecognizer(termsOfServiceTap)
}
@objc private func choseAnnualOffer() {
currentPeriod = .year
}
@objc private func choseMonthlyOffer() {
currentPeriod = .month
}
@objc private func showPrivacyPolicy() {
showModalWebView(title: .localized("Privacy Policy"), urlString: .lockdownUrlPrivacy)
}
@objc private func showRedeemCode() {
paywallService.showRedemptionSheet()
}
@objc private func showTermsOfService() {
showModalWebView(title: .localized("terms_of_service"), urlString: .lockdownUrlTerms)
}
private func updateScreenState() {
// If period and group of existing subscription (plan) are selected,
// we must force-select the period of the other plan.
if existingSubscription?.isSubscription(in: currentGroup, of: currentPeriod) == true {
currentPeriod = currentPeriod == .year ? .month : .year
}
updateFeatureStackView()
updateSaveMoneyLabel()
updateOfferViews()
updatePriceSubtitles()
annualOfferGestureRecognizer?.isEnabled = true
monthlyOfferGestureRecognizer?.isEnabled = true
guard let existingSubscription, existingSubscription.correspondingProductGroup == currentGroup else { return }
let isAnnual = [.proAnnual, .annual, .proAnnualLTO].contains(existingSubscription.planType)
// If existing subscription is annual, no need to show the save-label.
saveMoneyContainerView.isHidden = isAnnual
let offerView = isAnnual ? annualOfferView : monthlyOfferView
let subtitleLabel = isAnnual ? annualOfferSubtitleLabel : monthlyOfferSubtitleLabel
let gestureRecognizer = isAnnual ? annualOfferGestureRecognizer : monthlyOfferGestureRecognizer
offerView?.layer.borderWidth = 0
subtitleLabel?.text = .localized("current_plan")
gestureRecognizer?.isEnabled = false
}
private func updateChosenOffer() {
switch currentGroup {
case .firewallAndVpn:
if currentPeriod == .year {
VPNSubscription.selectedProductId = VPNSubscription.productIdAnnual
} else {
VPNSubscription.selectedProductId = VPNSubscription.productIdMonthly
}
case .pro:
if currentPeriod == .year {
VPNSubscription.selectedProductId = VPNSubscription.productIdAnnualPro
} else {
VPNSubscription.selectedProductId = VPNSubscription.productIdMonthlyPro
}
}
}
private func updatePriceSubtitles() {
switch currentGroup {
case .firewallAndVpn:
monthlyOfferSubtitleLabel.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthly, for: .new)
annualOfferSubtitleLabel.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnual, for: .new)
case .pro:
monthlyOfferSubtitleLabel.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthlyPro, for: .new)
annualOfferSubtitleLabel.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnualPro, for: .new)
}
}
}
extension TablePaywallViewController {
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
shadowView.layer.shadowColor = UIColor.systemBackground.cgColor
}
}
// MARK: - Purchase and Restore
extension TablePaywallViewController: ProductPurchasable {
@objc private func restorePurchase() {
restorePurchases()
}
}
private extension UIView {
func recolorToDefault() {
corners = .continuous(10)
layer.borderWidth = 2
layer.borderColor = UIColor.confirmedBlue.cgColor
backgroundColor = .systemBackground
}
}