Files
lockdown-iOS-mirror/LockdowniOS/HomeViewController.swift
2020-09-28 18:01:19 +03:00

1211 lines
62 KiB
Swift

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