// // LDFirewallViewController.swift // Lockdown // // Created by Aliaksandr Dvoineu on 17.04.23. // Copyright © 2023 Confirmed Inc. All rights reserved. // import UIKit import CocoaLumberjackSwift import PromiseKit import NetworkExtension import PopupDialog final class LDFirewallViewController: BaseViewController { // MARK: Properties let kHasViewedTutorial = "hasViewedTutorial" let kHasSeenInitialFirewallConnectedDialog = "hasSeenInitialFirewallConnectedDialog11" let kHasSeenShare = "hasSeenShareDialog4" let ratingCountKey = "ratingCount" + lastVersionToAskForRating let ratingTriggeredKey = "ratingTriggered" + lastVersionToAskForRating var lastFirewallStatus: NEVPNStatus? var activePlans: [Subscription.PlanType] = [] let vc = FirewallPaywallViewController() enum Mode { case newSubscription case upgrade(active: [Subscription.PlanType]) } var mode = Mode.newSubscription var metricsTimer : Timer? 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(800) return view }() 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 }() 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 }() lazy var protectionPlanLabel: UILabel = { let label = UILabel() label.text = NSLocalizedString("Basic Protection", comment: "") label.font = fontBold22 label.textColor = .label return label }() lazy var firewallTitle: UILabel = { let label = UILabel() label.text = NSLocalizedString("Get complete protection", comment: "") label.font = fontBold24 label.numberOfLines = 0 label.textColor = .label return label }() private lazy var firewallDescriptionLabel1: DescriptionLabel = { let label = DescriptionLabel() label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Block as many trackers as you want", comment: ""))) return label }() private lazy var firewallDescriptionLabel2: DescriptionLabel = { let label = DescriptionLabel() label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Import and export your own block lists", comment: ""))) return label }() private lazy var firewallDescriptionLabel3: DescriptionLabel = { let label = DescriptionLabel() label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Access to new curated lists of trackers", comment: ""))) return label }() private lazy var stackView: UIStackView = { let stackView = UIStackView() stackView.addArrangedSubview(firewallTitle) stackView.addArrangedSubview(firewallDescriptionLabel1) stackView.addArrangedSubview(firewallDescriptionLabel2) stackView.addArrangedSubview(firewallDescriptionLabel3) stackView.axis = .vertical stackView.distribution = .fillProportionally stackView.spacing = 10 return stackView }() private lazy var cpTitle: UILabel = { let label = UILabel() label.text = NSLocalizedString("Don't let those trackers know your every move – Upgrade to Advanced now!", comment: "") label.textColor = .black label.font = fontBold15 label.numberOfLines = 0 label.textAlignment = .center return label }() private lazy var cpTrackersGroupView1: TrackersGroupView = { let view = TrackersGroupView() view.placeNumber.text = "#1" view.placeNumber.textColor = .black view.titleLabel.textColor = .black view.number.textColor = .black view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_game_marketing")!, title: "Game Marketing", number: 4678)) view.number.isHidden = true return view }() private lazy var cpTrackersGroupView2: TrackersGroupView = { let view = TrackersGroupView() view.placeNumber.text = "#2" view.placeNumber.textColor = .black view.titleLabel.textColor = .black view.number.textColor = .black view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_marketing_trackers")!, title: "Marketing Trackers", number: 3432)) view.number.isHidden = true return view }() private lazy var cpTrackersGroupView3: TrackersGroupView = { let view = TrackersGroupView() view.placeNumber.text = "#3" view.placeNumber.textColor = .black view.titleLabel.textColor = .black view.number.textColor = .black view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_email_trackers")!, title: "Email Trackers", number: 2756)) view.number.isHidden = true return view }() private lazy var cpStackView: UIStackView = { let stackView = UIStackView() stackView.addArrangedSubview(cpTitle) stackView.addArrangedSubview(cpTrackersGroupView1) stackView.addArrangedSubview(cpTrackersGroupView2) stackView.addArrangedSubview(cpTrackersGroupView3) stackView.layer.cornerRadius = 8 stackView.layer.borderWidth = 2 stackView.layer.borderColor = UIColor.gray.cgColor stackView.axis = .vertical stackView.distribution = .fillEqually stackView.spacing = 0 stackView.backgroundColor = .extraLightGray 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 maTitle: UILabel = { let label = UILabel() label.text = NSLocalizedString("Most active this week", comment: "") label.textColor = .label label.font = fontBold15 label.textAlignment = .center return label }() private lazy var maTrackersGroupView1: TrackersGroupView = { let view = TrackersGroupView() view.placeNumber.text = "#1" view.number.textColor = .label view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_facebook_trackers")!, title: "Facebook Trackers", number: 89)) view.lockImage.isHidden = true return view }() private lazy var maTrackersGroupView2: TrackersGroupView = { let view = TrackersGroupView() view.placeNumber.text = "#2" view.number.textColor = .label view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_data_trackers")!, title: "Data Trackers", number: 32)) view.lockImage.isHidden = true return view }() private lazy var maTrackersGroupView3: TrackersGroupView = { let view = TrackersGroupView() view.placeNumber.text = "#3" view.number.textColor = .label view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_clickbait_trackers")!, title: "Clickbait", number: 21)) view.lockImage.isHidden = true return view }() private lazy var maStackView: UIStackView = { let stackView = UIStackView() stackView.addArrangedSubview(maTitle) stackView.addArrangedSubview(maTrackersGroupView1) stackView.addArrangedSubview(maTrackersGroupView2) stackView.addArrangedSubview(maTrackersGroupView3) stackView.layer.cornerRadius = 8 stackView.layer.borderWidth = 1 stackView.layer.borderColor = UIColor.lightGray.cgColor stackView.axis = .vertical stackView.distribution = .fillEqually stackView.spacing = 0 return stackView }() lazy var statisitcsView: OverallStatiscticView = { let view = OverallStatiscticView() return view }() private lazy var firewallSwitchControl: CustomUISwitch = { let uiSwitch = CustomUISwitch(onImage: UIImage(named: "firewall-on-image")!, offImage: UIImage(named: "firewall-off-image")!) uiSwitch.setOnClickListener { self.toggleFirewall() } return uiSwitch }() override func viewDidLoad() { super.viewDidLoad() VPNSubscription.cacheLocalizedPrices() updateFirewallButtonWithStatus(status: FirewallController.shared.status()) updateMetrics() if metricsTimer == nil { metricsTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(updateMetrics), userInfo: nil, repeats: true) metricsTimer?.fire() } view.backgroundColor = .systemBackground view.addSubview(firewallSwitchControl) firewallSwitchControl.anchors.bottom.safeAreaPin() firewallSwitchControl.anchors.leading.marginsPin() firewallSwitchControl.anchors.trailing.marginsPin() firewallSwitchControl.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: firewallSwitchControl.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(maStackView) maStackView.anchors.top.spacing(18, to: upgradeButton.anchors.bottom) maStackView.anchors.leading.marginsPin() maStackView.anchors.trailing.marginsPin() contentView.addSubview(statisitcsView) statisitcsView.anchors.top.spacing(18, to: maStackView.anchors.bottom) statisitcsView.anchors.leading.marginsPin() statisitcsView.anchors.trailing.marginsPin() updateProtectionPlanUI() // accountStateDidChange() NotificationCenter.default.addObserver(self, selector: #selector(tunnelStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) updateMetrics() } } extension LDFirewallViewController: Loadable { @objc func accountStateDidChange() { updateActiveSubscription() } func updateProtectionPlanUI() { if UserDefaults.hasSeenUniversalPaywall { updateUI() protectionPlanLabel.text = "Universal protection" } else if UserDefaults.hasSeenAnonymousPaywall { updateUI() protectionPlanLabel.text = "Anonymous protection" } else if UserDefaults.hasSeenAdvancedPaywall { updateUI() protectionPlanLabel.text = "Advanced protection" } else { protectionPlanLabel.text = "Basic protection" } } func updateUI() { firewallTitle.isHidden = true firewallDescriptionLabel1.isHidden = true firewallDescriptionLabel2.isHidden = true firewallDescriptionLabel3.isHidden = true upgradeButton.isHidden = true upgradeButton.anchors.height.equal(0) cpTrackersGroupView1.lockImage.isHidden = true cpTrackersGroupView1.number.isHidden = false cpTrackersGroupView2.lockImage.isHidden = true cpTrackersGroupView2.number.isHidden = false cpTrackersGroupView3.lockImage.isHidden = true cpTrackersGroupView3.number.isHidden = false } 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 == .universalAnnual || active.planType == .universalMonthly { protectionPlanLabel.text = "Universal protection" updateUI() } else if active.planType == .anonymousMonthly || active.planType == .anonymousAnnual { updateUI() protectionPlanLabel.text = "Anonymous protection" } else if active.planType == .advancedMonthly || active.planType == .advancedAnnual { updateUI() protectionPlanLabel.text = "Advanced protection" } else { firewallTitle.textColor = .red } } else { firewallTitle.textColor = .red } }.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 case kApiCodeSandboxReceiptNotAllowed: UserDefaults.hasSeenAdvancedPaywall = false default: DDLogError("Error loading plan: API error code - \(apiError.code)") UserDefaults.hasSeenAdvancedPaywall = false } } else { DDLogError("Error loading plan: Non-API Error - \(error.localizedDescription)") UserDefaults.hasSeenAdvancedPaywall = false } } } @objc func upgrade() { let vc = FirewallPaywallViewController() present(vc, animated: true) } @objc func tunnelStatusDidChange(_ notification: Notification) { // Firewall if let tunnelProviderSession = notification.object as? NETunnelProviderSession { DDLogInfo("VPNStatusDidChange as NETunnelProviderSession with status: \(tunnelProviderSession.status.description)"); if (!getUserWantsFirewallEnabled()) { updateFirewallButtonWithStatus(status: .disconnected) } else { updateFirewallButtonWithStatus(status: tunnelProviderSession.status) if (tunnelProviderSession.status == .connected && defaults.bool(forKey: kHasSeenInitialFirewallConnectedDialog) == false) { defaults.set(true, forKey: kHasSeenInitialFirewallConnectedDialog) // self.tapToActivateFirewallLabel.isHidden = true // if (VPNController.shared.status() == .invalid) { // self.showVPNSubscriptionDialog(title: NSLocalizedString("🔥🧱 Firewall Activated 🎊🎉", comment: ""), message: NSLocalizedString("Trackers, ads, and other malicious scripts are now blocked in all your apps, even outside of Safari.\n\nGet maximum privacy with a Secure Tunnel that protects connections, anonymizes your browsing, and hides your location.", comment: "")) // } } } } } @objc func updateMetrics() { DispatchQueue.main.async { [unowned self] in self.statisitcsView.enabledBoxView.numberLabel.text = String(getTotalEnabled().count) self.statisitcsView.disabledBoxView.numberLabel.text = String(getTotalDisabled().count) self.statisitcsView.blockedBoxView.numberLabel.text = String(getAllBlockedDomains().count) } } func toggleFirewall() { if (defaults.bool(forKey: kHasAgreedToFirewallPrivacyPolicy) == false) { let storyboard = UIStoryboard(name: "Main", bundle: nil) let viewController = storyboard.instantiateViewController(withIdentifier: "firewallPrivacyPolicyViewController") as! PrivacyPolicyViewController viewController.privacyPolicyKey = kHasAgreedToFirewallPrivacyPolicy viewController.parentVC1 = self self.present(viewController, animated: true, completion: nil) return } if getIsCombinedBlockListEmpty() { FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) self.showPopupDialog(title: NSLocalizedString("No Block Lists Enabled", comment: ""), message: NSLocalizedString("Please tap Block List and enable at least one block list to activate Firewall.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) return } switch FirewallController.shared.status() { case .invalid: FirewallController.shared.setEnabled(true, isUserExplicitToggle: true) //ensureFirewallWorkingAfterEnabling(waitingSeconds: 5.0) case .disconnected: updateFirewallButtonWithStatus(status: .connecting) FirewallController.shared.setEnabled(true, isUserExplicitToggle: true) //ensureFirewallWorkingAfterEnabling(waitingSeconds: 5.0) // checkForAskRating() case .connected: updateFirewallButtonWithStatus(status: .disconnecting) FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) case .connecting, .disconnecting, .reasserting: break; } } func ensureFirewallWorkingAfterEnabling(waitingSeconds: TimeInterval) { FirewallController.shared.existingManagerCount { (count) in if let count = count, count > 0 { DispatchQueue.main.asyncAfter(deadline: .now() + waitingSeconds) { DDLogInfo("\(waitingSeconds) seconds passed, checking if Firewall is enabled") guard getUserWantsFirewallEnabled() else { // firewall shouldn't be enabled, no need to act DDLogInfo("User doesn't want Firewall enabled, no action") return } let status = FirewallController.shared.status() switch status { case .connecting, .disconnecting, .reasserting: // check again in three seconds DDLogInfo("Firewall is in transient state, will check again in 3 seconds") self.ensureFirewallWorkingAfterEnabling(waitingSeconds: 3.0) case .connected: // all good DDLogInfo("Firewall is connected, no action") break case .disconnected, .invalid: // we suppose that the connection is somehow broken, trying to fix DDLogInfo("Firewall is not connected even though it should be, attempting to fix") self.showFixFirewallConnectionDialog { FirewallController.shared.deleteConfigurationAndAddAgain() } } } } else { DDLogInfo("No Firewall configurations in settings (likely fresh install): not checking") return } } } func updateFirewallButtonWithStatus(status: NEVPNStatus) { DDLogInfo("UpdateFirewallButton") switch status { case .connected: LatestKnowledge.isFirewallEnabled = true case .disconnected: LatestKnowledge.isFirewallEnabled = false default: break } updateToggleButtonWithStatus(lastStatus: lastFirewallStatus, newStatus: status, switchControl: firewallSwitchControl) } 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; } } } } }