// // HomeViewController.swift // Lockdown // // Created by Johnny Lin on 7/31/19. // Copyright © 2019 Confirmed Inc. All rights reserved. // import Foundation import NetworkExtension import CocoaLumberjackSwift import UIKit import PromiseKit import StoreKit import SwiftyStoreKit import PopupDialog import AwesomeSpotlightView class CircularView: UIView { override func layoutSubviews() { super.layoutSubviews() self.layer.cornerRadius = self.bounds.size.width * 0.50 } } let kHasSeenEmailSignup = "hasSeenEmailSignup" class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Loadable { let kHasViewedTutorial = "hasViewedTutorial" let kHasSeenInitialFirewallConnectedDialog = "hasSeenInitialFirewallConnectedDialog11" let kVPNBodyViewVisible = "VPNBodyViewVisible" let kHasSeenShare = "hasSeenShareDialog4" let ratingCountKey = "ratingCount" + lastVersionToAskForRating let ratingTriggeredKey = "ratingTriggered" + lastVersionToAskForRating @IBOutlet weak var menuButton: UIButton! @IBOutlet weak var menuButtonDot: UIView! @IBOutlet var stackEqualHeightConstraint: NSLayoutConstraint! @IBOutlet weak var firewallTitleLabel: UILabel! @IBOutlet weak var firewallActive: UILabel! @IBOutlet weak var firewallToggleCircle: UIButton! @IBOutlet weak var firewallToggleAnimatedCircle: NVActivityIndicatorView! @IBOutlet weak var firewallButton: UIButton! @IBOutlet weak var tapToActivateFirewallLabel: UILabel! var lastFirewallStatus: NEVPNStatus? @IBOutlet weak var metricsStack: UIStackView! @IBOutlet weak var dailyMetrics: UILabel? @IBOutlet weak var weeklyMetrics: UILabel? @IBOutlet weak var allTimeMetrics: UILabel? var metricsTimer : Timer? @IBOutlet weak var firewallSettingsButton: UIButton! @IBOutlet weak var firewallViewLogButton: UIButton! @IBOutlet weak var firewallShareButton: UIButton! @IBOutlet var vpnViewHeightConstraint: NSLayoutConstraint! @IBOutlet weak var vpnHeaderView: UIView! @IBOutlet weak var vpnHideButton: UIButton! @IBOutlet weak var vpnBodyView: UIView! @IBOutlet weak var vpnActive: UILabel! @IBOutlet var vpnActiveHeaderConstraint: NSLayoutConstraint! @IBOutlet var vpnActiveTopBodyConstraint: NSLayoutConstraint! @IBOutlet var vpnActiveVerticalBodyConstraint: NSLayoutConstraint! @IBOutlet weak var vpnToggleCircle: UIButton! @IBOutlet weak var vpnToggleAnimatedCircle: NVActivityIndicatorView! @IBOutlet weak var vpnButton: UIButton! // @IBOutlet weak var vpnIP: UILabel! // @IBOutlet weak var vpnSpeed: UILabel! var lastVPNStatus: NEVPNStatus? @IBOutlet weak var vpnSetRegionButton: UIButton! @IBOutlet weak var vpnRegionLabel: UILabel! @IBOutlet weak var vpnWhitelistButton: UIButton! var activePlans: [Subscription.PlanType] = [] override func viewDidLoad() { super.viewDidLoad() 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() } firewallViewLogButton.layer.cornerRadius = 8 firewallViewLogButton.layer.maskedCorners = [.layerMinXMaxYCorner] firewallSettingsButton.layer.cornerRadius = 8 firewallSettingsButton.layer.maskedCorners = [.layerMaxXMaxYCorner] vpnHeaderView.addGestureRecognizer( UITapGestureRecognizer(target: self, action: #selector (vpnHeaderTapped(_:))) ) updateVPNButtonWithStatus(status: VPNController.shared.status()) //updateIP() vpnWhitelistButton.layer.cornerRadius = 8 vpnWhitelistButton.layer.maskedCorners = [.layerMinXMaxYCorner] vpnSetRegionButton.layer.cornerRadius = 8 vpnSetRegionButton.layer.maskedCorners = [.layerMaxXMaxYCorner] updateVPNRegionLabel() // Check Subscription - if VPN active but not subscribed, then disconnect and show dialog (don't do this if connection error) 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.performSegue(withIdentifier: "showSignup", sender: self) }) default: _ = self.popupErrorAsApiError(error) } } else { self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""), message: "\(error)", acceptButton: "Okay") } } } NotificationCenter.default.addObserver(self, selector: #selector(tunnelStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil) // Check 3 conditions for firewall restart, but reload manager first to get non-stale one FirewallController.shared.refreshManager(completion: { error in if let e = error { DDLogError("Error refreshing Manager in Home viewdidappear: \(e)") return } if getUserWantsFirewallEnabled() && (FirewallController.shared.status() == .connected || FirewallController.shared.status() == .invalid) { DDLogInfo("User wants firewall enabled and connected/invalid, testing blocking in Home") // 1) If device has been restarted (current system uptime is lower than last stored System Uptime) if (deviceHasRestarted()) { DDLogInfo("HOMEVIEW: DEVICE RESTARTED, RESTART FIREWALL") FirewallController.shared.restart(completion: { error in if error != nil { DDLogError("Error restarting firewall on HomeView Device Restarted Check: \(error!)") } }) } // 2) if app has just been upgraded or is new install else if (appHasJustBeenUpgradedOrIsNewInstall()) { DDLogInfo("HOMEVIEW: APP UPGRADED, REFRESHING DEFAULT BLOCK LISTS, WHITELISTS, RESTARTING FIREWALL") setupFirewallDefaultBlockLists() setupLockdownWhitelistedDomains() FirewallController.shared.restart(completion: { error in if error != nil { DDLogError("Error restarting firewall on HomeView App Upgraded Check: \(error!)") } }) } // 3) Check that Firewall is still working correctly, restart it if it's not else { _ = Client.getBlockedDomainTest(connectionSuccessHandler: { DDLogError("Home Firewall Test: Connected to \(testFirewallDomain) even though it's supposed to be blocked, restart the Firewall") FirewallController.shared.restart(completion: { error in if error != nil { DDLogError("Error restarting firewall on Home: \(error!)") } }) }, connectionFailedHandler: { error in if error != nil { let nsError = error! as NSError if nsError.domain == NSURLErrorDomain { DDLogInfo("Home Firewall Test: Successful blocking of \(testFirewallDomain) with NSURLErrorDomain error: \(nsError)") } else { DDLogInfo("Home Firewall Test: Successful blocking of \(testFirewallDomain), but seeing non-NSURLErrorDomain error: \(error!)") } } }) } } }) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let inset = firewallButton.frame.width * 0.225 firewallButton.contentEdgeInsets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) vpnButton.contentEdgeInsets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) reloadMenuDot() //performSegue(withIdentifier: "showSignup", sender: nil) toggleVPNBodyView(animate: false, show: defaults.bool(forKey: kVPNBodyViewVisible)) if (defaults.bool(forKey: kHasViewedTutorial) == false) { startTutorial() } else if (defaults.bool(forKey: kHasSeenEmailSignup) == false) { self.performSegue(withIdentifier: "showCreateAccountFromHome", sender: nil) } if defaults.bool(forKey: kHasSeenInitialFirewallConnectedDialog) == false { tapToActivateFirewallLabel.isHidden = false } OneTimeActions.performOnce(ifHasNotSeen: .notificationAuthorizationRequestPopup) { PushNotifications.Authorization.requestWeeklyUpdateAuthorization(presentingDialogOn: self).done { status in DDLogInfo("Updated notifications status: \(status)") }.catch { error in DDLogWarn(error.localizedDescription) } } // If total blocked > 1000, and have not shown share dialog before, ask if user wants to share if (getTotalMetrics() > 1000 && defaults.bool(forKey: kHasSeenShare) != true) { defaults.set(true, forKey: kHasSeenShare) let popup = PopupDialog(title: "You've blocked over 1000 trackers! 🎊", message: NSLocalizedString("Share your anonymized metrics and show other people how to block invasive tracking.", comment: ""), image: nil, buttonAlignment: .horizontal, transitionStyle: .bounceDown, preferredWidth: 270, tapGestureDismissal: true, panGestureDismissal: false, hideStatusBar: false, completion: nil) popup.addButtons([ CancelButton(title: NSLocalizedString("Not Now", comment: ""), dismissOnTap: true) { let s0 = AwesomeSpotlight(withRect: self.getRectForView(self.firewallShareButton).insetBy(dx: -13.0, dy: -13.0), shape: .roundRectangle, text: NSLocalizedString("You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)", comment: "")) let spotlightView = AwesomeSpotlightView(frame: self.view.frame, spotlight: [s0]) spotlightView.cutoutRadius = 8 spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); spotlightView.enableArrowDown = true spotlightView.textLabelFont = fontMedium16 spotlightView.labelSpacing = 24; spotlightView.delegate = self self.view.addSubview(spotlightView) spotlightView.start() }, DefaultButton(title: NSLocalizedString("Next", comment: ""), dismissOnTap: true) { self.shareFirewallMetricsTapped("") } ]) self.present(popup, animated: true, completion: nil) } } func showVPNSubscriptionDialog(title: String, message: String) { let popup = PopupDialog( title: title, message: message, image: nil, buttonAlignment: .horizontal, transitionStyle: .bounceUp, preferredWidth: 300.0, tapGestureDismissal: false, panGestureDismissal: false, hideStatusBar: true, completion: nil) // let whatisVpnButton = DefaultButton(title: "What is a VPN?", dismissOnTap: true) { // self.toggleVPNBodyView(animate: true, show: true) // self.performSegue(withIdentifier: "showWhatIsVPN", sender: self) // } let getEnhancedPrivacyButton = DefaultButton(title: NSLocalizedString("1 Week Free", comment: ""), dismissOnTap: true) { self.toggleVPNBodyView(animate: true, show: true) self.performSegue(withIdentifier: "showSignup", sender: self) } let laterButton = CancelButton(title: NSLocalizedString("Skip Trial", comment: ""), dismissOnTap: true) { } popup.addButtons([laterButton, getEnhancedPrivacyButton]) self.present(popup, animated: true, completion: nil) } // This notification is triggered for both Firewall and VPN @objc func tunnelStatusDidChange(_ notification: Notification) { // Firewall if let tunnelProviderSession = notification.object as? NETunnelProviderSession { DDLogInfo("VPNStatusDidChange as NETunnelProviderSession with status: \(tunnelProviderSession.status.rawValue)"); 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: "")) } } } } // VPN else if let neVPNConnection = notification.object as? NEVPNConnection { DDLogInfo("VPNStatusDidChange as NEVPNConnection with status: \(neVPNConnection.status.rawValue)"); 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"); } } // MARK: - Top Buttons func reloadMenuDot() { if (getAPICredentials() == nil || getAPICredentialsConfirmed() == false) { menuButtonDot.isHidden = false } else { menuButtonDot.isHidden = true } } @IBAction func menuTapped(_ sender: Any) { let buttonHeight = UIDevice.is4InchIphone ? 40 : 45 var title = "⚠️ Not Signed In" var message: String? = "Sign up below to unlock benefits of a Lockdown account." var firstButton = DefaultButton(title: NSLocalizedString("Sign Up | Sign In", comment: ""), height: buttonHeight, dismissOnTap: true) { self.performSegue(withIdentifier: "showCreateAccountFromHome", sender: self) } if let apiCredentials = getAPICredentials() { message = apiCredentials.email if getAPICredentialsConfirmed() == true { title = "Signed In" firstButton = DefaultButton(title: NSLocalizedString("Sign Out", comment: ""), height: buttonHeight, dismissOnTap: true) { let confirm = PopupDialog(title: "Sign Out?", message: "You'll be signed out from this account.", 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.reloadMenuDot() self.showPopupDialog(title: "Success", message: "Signed out successfully.", acceptButton: NSLocalizedString("Okay", comment: "")) }, ]) self.present(confirm, animated: true, completion: nil) } } else { title = "⚠️ Email Not Confirmed" firstButton = DefaultButton(title: NSLocalizedString("Confirm Email", comment: ""), height: buttonHeight, dismissOnTap: true) { self.showLoadingView() firstly { try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) } .done { (signin: SignIn) in self.hideLoadingView() // successfully signed in with no errors, show confirmation success setAPICredentialsConfirmed(confirmed: true) // logged in and confirmed - update this email with the receipt and refresh VPN credentials firstly { () -> Promise in try Client.subscriptionEvent() } .then { (result: SubscriptionEvent) -> Promise in try Client.getKey() } .done { (getKey: GetKey) in try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) if (getUserWantsVPNEnabled() == true) { VPNController.shared.restart() } } .catch { error in // it's okay for this to error out with "no subscription in receipt" DDLogError("HomeViewController ConfirmEmail subscriptionevent error (ok for it to be \"no subscription in receipt\"): \(error)") } let popup = PopupDialog(title: "Success! 🎉", message: NSLocalizedString("Your account has been confirmed and you're now signed in. You'll get the latest block lists, access to Lockdown Mac, and get critical announcements.", comment: ""), image: nil, buttonAlignment: .horizontal, transitionStyle: .bounceDown, preferredWidth: 270, tapGestureDismissal: true, panGestureDismissal: false, hideStatusBar: false, completion: nil) popup.addButtons([ DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) { self.reloadMenuDot() } ]) self.present(popup, animated: true, completion: nil) } .catch { error in self.hideLoadingView() let popup = PopupDialog(title: "Check Your Inbox", message: "To complete your signup, click the confirmation link we sent to \(apiCredentials.email). Be sure to check your spam folder in case it got stuck there.\n\nYou can also request a re-send of the confirmation.", image: nil, buttonAlignment: .vertical, transitionStyle: .bounceDown, preferredWidth: 270, tapGestureDismissal: true, panGestureDismissal: false, hideStatusBar: false, completion: nil) popup.addButtons([ DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {}, DefaultButton(title: NSLocalizedString("Sign Out", comment: ""), dismissOnTap: true) { URLCache.shared.removeAllCachedResponses() Client.clearCookies() clearAPICredentials() setAPICredentialsConfirmed(confirmed: false) self.reloadMenuDot() self.showPopupDialog(title: "Success", message: "Signed out successfully.", acceptButton: NSLocalizedString("Okay", comment: "")) }, DefaultButton(title: NSLocalizedString("Re-send", comment: ""), dismissOnTap: true) { firstly { try Client.resendConfirmCode(email: apiCredentials.email) } .done { (success: Bool) in var message = "Successfully re-sent your email confirmation to \(apiCredentials.email)" if (success == false) { message = "Failed to re-send email confirmation." } self.showPopupDialog(title: "", message: message, acceptButton: NSLocalizedString("Okay", comment: "")) } .catch { error in if (self.popupErrorAsNSURLError(error)) { return } else if let apiError = error as? ApiError { _ = self.popupErrorAsApiError(apiError) } else { self.showPopupDialog(title: NSLocalizedString("Error Re-sending Email Confirmation", comment: ""), message: "\(error)", acceptButton: NSLocalizedString("Okay", comment: "")) } } }, ]) self.present(popup, animated: true, completion: nil) } } } } firstButton.buttonColor = UIColor.tunnelsBlue firstButton.titleColor = UIColor.white let upgradeButton = DefaultButton(title: "Loading Plan", height: buttonHeight, dismissOnTap: true) { self.performSegue(withIdentifier: "showUpgradePlan", sender: self) } upgradeButton.titleColor = UIColor.lightGray upgradeButton.startActivityIndicator() upgradeButton.isEnabled = false self.activePlans = [] firstly { try Client.signIn() }.then { _ in try Client.activeSubscriptions() }.ensure { upgradeButton.stopActivityIndicator() }.done { subscriptions in self.activePlans = subscriptions.map({ $0.planType }) if let active = subscriptions.first { if active.planType == .proAnnual { upgradeButton.isEnabled = false upgradeButton.setTitle("Plan: Annual Pro", for: UIControl.State()) } else { upgradeButton.isEnabled = true upgradeButton.buttonColor = UIColor.tunnelsDarkBlue upgradeButton.titleColor = UIColor.white upgradeButton.setTitle("View or Upgrade Plan", for: UIControl.State()) } } else { upgradeButton.isEnabled = true upgradeButton.buttonColor = UIColor.tunnelsDarkBlue upgradeButton.titleColor = UIColor.white upgradeButton.setTitle("View Upgrade Options", for: UIControl.State()) } }.catch { error in DDLogWarn(error.localizedDescription) if let apiError = error as? ApiError { switch apiError.code { case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: upgradeButton.isEnabled = true upgradeButton.buttonColor = UIColor.tunnelsDarkBlue upgradeButton.titleColor = UIColor.white upgradeButton.setTitle("View Upgrade Options", for: UIControl.State()) default: upgradeButton.isEnabled = false upgradeButton.setTitle("Cannot load your plan", for: UIControl.State()) } } else { upgradeButton.isEnabled = false upgradeButton.setTitle("Cannot load your plan", for: UIControl.State()) } } // The `DynamicButton` is a special subclass created for this case. // It's needed to dynamically update the title of the button after it's pressed let notificationsButton = DynamicButton(title: "", height: buttonHeight, dismissOnTap: false, action: nil) let updateNotificationButtonTitle = { (button: DynamicButton) in if PushNotifications.Authorization.getUserWantsNotificationsEnabled(forCategory: .weeklyUpdate) { button.setTitle(NSLocalizedString("Notifications: On", comment: ""), for: UIControl.State()) } else { button.setTitle(NSLocalizedString("Notifications: Off", comment: ""), for: UIControl.State()) } } updateNotificationButtonTitle(notificationsButton) let popup = PopupDialog(title: title, message: message, image: nil, buttonAlignment: .vertical, transitionStyle: .bounceDown, preferredWidth: 270, tapGestureDismissal: true, panGestureDismissal: false, hideStatusBar: false, completion: nil) notificationsButton.onTap = { button in if PushNotifications.Authorization.getUserWantsNotificationsEnabled(forCategory: .weeklyUpdate) { PushNotifications.Authorization.setUserWantsNotificationsEnabled(false, forCategory: .weeklyUpdate) updateNotificationButtonTitle(button) } else { PushNotifications.Authorization.requestWeeklyUpdateAuthorization(presentingDialogOn: popup).done { status in DDLogInfo("New authorization status for push notifications: \(status)") updateNotificationButtonTitle(button) }.catch { error in DDLogError("Error updating notification authorization status: \(error.localizedDescription)") } } } popup.addButtons([ firstButton, upgradeButton, notificationsButton, ]) popup.addButtons([ DefaultButton(title: NSLocalizedString("Tutorial", comment: ""), height: buttonHeight, dismissOnTap: true) { self.startTutorial() }, DefaultButton(title: NSLocalizedString("Why Trust Lockdown", comment: ""), height: buttonHeight, dismissOnTap: true) { self.showWhyTrustPopup() }, DefaultButton(title: NSLocalizedString("Privacy Policy", comment: ""), height: buttonHeight, dismissOnTap: true) { self.showPrivacyPolicyModal() }, DefaultButton(title: NSLocalizedString("What is VPN?", comment: ""), height: buttonHeight, dismissOnTap: true) { self.performSegue(withIdentifier: "showWhatIsVPN", sender: self) }, DefaultButton(title: NSLocalizedString("Email Support", comment: ""), height: buttonHeight, dismissOnTap: true) { self.emailTeam() }, DefaultButton(title: NSLocalizedString("Website", comment: ""), height: buttonHeight, dismissOnTap: true) { self.showWebsiteModal() }, CancelButton(title: NSLocalizedString("Close", comment: ""), height: buttonHeight, dismissOnTap: true) {} ]) self.present(popup, animated: true, completion: nil) } func startTutorial() { let centerPoint = UIScreen.main.bounds.center let s0 = AwesomeSpotlight(withRect: CGRect(x: centerPoint.x, y: centerPoint.y - 100, width: 0, height: 0), shape: .circle, text: NSLocalizedString("Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue.", comment: "")) let s1 = AwesomeSpotlight(withRect: getRectForView(firewallTitleLabel).insetBy(dx: -13.0, dy: -13.0), shape: .roundRectangle, text: NSLocalizedString("Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari.", comment: "")) let s2 = AwesomeSpotlight(withRect: getRectForView(firewallToggleCircle).insetBy(dx: -10.0, dy: -10.0), shape: .circle, text: NSLocalizedString("Activate Firewall with this button.", comment: "")) let s3 = AwesomeSpotlight(withRect: getRectForView(metricsStack).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: NSLocalizedString("See live metrics for how many bad connections Firewall has blocked.", comment: "")) let s4 = AwesomeSpotlight(withRect: getRectForView(firewallViewLogButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: NSLocalizedString("\"View Log\" shows exactly what connections were blocked in the past day. This log is cleared at midnight and stays on-device, so it's only visible to you.", comment: "")) let s5 = AwesomeSpotlight(withRect: getRectForView(firewallSettingsButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: NSLocalizedString("\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). You can also set custom domains to block.", comment: "")) let s6 = AwesomeSpotlight(withRect: getRectForView(vpnHeaderView).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: NSLocalizedString("For maximum privacy, activate Secure Tunnel, which uses bank-level encryption to protect connections, anonymize your browsing, and hide your location and IP.", comment: "")) let s7 = AwesomeSpotlight(withRect: getRectForView(menuButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: NSLocalizedString("To see this tutorial again, tap the Menu button.", comment: "")) let spotlightView = AwesomeSpotlightView(frame: view.frame, spotlight: [s0, s1, s2, s3, s4, s5, s6, s7]) spotlightView.accessibilityIdentifier = "tutorial" spotlightView.cutoutRadius = 8 spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); spotlightView.enableArrowDown = true spotlightView.textLabelFont = fontMedium16 spotlightView.labelSpacing = 24; spotlightView.delegate = self view.addSubview(spotlightView) spotlightView.start() } func spotlightViewDidCleanup(_ spotlightView: AwesomeSpotlightView) { guard spotlightView.accessibilityIdentifier == "tutorial" else { return } defaults.set(true, forKey: kHasViewedTutorial) if getAPICredentials() != nil { // already has email signup pending or confirmed, don't show create account } else { self.performSegue(withIdentifier: "showCreateAccountFromHome", sender: nil) } } @IBAction func shareFirewallMetricsTapped(_ sender: Any) { let thousandsFormatter = NumberFormatter() thousandsFormatter.groupingSeparator = "," thousandsFormatter.numberStyle = .decimal let imageSize = CGSize(width: 720, height: 420) let renderer = UIGraphicsImageRenderer(size: imageSize) let image = renderer.image { ctx in let rectangle = CGRect(origin: CGPoint.zero, size: imageSize) ctx.cgContext.setFillColor(UIColor.white.cgColor) ctx.cgContext.addRect(rectangle) ctx.cgContext.drawPath(using: .fill) UIImage(named: "share.png")!.draw(in: CGRect(origin: CGPoint.zero, size: imageSize)) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .center let sinceAttrs = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 30, weight: .semibold), NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.foregroundColor: UIColor(red: 176/255, green: 176/255, blue: 176/255, alpha: 0.59)] let sinceY = 90 var date = "INSTALL" let formatter = DateFormatter() formatter.dateFormat = "MMM d YYYY" if let appInstall = appInstallDate { date = formatter.string(from: appInstall).uppercased() } "SINCE \(date)".draw(with: CGRect(origin: CGPoint(x: 0, y: sinceY), size: CGSize(width: 720, height: 50)), options: .usesLineFragmentOrigin, attributes: sinceAttrs, context: nil) let attrs = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 46, weight: .bold), NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.foregroundColor: UIColor(red: 149/255, green: 149/255, blue: 149/255, alpha: 1.0)] let countSize = CGSize(width: 240, height: 50) let countY = 216 thousandsFormatter.string(for: getDayMetrics())!.draw(with: CGRect(origin: CGPoint(x: 0, y: countY), size: countSize), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) thousandsFormatter.string(for: getWeekMetrics())!.draw(with: CGRect(origin: CGPoint(x: 240, y: countY), size: countSize), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) thousandsFormatter.string(for: getTotalMetrics())!.draw(with: CGRect(origin: CGPoint(x: 480, y: countY), size: countSize), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) } let popup = PopupDialog( title: NSLocalizedString("Share Your Stats", comment: ""), message: NSLocalizedString("Show how invasive today's apps are, and help other people block trackers and badware, too.\n\nYour block log is not included - only the image above. Choose where to share in the next step.", comment: ""), image: image, buttonAlignment: .horizontal, transitionStyle: .bounceDown, preferredWidth: 300.0, tapGestureDismissal: true, panGestureDismissal: false, hideStatusBar: true, completion: nil) let cancelButton = CancelButton(title: NSLocalizedString("Cancel", comment: ""), dismissOnTap: true) { } let shareButton = DefaultButton(title: NSLocalizedString("Next", comment: ""), dismissOnTap: true) { let shareText = NSLocalizedString("I blocked \(thousandsFormatter.string(for: getTotalMetrics())!) trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownhq.com.", comment: "") let vc = UIActivityViewController(activityItems: [LockdownCustomActivityItemProvider(text: shareText), image], applicationActivities: []) vc.completionWithItemsHandler = { (activity, success, items, error) in if (success) { self.showPopupDialog(title: NSLocalizedString("Success!", comment: ""), message: NSLocalizedString("Thanks for helping to increase privacy and tracking awareness.", comment: ""), acceptButton: NSLocalizedString("Nice", comment: "")) } print(success ? "SUCCESS!" : "FAILURE") } vc.excludedActivityTypes = [ UIActivity.ActivityType.assignToContact, UIActivity.ActivityType.addToReadingList, UIActivity.ActivityType.openInIBooks, UIActivity.ActivityType.postToVimeo, UIActivity.ActivityType.print ] self.present(vc, animated: true) } popup.addButtons([cancelButton, shareButton]) self.present(popup, animated: true, completion: nil) } // MARK: - Firewall @objc func updateMetrics() { DispatchQueue.main.async { self.dailyMetrics?.text = getDayMetricsString() self.weeklyMetrics?.text = getWeekMetricsString() self.allTimeMetrics?.text = getTotalMetricsString() } } @IBAction func toggleFirewall(_ sender: Any) { 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.parentVC = self self.present(viewController, animated: true, completion: nil) return } switch FirewallController.shared.status() { case .invalid: FirewallController.shared.setEnabled(true, isUserExplicitToggle: true) case .disconnected: updateFirewallButtonWithStatus(status: .connecting) FirewallController.shared.setEnabled(true, isUserExplicitToggle: true) checkForAskRating() case .connected: updateFirewallButtonWithStatus(status: .disconnecting) FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) case .connecting, .disconnecting, .reasserting: break; } } 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, activeLabel: firewallActive, toggleCircle: firewallToggleCircle, toggleAnimatedCircle: firewallToggleAnimatedCircle, button: firewallButton, prefixText: NSLocalizedString("FIREWALL", comment: "")) } func updateToggleButtonWithStatus(lastStatus: NEVPNStatus?, newStatus: NEVPNStatus, activeLabel: UILabel, toggleCircle: UIButton, toggleAnimatedCircle: NVActivityIndicatorView, button: UIButton, prefixText: String) { DDLogInfo("UpdateToggleButton") if (newStatus == lastStatus) { DDLogInfo("No status change from last time, ignoring."); } else { DispatchQueue.main.async() { switch newStatus { case .connected: activeLabel.text = prefixText + NSLocalizedString(" ON", comment: "") activeLabel.backgroundColor = UIColor.tunnelsBlue toggleCircle.tintColor = .tunnelsBlue toggleCircle.isHidden = false toggleAnimatedCircle.stopAnimating() button.tintColor = .tunnelsBlue case .connecting: activeLabel.text = NSLocalizedString("ACTIVATING", comment: "") activeLabel.backgroundColor = .tunnelsBlue toggleCircle.isHidden = true toggleAnimatedCircle.color = .tunnelsBlue toggleAnimatedCircle.startAnimating() button.tintColor = .tunnelsBlue case .disconnected, .invalid: activeLabel.text = prefixText + NSLocalizedString(" OFF", comment: "") activeLabel.backgroundColor = .tunnelsWarning toggleCircle.tintColor = .lightGray toggleCircle.isHidden = false toggleAnimatedCircle.stopAnimating() button.tintColor = .lightGray case .disconnecting: activeLabel.text = NSLocalizedString("DEACTIVATING", comment: "") activeLabel.backgroundColor = .lightGray toggleCircle.isHidden = true toggleAnimatedCircle.color = .lightGray toggleAnimatedCircle.startAnimating() button.tintColor = .lightGray case .reasserting: break; } } } } func highlightBlockLog() { let blockLogSpotlight = AwesomeSpotlight(withRect: getRectForView(firewallViewLogButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: NSLocalizedString("Tap to see the blocked tracking attempts.", comment: "")) let spotlightView = AwesomeSpotlightView(frame: view.frame, spotlight: [blockLogSpotlight]) spotlightView.accessibilityIdentifier = "highlightBlockLog" spotlightView.cutoutRadius = 8 spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); spotlightView.enableArrowDown = true spotlightView.textLabelFont = fontMedium16 spotlightView.labelSpacing = 24; view.addSubview(spotlightView) spotlightView.start() } // MARK: - VPN @objc @IBAction func vpnHeaderTapped(_ sender: Any) { toggleVPNBodyView(animate: true) } @IBAction func vpnQuestionTapped(_ sender: Any) { toggleVPNBodyView(animate: false, show: true) self.performSegue(withIdentifier: "showWhatIsVPN", sender: self) } func toggleVPNBodyView(animate: Bool, show: Bool? = nil) { // always show self.vpnHideButton.setTitle(NSLocalizedString("HIDE", comment: ""), for: .normal) self.vpnBodyView.alpha = 1 self.vpnActiveHeaderConstraint.isActive = false self.vpnActiveTopBodyConstraint.isActive = true self.vpnActiveVerticalBodyConstraint.isActive = true self.stackEqualHeightConstraint.isActive = true self.vpnViewHeightConstraint.isActive = false self.view.layoutIfNeeded() return // If supplied a "show", use that. Otherwise, use the opposite of the current visible state (show -> hide, hide -> show) var shouldShow = false if show != nil { shouldShow = show! } else { shouldShow = !(defaults.bool(forKey: kVPNBodyViewVisible)) } var animationTime = 0.0 if (animate) { animationTime = 0.2 } if (shouldShow) { vpnBodyView.alpha = 0 self.vpnBodyView.isHidden = false UIView.animate(withDuration: animationTime, animations: { self.vpnHideButton.setTitle(NSLocalizedString("HIDE", comment: ""), for: .normal) self.vpnBodyView.alpha = 1 self.vpnActiveHeaderConstraint.isActive = false self.vpnActiveTopBodyConstraint.isActive = true self.vpnActiveVerticalBodyConstraint.isActive = true self.stackEqualHeightConstraint.isActive = true self.vpnViewHeightConstraint.isActive = false self.view.layoutIfNeeded() }, completion: { complete in defaults.set(true, forKey: self.kVPNBodyViewVisible) }) } else { UIView.animate(withDuration: animationTime, animations: { self.vpnHideButton.setTitle(NSLocalizedString("SHOW", comment: ""), for: .normal) self.vpnBodyView.alpha = 0 self.vpnActiveHeaderConstraint.isActive = true self.vpnActiveTopBodyConstraint.isActive = false self.vpnActiveVerticalBodyConstraint.isActive = false self.stackEqualHeightConstraint.isActive = false self.vpnViewHeightConstraint.constant = self.vpnHeaderView.frame.height self.vpnViewHeightConstraint.isActive = true self.view.layoutIfNeeded() }, completion: { complete in self.vpnBodyView.isHidden = true defaults.set(false, forKey: self.kVPNBodyViewVisible) }) } } 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, activeLabel: vpnActive, toggleCircle: vpnToggleCircle, toggleAnimatedCircle: vpnToggleAnimatedCircle, button: vpnButton, prefixText: NSLocalizedString("TUNNEL", comment: "")) } @IBAction func toggleVPN(_ sender: Any) { // redundant - privacy policy agreement already happens in Firewall activation // if (defaults.bool(forKey: kHasAgreedToVPNPrivacyPolicy) == false) { // let storyboard = UIStoryboard(name: "Main", bundle: nil) // let viewController = storyboard.instantiateViewController(withIdentifier: "vpnPrivacyPolicyViewController") as! PrivacyPolicyViewController // viewController.privacyPolicyKey = kHasAgreedToVPNPrivacyPolicy // viewController.parentVC = self // self.present(viewController, animated: true, completion: nil) // return // } 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 { print("have confirmed API credentials, using them") firstly { try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) } .then { (signin: SignIn) -> Promise in print("signin result: \(signin)") return try Client.subscriptionEvent() } .recover { error -> Promise in print("recovering from subscriptionevent error: \(error) - it's okay because we should try to GetKey anyways") return .value(SubscriptionEvent(message: "Recovery")) } .then { (result: SubscriptionEvent) -> Promise in print("subscriptionevent result: \(result)") return try Client.getKey() } .done { (getKey: GetKey) in try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) print("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.reloadMenuDot() self.showPopupDialog(title: "Success", message: "Signed out successfully.", acceptButton: NSLocalizedString("Okay", comment: "")) }, ]) self.present(confirm, animated: true, completion: nil) case kApiCodeNoSubscriptionInReceipt: 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.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.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.performSegue(withIdentifier: "showSignup", sender: self) }) default: _ = self.popupErrorAsApiError(error) } } else { self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""), message: "\(error)", acceptButton: NSLocalizedString("Okay", comment: "")) } } } } } @IBAction func viewAuditReportTapped(_ sender: Any) { showAuditModal() } @IBAction func showWhitelist(_ sender: Any) { performSegue(withIdentifier: "showWhitelist", sender: nil) } @IBAction func showSetRegion(_ sender: Any) { performSegue(withIdentifier: "showSetRegion", sender: nil) } func showBlockLog(_ sender: Any) { performSegue(withIdentifier: "showBlockLog", sender: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { switch segue.identifier { case "showSetRegion": if let vc = segue.destination as? SetRegionViewController { vc.homeVC = self } case "showWhatIsVPN": if let vc = segue.destination as? WhatIsVpnViewController { vc.parentVC = self } case "showUpgradePlan": if let vc = segue.destination as? SignupViewController { if activePlans.isEmpty { vc.mode = .newSubscription vc.enableVPNAfterSubscribe = false } else { vc.mode = .upgrade(active: activePlans) vc.enableVPNAfterSubscribe = false } } default: break } } func updateVPNRegionLabel() { vpnRegionLabel.text = getSavedVPNRegion().regionDisplayNameShort } // MARK: - Helpers func checkForAskRating(delayInSeconds: TimeInterval = 5.0) { DDLogInfo("Checking for ask rating") let ratingCount = defaults.integer(forKey: ratingCountKey) + 1 DDLogInfo("Incrementing Rating Count: " + String(ratingCount)) defaults.set(ratingCount, forKey: ratingCountKey) // not testflight if (isTestFlight) { DDLogInfo("Not doing rating for TestFlight") return } // greater than 3 days since install if let installDate = appInstallDate, let daysSinceInstall = Calendar.current.dateComponents([.day], from: installDate, to: Date()).day, daysSinceInstall <= 3 { DDLogInfo("Rating Check: Skipping - App was installed on \(installDate), fewer than 4 days since install - \(daysSinceInstall) days") return } // only check every 8th time connecting to this version if (ratingCount % 8 != 0) { DDLogInfo("Rating Check: Skipping - ratingCount % 8 != 0: \(ratingCount)") return } // hasn't asked for this version 2 times already let ratingTriggered = defaults.integer(forKey: ratingTriggeredKey) if (ratingTriggered >= 3) { DDLogInfo("Rating Check: Skipping - ratingTriggered greater or equal to 3: \(ratingTriggered)") return } // passed all checks, ask for rating DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { defaults.set(ratingTriggered + 1, forKey: self.ratingTriggeredKey) SKStoreReviewController.requestReview() } } // func updateIP() { // DDLogInfo("Updating IP") // self.vpnIP.text = "—" // firstly { // Client.getIP() // } // .done { (ip: IP) in // DispatchQueue.main.async { // self.vpnIP.text = ip.ip // } // } // .catch { error in // self.vpnIP.text = "error" // DDLogError("Error getting IP: \(error)") // } // } // @IBAction func runSpeedTest() { // DDLogInfo("Speed Test") // vpnSpeed.text = "Testing..." // vpnSpeed.alpha = 0.2 // UIView.animate(withDuration: 0.65, delay: 0, options: [.curveEaseInOut, .autoreverse, .repeat], animations: { // self.vpnSpeed.alpha = 1.0 // }) // firstly { // SpeedTest().testDownloadSpeedWithTimeout(timeout: 10.0) // } // .done { (mbps: Double) in // DispatchQueue.main.async { // self.vpnSpeed.layer.removeAllAnimations() // self.vpnSpeed.text = String(format: "%.1f", mbps) // + " Mbps" // UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseInOut], animations: { // self.vpnSpeed.alpha = 1.0 // }) // } // } // .catch { error in // self.vpnSpeed.text = "error" // DDLogError("Error testing speed: \(error)") // } // } } class LockdownCustomActivityItemProvider : UIActivityItemProvider { let shareText: String init(text: String) { self.shareText = text super.init(placeholderItem: text) } override func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { if let type = activityType { switch type { case UIActivity.ActivityType.postToTwitter: return shareText + " @lockdown_hq" default: return shareText } } else { return shareText } } } fileprivate extension PopupDialogButton { func startActivityIndicator() { let activity = UIActivityIndicatorView() if let label = titleLabel { label.addSubview(activity) activity.translatesAutoresizingMaskIntoConstraints = false activity.centerYAnchor.constraint(equalTo: label.centerYAnchor).isActive = true activity.leadingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1).isActive = true activity.startAnimating() } } func stopActivityIndicator() { if let label = titleLabel { let indicators = label.subviews.compactMap { $0 as? UIActivityIndicatorView } for indicator in indicators { indicator.stopAnimating() indicator.removeFromSuperview() } } } } final class DynamicButton: PopupDialogButton { var onTap: ((DynamicButton) -> ())? override var buttonAction: PopupDialogButton.PopupDialogButtonAction? { get { if let onTap = onTap { return { [weak self] in if let value = self { return onTap(value) } } } else { return nil } } } }