mirror of
https://github.com/confirmedcode/Lockdown-iOS.git
synced 2025-12-21 12:14:02 +01:00
1093 lines
48 KiB
Swift
1093 lines
48 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()
|
|
corners = .continuous(bounds.midX)
|
|
}
|
|
|
|
@IBInspectable var shadowUIColor: UIColor? {
|
|
didSet {
|
|
redrawShadowColor()
|
|
}
|
|
}
|
|
|
|
func redrawShadowColor() {
|
|
self.layer.shadowColor = shadowUIColor?.cgColor
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
redrawShadowColor()
|
|
}
|
|
}
|
|
|
|
let kHasSeenEmailSignup = "hasSeenEmailSignup"
|
|
|
|
// swiftlint:disable type_body_length
|
|
class HomeViewController: BaseViewController, Loadable {
|
|
|
|
let kHasViewedTutorial = "hasViewedTutorial"
|
|
let kHasSeenInitialFirewallConnectedDialog = "hasSeenInitialFirewallConnectedDialog11"
|
|
let kHasSeenShare = "hasSeenShareDialog4"
|
|
|
|
let ratingCountKey = "ratingCount" + lastVersionToAskForRating
|
|
let ratingTriggeredKey = "ratingTriggered" + lastVersionToAskForRating
|
|
|
|
@IBOutlet var mainStack: UIStackView!
|
|
|
|
@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 weak var vpnHeaderView: UIView!
|
|
@IBOutlet weak var vpnActive: UILabel!
|
|
@IBOutlet weak var vpnToggleCircle: UIButton!
|
|
@IBOutlet weak var vpnToggleAnimatedCircle: NVActivityIndicatorView!
|
|
@IBOutlet weak var vpnButton: UIButton!
|
|
var lastVPNStatus: NEVPNStatus?
|
|
@IBOutlet weak var vpnSetRegionButton: UIButton!
|
|
@IBOutlet weak var vpnRegionLabel: UILabel!
|
|
@IBOutlet weak var vpnWhitelistButton: UIButton!
|
|
|
|
private let paywallService = BasePaywallService.shared
|
|
private var countdownDisplayService: CountdownDisplayService { paywallService.countdownDisplayService }
|
|
|
|
private var ltoButton: 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.corners = .continuous(8)
|
|
firewallViewLogButton.layer.maskedCorners = [.layerMinXMaxYCorner]
|
|
firewallSettingsButton.corners = .continuous(8)
|
|
firewallSettingsButton.layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
|
|
updateVPNButtonWithStatus(status: VPNController.shared.status())
|
|
//updateIP()
|
|
vpnWhitelistButton.corners = .continuous(8)
|
|
vpnWhitelistButton.layer.maskedCorners = [.layerMinXMaxYCorner]
|
|
vpnSetRegionButton.corners = .continuous(8)
|
|
vpnSetRegionButton.layer.maskedCorners = [.layerMaxXMaxYCorner]
|
|
updateVPNRegionLabel()
|
|
|
|
updateStackViewAxis(basedOn: view.frame.size)
|
|
|
|
// 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 { _ 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: .localized("VPN Subscription Expired"),
|
|
message: .localized("Please renew your subscription to re-activate the VPN."),
|
|
acceptButton: .localizedOkay) {
|
|
self.showPaywallIfNoSubscription()
|
|
}
|
|
default:
|
|
_ = self.popupErrorAsApiError(error)
|
|
}
|
|
} else {
|
|
self.showPopupDialog(title: .localized("Error Signing In To Verify Subscription"),
|
|
message: "\(error)",
|
|
acceptButton: .localizedOkay)
|
|
}
|
|
}
|
|
}
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(tunnelStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil)
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
|
|
let inset = firewallButton.frame.width * 0.175
|
|
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)
|
|
|
|
// Used for debugging signup
|
|
//performSegue(withIdentifier: "showSignup", sender: nil)
|
|
|
|
if defaults.bool(forKey: kHasViewedTutorial) == false {
|
|
startTutorial()
|
|
} else if OneTimeActions.hasSeen(.newFancyOnboarding),
|
|
!OneTimeActions.hasSeen(.newEnableNotificationsController) {
|
|
showEnableNotifications()
|
|
} else if UserDefaults.hasSeenPaywallOnHomeScreen == false {
|
|
showPaywallIfNoSubscription()
|
|
} else if Calendar.hasExceededLapse(between: Date(), and: UserDefaults.lastPaywallDisplayDate) {
|
|
showPaywallIfNoSubscription()
|
|
}
|
|
|
|
if defaults.bool(forKey: kHasSeenInitialFirewallConnectedDialog) == false {
|
|
tapToActivateFirewallLabel.isHidden = false
|
|
}
|
|
|
|
// If total blocked > 1000, and have not shown share dialog before, ask if user wants to share
|
|
if getTotalMetrics() > 1000 && defaults.bool(forKey: kHasSeenShare) == false {
|
|
defaults.set(true, forKey: kHasSeenShare)
|
|
let popup = PopupDialog(
|
|
title: "You've blocked over 1000 trackers! 🎊",
|
|
message: .localized("Share your anonymized metrics and show other people how to block invasive tracking."),
|
|
image: nil,
|
|
buttonAlignment: .horizontal,
|
|
transitionStyle: .bounceDown,
|
|
preferredWidth: 270,
|
|
tapGestureDismissal: true,
|
|
panGestureDismissal: false,
|
|
hideStatusBar: false,
|
|
completion: nil)
|
|
popup.addButtons([
|
|
CancelButton(title: .localized("Not Now"), dismissOnTap: true) {
|
|
let s0 = AwesomeSpotlight(
|
|
withRect: self.getRectForView(self.firewallShareButton).insetBy(dx: -13.0, dy: -13.0),
|
|
shape: .roundRectangle,
|
|
text: .localized("You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)"))
|
|
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 = .mediumLockdownFont(size: 16)
|
|
spotlightView.labelSpacing = 24
|
|
spotlightView.delegate = self
|
|
self.view.addSubview(spotlightView)
|
|
spotlightView.start()
|
|
},
|
|
DefaultButton(title: .localized("Next"), dismissOnTap: true) {
|
|
self.shareFirewallMetricsTapped("")
|
|
}
|
|
])
|
|
self.present(popup, animated: true, completion: nil)
|
|
}
|
|
|
|
if paywallService.context == .followUpLimitedTimeOffer {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
self.showLTOButtonIfEligible()
|
|
}
|
|
}
|
|
|
|
addLTOButtonIfEligible()
|
|
}
|
|
|
|
func updateStackViewAxis(basedOn size: CGSize) {
|
|
guard traitCollection.userInterfaceIdiom == .pad else {
|
|
// axis always vertical on iPhone
|
|
return
|
|
}
|
|
|
|
if size.width > size.height {
|
|
mainStack.axis = .horizontal
|
|
} else {
|
|
mainStack.axis = .vertical
|
|
}
|
|
}
|
|
|
|
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
|
guard traitCollection.userInterfaceIdiom == .pad else {
|
|
return
|
|
}
|
|
|
|
coordinator.animate { [unowned self] _ in
|
|
self.updateLtoButtonFrame()
|
|
self.updateStackViewAxis(basedOn: size)
|
|
} completion: { (_) in
|
|
return
|
|
}
|
|
}
|
|
|
|
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 getEnhancedPrivacyButton = DefaultButton(title: .localized("1 Week Free"), dismissOnTap: true) {
|
|
self.showPaywallIfNoSubscription()
|
|
}
|
|
let laterButton = CancelButton(title: .localized("Skip Trial"), 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.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 {
|
|
guard BaseUserService.shared.user.currentSubscription == nil else { return }
|
|
let message = "trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy"
|
|
self.showVPNSubscriptionDialog(title: .localized("🔥🧱 Firewall Activated 🎊🎉"), message: .localized(message))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// VPN
|
|
else if let neVPNConnection = notification.object as? NEVPNConnection {
|
|
DDLogInfo("VPNStatusDidChange as NEVPNConnection with status: \(neVPNConnection.status.description)")
|
|
updateVPNButtonWithStatus(status: neVPNConnection.status)
|
|
updateVPNRegionLabel()
|
|
if NEVPNManager.shared().connection.status == .connected || NEVPNManager.shared().connection.status == .disconnected {
|
|
//self.updateIP();
|
|
}
|
|
} else {
|
|
DDLogInfo("VPNStatusDidChange neither TunnelProviderSession nor NEVPNConnection")
|
|
}
|
|
}
|
|
|
|
// MARK: - Top Buttons
|
|
|
|
func startTutorial() {
|
|
var spotlights: [AwesomeSpotlight] = []
|
|
let centerPoint = UIScreen.main.bounds.center
|
|
|
|
let spotlight0Message: String = .localized("Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue.")
|
|
let s0 = AwesomeSpotlight(withRect: CGRect(x: centerPoint.x, y: centerPoint.y - 100, width: 0, height: 0), shape: .circle, text: spotlight0Message)
|
|
|
|
let spotlight1Message: String = .localized("Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari.")
|
|
let firewallTitleLabelRect = getRectForView(firewallTitleLabel).insetBy(dx: -13.0, dy: -30.0)
|
|
let s1 = AwesomeSpotlight(withRect: firewallTitleLabelRect, shape: .roundRectangle, text: spotlight1Message)
|
|
|
|
let spotlight2Message: String = .localized("Activate Firewall with this button.")
|
|
let s2 = AwesomeSpotlight(withRect: getRectForView(firewallToggleCircle).insetBy(dx: -10.0, dy: -10.0), shape: .circle, text: spotlight2Message)
|
|
|
|
let spotlight3Message: String = .localized("See live metrics for how many bad connections Firewall has blocked.")
|
|
let s3 = AwesomeSpotlight(withRect: getRectForView(metricsStack).insetBy(dx: -10.0, dy: 0.0), shape: .roundRectangle, text: spotlight3Message)
|
|
|
|
let spotlight4Message = """
|
|
\"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.
|
|
"""
|
|
let firewallLogButtonRect = getRectForView(firewallViewLogButton).insetBy(dx: -10.0, dy: -10.0)
|
|
let s4 = AwesomeSpotlight(withRect: firewallLogButtonRect, shape: .roundRectangle, text: .localized(spotlight4Message))
|
|
|
|
let spotlight5Message = """
|
|
\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). \
|
|
You can also set custom domains to block.
|
|
"""
|
|
let firewallSettingButtonRect = getRectForView(firewallSettingsButton).insetBy(dx: -10.0, dy: -10.0)
|
|
let s5 = AwesomeSpotlight(withRect: firewallSettingButtonRect, shape: .roundRectangle, text: .localized(spotlight5Message))
|
|
|
|
let spotlight6Message = """
|
|
For maximum privacy, activate Secure Tunnel, which uses bank-level encryption to protect connections, anonymize your browsing, \
|
|
and hide your location and IP.
|
|
"""
|
|
let headerViewRect = getRectForView(vpnHeaderView).insetBy(dx: -10.0, dy: -10.0)
|
|
let s6 = AwesomeSpotlight(withRect: headerViewRect, shape: .roundRectangle, text: .localized(spotlight6Message))
|
|
|
|
spotlights.append(contentsOf: [s0, s1, s2, s3, s4, s5, s6])
|
|
|
|
if let tabBarButton = (tabBarController as? MainTabBarController)?.accountTabBarButton {
|
|
let spotlight7Message: String = .localized("To see this tutorial again, open the Account tab.")
|
|
let tabBarButtonRect = getRectForView(tabBarButton).insetBy(dx: -10.0, dy: -10.0)
|
|
let s7 = AwesomeSpotlight(withRect: tabBarButtonRect, shape: .roundRectangle, text: spotlight7Message)
|
|
spotlights.append(s7)
|
|
}
|
|
|
|
let spotlightView = AwesomeSpotlightView(frame: view.frame,
|
|
spotlight: spotlights)
|
|
spotlightView.accessibilityIdentifier = "tutorial"
|
|
spotlightView.cutoutRadius = 8
|
|
spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
|
|
spotlightView.enableArrowDown = true
|
|
spotlightView.textLabelFont = .mediumLockdownFont(size: 16)
|
|
spotlightView.labelSpacing = 24
|
|
spotlightView.delegate = self
|
|
tabBarController?.view.addSubview(spotlightView)
|
|
spotlightView.start()
|
|
}
|
|
|
|
func spotlightViewDidCleanup(_ spotlightView: AwesomeSpotlightView) {
|
|
guard spotlightView.accessibilityIdentifier == "tutorial" else {
|
|
return
|
|
}
|
|
|
|
defaults.set(true, forKey: kHasViewedTutorial)
|
|
}
|
|
|
|
@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.setLocalizedDateFormatFromTemplate("MMM d YYYY")
|
|
if let appInstall = appInstallDate {
|
|
date = formatter.string(from: appInstall).uppercased()
|
|
}
|
|
|
|
"since_date".localized(inserting: date).draw(
|
|
with: CGRect(origin: CGPoint(x: 0, y: sinceY), size: CGSize(width: 720, height: 50)),
|
|
options: .usesLineFragmentOrigin, attributes: sinceAttrs, context: nil)
|
|
|
|
let attributes = [
|
|
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: attributes,
|
|
context: nil)
|
|
thousandsFormatter.string(for: getWeekMetrics())!.draw(
|
|
with: CGRect(origin: CGPoint(x: 240, y: countY), size: countSize),
|
|
options: .usesLineFragmentOrigin,
|
|
attributes: attributes,
|
|
context: nil)
|
|
thousandsFormatter.string(for: getTotalMetrics())!.draw(
|
|
with: CGRect(origin: CGPoint(x: 480, y: countY), size: countSize),
|
|
options: .usesLineFragmentOrigin,
|
|
attributes: attributes,
|
|
context: nil)
|
|
}
|
|
|
|
let message = """
|
|
Show how invasive today's apps are, and help other people block trackers and badware, too.
|
|
|
|
Your block log is not included - only the image above. Choose where to share in the next step.
|
|
"""
|
|
let popup = PopupDialog(
|
|
title: .localized("Share Your Stats"),
|
|
message: .localized(message),
|
|
image: image,
|
|
buttonAlignment: .horizontal,
|
|
transitionStyle: .bounceDown,
|
|
preferredWidth: 300.0,
|
|
tapGestureDismissal: true,
|
|
panGestureDismissal: false,
|
|
hideStatusBar: true,
|
|
completion: nil)
|
|
|
|
let cancelButton = CancelButton(title: .localizedCancel, dismissOnTap: true) { }
|
|
|
|
let shareButton = DefaultButton(title: .localized("Next"), dismissOnTap: true) {
|
|
|
|
let metrics = thousandsFormatter.string(for: getTotalMetrics())!
|
|
let shareText = "i_blocked_trackers_ads_and_badware_with_lockdown".localized(inserting: metrics)
|
|
|
|
let vc = UIActivityViewController(activityItems: [LockdownCustomActivityItemProvider(text: shareText), image], applicationActivities: [])
|
|
vc.completionWithItemsHandler = { (_, success, _, _) in
|
|
if success {
|
|
self.showPopupDialog(
|
|
title: .localized("Success!"),
|
|
message: .localized("Thanks for helping to increase privacy and tracking awareness."),
|
|
acceptButton: .localized("Nice", comment: "Used as a button text in a popup. Like 'OK' except more excited."))
|
|
}
|
|
}
|
|
vc.excludedActivityTypes = [.assignToContact, .addToReadingList, .openInIBooks, .postToVimeo, .print]
|
|
|
|
if let popoverPC = vc.popoverPresentationController {
|
|
popoverPC.sourceView = self.firewallShareButton
|
|
popoverPC.sourceRect = self.firewallShareButton.bounds
|
|
popoverPC.permittedArrowDirections = .up
|
|
}
|
|
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 identifier = "firewallPrivacyPolicyViewController"
|
|
guard let viewController = storyboard.instantiateViewController(withIdentifier: identifier) as? PrivacyPolicyViewController else { return }
|
|
viewController.privacyPolicyKey = kHasAgreedToFirewallPrivacyPolicy
|
|
viewController.parentVC = self
|
|
self.present(viewController, animated: true, completion: nil)
|
|
return
|
|
}
|
|
|
|
if getIsCombinedBlockListEmpty() {
|
|
FirewallController.shared.setEnabled(false, isUserExplicitToggle: true)
|
|
self.showPopupDialog(
|
|
title: .localized("No Block Lists Enabled"),
|
|
message: .localized("Please tap Block List and enable at least one block list to activate Firewall."),
|
|
acceptButton: .localizedOkay)
|
|
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 count ?? 0 > 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")
|
|
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,
|
|
activeLabel: firewallActive,
|
|
toggleCircle: firewallToggleCircle,
|
|
toggleAnimatedCircle: firewallToggleAnimatedCircle,
|
|
button: firewallButton,
|
|
prefixText: .localized("Firewall").uppercased())
|
|
}
|
|
|
|
// swiftlint:disable function_parameter_count
|
|
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)\(String.localized(" On").uppercased())"
|
|
activeLabel.backgroundColor = UIColor.tunnelsBlue
|
|
toggleCircle.tintColor = .tunnelsBlue
|
|
toggleCircle.isHidden = false
|
|
toggleAnimatedCircle.stopAnimating()
|
|
button.tintColor = .tunnelsBlue
|
|
case .connecting:
|
|
activeLabel.text = .localized("Activating").uppercased()
|
|
activeLabel.backgroundColor = .tunnelsBlue
|
|
toggleCircle.isHidden = true
|
|
toggleAnimatedCircle.color = .tunnelsBlue
|
|
toggleAnimatedCircle.startAnimating()
|
|
button.tintColor = .tunnelsBlue
|
|
case .disconnected, .invalid:
|
|
activeLabel.text = "\(prefixText)\(String.localized(" Off").uppercased())"
|
|
activeLabel.backgroundColor = .tunnelsWarning
|
|
toggleCircle.tintColor = .lightGray
|
|
toggleCircle.isHidden = false
|
|
toggleAnimatedCircle.stopAnimating()
|
|
button.tintColor = .lightGray
|
|
case .disconnecting:
|
|
activeLabel.text = .localized("Deactivating").uppercased()
|
|
activeLabel.backgroundColor = .lightGray
|
|
toggleCircle.isHidden = true
|
|
toggleAnimatedCircle.color = .lightGray
|
|
toggleAnimatedCircle.startAnimating()
|
|
button.tintColor = .lightGray
|
|
case .reasserting:
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func highlightBlockLog() {
|
|
let logButtonRect = getRectForView(firewallViewLogButton).insetBy(dx: -10.0, dy: -10.0)
|
|
let logButtonSpotlightText: String = .localized("Tap to see the blocked tracking attempts.")
|
|
let blockLogSpotlight = AwesomeSpotlight(withRect: logButtonRect, shape: .roundRectangle, text: logButtonSpotlightText)
|
|
|
|
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 = .mediumLockdownFont(size: 16)
|
|
spotlightView.labelSpacing = 24
|
|
view.addSubview(spotlightView)
|
|
spotlightView.start()
|
|
}
|
|
|
|
// MARK: - VPN
|
|
|
|
|
|
@IBAction func vpnQuestionTapped(_ sender: Any) {
|
|
self.performSegue(withIdentifier: "showWhatIsVPN", sender: self)
|
|
}
|
|
|
|
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: .localized("Tunnel").uppercased())
|
|
}
|
|
|
|
@IBAction func toggleVPN(_ sender: Any) {
|
|
|
|
DDLogInfo("Toggle VPN")
|
|
switch VPNController.shared.status() {
|
|
case .connected, .connecting, .reasserting:
|
|
DDLogInfo("Toggle VPN: on currently, turning it off")
|
|
updateVPNButtonWithStatus(status: .disconnecting)
|
|
VPNController.shared.setEnabled(false)
|
|
case .disconnected, .disconnecting, .invalid:
|
|
DDLogInfo("Toggle VPN: off currently, turning it on")
|
|
updateVPNButtonWithStatus(status: .connecting)
|
|
// if there's a confirmed email, use that and sync the receipt with it
|
|
if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true {
|
|
DDLogInfo("have confirmed API credentials, using them")
|
|
firstly {
|
|
try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password)
|
|
}
|
|
.then { (signin: SignIn) -> Promise<SubscriptionEvent> in
|
|
DDLogInfo("signin result: \(signin)")
|
|
return try Client.subscriptionEvent()
|
|
}
|
|
.then { (result: SubscriptionEvent) -> Promise<GetKey> in
|
|
DDLogInfo("subscriptionevent result: \(result)")
|
|
return try Client.getKey()
|
|
}
|
|
.done { (getKey: GetKey) in
|
|
try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64)
|
|
DDLogInfo("setting VPN creds with ID: \(getKey.id)")
|
|
VPNController.shared.setEnabled(true)
|
|
}
|
|
.catch { error in
|
|
DDLogError("Error doing email-login -> subscription-event: \(error)")
|
|
self.updateVPNButtonWithStatus(status: .disconnected)
|
|
if self.popupErrorAsNSURLError(error) {
|
|
return
|
|
} else if let apiError = error as? ApiError {
|
|
switch apiError.code {
|
|
case kApiCodeInvalidAuth, kApiCodeIncorrectLogin:
|
|
let confirm = PopupDialog(title: "Incorrect Login",
|
|
message: "Your saved login credentials are incorrect. Please sign out and try again.",
|
|
image: nil,
|
|
buttonAlignment: .horizontal,
|
|
transitionStyle: .bounceDown,
|
|
preferredWidth: 270,
|
|
tapGestureDismissal: true,
|
|
panGestureDismissal: false,
|
|
hideStatusBar: false,
|
|
completion: nil)
|
|
confirm.addButtons([
|
|
DefaultButton(title: .localizedCancel, dismissOnTap: true) {
|
|
},
|
|
DefaultButton(title: .localizedSignOut, dismissOnTap: true) {
|
|
URLCache.shared.removeAllCachedResponses()
|
|
Client.clearCookies()
|
|
clearAPICredentials()
|
|
setAPICredentialsConfirmed(confirmed: false)
|
|
self.showPopupDialog(
|
|
title: .localized("Success"),
|
|
message: .localized("Signed out successfully."),
|
|
acceptButton: .localizedOkay)
|
|
},
|
|
])
|
|
self.present(confirm, animated: true, completion: nil)
|
|
case kApiCodeNoSubscriptionInReceipt:
|
|
self.showPaywallIfNoSubscription()
|
|
case kApiCodeNoActiveSubscription:
|
|
self.showPopupDialog(
|
|
title: .localized("Subscription Expired"),
|
|
message: .localized("Please renew your subscription to activate the Secure Tunnel."),
|
|
acceptButton: .localizedOkay) {
|
|
self.showPaywallIfNoSubscription()
|
|
}
|
|
default:
|
|
_ = self.popupErrorAsApiError(error)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
firstly {
|
|
try Client.signIn() // this will fetch and set latest receipt, then submit to API to get cookie
|
|
}
|
|
.then { _ -> 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.showPaywallIfNoSubscription()
|
|
case kApiCodeNoActiveSubscription:
|
|
self.showPopupDialog(
|
|
title: .localized("Subscription Expired"),
|
|
message: .localized("Please renew your subscription to activate the Secure Tunnel."),
|
|
acceptButton: .localizedOkay) {
|
|
self.showPaywallIfNoSubscription()
|
|
}
|
|
default:
|
|
if apiError.code == kApiCodeNegativeError {
|
|
if getVPNCredentials() != nil {
|
|
DDLogError("Unknown error -1 from API, but VPNCredentials exists, so activating anyway.")
|
|
self.updateVPNButtonWithStatus(status: .connecting)
|
|
VPNController.shared.setEnabled(true)
|
|
} else {
|
|
let message = """
|
|
There is currently an outage at Apple which is preventing Secure Tunnel from activating. \
|
|
This will likely by resolved by Apple soon, and we apologize for this issue in the meantime.
|
|
""" + .localized("\n\n If this error persists, please contact team@lockdownprivacy.com.")
|
|
self.showPopupDialog(
|
|
title: .localized("Apple Outage"),
|
|
message: message,
|
|
acceptButton: .localizedOkay)
|
|
}
|
|
} else {
|
|
_ = self.popupErrorAsApiError(error)
|
|
}
|
|
}
|
|
} else {
|
|
self.showPopupDialog(title: .localized("Error Signing In To Verify Subscription"),
|
|
message: "\(error)",
|
|
acceptButton: .localizedOkay)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
@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? OldSignupViewController {
|
|
if activePlans.isEmpty {
|
|
vc.mode = .newSubscription
|
|
} else {
|
|
vc.mode = .upgrade(active: activePlans)
|
|
}
|
|
}
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
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 .postToTwitter:
|
|
return shareText + " @lockdown_hq"
|
|
default:
|
|
return shareText
|
|
}
|
|
} else {
|
|
return shareText
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Paywalling
|
|
extension HomeViewController: PaywallViewControllerCloseDelegate {
|
|
func didClosePaywall() {
|
|
guard UserDefaults.hasSeenLTO == false else { return }
|
|
|
|
showLTOButtonIfEligible()
|
|
}
|
|
|
|
private func showEnableNotifications() {
|
|
let enableNotificationsViewController = EnableNotificationsViewController()
|
|
enableNotificationsViewController.modalPresentationStyle = .overFullScreen
|
|
present(enableNotificationsViewController, animated: true)
|
|
}
|
|
|
|
private func showPaywallIfNoSubscription() {
|
|
guard BaseUserService.shared.user.currentSubscription == nil else { return }
|
|
guard BasePaywallService.shared.context == .normal else { return }
|
|
|
|
BasePaywallService.shared.showPaywall(on: self)
|
|
|
|
UserDefaults.hasSeenPaywallOnHomeScreen = true
|
|
}
|
|
}
|
|
|
|
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) -> Void)?
|
|
|
|
override var buttonAction: PopupDialogButton.PopupDialogButtonAction? {
|
|
if let onTap = onTap {
|
|
return { [weak self] in if let value = self { return onTap(value) } }
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
extension NEVPNStatus: CustomStringConvertible {
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .invalid:
|
|
return "invalid"
|
|
case .disconnected:
|
|
return "disconnected"
|
|
case .connecting:
|
|
return "connecting"
|
|
case .connected:
|
|
return "connected"
|
|
case .reasserting:
|
|
return "reasserting"
|
|
case .disconnecting:
|
|
return "disconnecting"
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension HomeViewController: AwesomeSpotlightViewDelegate {
|
|
func spotlightView(_ spotlightView: AwesomeSpotlightView, willNavigateToIndex index: Int) {
|
|
let accountTabSpotlightIndex = 7
|
|
|
|
if index == accountTabSpotlightIndex {
|
|
spotlightView.backgroundColor = .white.withAlphaComponent(0.2)
|
|
spotlightView.spotlightMaskColor = .clear.withAlphaComponent(0.8)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - LTO
|
|
extension HomeViewController: LTOViewControllerCloseDelegate, CountdownDisplayDelegate {
|
|
func didFinishCountdown() {
|
|
DDLogInfo("Removing lto button.")
|
|
UIView.animate(withDuration: 0.4, animations: {
|
|
self.ltoButton?.transform = .init(scaleX: 0.001, y: 0.001)
|
|
self.ltoButton?.titleLabel?.alpha = 0
|
|
}) { _ in
|
|
self.ltoButton?.removeFromSuperview()
|
|
self.ltoButton = nil
|
|
}
|
|
}
|
|
|
|
func didCloseLTOPaywall() {
|
|
guard let ltoButton else { return }
|
|
countdownDisplayService.startUpdating(button: ltoButton)
|
|
}
|
|
|
|
func showLTOButtonIfEligible() {
|
|
// To check for case when when user has redeemed code and has got a subscription.
|
|
// In this case, the button has already been created, but we should remove it and show nothing.
|
|
guard BaseUserService.shared.user.currentSubscription == nil else {
|
|
DDLogInfo("Removing lto button when user has redeemed code but button has been created.")
|
|
ltoButton?.removeFromSuperview()
|
|
ltoButton = nil
|
|
return
|
|
}
|
|
guard let ltoButton = self.ltoButton else { return }
|
|
|
|
countdownDisplayService.startUpdating(button: ltoButton)
|
|
countdownDisplayService.delegates.append(WeakObject(self))
|
|
|
|
DDLogInfo("Showing lto button.")
|
|
ltoButton.showAnimatedPress(duration: 0.4, withScale: 1.2) {
|
|
UserDefaults.hasSeenLTO = true
|
|
}
|
|
}
|
|
|
|
private func addLTOButtonIfEligible() {
|
|
guard UserDefaults.hasSeenLTO == false else { return }
|
|
guard LimitedTimeOffer() != nil else { return }
|
|
guard ltoButton == nil else { return }
|
|
|
|
DDLogInfo("Creating lto button")
|
|
ltoButton = UIButton()
|
|
|
|
guard let ltoButton else { return }
|
|
updateLtoButtonFrame()
|
|
|
|
ltoButton.addTarget(self, action: #selector(didTapLTOButton), for: .touchUpInside)
|
|
|
|
view.addSubview(ltoButton)
|
|
|
|
ltoButton.backgroundColor = .fromHex("#F93B58")
|
|
ltoButton.corners = .continuous(ltoButton.frame.height / 2)
|
|
|
|
ltoButton.titleLabel?.font = .boldLockdownFont(size: 18)
|
|
ltoButton.transform = .init(scaleX: 0, y: 0)
|
|
ltoButton.applyGradient(.ltoButtonOnHomePage, corners: ltoButton.corners)
|
|
}
|
|
|
|
private func updateLtoButtonFrame() {
|
|
guard let tabBarButton = (tabBarController as? MainTabBarController)?.accountTabBarButton else { return }
|
|
|
|
let buttonHeight: CGFloat = 64
|
|
let buttonWidth: CGFloat = 100
|
|
let buttonTrailingOffset: CGFloat = 16
|
|
let buttonBottomOffset: CGFloat = 50
|
|
ltoButton?.frame = .init(origin: .init(x: tabBarButton.frame.maxX - buttonWidth - buttonTrailingOffset,
|
|
y: view.frame.maxY - tabBarButton.frame.height - buttonBottomOffset - buttonHeight),
|
|
size: .init(width: buttonWidth, height: buttonHeight))
|
|
}
|
|
|
|
@objc private func didTapLTOButton() {
|
|
ltoButton?.showAnimatedPress { [weak self] in
|
|
guard let self else { return }
|
|
self.paywallService.showPaywall(on: self, forceSpecialOffer: true)
|
|
}
|
|
}
|
|
}
|