// // 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 in DDLogInfo("signin result: \(signin)") return try Client.subscriptionEvent() } .then { (result: SubscriptionEvent) -> Promise 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 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"); } } }