Files
lockdown-iOS-mirror/LockdowniOS/Controllers/FirewallPaywallViewController.swift
2023-05-16 06:20:10 +03:00

515 lines
26 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// AdvancedPaywall.swift
// LockdownSandbox
//
// Created by Алишер Ахметжанов on 29.04.2023.
//
import UIKit
import SwiftyStoreKit
import NetworkExtension
import PromiseKit
import CocoaLumberjackSwift
import StoreKit
protocol PaywallViewControllerCloseDelegate: AnyObject {
func didClosePaywall()
}
final class FirewallPaywallViewController: BaseViewController, Loadable {
var parentVC: UIViewController?
weak var firewallVC: LDFirewallViewController?
var advancedPlanUpdated: (() -> ())?
enum Mode {
case newSubscription
case upgrade(active: [Subscription.PlanType])
}
var mode = Mode.newSubscription
//MARK: Properties
private var titleName = NSLocalizedString("Lockdown", comment: "")
private lazy var navigationView: ConfiguredNavigationView =
{
let view = ConfiguredNavigationView()
view.rightNavButton.setTitle(NSLocalizedString("RESTORE", comment: ""), for: .normal)
view.rightNavButton.addTarget(self, action: #selector(restorePurchase), for: .touchUpInside)
view.titleLabel.text = NSLocalizedString(titleName, comment: "")
view.leftNavButton.setTitle(NSLocalizedString("CLOSE", comment: ""), for: .normal)
view.leftNavButton.addTarget(self, action: #selector(closeButtonClicked), for: .touchUpInside)
return view
}()
private lazy var annualPlan: AdvancedPlansViews = {
let view = AdvancedPlansViews()
view.title.text = "Annual"
view.detailTitle.text = "\(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedYearly))/year"
view.detailTitle2.text = "$2.99/month"
view.discountImageView.image = UIImage(named: "saveDiscount")
view.iconImageView.image = UIImage(named: "fill-1")
view.backgroundView.layer.borderColor = UIColor.white.cgColor
view.isUserInteractionEnabled = true
view.setOnClickListener { [unowned self] in
selectAdvancedYearly()
annualView.isHidden = false
monthlyView.isHidden = true
view.iconImageView.image = UIImage(named: "fill-1")
view.backgroundView.layer.borderColor = UIColor.white.cgColor
monthlyPlan.iconImageView.image = UIImage(named: "grey-ellipse-1")
monthlyPlan.backgroundView.layer.borderColor = UIColor.borderGray.cgColor
ftPriceLabel.text = "7-day free trial, then \(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedYearly)) yearly. Cancel anytime."
}
return view
}()
private lazy var monthlyPlan: AdvancedPlansViews = {
let view = AdvancedPlansViews()
view.title.text = "Monthly"
view.detailTitle.text = "\(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedMonthly))/month"
view.detailTitle2.text = " "
view.isUserInteractionEnabled = true
view.setOnClickListener { [unowned self] in
selectAdvancedMonthly()
monthlyView.isHidden = false
annualView.isHidden = true
view.iconImageView.image = UIImage(named: "fill-1")
view.backgroundView.layer.borderColor = UIColor.white.cgColor
annualPlan.iconImageView.image = UIImage(named: "grey-ellipse-1")
annualPlan.backgroundView.layer.borderColor = UIColor.borderGray.cgColor
ftPriceLabel.text = "7-day free trial, then \(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedMonthly)) monthly. Cancel anytime."
}
return view
}()
private lazy var plansStack: UIStackView = {
let stack = UIStackView()
stack.axis = .horizontal
stack.addArrangedSubview(annualPlan)
stack.addArrangedSubview(monthlyPlan)
stack.alignment = .leading
stack.distribution = .fillEqually
stack.spacing = 16
return stack
}()
lazy var ftPriceLabel: UILabel = {
let label = UILabel()
label.text = NSLocalizedString("7-day free trial, then \(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedYearly)) yearly. Cancel anytime.", comment: "")
label.textColor = .smallGrey
label.font = fontMedium11
label.textAlignment = .center
label.numberOfLines = 0
return label
}()
private lazy var freeTrialButton: UIButton = {
let button = UIButton(type: .system)
button.tintColor = .white
button.backgroundColor = .tunnelsBlue
button.layer.cornerRadius = 28
button.addTarget(self, action: #selector(startTrial), for: .touchUpInside)
let titleLabel = UILabel()
let title = NSLocalizedString("Try 7-day free trial", comment: "")
titleLabel.font = fontSemiBold17
titleLabel.text = title
titleLabel.textColor = .white
titleLabel.textAlignment = .center
button.addSubview(titleLabel)
titleLabel.anchors.top.pin(inset: 16)
titleLabel.anchors.bottom.pin(inset: 16)
titleLabel.anchors.leading.pin(inset: 24)
titleLabel.anchors.trailing.pin(inset: 24)
button.anchors.height.equal(56)
return button
}()
private lazy var annualView: AnnualPlanView = {
let view = AnnualPlanView()
return view
}()
private lazy var monthlyView: MonthlyPlanView = {
let view = MonthlyPlanView()
view.isHidden = true
return view
}()
private lazy var privacyLabel: UILabel = {
let label = UILabel()
label.font = fontMedium11
label.textAlignment = .center
label.numberOfLines = 0
let attributedText = NSMutableAttributedString(string: NSLocalizedString("By continuing you agree with our ", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.smallGrey])
let termsRange = NSRange(location: attributedText.length, length: NSLocalizedString("Terms of Service", comment: "").count)
attributedText.append(NSAttributedString(string: NSLocalizedString("Terms of Service", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.white]))
attributedText.append(NSAttributedString(string: NSLocalizedString(" and ", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.smallGrey]))
let privacyRange = NSRange(location: attributedText.length, length: NSLocalizedString("Privacy Policy", comment: "").count)
attributedText.append(NSAttributedString(string: NSLocalizedString("Privacy Policy", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.white]))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
attributedText.addAttributes([NSAttributedString.Key.paragraphStyle: paragraphStyle], range: NSRange(location: 0, length: attributedText.length))
label.attributedText = attributedText
label.isUserInteractionEnabled = true
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(labelTapped(sender:)))
label.addGestureRecognizer(tapGesture)
return label
}()
//MARK: Lificycle
override func viewDidLoad() {
super.viewDidLoad()
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.purplePaywall.cgColor, UIColor.purplePaywall2.cgColor]
gradientLayer.frame = view.bounds
view.layer.insertSublayer(gradientLayer, at: 0)
configureUI()
}
//MARK: ConfigureUI
private func configureUI() {
view.addSubview(navigationView)
navigationView.anchors.top.safeAreaPin(inset: 18)
navigationView.anchors.leading.marginsPin()
navigationView.anchors.trailing.marginsPin()
view.addSubview(privacyLabel)
privacyLabel.anchors.bottom.safeAreaPin()
privacyLabel.anchors.leading.marginsPin()
privacyLabel.anchors.trailing.marginsPin()
privacyLabel.anchors.height.equal(34)
view.addSubview(freeTrialButton)
freeTrialButton.anchors.bottom.spacing(8, to: privacyLabel.anchors.top)
freeTrialButton.anchors.leading.marginsPin()
freeTrialButton.anchors.trailing.marginsPin()
view.addSubview(ftPriceLabel)
ftPriceLabel.anchors.bottom.spacing(8, to: freeTrialButton.anchors.top)
ftPriceLabel.anchors.leading.marginsPin()
ftPriceLabel.anchors.trailing.marginsPin()
view.addSubview(plansStack)
plansStack.anchors.bottom.spacing(20, to: ftPriceLabel.anchors.top)
plansStack.anchors.leading.pin(inset: 16)
plansStack.anchors.trailing.pin(inset: 16)
view.addSubview(annualView)
annualView.anchors.top.spacing(24, to: navigationView.anchors.bottom)
annualView.anchors.leading.pin()
annualView.anchors.trailing.pin()
annualView.anchors.bottom.spacing(8, to: plansStack.anchors.top)
view.addSubview(monthlyView)
monthlyView.anchors.top.spacing(24, to: navigationView.anchors.bottom)
monthlyView.anchors.leading.pin()
monthlyView.anchors.trailing.pin()
monthlyView.anchors.bottom.spacing(8, to: plansStack.anchors.top)
}
//MARK: Functions
@objc func closeButtonClicked() {
dismiss(animated: true)
}
@objc private func labelTapped(sender: UITapGestureRecognizer) {
let termsRange = NSRange(location: privacyLabel.attributedText!.length - NSLocalizedString("Terms of Service", comment: "").count - 18, length: NSLocalizedString("Terms of Service", comment: "").count)
let privacyRange = NSRange(location: privacyLabel.attributedText!.length - NSLocalizedString("Privacy Policy", comment: "").count, length: NSLocalizedString("Privacy Policy", comment: "").count)
if sender.didTapAttributedTextInLabel(label: privacyLabel, inRange: privacyRange),
let url = URL(string: "https://lockdownprivacy.com/privacy") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
} else if sender.didTapAttributedTextInLabel(label: privacyLabel, inRange: termsRange),
let url = URL(string: "https://lockdownprivacy.com/terms") {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
}
extension UITapGestureRecognizer {
func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool {
guard let attributedText = label.attributedText else { return false }
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize.zero)
let textStorage = NSTextStorage(attributedString: attributedText)
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
textContainer.lineFragmentPadding = 0.0
textContainer.lineBreakMode = label.lineBreakMode
textContainer.maximumNumberOfLines = label.numberOfLines
textContainer.size = label.bounds.size
let locationOfTouchInLabel = self.location(in: label)
let textBoundingBox = layoutManager.usedRect(for: textContainer)
let textContainerOffset = CGPoint(x: (label.bounds.width - textBoundingBox.width) * 0.5 - textBoundingBox.minX,
y: (label.bounds.height - textBoundingBox.height) * 0.5 - textBoundingBox.minY)
let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x,
y: locationOfTouchInLabel.y - textContainerOffset.y)
let index = layoutManager.characterIndex(for: locationOfTouchInTextContainer,
in: textContainer,
fractionOfDistanceBetweenInsertionPoints: nil)
return NSLocationInRange(index, targetRange)
}
}
extension FirewallPaywallViewController: ProductPurchasable {
@objc private func restorePurchase() {
//toggleRestorePurchasesButton(false)
firstly {
try Client.signIn(forceRefresh: true)
}
.then { (signin: SignIn) -> Promise<GetKey> in
try Client.getKey()
}
.done { (getKey: GetKey) in
// we were able to get key, so subscription is valid -- follow pathway from HomeViewController to associate this with the email account if there is one
let presentingViewController = self.presentingViewController as? HomeViewController
self.dismiss(animated: true, completion: {
if presentingViewController != nil {
presentingViewController?.toggleVPN("me")
}
else {
VPNController.shared.setEnabled(true)
}
})
}
.catch { error in
// self.toggleRestorePurchasesButton(true)
DDLogError("Restore Failed: \(error)")
if let apiError = error as? ApiError {
switch apiError.code {
case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription:
// now try email if it exists
if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true {
DDLogInfo("restore: have confirmed API credentials, using them")
// self.toggleRestorePurchasesButton(false)
firstly {
try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password)
}
.then { (signin: SignIn) -> Promise<GetKey> in
DDLogInfo("restore: signin result: \(signin)")
return try Client.getKey()
}
.done { (getKey: GetKey) in
// self.toggleRestorePurchasesButton(true)
try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64)
DDLogInfo("restore: setting VPN creds with ID and Dismissing: \(getKey.id)")
let presentingViewController = self.presentingViewController as? LDFirewallViewController
self.dismiss(animated: true, completion: {
if presentingViewController != nil {
presentingViewController?.toggleFirewall()
}
else {
VPNController.shared.setEnabled(true)
}
})
}
.catch { error in
// self.toggleRestorePurchasesButton(true)
DDLogError("restore: Error doing restore with email-login: \(error)")
if (self.popupErrorAsNSURLError(error)) {
return
}
else if let apiError = error as? ApiError {
switch apiError.code {
case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription:
self.showPopupDialog(title: NSLocalizedString("No Active Subscription", comment: ""),
message: NSLocalizedString("Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com", comment: ""),
acceptButton: NSLocalizedString("OK", comment: ""))
default:
_ = self.popupErrorAsApiError(error)
}
}
}
}
else {
self.showPopupDialog(title: NSLocalizedString("No Active Subscription", comment: ""),
message: NSLocalizedString("Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com", comment: ""),
acceptButton: NSLocalizedString("OK", comment: ""))
}
default:
self.showPopupDialog(title: NSLocalizedString("Error Restoring Subscription", comment: ""),
message: NSLocalizedString("Please email team@lockdownprivacy.com with the following Error Code ", comment: "") + "\(apiError.code) : \(apiError.message)",
acceptButton: NSLocalizedString("OK", comment: ""))
}
}
else {
self.showPopupDialog(title: NSLocalizedString("Error Restoring Subscription", comment: ""),
message: NSLocalizedString("Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with the following error message: ", comment: "") + "\(error)",
acceptButton: NSLocalizedString("OK", comment: ""))
}
}
}
func selectAdvancedYearly() {
VPNSubscription.selectedProductId = VPNSubscription.productIdAdvancedYearly
updatePricingSubtitle()
}
func selectAdvancedMonthly() {
VPNSubscription.selectedProductId = VPNSubscription.productIdAdvancedMonthly
updatePricingSubtitle()
}
func updatePricingSubtitle() {
let context: VPNSubscription.SubscriptionContext = {
switch mode {
case .newSubscription:
return .new
case .upgrade:
return .upgrade
}
}()
if monthlyPlan.isSelected {
let priceLabel = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedMonthly, for: context)
ftPriceLabel.text = "7-day free trial, then \(priceLabel). Cancel anytime."
} else if annualPlan.isSelected {
let priceLabel = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedYearly, for: context)
ftPriceLabel.text = "7-day free trial, then \(priceLabel). Cancel anytime."
}
}
@objc func startTrial() {
showLoadingView()
VPNSubscription.purchase(
succeeded: {
self.dismiss(animated: true, completion: { [self] in
if let presentingViewController = self.parentVC as? LDFirewallViewController {
// TODO: change view of LDFirewallViewController
}
UserDefaults.hasSeenAdvancedPaywall = true
// force refresh receipt, and sync with email if it exists, activate VPNte
if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true {
DDLogInfo("purchase complete: syncing with confirmed email")
firstly {
try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password)
}
.then { (signin: SignIn) -> Promise<SubscriptionEvent> in
DDLogInfo("purchase complete: signin result: \(signin)")
return try Client.subscriptionEvent(forceRefresh: true)
}
.then { (result: SubscriptionEvent) -> Promise<GetKey> in
DDLogInfo("purchase complete: subscriptionevent result: \(result)")
return try Client.getKey()
}
.done { (getKey: GetKey) in
try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64)
DDLogInfo("purchase complete: setting VPN creds with ID: \(getKey.id)")
VPNController.shared.setEnabled(true)
}
.catch { error in
DDLogError("purchase complete: Error: \(error)")
if self.popupErrorAsNSURLError("Error activating Secure Tunnel: \(error)") {
return
} else if let apiError = error as? ApiError {
switch apiError.code {
default:
_ = self.popupErrorAsApiError("API Error activating Secure Tunnel: \(error)")
}
}
}
} else {
firstly {
try Client.signIn(forceRefresh: true) // this will fetch and set latest receipt, then submit to API to get cookie
}
.then { _ in
// TODO: don't always do this -- if we already have a key, then only do it once per day max
try Client.getKey()
}
.done { (getKey: GetKey) in
try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64)
VPNController.shared.setEnabled(true)
}
.catch { error in
DDLogError("purchase complete - no email: Error: \(error)")
if self.popupErrorAsNSURLError("Error activating Secure Tunnel: \(error)") {
return
} else if let apiError = error as? ApiError {
switch apiError.code {
default:
_ = self.popupErrorAsApiError("API Error activating Secure Tunnel: \(error)")
}
}
}
}
})
},
errored: { error in
DDLogError("Start Trial Failed: \(error)")
self.hideLoadingView()
if let skError = error as? SKError {
var errorText = ""
switch skError.code {
case .unknown:
errorText = .localized("Unknown error. Please contact support at team@lockdownprivacy.com.")
case .clientInvalid:
errorText = .localized("Not allowed to make the payment")
case .paymentCancelled:
errorText = .localized("Payment was cancelled")
case .paymentInvalid:
errorText = .localized("The purchase identifier was invalid")
case .paymentNotAllowed:
errorText = .localized("""
Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. \
Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again.
""")
case .storeProductNotAvailable:
errorText = .localized("The product is not available in the current storefront")
case .cloudServicePermissionDenied:
errorText = .localized("Access to cloud service information is not allowed")
case .cloudServiceNetworkConnectionFailed:
errorText = .localized("Could not connect to the network")
case .cloudServiceRevoked:
errorText = .localized("User has revoked permission to use this cloud service")
default:
errorText = skError.localizedDescription
}
self.showPopupDialog(title: .localized("Error Starting Trial"), message: errorText, acceptButton: .localizedOkay)
} else if self.popupErrorAsNSURLError(error) {
return
} else if self.popupErrorAsApiError(error) {
return
} else {
self.showPopupDialog(
title: .localized("Error Starting Trial"),
message: .localized("Please contact team@lockdownprivacy.com.\n\nError details:\n") + "\(error)",
acceptButton: .localizedOkay)
}
})
}
private func showPaywallIfNoSubscription() {
guard BaseUserService.shared.user.currentSubscription == nil else { return }
guard BasePaywallService.shared.context == .normal else { return }
BasePaywallService.shared.showPaywall(on: self)
UserDefaults.hasSeenPaywallOnHomeScreen = true
}
}