Files
lockdown-iOS-mirror/LockdowniOS/LDVpnViewController.swift
2023-07-12 15:26:25 +03:00

575 lines
27 KiB
Swift

//
// LDVpnViewController.swift
// Lockdown
//
// Created by Aliaksandr Dvoineu on 19.04.23.
// Copyright © 2023 Confirmed Inc. All rights reserved.
//
import UIKit
import CocoaLumberjackSwift
import PromiseKit
import NetworkExtension
import PopupDialog
final class LDVpnViewController: BaseViewController {
// MARK: - Properties
var activePlans: [Subscription.PlanType] = []
var lastVPNStatus: NEVPNStatus?
private lazy var scrollView: UIScrollView = {
let view = UIScrollView()
view.isScrollEnabled = true
return view
}()
private lazy var contentView: UIView = {
let view = UIView()
view.anchors.height.equal(640)
return view
}()
private lazy var yourCurrentPlanLabel: UILabel = {
let label = UILabel()
label.text = NSLocalizedString("Your current plan is", comment: "")
label.font = fontRegular14
label.numberOfLines = 0
label.textColor = .label
return label
}()
private lazy var upgradeLabel: UILabel = {
let label = UILabel()
label.text = NSLocalizedString("Upgrade?", comment: "")
label.font = fontBold13
label.textColor = .tunnelsBlue
label.isUserInteractionEnabled = true
label.setOnClickListener {
let vc = VPNPaywallViewController()
self.present(vc, animated: true)
}
return label
}()
private lazy var protectionPlanLabel: UILabel = {
let label = UILabel()
label.text = NSLocalizedString("Basic Protection", comment: "")
label.font = fontBold22
label.textColor = .label
return label
}()
private lazy var mainTitle: UILabel = {
let label = UILabel()
label.text = NSLocalizedString("Get Anonymous protection", comment: "")
label.font = fontBold24
label.numberOfLines = 0
label.textColor = .label
return label
}()
private lazy var descriptionLabel1: DescriptionLabel = {
let label = DescriptionLabel()
label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Block as many trackers as you want", comment: "")))
return label
}()
private lazy var descriptionLabel2: DescriptionLabel = {
let label = DescriptionLabel()
label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Import and export your own block lists", comment: "")))
return label
}()
private lazy var descriptionLabel3: DescriptionLabel = {
let label = DescriptionLabel()
label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Access to new curated lists of trackers", comment: "")))
return label
}()
private lazy var descriptionLabel4: DescriptionLabel = {
let label = DescriptionLabel()
label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("The only fully open source VPN", comment: "")))
return label
}()
private lazy var descriptionLabel5: DescriptionLabel = {
let label = DescriptionLabel()
label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Hide your identity around the world", comment: "")))
return label
}()
private lazy var descriptionLabel6: DescriptionLabel = {
let label = DescriptionLabel()
label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Protect all Apple devices", comment: "")))
return label
}()
private lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.addArrangedSubview(mainTitle)
stackView.addArrangedSubview(descriptionLabel1)
stackView.addArrangedSubview(descriptionLabel2)
stackView.addArrangedSubview(descriptionLabel3)
stackView.addArrangedSubview(descriptionLabel4)
stackView.addArrangedSubview(descriptionLabel5)
stackView.addArrangedSubview(descriptionLabel6)
stackView.axis = .vertical
stackView.distribution = .fillProportionally
stackView.spacing = 10
return stackView
}()
private lazy var upgradeButton: UIButton = {
let button = UIButton(type: .system)
button.tintColor = .white
button.setTitle(NSLocalizedString("Upgrade", comment: ""), for: .normal)
button.titleLabel?.font = fontBold18
button.backgroundColor = .tunnelsBlue
button.layer.cornerRadius = 28
button.anchors.height.equal(56)
button.addTarget(self, action: #selector(upgrade), for: .touchUpInside)
return button
}()
private lazy var whitelistCard: LDCardView = {
let view = LDCardView()
view.title.text = "Whitelist"
view.iconImageView.image = UIImage(named: "icn_whitelist")
view.isUserInteractionEnabled = true
view.setOnClickListener { [weak self] in
guard let self else { return }
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyBoard.instantiateViewController(withIdentifier: "WhitelistViewController") as! WhitelistViewController
self.present(vc, animated: true, completion: nil)
}
return view
}()
private lazy var regionCard: LDCardView = {
let view = LDCardView()
view.title.text = "Region"
view.iconImageView.image = UIImage(named: "icn_globe")
view.isUserInteractionEnabled = true
view.setOnClickListener { [weak self] in
guard let self else { return }
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyBoard.instantiateViewController(withIdentifier: "SetRegionViewController") as! SetRegionViewController
self.present(vc, animated: true, completion: nil)
}
return view
}()
private lazy var hStack: UIStackView = {
let stack = UIStackView()
stack.addArrangedSubview(whitelistCard)
stack.addArrangedSubview(regionCard)
stack.alignment = .center
stack.distribution = .equalCentering
stack.spacing = 16
return stack
}()
private lazy var vpnSwitchControl: CustomUISwitch = {
let uiSwitch = CustomUISwitch(onImage: UIImage(named: "vpn-on-image")!, offImage: UIImage(named: "vpn-off-image")!)
uiSwitch.setOnClickListener {
self.toggleVPN()
}
return uiSwitch
}()
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(vpnSwitchControl)
vpnSwitchControl.anchors.bottom.safeAreaPin()
vpnSwitchControl.anchors.leading.marginsPin()
vpnSwitchControl.anchors.trailing.marginsPin()
vpnSwitchControl.anchors.height.equal(56)
view.addSubview(yourCurrentPlanLabel)
yourCurrentPlanLabel.anchors.leading.marginsPin()
yourCurrentPlanLabel.anchors.top.safeAreaPin()
view.addSubview(upgradeLabel)
upgradeLabel.anchors.trailing.marginsPin()
upgradeLabel.anchors.centerY.equal(yourCurrentPlanLabel.anchors.centerY)
view.addSubview(protectionPlanLabel)
protectionPlanLabel.anchors.top.spacing(8, to: yourCurrentPlanLabel.anchors.bottom)
protectionPlanLabel.anchors.leading.marginsPin()
view.addSubview(scrollView)
scrollView.anchors.top.spacing(12, to: protectionPlanLabel.anchors.bottom)
scrollView.anchors.leading.pin()
scrollView.anchors.trailing.pin()
scrollView.anchors.bottom.spacing(8, to: vpnSwitchControl.anchors.top)
scrollView.addSubview(contentView)
contentView.anchors.top.pin()
contentView.anchors.centerX.align()
contentView.anchors.width.equal(scrollView.anchors.width)
contentView.anchors.bottom.pin()
contentView.addSubview(stackView)
stackView.anchors.top.marginsPin()
stackView.anchors.leading.marginsPin()
stackView.anchors.trailing.marginsPin()
contentView.addSubview(upgradeButton)
upgradeButton.anchors.top.spacing(18, to: stackView.anchors.bottom)
upgradeButton.anchors.leading.marginsPin()
upgradeButton.anchors.trailing.marginsPin()
contentView.addSubview(hStack)
hStack.anchors.top.spacing(18, to: upgradeButton.anchors.bottom)
hStack.anchors.centerX.align()
whitelistCard.anchors.width.equal(view.bounds.width / 2 - 20)
whitelistCard.anchors.height.equal(view.bounds.width / 2 - 20)
regionCard.anchors.width.equal(view.bounds.width / 2 - 20)
regionCard.anchors.height.equal(view.bounds.width / 2 - 20)
updateVPNButtonWithStatus(status: VPNController.shared.status())
updateVPNRegionLabel()
if (VPNController.shared.status() == .connected) {
firstly {
try Client.signIn()
}
.done { (signin: SignIn) in
// successfully signed in with no subscription errors, do nothing
}
.catch { error in
if (self.popupErrorAsNSURLError(error)) {
return
}
else if let apiError = error as? ApiError {
switch apiError.code {
case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription:
self.showPopupDialog(title: NSLocalizedString("VPN Subscription Expired", comment: ""), message: NSLocalizedString("Please renew your subscription to re-activate the VPN.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""), completionHandler: {
self.present(VPNPaywallViewController(), animated: true)
// self.performSegue(withIdentifier: "showSignup", sender: self)
})
default:
// _ = self.popupErrorAsApiError(error)
break
}
}
else {
self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""),
message: "\(error)",
acceptButton: "Okay")
}
}
}
if UserDefaults.hasSeenAnonymousPaywall {
mainTitle.text = "Get Universal protection"
descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel4.lockImage.image = UIImage(named: "icn_checkmark")
protectionPlanLabel.text = "Anonymous protection"
} else if UserDefaults.hasSeenUniversalPaywall {
mainTitle.text = "You're fully protected"
descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel4.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel5.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel6.lockImage.image = UIImage(named: "icn_checkmark")
upgradeButton.isHidden = true
upgradeButton.anchors.height.equal(0)
protectionPlanLabel.text = "Universal protection"
} else if UserDefaults.hasSeenAdvancedPaywall {
descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark")
protectionPlanLabel.text = "Advanced protection"
} else {
protectionPlanLabel.text = "Basic protection"
}
// accountStateDidChange()
NotificationCenter.default.addObserver(self, selector: #selector(tunnelStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil)
}
}
// MARK: - Private functions
extension LDVpnViewController: Loadable {
@objc func accountStateDidChange() {
updateActiveSubscription()
}
func updateActiveSubscription() {
showLoadingView()
// not logged in via email, use receipt
firstly {
try Client.signIn()
}.then { _ in
try Client.activeSubscriptions()
}.ensure {
self.hideLoadingView()
}.done { [self] subscriptions in
self.activePlans = subscriptions.map({ $0.planType })
if let active = subscriptions.first {
if active.planType == .advancedMonthly || active.planType == .advancedAnnual {
descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark")
UserDefaults.hasSeenAdvancedPaywall = true
} else if active.planType == .anonymousMonthly || active.planType == .anonymousAnnual {
mainTitle.text = "Get Universal protection"
descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel4.lockImage.image = UIImage(named: "icn_checkmark")
UserDefaults.hasSeenAdvancedPaywall = true
UserDefaults.hasSeenAnonymousPaywall = true
} else if active.planType == .universalAnnual || active.planType == .universalMonthly {
mainTitle.text = "You're fully protected"
descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel4.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel5.lockImage.image = UIImage(named: "icn_checkmark")
descriptionLabel6.lockImage.image = UIImage(named: "icn_checkmark")
upgradeButton.isHidden = true
upgradeButton.anchors.height.equal(0)
UserDefaults.hasSeenAdvancedPaywall = true
UserDefaults.hasSeenAnonymousPaywall = true
UserDefaults.hasSeenUniversalPaywall = true
}
}
}.catch { [self] error in
DDLogError("Error reloading subscription: \(error.localizedDescription)")
hideLoadingView()
if let apiError = error as? ApiError {
switch apiError.code {
case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription:
UserDefaults.hasSeenAdvancedPaywall = false
UserDefaults.hasSeenAnonymousPaywall = false
UserDefaults.hasSeenUniversalPaywall = false
case kApiCodeSandboxReceiptNotAllowed:
UserDefaults.hasSeenAdvancedPaywall = false
UserDefaults.hasSeenAnonymousPaywall = false
UserDefaults.hasSeenUniversalPaywall = false
default:
DDLogError("Error loading plan: API error code - \(apiError.code)")
UserDefaults.hasSeenAdvancedPaywall = false
UserDefaults.hasSeenAnonymousPaywall = false
UserDefaults.hasSeenUniversalPaywall = false
}
} else {
DDLogError("Error loading plan: Non-API Error - \(error.localizedDescription)")
UserDefaults.hasSeenAdvancedPaywall = false
UserDefaults.hasSeenAnonymousPaywall = false
UserDefaults.hasSeenUniversalPaywall = false
}
}
}
@objc func upgrade() {
let vc = VPNPaywallViewController()
present(vc, animated: true)
}
func toggleVPN() {
DDLogInfo("Toggle VPN")
switch VPNController.shared.status() {
case .connected, .connecting, .reasserting:
DDLogInfo("Toggle VPN: on currently, turning it off")
updateVPNButtonWithStatus(status: .disconnecting)
VPNController.shared.setEnabled(false)
case .disconnected, .disconnecting, .invalid:
DDLogInfo("Toggle VPN: off currently, turning it on")
updateVPNButtonWithStatus(status: .connecting)
// if there's a confirmed email, use that and sync the receipt with it
if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true {
DDLogInfo("have confirmed API credentials, using them")
firstly {
try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password)
}
.then { (signin: SignIn) -> Promise<SubscriptionEvent> in
DDLogInfo("signin result: \(signin)")
return try Client.subscriptionEvent()
}
.then { (result: SubscriptionEvent) -> Promise<GetKey> in
DDLogInfo("subscriptionevent result: \(result)")
return try Client.getKey()
}
.done { (getKey: GetKey) in
try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64)
DDLogInfo("setting VPN creds with ID: \(getKey.id)")
VPNController.shared.setEnabled(true)
}
.catch { error in
DDLogError("Error doing email-login -> subscription-event: \(error)")
self.updateVPNButtonWithStatus(status: .disconnected)
if (self.popupErrorAsNSURLError(error)) {
return
}
else if let apiError = error as? ApiError {
switch apiError.code {
case kApiCodeInvalidAuth, kApiCodeIncorrectLogin:
let confirm = PopupDialog(title: "Incorrect Login",
message: "Your saved login credentials are incorrect. Please sign out and try again.",
image: nil,
buttonAlignment: .horizontal,
transitionStyle: .bounceDown,
preferredWidth: 270,
tapGestureDismissal: true,
panGestureDismissal: false,
hideStatusBar: false,
completion: nil)
confirm.addButtons([
DefaultButton(title: NSLocalizedString("Cancel", comment: ""), dismissOnTap: true) {
},
DefaultButton(title: NSLocalizedString("Sign Out", comment: ""), dismissOnTap: true) {
URLCache.shared.removeAllCachedResponses()
Client.clearCookies()
clearAPICredentials()
setAPICredentialsConfirmed(confirmed: false)
self.showPopupDialog(title: "Success", message: "Signed out successfully.", acceptButton: NSLocalizedString("Okay", comment: ""))
},
])
self.present(confirm, animated: true, completion: nil)
case kApiCodeNoSubscriptionInReceipt:
self.present(VPNPaywallViewController(), animated: true)
// self.performSegue(withIdentifier: "showSignup", sender: self)
case kApiCodeNoActiveSubscription:
self.showPopupDialog(title: NSLocalizedString("Subscription Expired", comment: ""), message: NSLocalizedString("Please renew your subscription to activate the Secure Tunnel.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""), completionHandler: {
self.present(VPNPaywallViewController(), animated: true)
// self.performSegue(withIdentifier: "showSignup", sender: self)
})
default:
_ = self.popupErrorAsApiError(error)
}
}
}
}
else {
firstly {
try Client.signIn() // this will fetch and set latest receipt, then submit to API to get cookie
}
.then { (signin: SignIn) -> Promise<GetKey> 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
self.updateVPNButtonWithStatus(status: .disconnected)
if (self.popupErrorAsNSURLError(error)) {
return
}
else if let apiError = error as? ApiError {
switch apiError.code {
case kApiCodeNoSubscriptionInReceipt:
self.present(VPNPaywallViewController(), animated: true)
// self.performSegue(withIdentifier: "showSignup", sender: self)
case kApiCodeNoActiveSubscription:
self.showPopupDialog(title: NSLocalizedString("Subscription Expired", comment: ""), message: NSLocalizedString("Please renew your subscription to activate the Secure Tunnel.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""), completionHandler: {
self.present(VPNPaywallViewController(), animated: true)
// self.performSegue(withIdentifier: "showSignup", sender: self)
})
default:
if (apiError.code == kApiCodeNegativeError) {
if (getVPNCredentials() != nil) {
DDLogError("Unknown error -1 from API, but VPNCredentials exists, so activating anyway.")
self.updateVPNButtonWithStatus(status: .connecting)
VPNController.shared.setEnabled(true)
}
else {
self.showPopupDialog(title: NSLocalizedString("Apple Outage", comment: ""), message: "There is currently an outage at Apple which is preventing Secure Tunnel from activating. This will likely by resolved by Apple soon, and we apologize for this issue in the meantime." + NSLocalizedString("\n\n If this error persists, please contact team@lockdownprivacy.com.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""))
}
}
else {
// _ = self.popupErrorAsApiError(error)
self.updateVPNButtonWithStatus(status: .connecting)
VPNController.shared.setEnabled(true)
}
}
}
else {
self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""),
message: "\(error)",
acceptButton: NSLocalizedString("Okay", comment: ""))
}
}
}
}
}
func updateVPNButtonWithStatus(status: NEVPNStatus) {
DDLogInfo("UpdateVPNButton")
switch status {
case .connected:
LatestKnowledge.isVPNEnabled = true
case .disconnected:
LatestKnowledge.isVPNEnabled = false
default:
break
}
updateToggleButtonWithStatus(lastStatus: lastVPNStatus,
newStatus: status,
switchControl: vpnSwitchControl)
}
func updateToggleButtonWithStatus(lastStatus: NEVPNStatus?,
newStatus: NEVPNStatus,
switchControl: CustomUISwitch) {
DDLogInfo("UpdateToggleButton")
if (newStatus == lastStatus) {
DDLogInfo("No status change from last time, ignoring.");
}
else {
DispatchQueue.main.async() {
switch newStatus {
case .connected:
switchControl.status = true
case .connecting:
switchControl.status = true
case .disconnected, .invalid:
switchControl.status = false
case .disconnecting:
switchControl.status = false
case .reasserting:
break;
}
}
}
}
func updateVPNRegionLabel() {
regionCard.subTitle.text = getSavedVPNRegion().regionDisplayNameShort
}
@objc func tunnelStatusDidChange(_ notification: Notification) {
if let neVPNConnection = notification.object as? NEVPNConnection {
DDLogInfo("VPNStatusDidChange as NEVPNConnection with status: \(neVPNConnection.status.description)");
updateVPNButtonWithStatus(status: neVPNConnection.status);
updateVPNRegionLabel()
if NEVPNManager.shared().connection.status == .connected || NEVPNManager.shared().connection.status == .disconnected {
//self.updateIP();
}
}
else {
DDLogInfo("VPNStatusDidChange neither TunnelProviderSession nor NEVPNConnection");
}
}
}