diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..4e5b18a --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,71 @@ +warning_threshold: 1 + +disabled_rules: + - unused_setter_value + - trailing_whitespace + - comment_spacing + - vertical_whitespace + - colon + - function_body_length + - cyclomatic_complexity + - weak_delegate + - multiple_closures_with_trailing_closure + - compiler_protocol_init + - todo + - trailing_comma + - notification_center_detachment + - identifier_name + - block_based_kvo + - empty_enum_arguments + - inclusive_language + +opt_in_rules: # some rules are only opt-in + #- control_statement + - empty_count + #- trailing_newline + #- colon + #- comma + - empty_string + - unused_declaration + - explicit_self + +included: # paths to include during linting. `--path` is ignored if present. + - LockdowniOS + +excluded: # paths to ignore during linting. Takes precedence over `included`. + - Pods + - lockdownTests + +line_length: + warning: 155 + ignores_comments: true + +force_try: + severity: warning + +type_body_length: + warning: 500 + error: 1000 + +file_length: + warning: 1200 + error: 1200 + +large_tuple: + warning: 7 + error: 10 + +function_parameter_count: + warning: 5 + +type_name: + min_length: 3 + max_length: 40 + +deployment_target: + iOS_deployment_target: 11.0 + +unused_declaration: + include_public_and_open: true + +reporter: "xcode" diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/ComparisonTableFirewallDark.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/ComparisonTableFirewallDark.pdf new file mode 100644 index 0000000..f4be6db Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/ComparisonTableFirewallDark.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Contents.json b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Contents.json new file mode 100644 index 0000000..3f10925 --- /dev/null +++ b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "filename" : "Firewall-1.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "Firewall.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ComparisonTableFirewallDark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Firewall-1.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Firewall-1.pdf new file mode 100644 index 0000000..3c2eacf Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Firewall-1.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Firewall.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Firewall.pdf new file mode 100644 index 0000000..3c2eacf Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewall.imageset/Firewall.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/ComparisonTableFirewallAndVpnDark.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/ComparisonTableFirewallAndVpnDark.pdf new file mode 100644 index 0000000..ec6678c Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/ComparisonTableFirewallAndVpnDark.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/Contents.json b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/Contents.json new file mode 100644 index 0000000..c01f4bf --- /dev/null +++ b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "filename" : "FirewallAndVPN.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "FirewallAndVPN-1.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "ComparisonTableFirewallAndVpnDark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/FirewallAndVPN-1.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/FirewallAndVPN-1.pdf new file mode 100644 index 0000000..cf180f3 Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/FirewallAndVPN-1.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/FirewallAndVPN.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/FirewallAndVPN.pdf new file mode 100644 index 0000000..cf180f3 Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableFirewallAndVPN.imageset/FirewallAndVPN.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/ComparisonTableProDark.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/ComparisonTableProDark.pdf new file mode 100644 index 0000000..21bebd3 Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/ComparisonTableProDark.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Contents.json b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Contents.json new file mode 100644 index 0000000..9569be0 --- /dev/null +++ b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "filename" : "Table.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "Table 1.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Table 2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/PRO-1.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/PRO-1.pdf new file mode 100644 index 0000000..2dd273a Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/PRO-1.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/PRO.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/PRO.pdf new file mode 100644 index 0000000..2dd273a Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/PRO.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table 1.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table 1.pdf new file mode 100644 index 0000000..9414ffc Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table 1.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table 2.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table 2.pdf new file mode 100644 index 0000000..056b923 Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table 2.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table.pdf new file mode 100644 index 0000000..9414ffc Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTablePRO.imageset/Table.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Contents.json b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Contents.json new file mode 100644 index 0000000..b2c13f0 --- /dev/null +++ b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "filename" : "Table 2.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "Table.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Table 1.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table 1.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table 1.pdf new file mode 100644 index 0000000..089d645 Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table 1.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table 2.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table 2.pdf new file mode 100644 index 0000000..8287a4e Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table 2.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table.pdf b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table.pdf new file mode 100644 index 0000000..8287a4e Binary files /dev/null and b/Assets.xcassets/Complex Paywall Table Images/ComparisonTableVPN.imageset/Table.pdf differ diff --git a/Assets.xcassets/Complex Paywall Table Images/Contents.json b/Assets.xcassets/Complex Paywall Table Images/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Complex Paywall Table Images/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Lock.imageset/Contents.json b/Assets.xcassets/Lock.imageset/Contents.json new file mode 100644 index 0000000..a8bf32a --- /dev/null +++ b/Assets.xcassets/Lock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Lock.imageset/Lock.pdf b/Assets.xcassets/Lock.imageset/Lock.pdf new file mode 100644 index 0000000..87608e3 Binary files /dev/null and b/Assets.xcassets/Lock.imageset/Lock.pdf differ diff --git a/Assets.xcassets/checkmark.imageset/Contents.json b/Assets.xcassets/checkmark.imageset/Contents.json new file mode 100644 index 0000000..e20b6aa --- /dev/null +++ b/Assets.xcassets/checkmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "check.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/checkmark.imageset/check.pdf b/Assets.xcassets/checkmark.imageset/check.pdf new file mode 100644 index 0000000..1bf4335 Binary files /dev/null and b/Assets.xcassets/checkmark.imageset/check.pdf differ diff --git a/Cartfile b/Cartfile index 5c46a67..718d500 100755 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "https://github.com/confirmedcode/NEKit.git" "master" \ No newline at end of file +github "https://github.com/confirmedcode/NEKit.git" "c615bf40940bb9b42f3c10cf1e887bfb921544c8" \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved index e6e0554..ac0a051 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ -github "CocoaLumberjack/CocoaLumberjack" "3.7.2" +github "CocoaLumberjack/CocoaLumberjack" "3.8.0" github "behrang/YamlSwift" "3.4.3" github "confirmedcode/NEKit" "c615bf40940bb9b42f3c10cf1e887bfb921544c8" github "robbiehanson/CocoaAsyncSocket" "7.6.5" diff --git a/ClientModels.swift b/ClientModels.swift index 552c536..ff86eb4 100644 --- a/ClientModels.swift +++ b/ClientModels.swift @@ -7,6 +7,7 @@ // import Foundation +import UIKit struct IP: Codable { let ip: String @@ -25,7 +26,7 @@ struct SubscriptionEvent: Codable { let message: String } -struct Subscription: Codable { +public struct Subscription: Codable { let planType: PlanType let receiptId: String let expirationDate: String @@ -44,7 +45,36 @@ struct Subscription: Codable { static let monthly = PlanType(rawValue: "ios-monthly") static let annual = PlanType(rawValue: "ios-annual") static let proMonthly = PlanType(rawValue: "all-monthly") - static let proAnnual = PlanType(rawValue: "all-annual") + static let proAnnual = PlanType(rawValue: "all-annual") + static let proAnnualLTO = PlanType(rawValue: "all-annual") + } + + var correspondingProductGroup: AppStoreProductGroup { + switch planType { + case .monthly, .annual: + return .firewallAndVpn + case .proMonthly, .proAnnual, .proAnnualLTO: + return .pro + default: + return .firewallAndVpn + } + } + + public var hasVPN: Bool { [.firewallAndVpn, .pro].contains(correspondingProductGroup) } + + var correspondingPeriodUnit: SubscriptionOfferPeriodUnit { + switch planType { + case .monthly, .proMonthly: + return .month + case .annual, .proAnnual, .proAnnualLTO: + return .year + default: + return .year + } + } + + func isSubscription(in group: AppStoreProductGroup, of period: SubscriptionOfferPeriodUnit) -> Bool { + return group == correspondingProductGroup && period == correspondingPeriodUnit } } diff --git a/Environment.swift b/Environment.swift index 3c6c4d7..b1215a3 100644 --- a/Environment.swift +++ b/Environment.swift @@ -9,11 +9,35 @@ import Foundation import CocoaLumberjackSwift -let vpnSourceID = "-111818" //getEnvironmentVariable(key: "vpnSourceID", default: "-111818") -let vpnDomain = "confirmedvpn.com" //getEnvironmentVariable(key: "vpnDomain", default: "confirmedvpn.com") +// getEnvironmentVariable(key: "vpnSourceID", default: "-111818") +let vpnSourceID: String = { + #if DEBUG + return "-111618" + #else + return "-111818" + #endif +}() + +// getEnvironmentVariable(key: "vpnDomain", default: "confirmedvpn.com") +let vpnDomain: String = { + #if DEBUG + return "trusty-ap.science" + #else + return "confirmedvpn.com" + #endif +}() + let vpnRemoteIdentifier = "www" + vpnSourceID + "." + vpnDomain -let mainDomain = "confirmedvpn.com" //getEnvironmentVariable(key: "mainDomain", default: "confirmedvpn.com") +// getEnvironmentVariable(key: "mainDomain", default: "confirmedvpn.com") +let mainDomain: String = { + #if DEBUG + return "trusty-ap.science" + #else + return "confirmedvpn.com" + #endif +}() + let mainURL = "https://www." + mainDomain let testFirewallDomain = "example.com" diff --git a/Lockdown Firewall Today/FirewallTodayViewController.swift b/Lockdown Firewall Today/FirewallTodayViewController.swift index e48f51a..9f33dc3 100644 --- a/Lockdown Firewall Today/FirewallTodayViewController.swift +++ b/Lockdown Firewall Today/FirewallTodayViewController.swift @@ -85,7 +85,7 @@ class FirewallTodayViewController: UIViewController, NCWidgetProviding { @objc func updateMetrics() { DispatchQueue.main.async { - self.blockedTodayLabel.text = getDayMetricsString() + NSLocalizedString(" Blocked Today", comment: "refers to number of connections blocked today, as in '34 Blocked Today'") + self.blockedTodayLabel.text = getDayMetricsString() + .localized(" Blocked Today", comment: "refers to number of connections blocked today, as in '34 Blocked Today'") } } @@ -132,23 +132,23 @@ class FirewallTodayViewController: UIViewController, NCWidgetProviding { } func setFirewallButtonConnected() { - firewallStatusLabel.text = NSLocalizedString("Firewall Active", comment: "") + firewallStatusLabel.text = .localized("Firewall Active") toggleFirewall?.tintColor = .tunnelsBlue toggleFirewall.layer.borderColor = UIColor.tunnelsBlue.cgColor } func setFirewallButtonDisconnected() { - firewallStatusLabel.text = NSLocalizedString("Firewall Not Active", comment: "") + firewallStatusLabel.text = .localized("Firewall Not Active") toggleFirewall?.tintColor = UIColor.darkGray toggleFirewall?.layer.borderColor = UIColor.darkGray.cgColor } func setFirewallButtonConnecting() { - firewallStatusLabel.text = NSLocalizedString("Activating", comment: "") + firewallStatusLabel.text = .localized("Activating") } func setFirewallButtonDisconnecting() { - firewallStatusLabel.text = NSLocalizedString("Deactivating", comment: "") + firewallStatusLabel.text = .localized("Deactivating") } // MARK: - Helpers diff --git a/Lockdown Firewall Today/ar.lproj/MainInterface.strings b/Lockdown Firewall Today/ar.lproj/MainInterface.strings new file mode 100644 index 0000000..607d5d5 --- /dev/null +++ b/Lockdown Firewall Today/ar.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "Firewall Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "-"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "-"; diff --git a/Lockdown Firewall Today/de.lproj/MainInterface.strings b/Lockdown Firewall Today/de.lproj/MainInterface.strings index 04880b0..607d5d5 100644 --- a/Lockdown Firewall Today/de.lproj/MainInterface.strings +++ b/Lockdown Firewall Today/de.lproj/MainInterface.strings @@ -1,3 +1,4 @@ + /* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ "1pC-80-fVF.text" = "Firewall Active"; diff --git a/Lockdown Firewall Today/ru.lproj/MainInterface.strings b/Lockdown Firewall Today/ru.lproj/MainInterface.strings new file mode 100644 index 0000000..607d5d5 --- /dev/null +++ b/Lockdown Firewall Today/ru.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "Firewall Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "-"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "-"; diff --git a/Lockdown Firewall Today/vi.lproj/MainInterface.strings b/Lockdown Firewall Today/vi.lproj/MainInterface.strings new file mode 100644 index 0000000..607d5d5 --- /dev/null +++ b/Lockdown Firewall Today/vi.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "Firewall Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "-"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "-"; diff --git a/Lockdown Firewall Today/zh-Hans.lproj/MainInterface.strings b/Lockdown Firewall Today/zh-Hans.lproj/MainInterface.strings new file mode 100644 index 0000000..607d5d5 --- /dev/null +++ b/Lockdown Firewall Today/zh-Hans.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "Firewall Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "-"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "-"; diff --git a/Lockdown Firewall Today/zh-Hant.lproj/MainInterface.strings b/Lockdown Firewall Today/zh-Hant.lproj/MainInterface.strings new file mode 100644 index 0000000..607d5d5 --- /dev/null +++ b/Lockdown Firewall Today/zh-Hant.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "Firewall Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "-"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "-"; diff --git a/Lockdown Tunnel/PacketTunnelProvider.swift b/Lockdown Tunnel/PacketTunnelProvider.swift index 7b6ea26..4c85f13 100644 --- a/Lockdown Tunnel/PacketTunnelProvider.swift +++ b/Lockdown Tunnel/PacketTunnelProvider.swift @@ -12,13 +12,13 @@ var latestBlockedDomains = getAllBlockedDomains() class PacketTunnelProvider: NEPacketTunnelProvider { - let proxyServerPort: UInt16 = 9090; - let proxyServerAddress = "127.0.0.1"; + let proxyServerPort: UInt16 = 9090 + let proxyServerAddress = "127.0.0.1" var proxyServer: GCDHTTPProxyServer! - //MARK: - OVERRIDES + // MARK: - OVERRIDES - override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { + override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { if proxyServer != nil { proxyServer.stop() } @@ -28,19 +28,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider { self.connect(options: options, completionHandler: completionHandler) } - private func connect(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { + private func connect(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress) settings.mtu = NSNumber(value: 1500) let proxySettings = NEProxySettings() - proxySettings.httpEnabled = true; + proxySettings.httpEnabled = true proxySettings.httpServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort)) - proxySettings.httpsEnabled = true; + proxySettings.httpsEnabled = true proxySettings.httpsServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort)) - proxySettings.excludeSimpleHostnames = false; + proxySettings.excludeSimpleHostnames = false proxySettings.exceptionList = [] - var combined: Array = getAllBlockedDomains() + [testFirewallDomain] // probably not blocking whitelisted so this is safe, example.com is used to ensure firewall is still working - combined = combined + getAllWhitelistedDomains() + // probably not blocking whitelisted so this is safe, example.com is used to ensure firewall is still working + var combined: [String] = getAllBlockedDomains() + [testFirewallDomain] + combined += getAllWhitelistedDomains() if combined.count <= 1 { #if DEBUG debugLog("PTP: COMBINED BLOCK LIST IS INVALID, LIKELY JUST RESTARTED") @@ -52,7 +53,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider { proxySettings.matchDomains = combined settings.dnsSettings = NEDNSSettings(servers: ["127.0.0.1"]) - settings.proxySettings = proxySettings; + settings.proxySettings = proxySettings RawSocketFactory.TunnelProvider = self self.setTunnelNetworkSettings(settings, completionHandler: { error in diff --git a/LockdownFirewallWidget/LockdownFirewallWidget.swift b/LockdownFirewallWidget/LockdownFirewallWidget.swift index 2737a75..eb94317 100644 --- a/LockdownFirewallWidget/LockdownFirewallWidget.swift +++ b/LockdownFirewallWidget/LockdownFirewallWidget.swift @@ -64,9 +64,9 @@ struct LockdownFirewallWidgetEntryView : View { ) .padding(EdgeInsets(top: 12, leading: 0, bottom: 2, trailing: 0)) if entry.isFirewallEnabled { - StatusLabel(text: NSLocalizedString("FIREWALL ON", comment: ""), color: .confirmedBlue) + StatusLabel(text: .localized("FIREWALL ON"), color: .confirmedBlue) } else { - StatusLabel(text: NSLocalizedString("FIREWALL OFF", comment: ""), color: .flatRed) + StatusLabel(text: .localized("FIREWALL OFF"), color: .flatRed) } if entry.size.height < 160 { Spacer().frame(minHeight: 4) @@ -77,7 +77,7 @@ struct LockdownFirewallWidgetEntryView : View { VStack(spacing: 0) { Text(entry.dayMetricsString) .font(.system(size: 21, weight: .semibold)) - Text(NSLocalizedString("Blocked Today", comment: "")) + Text(String.localized("Blocked Today")) .font(.system(size: 12, weight: .medium)) } .padding(.bottom, 12) @@ -141,9 +141,9 @@ struct LockdownVPNWidgetEntryView : View { ) .padding(EdgeInsets(top: 12, leading: 0, bottom: 2, trailing: 0)) if entry.isVPNEnabled { - StatusLabel(text: NSLocalizedString("Tunnel On", comment: "").uppercased(), color: .confirmedBlue) + StatusLabel(text: .localized("Tunnel On").uppercased(), color: .confirmedBlue) } else { - StatusLabel(text: NSLocalizedString("Tunnel Off", comment: "").uppercased(), color: .flatRed) + StatusLabel(text: .localized("Tunnel Off").uppercased(), color: .flatRed) } if entry.size.height < 160 { Spacer().frame(minHeight: 4) @@ -216,7 +216,7 @@ struct LockdownVPNWidget: Widget { } } ) - .configurationDisplayName("Secure Tunnel") + .configurationDisplayName("Secure Tunnel VPN") .supportedFamilies([.systemSmall]) } } diff --git a/LockdownIntents/DisableVPNIntentHandler.swift b/LockdownIntents/DisableVPNIntentHandler.swift new file mode 100644 index 0000000..5d3f5db --- /dev/null +++ b/LockdownIntents/DisableVPNIntentHandler.swift @@ -0,0 +1,33 @@ +// +// DisableVPNIntentHandler.swift +// LockdownIntents +// +// Created by Alexander Parshakov on 12/7/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Intents + +final class DisableVPNIntentHandler: NSObject, DisableVPNIntentHandling { + + func resolve(intent: DisableVPNIntent, completion: @escaping (DisableVPNIntentResponse) -> Void) { + completion(DisableVPNIntentResponse(code: .success, userActivity: nil)) + } + + func confirm(intent: DisableVPNIntent, completion: @escaping (DisableVPNIntentResponse) -> Void) { + completion(DisableVPNIntentResponse(code: .ready, userActivity: nil)) + } + + func handle(intent: DisableVPNIntent, completion: @escaping (DisableVPNIntentResponse) -> Void) { + // if the VPN is enabled, disable it, send the success code, and update LatestKnowledge + if LatestKnowledge.isVPNEnabled { + VPNController.shared.setEnabled(false) + completion(DisableVPNIntentResponse(code: .success, userActivity: nil)) + LatestKnowledge.isVPNEnabled = false + } else { + // otherwise, send the failure code because VPN doesn't need to be disabled + // using the failure response code below sends the predefined response + completion(DisableVPNIntentResponse(code: .failure, userActivity: nil)) + } + } +} diff --git a/LockdownIntents/EnableVPNIntentHandler.swift b/LockdownIntents/EnableVPNIntentHandler.swift new file mode 100644 index 0000000..0d9b9f9 --- /dev/null +++ b/LockdownIntents/EnableVPNIntentHandler.swift @@ -0,0 +1,48 @@ +// +// EnableVPNIntentHandler.swift +// LockdownIntents +// +// Created by Alexander Parshakov on 12/7/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Intents + +final class EnableVPNIntentHandler: NSObject, EnableVPNIntentHandling { + + func resolve(intent: EnableVPNIntent, completion: @escaping (EnableVPNIntentResponse) -> Void) { + completion(EnableVPNIntentResponse(code: .success, userActivity: nil)) + } + + func confirm(intent: EnableVPNIntent, completion: @escaping (EnableVPNIntentResponse) -> Void) { + completion(EnableVPNIntentResponse(code: .ready, userActivity: nil)) + } + + func handle(intent: EnableVPNIntent, completion: @escaping (EnableVPNIntentResponse) -> Void) { + if LatestKnowledge.isVPNEnabled { + completion(EnableVPNIntentResponse(code: .failure, userActivity: nil)) + } else { + BaseUserService.shared.updateUserSubscription { subscription in + guard subscription?.hasVPN == true else { + completion(EnableVPNIntentResponse(code: .notVpnSubscriber, userActivity: nil)) + return + } + + VPNController.shared.setEnabled(true) + + // wait 0.5s + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + // force a network check + reachability?.whenReachable = { _ in + // do nothing + } + // update Latest Knowledge + LatestKnowledge.isVPNEnabled = true + // send the success code + completion(EnableVPNIntentResponse(code: .success, userActivity: nil)) + } + } + } + } +} + diff --git a/LockdownIntents/Info.plist b/LockdownIntents/Info.plist new file mode 100644 index 0000000..db63e89 --- /dev/null +++ b/LockdownIntents/Info.plist @@ -0,0 +1,25 @@ + + + + + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsRestrictedWhileProtectedDataUnavailable + + IntentsSupported + + DisableVPNIntent + EnableVPNIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/LockdownIntents/IntentHandler.swift b/LockdownIntents/IntentHandler.swift new file mode 100644 index 0000000..aedaf85 --- /dev/null +++ b/LockdownIntents/IntentHandler.swift @@ -0,0 +1,22 @@ +// +// IntentHandler.swift +// LockdownIntents +// +// Created by Alexander Parshakov on 12/7/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Intents + +final class IntentHandler: INExtension { + + override func handler(for intent: INIntent) -> Any { + if intent is EnableVPNIntent { + return EnableVPNIntentHandler() + } else if intent is DisableVPNIntent { + return DisableVPNIntentHandler() + } else { + fatalError("Unknown intent type: \(intent)") + } + } +} diff --git a/LockdownIntents/LockdownIntents.entitlements b/LockdownIntents/LockdownIntents.entitlements new file mode 100644 index 0000000..8f68fd4 --- /dev/null +++ b/LockdownIntents/LockdownIntents.entitlements @@ -0,0 +1,24 @@ + + + + + com.apple.developer.networking.networkextension + + app-proxy-provider + content-filter-provider + packet-tunnel-provider + + com.apple.developer.networking.vpn.api + + allow-vpn + + com.apple.security.application-groups + + group.com.confirmed + + keychain-access-groups + + $(AppIdentifierPrefix)com.confirmed.lockdown + + + diff --git a/LockdowniOS.xcodeproj/project.pbxproj b/LockdowniOS.xcodeproj/project.pbxproj index 0e07d37..e0af2b7 100644 --- a/LockdowniOS.xcodeproj/project.pbxproj +++ b/LockdowniOS.xcodeproj/project.pbxproj @@ -7,12 +7,179 @@ objects = { /* Begin PBXBuildFile section */ + 0B087B07291C184C006953DA /* EnableNotificationsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B087B06291C184C006953DA /* EnableNotificationsViewController.xib */; }; + 0B087B09291C1852006953DA /* EnableNotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B087B08291C1852006953DA /* EnableNotificationsViewController.swift */; }; + 0B0CCD432901B25300F5C632 /* Onboarding_launch_firewall.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 0B0CCD422901B25300F5C632 /* Onboarding_launch_firewall.mp4 */; }; + 0B0CCD452901B28200F5C632 /* Onboarding_showcase_vpn.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 0B0CCD442901B28100F5C632 /* Onboarding_showcase_vpn.mp4 */; }; + 0B1B6AC4293A9977003DCF72 /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AC3293A9977003DCF72 /* UserService.swift */; }; + 0B1B6AC6293A99C1003DCF72 /* EmailComposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AC5293A99C1003DCF72 /* EmailComposable.swift */; }; + 0B1B6AC9293A9AF1003DCF72 /* DeleteMyAccountViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B1B6AC7293A9AF1003DCF72 /* DeleteMyAccountViewController.xib */; }; + 0B1B6ACA293A9AF1003DCF72 /* DeleteMyAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AC8293A9AF1003DCF72 /* DeleteMyAccountViewController.swift */; }; + 0B1B6ACD293A9B1E003DCF72 /* TablePaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B1B6ACB293A9B1E003DCF72 /* TablePaywallViewController.xib */; }; + 0B1B6ACE293A9B1E003DCF72 /* TablePaywallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6ACC293A9B1E003DCF72 /* TablePaywallViewController.swift */; }; + 0B1B6AD8293AA464003DCF72 /* PaywallTableFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */; }; + 0B1B6ADA293DFB50003DCF72 /* TablePaywallFeatureRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD9293DFB50003DCF72 /* TablePaywallFeatureRowView.swift */; }; + 0B1C8ED228E44A8800E9CA57 /* ProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1C8ED128E44A8800E9CA57 /* ProgressView.swift */; }; + 0B1FFD5C293E15AA0027341C /* UIStackView+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1FFD5B293E15AA0027341C /* UIStackView+Ext.swift */; }; + 0B1FFD63293E16BF0027341C /* Separator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1FFD62293E16BF0027341C /* Separator.swift */; }; + 0B2205BE291416D4002F2562 /* SignUpViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B2205BD291416D4002F2562 /* SignUpViewController.xib */; }; + 0B2205C5291416DE002F2562 /* SignUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2205C4291416DE002F2562 /* SignUpViewController.swift */; }; + 0B2205C729142601002F2562 /* FloatingTextInputTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2205C629142601002F2562 /* FloatingTextInputTextField.swift */; }; + 0B2205CA29142A84002F2562 /* TextBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2205C929142A84002F2562 /* TextBox.swift */; }; + 0B2205CC29142AA2002F2562 /* TextInputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2205CB29142AA2002F2562 /* TextInputState.swift */; }; + 0B2205CE29142ACD002F2562 /* TextBoxLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2205CD29142ACD002F2562 /* TextBoxLabel.swift */; }; + 0B2205D02915B093002F2562 /* UIViewController+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B2205CF2915B093002F2562 /* UIViewController+Ext.swift */; }; + 0B312AD42910411E00A218FB /* OnboardingPageContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B312AD32910411E00A218FB /* OnboardingPageContent.swift */; }; + 0B363A2728F56B6600E881BF /* Onboarding_blocklists.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 0B363A2628F56B6600E881BF /* Onboarding_blocklists.mp4 */; }; + 0B3AB7EA2934EFAD00B254C0 /* UIView+Corners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB7E92934EFAD00B254C0 /* UIView+Corners.swift */; }; + 0B3AB80A293508DD00B254C0 /* AppStoreProductGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */; }; + 0B3AB810293508DE00B254C0 /* AppStoreProductGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */; }; + 0B3AB811293508DF00B254C0 /* AppStoreProductGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */; }; + 0B3AB812293508DF00B254C0 /* AppStoreProductGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */; }; + 0B3AB813293508E000B254C0 /* AppStoreProductGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */; }; + 0B3AB814293508E000B254C0 /* AppStoreProductGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */; }; + 0B3AB8172935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */; }; + 0B3AB8182935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */; }; + 0B3AB8192935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */; }; + 0B3AB81A2935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */; }; + 0B3AB81B2935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */; }; + 0B3AB81C2935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */; }; + 0B3AB81D2935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */; }; + 0B3AB83C29354B9100B254C0 /* LockdownGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB83B29354B9100B254C0 /* LockdownGradient.swift */; }; + 0B3E5FCD28BFD5A500BAB0E7 /* AppStoreProductGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */; }; + 0B4B8B0029412BD50060C072 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B4B8AFF29412BD50060C072 /* Intents.framework */; }; + 0B4B8B0329412BD50060C072 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B4B8B0229412BD50060C072 /* IntentHandler.swift */; }; + 0B4B8B1829412BD50060C072 /* LockdownIntents.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0B4B8AFE29412BD50060C072 /* LockdownIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 0B4B8B2029412C9C0060C072 /* IntentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B4B8B1F29412C9C0060C072 /* IntentService.swift */; }; + 0B4B8B242941352A0060C072 /* DisableVPNIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B4B8B232941352A0060C072 /* DisableVPNIntentHandler.swift */; }; + 0B4B8B26294135350060C072 /* EnableVPNIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B4B8B25294135350060C072 /* EnableVPNIntentHandler.swift */; }; + 0B4B8B2F294136340060C072 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD8C285A5A120023B555 /* UserDefaults.swift */; }; + 0B4B8B4A2941387F0060C072 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D94AB1122FE3A460012B0DE /* Environment.swift */; }; + 0B4B8B512941388B0060C072 /* VPNRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C952521ED5E009D8269 /* VPNRegion.swift */; }; + 0B4E082628C8EBE000129620 /* SplashscreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B4E082528C8EBE000129620 /* SplashscreenViewController.swift */; }; + 0B4E082828C8EBED00129620 /* SplashscreenViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B4E082728C8EBED00129620 /* SplashscreenViewController.xib */; }; + 0B4EF2A0293A98F600A32554 /* LockdownUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B4EF29F293A98F600A32554 /* LockdownUser.swift */; }; + 0B564B68294368C00082C27C /* EmailAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B564B67294368C00082C27C /* EmailAddress.swift */; }; + 0B6DC540292E402A00DCB8E8 /* Font+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B6DC53F292E402A00DCB8E8 /* Font+Ext.swift */; }; + 0B80623F2860A1DB003EFF32 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B80623E2860A1DB003EFF32 /* String+Localized.swift */; }; + 0B8445472940AF2700FA2F35 /* ConnectivityServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCD08A52940AE8600DC6DD1 /* ConnectivityServiceTests.swift */; }; + 0B84454E2940BDE300FA2F35 /* EmailValidatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B84454D2940BDE300FA2F35 /* EmailValidatable.swift */; }; + 0B8445512940CE4200FA2F35 /* EmailValidatableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B84454F2940CE2A00FA2F35 /* EmailValidatableTests.swift */; }; + 0B860F6228FE9D06006B0AA7 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; + 0B8DBDA8286482D500831AC4 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B80623E2860A1DB003EFF32 /* String+Localized.swift */; }; + 0B8DBDAE286482D500831AC4 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B80623E2860A1DB003EFF32 /* String+Localized.swift */; }; + 0B8DBDAF286482D600831AC4 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B80623E2860A1DB003EFF32 /* String+Localized.swift */; }; + 0B8DBDB0286482D600831AC4 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B80623E2860A1DB003EFF32 /* String+Localized.swift */; }; + 0B8DBDB1286482D700831AC4 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B80623E2860A1DB003EFF32 /* String+Localized.swift */; }; + 0B8DBDB2286482D700831AC4 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B80623E2860A1DB003EFF32 /* String+Localized.swift */; }; + 0B99BA212948EB3E00A47A45 /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AC3293A9977003DCF72 /* UserService.swift */; }; + 0B99BA272948EB4C00A47A45 /* LockdownUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B4EF29F293A98F600A32554 /* LockdownUser.swift */; }; + 0B99BA282948EB5000A47A45 /* ClientModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F3022F190AE0017740D /* ClientModels.swift */; }; + 0B99BA292948EB5800A47A45 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AF22FC14CC00DE189F /* Shared.swift */; }; + 0B99BA2A2948EB6500A47A45 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F2D22F190720017740D /* Client.swift */; }; + 0B99BA2B2948EB7300A47A45 /* AppStoreProductGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */; }; + 0B99BA2C2948EB7B00A47A45 /* PaywallTableFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */; }; + 0B99BA2D2948EB8500A47A45 /* SubscriptionOfferPeriodUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */; }; + 0B99BA2E2948EBCC00A47A45 /* AccountUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4D9BBA252C8748004175EA /* AccountUI.swift */; }; + 0B99BA322948EDFA00A47A45 /* VPNController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18921B77C80008A9322 /* VPNController.swift */; }; + 0B99BA332948FE6300A47A45 /* LockdownIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA362948FE6300A47A45 /* LockdownIntents.intentdefinition */; }; + 0B99BA342948FE6300A47A45 /* LockdownIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA362948FE6300A47A45 /* LockdownIntents.intentdefinition */; }; + 0B99BA4C294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */; }; + 0B99BA4D294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */; }; + 0B99BA4E294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */; }; + 0B99BA4F294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */; }; + 0B99BA50294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */; }; + 0B99BA51294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */; }; + 0B99BA52294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */; }; + 0B99BA53294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */; }; + 0B99BA58294A266800A47A45 /* ChristmasPaywallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B99BA57294A266800A47A45 /* ChristmasPaywallViewController.swift */; }; + 0B99BA61294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */; }; + 0B99BA62294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */; }; + 0B99BA63294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */; }; + 0B99BA64294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */; }; + 0B99BA65294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */; }; + 0B99BA66294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */; }; + 0B99BA67294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */; }; + 0B99BA68294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */; }; + 0B9D69CB28E1AB2600A3EFCE /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B9D69CA28E1AB2600A3EFCE /* OnboardingViewController.swift */; }; + 0B9D69CD28E1AB3100A3EFCE /* OnboardingViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0B9D69CC28E1AB3100A3EFCE /* OnboardingViewController.xib */; }; + 0B9D69D028E1C0E700A3EFCE /* OnboardingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B9D69CF28E1C0E700A3EFCE /* OnboardingPage.swift */; }; + 0B9D69D528E1C1C700A3EFCE /* Onboarding_installation.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 0B9D69D428E1C1C700A3EFCE /* Onboarding_installation.mp4 */; }; + 0BA24F71293DFE28007FEA9B /* TablePaywallHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0BA24F70293DFE28007FEA9B /* TablePaywallHeaderView.xib */; }; + 0BA24F73293DFE3B007FEA9B /* TablePaywallHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA24F72293DFE3B007FEA9B /* TablePaywallHeaderView.swift */; }; + 0BA77E2128E44FB7007ECE45 /* Onboarding_notifications.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 0BA77E2028E44FB7007ECE45 /* Onboarding_notifications.mp4 */; }; + 0BA77E2428E45C94007ECE45 /* WeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA77E2328E45C94007ECE45 /* WeakObject.swift */; }; + 0BA77E2628E45F73007ECE45 /* ArrayRestrictable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA77E2528E45F73007ECE45 /* ArrayRestrictable.swift */; }; + 0BA77E2828E46F61007ECE45 /* OnboardingConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA77E2728E46F61007ECE45 /* OnboardingConfiguration.swift */; }; + 0BB39F302948AD7A002C9E57 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B80623E2860A1DB003EFF32 /* String+Localized.swift */; }; + 0BB5629829437C1B00EEE4B5 /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB5629729437C1B00EEE4B5 /* UserDefault.swift */; }; + 0BBEEF402948C3C600F4405E /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AF22FC14CC00DE189F /* Shared.swift */; }; + 0BBEEF422948C3D700F4405E /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AF22FC14CC00DE189F /* Shared.swift */; }; + 0BBEEF602948C7E700F4405E /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AF22FC14CC00DE189F /* Shared.swift */; }; + 0BCD08A0293FDCF400DC6DD1 /* ConnectivityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCD089F293FDCF400DC6DD1 /* ConnectivityService.swift */; }; + 0BCD08A3293FDD0A00DC6DD1 /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCD08A2293FDD0A00DC6DD1 /* ConnectionState.swift */; }; + 0BDBDDE7293E287C009A1184 /* PaywallTableFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */; }; + 0BDBDDED293E287C009A1184 /* PaywallTableFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */; }; + 0BDBDDEE293E2880009A1184 /* PaywallTableFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */; }; + 0BDBDDEF293E2881009A1184 /* PaywallTableFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */; }; + 0BDBDDF0293E2881009A1184 /* PaywallTableFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */; }; + 0BDBDDF1293E2882009A1184 /* PaywallTableFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */; }; + 0BE113FA293A6B220070DC51 /* CALayer+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE113F9293A6B220070DC51 /* CALayer+Ext.swift */; }; + 0BE113FD293A94930070DC51 /* TablePaywallFeatureRowView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0BE113FC293A94930070DC51 /* TablePaywallFeatureRowView.xib */; }; + 0BE9DD69285A59710023B555 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD68285A59710023B555 /* OneTimeActions.swift */; }; + 0BE9DD6A285A59710023B555 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD68285A59710023B555 /* OneTimeActions.swift */; }; + 0BE9DD6B285A59710023B555 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD68285A59710023B555 /* OneTimeActions.swift */; }; + 0BE9DD6C285A59710023B555 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD68285A59710023B555 /* OneTimeActions.swift */; }; + 0BE9DD6D285A59710023B555 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD68285A59710023B555 /* OneTimeActions.swift */; }; + 0BE9DD6F285A59930023B555 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD6E285A59930023B555 /* BlockDayLog.swift */; }; + 0BE9DD70285A59930023B555 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD6E285A59930023B555 /* BlockDayLog.swift */; }; + 0BE9DD71285A59930023B555 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD6E285A59930023B555 /* BlockDayLog.swift */; }; + 0BE9DD72285A59930023B555 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD6E285A59930023B555 /* BlockDayLog.swift */; }; + 0BE9DD73285A59930023B555 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD6E285A59930023B555 /* BlockDayLog.swift */; }; + 0BE9DD75285A59B70023B555 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD74285A59B70023B555 /* FirewallUtilities.swift */; }; + 0BE9DD76285A59B70023B555 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD74285A59B70023B555 /* FirewallUtilities.swift */; }; + 0BE9DD77285A59B70023B555 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD74285A59B70023B555 /* FirewallUtilities.swift */; }; + 0BE9DD78285A59B70023B555 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD74285A59B70023B555 /* FirewallUtilities.swift */; }; + 0BE9DD79285A59B70023B555 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD74285A59B70023B555 /* FirewallUtilities.swift */; }; + 0BE9DD7B285A59E30023B555 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD7A285A59E30023B555 /* Metrics.swift */; }; + 0BE9DD7C285A59E30023B555 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD7A285A59E30023B555 /* Metrics.swift */; }; + 0BE9DD7D285A59E30023B555 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD7A285A59E30023B555 /* Metrics.swift */; }; + 0BE9DD7E285A59E30023B555 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD7A285A59E30023B555 /* Metrics.swift */; }; + 0BE9DD7F285A59E30023B555 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD7A285A59E30023B555 /* Metrics.swift */; }; + 0BE9DD81285A59F20023B555 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD80285A59F20023B555 /* PushNotifications.swift */; }; + 0BE9DD82285A59F20023B555 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD80285A59F20023B555 /* PushNotifications.swift */; }; + 0BE9DD83285A59F20023B555 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD80285A59F20023B555 /* PushNotifications.swift */; }; + 0BE9DD84285A59F20023B555 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD80285A59F20023B555 /* PushNotifications.swift */; }; + 0BE9DD85285A59F20023B555 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD80285A59F20023B555 /* PushNotifications.swift */; }; + 0BE9DD87285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD86285A5A070023B555 /* PushNotificationsAuthorization.swift */; }; + 0BE9DD88285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD86285A5A070023B555 /* PushNotificationsAuthorization.swift */; }; + 0BE9DD89285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD86285A5A070023B555 /* PushNotificationsAuthorization.swift */; }; + 0BE9DD8A285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD86285A5A070023B555 /* PushNotificationsAuthorization.swift */; }; + 0BE9DD8B285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD86285A5A070023B555 /* PushNotificationsAuthorization.swift */; }; + 0BE9DD8D285A5A120023B555 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD8C285A5A120023B555 /* UserDefaults.swift */; }; + 0BE9DD8E285A5A120023B555 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD8C285A5A120023B555 /* UserDefaults.swift */; }; + 0BE9DD8F285A5A120023B555 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD8C285A5A120023B555 /* UserDefaults.swift */; }; + 0BE9DD90285A5A120023B555 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD8C285A5A120023B555 /* UserDefaults.swift */; }; + 0BE9DD91285A5A120023B555 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD8C285A5A120023B555 /* UserDefaults.swift */; }; + 0BE9DD93285A5A200023B555 /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD92285A5A200023B555 /* WhitelistUtilities.swift */; }; + 0BE9DD94285A5A200023B555 /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD92285A5A200023B555 /* WhitelistUtilities.swift */; }; + 0BE9DD95285A5A200023B555 /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD92285A5A200023B555 /* WhitelistUtilities.swift */; }; + 0BE9DD96285A5A200023B555 /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD92285A5A200023B555 /* WhitelistUtilities.swift */; }; + 0BE9DD97285A5A200023B555 /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BE9DD92285A5A200023B555 /* WhitelistUtilities.swift */; }; + 0BECE945294B712500F3BFB8 /* CountdownDisplayService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BECE944294B712500F3BFB8 /* CountdownDisplayService.swift */; }; + 0BECE94E294D11C600F3BFB8 /* PaywallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BECE94D294D11C600F3BFB8 /* PaywallService.swift */; }; + 0BECE951294D1F8E00F3BFB8 /* Date+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BECE950294D1F8E00F3BFB8 /* Date+Ext.swift */; }; + 0BECE952294D1F8E00F3BFB8 /* Date+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BECE950294D1F8E00F3BFB8 /* Date+Ext.swift */; }; + 0BECE95D294F624B00F3BFB8 /* Keychainable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BECE95C294F624B00F3BFB8 /* Keychainable.swift */; }; + 0BECE9652950E27700F3BFB8 /* ProductPurchasable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BECE9642950E27700F3BFB8 /* ProductPurchasable.swift */; }; + 0BECE98B2951F3C300F3BFB8 /* String+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BECE98A2951F3C300F3BFB8 /* String+URL.swift */; }; + 0BECF18228C6091D0092D430 /* UICollectionView+Dequeue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BECF18128C6091D0092D430 /* UICollectionView+Dequeue.swift */; }; + 0BF1E99F28C67E1700ACD0FD /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF1E99E28C67E1700ACD0FD /* NibLoadable.swift */; }; + 0BF1E9A828C696F900ACD0FD /* UIView+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BF1E9A728C696F900ACD0FD /* UIView+Ext.swift */; }; 1579100974C8086B190B35BB /* Pods-Lockdown VPN Widget-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = A19DA148E491FF88E4B0B408 /* Pods-Lockdown VPN Widget-settings-metadata.plist */; }; + 200B3ACD2F983ADCB1128C42 /* Pods_Lockdown_VPN_Widget.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BBFF5B209ADA1F1C8193F194 /* Pods_Lockdown_VPN_Widget.framework */; }; 20816D1FD569053C0994232B /* Pods-Lockdown-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = E4A025BF9012D4E6454AE1D6 /* Pods-Lockdown-metadata.plist */; }; - 2DDD126D8F073FC42E1C6A28 /* Pods_Lockdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CA426CE326B009F3E4493D1 /* Pods_Lockdown.framework */; }; 388CD7581B88A7E496467546 /* Pods-Lockdown Firewall Widget-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2DF472CA81A935DEF14D7039 /* Pods-Lockdown Firewall Widget-settings-metadata.plist */; }; - 39FD925FB5AC24FB6118A2E8 /* Pods_Lockdown_VPN_Widget.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6FE1DD136CBC73DC96B7009A /* Pods_Lockdown_VPN_Widget.framework */; }; - 3B392ABD6D5830F835A8F8F5 /* Pods_Lockdown_Firewall_Widget.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE616CC875B5A9730B25D559 /* Pods_Lockdown_Firewall_Widget.framework */; }; 3D01D97B2480DCB3003A710C /* data_trackers.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3D01D97A2480DBED003A710C /* data_trackers.txt */; }; 3D01D99E2481E42B003A710C /* reporting.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3D01D99D2481E252003A710C /* reporting.txt */; }; 3D01D99F2481E42E003A710C /* general_ads.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3D01D99C2481E241003A710C /* general_ads.txt */; }; @@ -108,10 +275,8 @@ 3D94AAF022FD7BFA0012B0DE /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1FCDA6222C7616400C928BC /* NetworkExtension.framework */; }; 3D94AAF122FDEAC00012B0DE /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F2D22F190720017740D /* Client.swift */; }; 3D94AAF222FDEAC20012B0DE /* ClientModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F3022F190AE0017740D /* ClientModels.swift */; }; - 3D94AAF422FDEAC80012B0DE /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */; }; 3D94AAF522FDEACD0012B0DE /* VPNController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18921B77C80008A9322 /* VPNController.swift */; }; 3D94AAF722FDEAD70012B0DE /* FirewallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F4022F252720017740D /* FirewallController.swift */; }; - 3D94AAF822FDEADC0012B0DE /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AF22FC14CC00DE189F /* Shared.swift */; }; 3D94AAFD22FDEB460012B0DE /* VPNSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18521B77C66008A9322 /* VPNSubscription.swift */; }; 3D94AB0F22FE0CF60012B0DE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1141A1B1F46230500F54698 /* Assets.xcassets */; }; 3D94AB1022FE0CFB0012B0DE /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3D94AB0222FDEDEB0012B0DE /* MainInterface.storyboard */; }; @@ -127,30 +292,15 @@ 3DAA6B5322EA988F0018FC09 /* ransomware.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3DAA6B5222EA988F0018FC09 /* ransomware.txt */; }; 3DABD9FD22F7961F00480AAC /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F2D22F190720017740D /* Client.swift */; }; 3DABDA0222F7DD7700480AAC /* ClientModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F3022F190AE0017740D /* ClientModels.swift */; }; - 3DAF73522768572300D97BB0 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAF734C2768572300D97BB0 /* FirewallUtilities.swift */; }; - 3DAF73532768572300D97BB0 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAF734C2768572300D97BB0 /* FirewallUtilities.swift */; }; - 3DAF73542768572300D97BB0 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAF734C2768572300D97BB0 /* FirewallUtilities.swift */; }; - 3DAF73552768572300D97BB0 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAF734C2768572300D97BB0 /* FirewallUtilities.swift */; }; - 3DAF73562768572300D97BB0 /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAF734C2768572300D97BB0 /* FirewallUtilities.swift */; }; - 3DAF73602768583700D97BB0 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE079247FF87E0000A7D3 /* OneTimeActions.swift */; }; - 3DAF73612768584200D97BB0 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C3E8D20247D8057004B81D6 /* PushNotifications.swift */; }; - 3DAF73622768584500D97BB0 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6619BB247810E2005E8BB1 /* BlockDayLog.swift */; }; - 3DAF73632768584D00D97BB0 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE072247FD82A0000A7D3 /* PushNotificationsAuthorization.swift */; }; - 3DAF73642768586200D97BB0 /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */; }; 3DAF7C5622F4568C003C8F9C /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F2D22F190720017740D /* Client.swift */; }; 3DAF7C5722F456F2003C8F9C /* ClientModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F3022F190AE0017740D /* ClientModels.swift */; }; 3DAF907922EFD70200FB29E0 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1FCDA6222C7616400C928BC /* NetworkExtension.framework */; }; 3DAF907A22EFD70900FB29E0 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1FCDA6222C7616400C928BC /* NetworkExtension.framework */; }; 3DBD57A222FBB0D900DE189F /* WebViewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57A122FBB0D900DE189F /* WebViewViewController.swift */; }; 3DBD57A622FBCD7A00DE189F /* WhitelistViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57A522FBCD7A00DE189F /* WhitelistViewController.swift */; }; - 3DBD57A822FBD7A100DE189F /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */; }; - 3DBD57A922FBD7A100DE189F /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */; }; - 3DBD57AA22FBD7A100DE189F /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */; }; 3DBD57AC22FBDFE300DE189F /* WhitelistCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AB22FBDFE200DE189F /* WhitelistCell.swift */; }; 3DBD57AE22FBE04300DE189F /* WhitelistAddCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AD22FBE04300DE189F /* WhitelistAddCell.swift */; }; 3DBD57B022FC14CD00DE189F /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AF22FC14CC00DE189F /* Shared.swift */; }; - 3DBD57B122FC14CD00DE189F /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AF22FC14CC00DE189F /* Shared.swift */; }; - 3DBD57B222FC14CD00DE189F /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57AF22FC14CC00DE189F /* Shared.swift */; }; 3DBD57B422FCFF2500DE189F /* SetRegionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57B322FCFF2400DE189F /* SetRegionViewController.swift */; }; 3DBD57B622FD00BC00DE189F /* SetRegionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57B522FD00BB00DE189F /* SetRegionCell.swift */; }; 3DBD57BC22FD727900DE189F /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1912FE91F58B2D00007F6D4 /* NotificationCenter.framework */; }; @@ -171,10 +321,13 @@ 3DF2455623A306DB00E46613 /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF2455523A306DB00E46613 /* Loader.swift */; }; 3DF5D75F2633B1E100F77D79 /* amazon_trackers.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3DF5D75E2633B1E100F77D79 /* amazon_trackers.txt */; }; 4A86219093026DE70A097E79 /* Pods-LockdownTests-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8DA68459884385F76BF86234 /* Pods-LockdownTests-metadata.plist */; }; + 510EA50D772A435C202CC370 /* Pods-LockdownIntents-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 89C1B1F2419FFC1E995F10BC /* Pods-LockdownIntents-settings-metadata.plist */; }; 54F0B1A0273200B0002F3630 /* FirewallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F4022F252720017740D /* FirewallController.swift */; }; 5647ACFEBBAB001FAE27CAF9 /* Pods-LockdownTunnel-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6F089C7008AB8F59DE3EA7BD /* Pods-LockdownTunnel-settings-metadata.plist */; }; 5666ABC4D0064E4669D1943F /* Pods-LockdownTunnel-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = B2AFAE1E2F56A1CA9EC153D4 /* Pods-LockdownTunnel-metadata.plist */; }; + 5B535D3E36C811A6CDFC5F61 /* Pods_Lockdown_Firewall_Widget.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B52A28F4337A6E086CB1F96A /* Pods_Lockdown_Firewall_Widget.framework */; }; 601BF3ED11EB7CBF95BF5720 /* Pods-Lockdown Firewall Widget-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 12884CAB7C53B842E9E3745C /* Pods-Lockdown Firewall Widget-metadata.plist */; }; + 72251A33DE32085D77F6855E /* Pods_LockdownIntents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 20E68D70EBEDB6917C2C9994 /* Pods_LockdownIntents.framework */; }; 78010EFC9ED40D77BD40C924 /* Pods-LockdownTests-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2ADD2E8AC036859E49987E8B /* Pods-LockdownTests-settings-metadata.plist */; }; 7C0156542521C2F200670CB5 /* Montserrat-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377A22DFB22600908CDC /* Montserrat-Medium.ttf */; }; 7C0156552521C2F200670CB5 /* Montserrat-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377D22DFB22600908CDC /* Montserrat-Bold.ttf */; }; @@ -185,18 +338,8 @@ 7C0D11122473EE2E00A26E04 /* DomainNameValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0D11112473EE2E00A26E04 /* DomainNameValidator.swift */; }; 7C0D111D2473FC7E00A26E04 /* LockdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0D111C2473FC7E00A26E04 /* LockdownTests.swift */; }; 7C0D11252473FD6500A26E04 /* DomainNameValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0D11242473FD6500A26E04 /* DomainNameValidatorTests.swift */; }; - 7C1AE073247FD82A0000A7D3 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE072247FD82A0000A7D3 /* PushNotificationsAuthorization.swift */; }; 7C1AE075247FE1FB0000A7D3 /* PushNotificationsAuthorizationUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE074247FE1FB0000A7D3 /* PushNotificationsAuthorizationUI.swift */; }; - 7C1AE076247FE2000000A7D3 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE072247FD82A0000A7D3 /* PushNotificationsAuthorization.swift */; }; - 7C1AE077247FE2010000A7D3 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE072247FD82A0000A7D3 /* PushNotificationsAuthorization.swift */; }; - 7C1AE078247FE2010000A7D3 /* PushNotificationsAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE072247FD82A0000A7D3 /* PushNotificationsAuthorization.swift */; }; - 7C1AE07A247FF87F0000A7D3 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE079247FF87E0000A7D3 /* OneTimeActions.swift */; }; - 7C1AE07B247FF87F0000A7D3 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE079247FF87E0000A7D3 /* OneTimeActions.swift */; }; - 7C1AE07C247FF87F0000A7D3 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE079247FF87E0000A7D3 /* OneTimeActions.swift */; }; - 7C1AE07D247FF87F0000A7D3 /* OneTimeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE079247FF87E0000A7D3 /* OneTimeActions.swift */; }; 7C1AE080248028F40000A7D3 /* UIKit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AE07F248028F40000A7D3 /* UIKit+Extensions.swift */; }; - 7C3E8D21247D8057004B81D6 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C3E8D20247D8057004B81D6 /* PushNotifications.swift */; }; - 7C3E8D22247D8057004B81D6 /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C3E8D20247D8057004B81D6 /* PushNotifications.swift */; }; 7C3EFA0224867DEE00719D96 /* TrackerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C3EFA0124867DEE00719D96 /* TrackerInfo.swift */; }; 7C3EFA042486879800719D96 /* tracker_info.json in Resources */ = {isa = PBXBuildFile; fileRef = 7C3EFA032486879800719D96 /* tracker_info.json */; }; 7C422E97252796EE007F9C22 /* StaticTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C422E96252796EE007F9C22 /* StaticTableView.swift */; }; @@ -208,10 +351,6 @@ 7C44081D2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C44081A2539BCCE003FAD1E /* ProtectedFileAccess.swift */; }; 7C44081E2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C44081A2539BCCE003FAD1E /* ProtectedFileAccess.swift */; }; 7C4D9BBB252C8748004175EA /* AccountUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C4D9BBA252C8748004175EA /* AccountUI.swift */; }; - 7C6619BC247810E2005E8BB1 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6619BB247810E2005E8BB1 /* BlockDayLog.swift */; }; - 7C6619BD247810EE005E8BB1 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6619BB247810E2005E8BB1 /* BlockDayLog.swift */; }; - 7C6619BE247810EE005E8BB1 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6619BB247810E2005E8BB1 /* BlockDayLog.swift */; }; - 7C6619BF247810EF005E8BB1 /* BlockDayLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6619BB247810E2005E8BB1 /* BlockDayLog.swift */; }; 7C798A1A25409F8100A99695 /* Mailto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C798A1925409F8100A99695 /* Mailto.swift */; }; 7C9A936C251E1EC700DA5721 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C9A936B251E1EC700DA5721 /* WidgetKit.framework */; }; 7C9A936E251E1EC700DA5721 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C9A936D251E1EC700DA5721 /* SwiftUI.framework */; }; @@ -223,18 +362,6 @@ 7CC8EFED254036050005054C /* FirewallRepair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC8EFEC254036050005054C /* FirewallRepair.swift */; }; 7CD1435F248798D4009206A9 /* TrackerInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD1435E248798D4009206A9 /* TrackerInfoTests.swift */; }; 7CD52D81247E850D00D0530F /* SnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD52D80247E850D00D0530F /* SnapshotTests.swift */; }; - 7CD52D82247EC18800D0530F /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C3E8D20247D8057004B81D6 /* PushNotifications.swift */; }; - 7CD52D83247EC18900D0530F /* PushNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C3E8D20247D8057004B81D6 /* PushNotifications.swift */; }; - 7CE91C592521D54F009D8269 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C582521D54F009D8269 /* UserDefaults.swift */; }; - 7CE91C602521D564009D8269 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C582521D54F009D8269 /* UserDefaults.swift */; }; - 7CE91C672521D565009D8269 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C582521D54F009D8269 /* UserDefaults.swift */; }; - 7CE91C682521D565009D8269 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C582521D54F009D8269 /* UserDefaults.swift */; }; - 7CE91C692521D566009D8269 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C582521D54F009D8269 /* UserDefaults.swift */; }; - 7CE91C712521D58C009D8269 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C702521D58C009D8269 /* Metrics.swift */; }; - 7CE91C7E2521D5B6009D8269 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C702521D58C009D8269 /* Metrics.swift */; }; - 7CE91C852521D5B7009D8269 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C702521D58C009D8269 /* Metrics.swift */; }; - 7CE91C862521D5B7009D8269 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C702521D58C009D8269 /* Metrics.swift */; }; - 7CE91C872521D5B8009D8269 /* Metrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C702521D58C009D8269 /* Metrics.swift */; }; 7CE91C962521ED5E009D8269 /* VPNRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C952521ED5E009D8269 /* VPNRegion.swift */; }; 7CE91C972521ED5E009D8269 /* VPNRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C952521ED5E009D8269 /* VPNRegion.swift */; }; 7CE91C982521ED5E009D8269 /* VPNRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C952521ED5E009D8269 /* VPNRegion.swift */; }; @@ -256,7 +383,7 @@ A118F64520B33FED009A64E7 /* TimerEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = A118F63E20B33FED009A64E7 /* TimerEx.swift */; }; A118F64720B33FED009A64E7 /* SpinerLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A118F63F20B33FED009A64E7 /* SpinerLayer.swift */; }; A118F64920B33FED009A64E7 /* CGRectEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = A118F64020B33FED009A64E7 /* CGRectEx.swift */; }; - A12186271FB8F691007058B3 /* SignupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12186261FB8F691007058B3 /* SignupViewController.swift */; }; + A12186271FB8F691007058B3 /* OldSignupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12186261FB8F691007058B3 /* OldSignupViewController.swift */; }; A12229AB22C014CB00BFF624 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A12229AA22C014CA00BFF624 /* StoreKit.framework */; }; A12473F41FE44285008493B8 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1912FE91F58B2D00007F6D4 /* NotificationCenter.framework */; }; A12473F71FE44285008493B8 /* VPNTodayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12473F61FE44285008493B8 /* VPNTodayViewController.swift */; }; @@ -313,12 +440,21 @@ A1FCDA8B22D3BA1900C928BC /* facebook_inc.txt in Resources */ = {isa = PBXBuildFile; fileRef = A1FCDA8922D3BA1900C928BC /* facebook_inc.txt */; }; A1FCDA8D22D3C50A00C928BC /* email_opens.txt in Resources */ = {isa = PBXBuildFile; fileRef = A1FCDA8C22D3C50A00C928BC /* email_opens.txt */; }; A1FCDA9122D3D52C00C928BC /* facebook_inc_ipv6.txt in Resources */ = {isa = PBXBuildFile; fileRef = A1FCDA9022D3D52C00C928BC /* facebook_inc_ipv6.txt */; }; - B05197BCB274B38A23A35CAF /* Pods_LockdownTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1ACB305D330C07B959830513 /* Pods_LockdownTests.framework */; }; C9E66BB880A29A48D055FBFF /* Pods-Lockdown-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 50F9BE503587CE4933CB7983 /* Pods-Lockdown-settings-metadata.plist */; }; - E82C3745DDABA3CDE9D1D776 /* Pods_LockdownTunnel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD91685BB7BEC612310EC3ED /* Pods_LockdownTunnel.framework */; }; + EF0E051F58B4991FA5262206 /* Pods_LockdownTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FFE976725BA472E2F1CE7B83 /* Pods_LockdownTests.framework */; }; + F0FA63CE72E377E80B262592 /* Pods_Lockdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5087F67CEF771725FC357970 /* Pods_Lockdown.framework */; }; + FBD19586D73B1D8573C2AD98 /* Pods-LockdownIntents-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = EEF3612A1027943CE53E6070 /* Pods-LockdownIntents-metadata.plist */; }; + FF6327EF056A4C0DCA5E8123 /* Pods_LockdownTunnel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2AF55B2F4F7EBC056DED63C /* Pods_LockdownTunnel.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 0B4B8B1629412BD50060C072 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A1141A091F46230500F54698 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0B4B8AFD29412BD50060C072; + remoteInfo = LockdownIntents; + }; 3D8A254227487B2A001B99E1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 3D8A253A27487B2A001B99E1 /* NEKit.xcodeproj */; @@ -422,6 +558,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + 0B4B8B1829412BD50060C072 /* LockdownIntents.appex in Embed App Extensions */, A1931D0420791F5900E695EB /* Lockdown Blocker.appex in Embed App Extensions */, 3DBD57C622FD727900DE189F /* Lockdown Firewall Widget.appex in Embed App Extensions */, A12473FE1FE44285008493B8 /* Lockdown VPN Widget.appex in Embed App Extensions */, @@ -434,10 +571,133 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0B014050F770126F5808AF28 /* Pods-LockdownTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTests/Pods-LockdownTests.release.xcconfig"; sourceTree = ""; }; + 0B087B06291C184C006953DA /* EnableNotificationsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EnableNotificationsViewController.xib; sourceTree = ""; }; + 0B087B08291C1852006953DA /* EnableNotificationsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableNotificationsViewController.swift; sourceTree = ""; }; + 0B0CCD422901B25300F5C632 /* Onboarding_launch_firewall.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = Onboarding_launch_firewall.mp4; sourceTree = ""; }; + 0B0CCD442901B28100F5C632 /* Onboarding_showcase_vpn.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = Onboarding_showcase_vpn.mp4; sourceTree = ""; }; + 0B1B6AC3293A9977003DCF72 /* UserService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = ""; }; + 0B1B6AC5293A99C1003DCF72 /* EmailComposable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailComposable.swift; sourceTree = ""; }; + 0B1B6AC7293A9AF1003DCF72 /* DeleteMyAccountViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DeleteMyAccountViewController.xib; sourceTree = ""; }; + 0B1B6AC8293A9AF1003DCF72 /* DeleteMyAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMyAccountViewController.swift; sourceTree = ""; }; + 0B1B6ACB293A9B1E003DCF72 /* TablePaywallViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TablePaywallViewController.xib; sourceTree = ""; }; + 0B1B6ACC293A9B1E003DCF72 /* TablePaywallViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TablePaywallViewController.swift; sourceTree = ""; }; + 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallTableFeature.swift; sourceTree = ""; }; + 0B1B6AD9293DFB50003DCF72 /* TablePaywallFeatureRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TablePaywallFeatureRowView.swift; sourceTree = ""; }; + 0B1C8ED128E44A8800E9CA57 /* ProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressView.swift; sourceTree = ""; }; + 0B1FFD5B293E15AA0027341C /* UIStackView+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Ext.swift"; sourceTree = ""; }; + 0B1FFD62293E16BF0027341C /* Separator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Separator.swift; sourceTree = ""; }; + 0B2205BD291416D4002F2562 /* SignUpViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SignUpViewController.xib; sourceTree = ""; }; + 0B2205C4291416DE002F2562 /* SignUpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpViewController.swift; sourceTree = ""; }; + 0B2205C629142601002F2562 /* FloatingTextInputTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTextInputTextField.swift; sourceTree = ""; }; + 0B2205C929142A84002F2562 /* TextBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBox.swift; sourceTree = ""; }; + 0B2205CB29142AA2002F2562 /* TextInputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputState.swift; sourceTree = ""; }; + 0B2205CD29142ACD002F2562 /* TextBoxLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBoxLabel.swift; sourceTree = ""; }; + 0B2205CF2915B093002F2562 /* UIViewController+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Ext.swift"; sourceTree = ""; }; + 0B312AD32910411E00A218FB /* OnboardingPageContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageContent.swift; sourceTree = ""; }; + 0B363A2628F56B6600E881BF /* Onboarding_blocklists.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = Onboarding_blocklists.mp4; sourceTree = ""; }; + 0B3AB7E92934EFAD00B254C0 /* UIView+Corners.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Corners.swift"; sourceTree = ""; }; + 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionOfferPeriodUnit.swift; sourceTree = ""; }; + 0B3AB83B29354B9100B254C0 /* LockdownGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockdownGradient.swift; sourceTree = ""; }; + 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreProductGroup.swift; sourceTree = ""; }; + 0B4B8AF52941079D0060C072 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 0B4B8AF62941079D0060C072 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + 0B4B8AF72941079D0060C072 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainInterface.strings"; sourceTree = ""; }; + 0B4B8AF82941079D0060C072 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/MainInterface.strings"; sourceTree = ""; }; + 0B4B8AF92941079E0060C072 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + 0B4B8AFE29412BD50060C072 /* LockdownIntents.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockdownIntents.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 0B4B8AFF29412BD50060C072 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; + 0B4B8B0229412BD50060C072 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + 0B4B8B0429412BD50060C072 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0B4B8B0A29412BD50060C072 /* IntentsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IntentsUI.framework; path = System/Library/Frameworks/IntentsUI.framework; sourceTree = SDKROOT; }; + 0B4B8B1F29412C9C0060C072 /* IntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentService.swift; sourceTree = ""; }; + 0B4B8B232941352A0060C072 /* DisableVPNIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableVPNIntentHandler.swift; sourceTree = ""; }; + 0B4B8B25294135350060C072 /* EnableVPNIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableVPNIntentHandler.swift; sourceTree = ""; }; + 0B4B8B5329420AA50060C072 /* LockdownIntents.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LockdownIntents.entitlements; sourceTree = ""; }; + 0B4E082528C8EBE000129620 /* SplashscreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashscreenViewController.swift; sourceTree = ""; }; + 0B4E082728C8EBED00129620 /* SplashscreenViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SplashscreenViewController.xib; sourceTree = ""; }; + 0B4EF29F293A98F600A32554 /* LockdownUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockdownUser.swift; sourceTree = ""; }; + 0B564B67294368C00082C27C /* EmailAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailAddress.swift; sourceTree = ""; }; + 0B6DC53F292E402A00DCB8E8 /* Font+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Ext.swift"; sourceTree = ""; }; + 0B80623E2860A1DB003EFF32 /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; + 0B84454D2940BDE300FA2F35 /* EmailValidatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailValidatable.swift; sourceTree = ""; }; + 0B84454F2940CE2A00FA2F35 /* EmailValidatableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailValidatableTests.swift; sourceTree = ""; }; + 0B99BA352948FE6300A47A45 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/LockdownIntents.intentdefinition; sourceTree = ""; }; + 0B99BA382948FE7600A47A45 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/LockdownIntents.strings; sourceTree = ""; }; + 0B99BA3A2948FE7700A47A45 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/LockdownIntents.strings; sourceTree = ""; }; + 0B99BA3C2948FE7800A47A45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LockdownIntents.strings"; sourceTree = ""; }; + 0B99BA3E2948FEBC00A47A45 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/LockdownIntents.strings"; sourceTree = ""; }; + 0B99BA402948FEBD00A47A45 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/LockdownIntents.strings; sourceTree = ""; }; + 0B99BA422948FEBD00A47A45 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/LockdownIntents.strings; sourceTree = ""; }; + 0B99BA442948FEBE00A47A45 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/LockdownIntents.strings; sourceTree = ""; }; + 0B99BA462948FEBF00A47A45 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LockdownIntents.strings; sourceTree = ""; }; + 0B99BA482948FEBF00A47A45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/LockdownIntents.strings; sourceTree = ""; }; + 0B99BA4A2948FEC000A47A45 /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/LockdownIntents.strings; sourceTree = ""; }; + 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallDisplayConfiguration.swift; sourceTree = ""; }; + 0B99BA57294A266800A47A45 /* ChristmasPaywallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChristmasPaywallViewController.swift; sourceTree = ""; }; + 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ChristmasPaywallViewController.xib; sourceTree = ""; }; + 0B9D69CA28E1AB2600A3EFCE /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = ""; }; + 0B9D69CC28E1AB3100A3EFCE /* OnboardingViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardingViewController.xib; sourceTree = ""; }; + 0B9D69CF28E1C0E700A3EFCE /* OnboardingPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPage.swift; sourceTree = ""; }; + 0B9D69D428E1C1C700A3EFCE /* Onboarding_installation.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = Onboarding_installation.mp4; sourceTree = ""; }; + 0BA24F70293DFE28007FEA9B /* TablePaywallHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TablePaywallHeaderView.xib; sourceTree = ""; }; + 0BA24F72293DFE3B007FEA9B /* TablePaywallHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TablePaywallHeaderView.swift; sourceTree = ""; }; + 0BA77E2028E44FB7007ECE45 /* Onboarding_notifications.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = Onboarding_notifications.mp4; sourceTree = ""; }; + 0BA77E2328E45C94007ECE45 /* WeakObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakObject.swift; sourceTree = ""; }; + 0BA77E2528E45F73007ECE45 /* ArrayRestrictable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayRestrictable.swift; sourceTree = ""; }; + 0BA77E2728E46F61007ECE45 /* OnboardingConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingConfiguration.swift; sourceTree = ""; }; + 0BB5629729437C1B00EEE4B5 /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = ""; }; + 0BCD089F293FDCF400DC6DD1 /* ConnectivityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityService.swift; sourceTree = ""; }; + 0BCD08A2293FDD0A00DC6DD1 /* ConnectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionState.swift; sourceTree = ""; }; + 0BCD08A52940AE8600DC6DD1 /* ConnectivityServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityServiceTests.swift; sourceTree = ""; }; + 0BE113F9293A6B220070DC51 /* CALayer+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+Ext.swift"; sourceTree = ""; }; + 0BE113FC293A94930070DC51 /* TablePaywallFeatureRowView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TablePaywallFeatureRowView.xib; sourceTree = ""; }; + 0BE9DD68285A59710023B555 /* OneTimeActions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OneTimeActions.swift; path = ../nekit/src/LockdownClonedFiles/OneTimeActions.swift; sourceTree = ""; }; + 0BE9DD6E285A59930023B555 /* BlockDayLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BlockDayLog.swift; path = ../nekit/src/LockdownClonedFiles/BlockDayLog.swift; sourceTree = ""; }; + 0BE9DD74285A59B70023B555 /* FirewallUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FirewallUtilities.swift; path = ../nekit/src/LockdownClonedFiles/FirewallUtilities.swift; sourceTree = ""; }; + 0BE9DD7A285A59E30023B555 /* Metrics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Metrics.swift; path = ../nekit/src/LockdownClonedFiles/Metrics.swift; sourceTree = ""; }; + 0BE9DD80285A59F20023B555 /* PushNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PushNotifications.swift; path = ../nekit/src/LockdownClonedFiles/PushNotifications.swift; sourceTree = ""; }; + 0BE9DD86285A5A070023B555 /* PushNotificationsAuthorization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PushNotificationsAuthorization.swift; path = ../nekit/src/LockdownClonedFiles/PushNotificationsAuthorization.swift; sourceTree = ""; }; + 0BE9DD8C285A5A120023B555 /* UserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UserDefaults.swift; path = ../nekit/src/LockdownClonedFiles/UserDefaults.swift; sourceTree = ""; }; + 0BE9DD92285A5A200023B555 /* WhitelistUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WhitelistUtilities.swift; path = ../nekit/src/LockdownClonedFiles/WhitelistUtilities.swift; sourceTree = ""; }; + 0BECE944294B712500F3BFB8 /* CountdownDisplayService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownDisplayService.swift; sourceTree = ""; }; + 0BECE94D294D11C600F3BFB8 /* PaywallService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallService.swift; sourceTree = ""; }; + 0BECE950294D1F8E00F3BFB8 /* Date+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Ext.swift"; sourceTree = ""; }; + 0BECE95C294F624B00F3BFB8 /* Keychainable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychainable.swift; sourceTree = ""; }; + 0BECE9642950E27700F3BFB8 /* ProductPurchasable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPurchasable.swift; sourceTree = ""; }; + 0BECE98A2951F3C300F3BFB8 /* String+URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+URL.swift"; sourceTree = ""; }; + 0BECF18128C6091D0092D430 /* UICollectionView+Dequeue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Dequeue.swift"; sourceTree = ""; }; + 0BF1E99E28C67E1700ACD0FD /* NibLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NibLoadable.swift; sourceTree = ""; }; + 0BF1E9A728C696F900ACD0FD /* UIView+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Ext.swift"; sourceTree = ""; }; + 0BF901482938BFDF001A2F2B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/LaunchScreen.strings; sourceTree = ""; }; + 0BF901492938BFE0001A2F2B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Main.strings; sourceTree = ""; }; + 0BF9014A2938BFE0001A2F2B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/MainInterface.strings; sourceTree = ""; }; + 0BF9014B2938BFE0001A2F2B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/MainInterface.strings; sourceTree = ""; }; + 0BF9014C2938BFE0001A2F2B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + 0BF9014D2938BFF4001A2F2B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainInterface.strings; sourceTree = ""; }; + 0BF9014E2938BFF4001A2F2B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/LaunchScreen.strings; sourceTree = ""; }; + 0BF9014F2938BFF4001A2F2B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Main.strings; sourceTree = ""; }; + 0BF901502938BFF4001A2F2B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/MainInterface.strings; sourceTree = ""; }; + 0BF901512938BFF4001A2F2B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 0BF901522938C008001A2F2B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; + 0BF901532938C008001A2F2B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Main.strings; sourceTree = ""; }; + 0BF901542938C008001A2F2B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/MainInterface.strings; sourceTree = ""; }; + 0BF901552938C008001A2F2B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/MainInterface.strings; sourceTree = ""; }; + 0BF901562938C008001A2F2B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 0BF901572938C015001A2F2B /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/LaunchScreen.strings; sourceTree = ""; }; + 0BF901582938C015001A2F2B /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Main.strings; sourceTree = ""; }; + 0BF901592938C015001A2F2B /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/MainInterface.strings; sourceTree = ""; }; + 0BF9015A2938C015001A2F2B /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/MainInterface.strings; sourceTree = ""; }; + 0BF9015B2938C015001A2F2B /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = ""; }; + 0BF901612938C025001A2F2B /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 0BF901622938C025001A2F2B /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Main.strings"; sourceTree = ""; }; + 0BF901632938C025001A2F2B /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/MainInterface.strings"; sourceTree = ""; }; + 0BF901642938C026001A2F2B /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/MainInterface.strings"; sourceTree = ""; }; + 0BF901652938C026001A2F2B /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 0CDA77C17BF2DEC43E3D56EA /* Pods-LockdownTunnel-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownTunnel-settings-metadata.plist"; path = "LockdowniOS/Settings.bundle/Pods-LockdownTunnel-settings-metadata.plist"; sourceTree = ""; }; + 12578876980678A072282A2E /* Pods-Lockdown VPN Widget.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown VPN Widget.release.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown VPN Widget/Pods-Lockdown VPN Widget.release.xcconfig"; sourceTree = ""; }; 12884CAB7C53B842E9E3745C /* Pods-Lockdown Firewall Widget-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown Firewall Widget-metadata.plist"; path = "Pods/Pods-Lockdown Firewall Widget-metadata.plist"; sourceTree = ""; }; - 1ACB305D330C07B959830513 /* Pods_LockdownTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LockdownTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1F90F5BA7CFAB9814D768F16 /* Pods-LockdownTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTests/Pods-LockdownTests.debug.xcconfig"; sourceTree = ""; }; + 20E68D70EBEDB6917C2C9994 /* Pods_LockdownIntents.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LockdownIntents.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2ADD2E8AC036859E49987E8B /* Pods-LockdownTests-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownTests-settings-metadata.plist"; path = "Settings.bundle/Pods-LockdownTests-settings-metadata.plist"; sourceTree = ""; }; 2DF472CA81A935DEF14D7039 /* Pods-Lockdown Firewall Widget-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown Firewall Widget-settings-metadata.plist"; path = "Settings.bundle/Pods-Lockdown Firewall Widget-settings-metadata.plist"; sourceTree = ""; }; 3D01D97A2480DBED003A710C /* data_trackers.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = data_trackers.txt; sourceTree = ""; }; @@ -527,10 +787,8 @@ 3DA14D3E255DF5D400A3658E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainInterface.strings; sourceTree = ""; }; 3DAA6B4E22EA76420018FC09 /* clickbait.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = clickbait.txt; sourceTree = ""; }; 3DAA6B5222EA988F0018FC09 /* ransomware.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ransomware.txt; sourceTree = ""; }; - 3DAF734C2768572300D97BB0 /* FirewallUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirewallUtilities.swift; sourceTree = ""; }; 3DBD57A122FBB0D900DE189F /* WebViewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewViewController.swift; sourceTree = ""; }; 3DBD57A522FBCD7A00DE189F /* WhitelistViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhitelistViewController.swift; sourceTree = ""; }; - 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhitelistUtilities.swift; sourceTree = ""; }; 3DBD57AB22FBDFE200DE189F /* WhitelistCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhitelistCell.swift; sourceTree = ""; }; 3DBD57AD22FBE04300DE189F /* WhitelistAddCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhitelistAddCell.swift; sourceTree = ""; }; 3DBD57AF22FC14CC00DE189F /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; @@ -558,24 +816,22 @@ 3DF2455323A2F8A400E46613 /* EmailSignUpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignUpViewController.swift; sourceTree = ""; }; 3DF2455523A306DB00E46613 /* Loader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = ""; }; 3DF5D75E2633B1E100F77D79 /* amazon_trackers.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = amazon_trackers.txt; sourceTree = ""; }; - 4CA426CE326B009F3E4493D1 /* Pods_Lockdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 489F7CCEAADF147B19320AAE /* Pods-LockdownTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTests/Pods-LockdownTests.release.xcconfig"; sourceTree = ""; }; + 5087F67CEF771725FC357970 /* Pods_Lockdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 50F9BE503587CE4933CB7983 /* Pods-Lockdown-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown-settings-metadata.plist"; path = "Settings.bundle/Pods-Lockdown-settings-metadata.plist"; sourceTree = ""; }; 50FB8ADA1D444FD9486F2D44 /* Pods-Lockdown Firewall Widget-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown Firewall Widget-settings-metadata.plist"; path = "LockdowniOS/Settings.bundle/Pods-Lockdown Firewall Widget-settings-metadata.plist"; sourceTree = ""; }; - 549133AE36DE5B896F995988 /* Pods-LockdownTunnel.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTunnel.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTunnel/Pods-LockdownTunnel.debug.xcconfig"; sourceTree = ""; }; + 569A4A1747E7C83C7F07C755 /* Pods-LockdownTunnel.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTunnel.release.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTunnel/Pods-LockdownTunnel.release.xcconfig"; sourceTree = ""; }; + 628519CE6C4D40332EFBFB08 /* Pods-LockdownTunnel.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTunnel.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTunnel/Pods-LockdownTunnel.debug.xcconfig"; sourceTree = ""; }; 6F089C7008AB8F59DE3EA7BD /* Pods-LockdownTunnel-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownTunnel-settings-metadata.plist"; path = "Settings.bundle/Pods-LockdownTunnel-settings-metadata.plist"; sourceTree = ""; }; - 6FE1DD136CBC73DC96B7009A /* Pods_Lockdown_VPN_Widget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown_VPN_Widget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 707A358300D4A3D33EFC8749 /* Pods-Lockdown Firewall Widget.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown Firewall Widget.release.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown Firewall Widget/Pods-Lockdown Firewall Widget.release.xcconfig"; sourceTree = ""; }; - 7435DF998DFB59C9808F1638 /* Pods-LockdownTunnel.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTunnel.release.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTunnel/Pods-LockdownTunnel.release.xcconfig"; sourceTree = ""; }; + 71855E12B7B5B939F0E2EB29 /* Pods-Lockdown Firewall Widget.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown Firewall Widget.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown Firewall Widget/Pods-Lockdown Firewall Widget.debug.xcconfig"; sourceTree = ""; }; + 777648CC444D5A906BBFC729 /* Pods-LockdownIntents.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownIntents.release.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownIntents/Pods-LockdownIntents.release.xcconfig"; sourceTree = ""; }; 7C0D11112473EE2E00A26E04 /* DomainNameValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainNameValidator.swift; sourceTree = ""; }; 7C0D111A2473FC7E00A26E04 /* LockdownTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LockdownTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7C0D111C2473FC7E00A26E04 /* LockdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockdownTests.swift; sourceTree = ""; }; 7C0D111E2473FC7E00A26E04 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7C0D11242473FD6500A26E04 /* DomainNameValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainNameValidatorTests.swift; sourceTree = ""; }; - 7C1AE072247FD82A0000A7D3 /* PushNotificationsAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsAuthorization.swift; sourceTree = ""; }; 7C1AE074247FE1FB0000A7D3 /* PushNotificationsAuthorizationUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsAuthorizationUI.swift; sourceTree = ""; }; - 7C1AE079247FF87E0000A7D3 /* OneTimeActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneTimeActions.swift; sourceTree = ""; }; 7C1AE07F248028F40000A7D3 /* UIKit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIKit+Extensions.swift"; sourceTree = ""; }; - 7C3E8D20247D8057004B81D6 /* PushNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotifications.swift; sourceTree = ""; }; 7C3EFA0124867DEE00719D96 /* TrackerInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerInfo.swift; sourceTree = ""; }; 7C3EFA032486879800719D96 /* tracker_info.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = tracker_info.json; sourceTree = ""; }; 7C422E96252796EE007F9C22 /* StaticTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticTableView.swift; sourceTree = ""; }; @@ -584,7 +840,6 @@ 7C422EB62527A2D1007F9C22 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; 7C44081A2539BCCE003FAD1E /* ProtectedFileAccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectedFileAccess.swift; sourceTree = ""; }; 7C4D9BBA252C8748004175EA /* AccountUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountUI.swift; sourceTree = ""; }; - 7C6619BB247810E2005E8BB1 /* BlockDayLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockDayLog.swift; sourceTree = ""; }; 7C798A1925409F8100A99695 /* Mailto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mailto.swift; sourceTree = ""; }; 7C9A936A251E1EC700DA5721 /* LockdownFirewallWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LockdownFirewallWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7C9A936B251E1EC700DA5721 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; @@ -597,17 +852,13 @@ 7CC8EFEC254036050005054C /* FirewallRepair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallRepair.swift; sourceTree = ""; }; 7CD1435E248798D4009206A9 /* TrackerInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerInfoTests.swift; sourceTree = ""; }; 7CD52D80247E850D00D0530F /* SnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTests.swift; sourceTree = ""; }; - 7CE91C582521D54F009D8269 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; - 7CE91C702521D58C009D8269 /* Metrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metrics.swift; sourceTree = ""; }; 7CE91C8E2521D6CF009D8269 /* LockdownFirewallWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LockdownFirewallWidgetExtension.entitlements; sourceTree = ""; }; 7CE91C952521ED5E009D8269 /* VPNRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNRegion.swift; sourceTree = ""; }; 7CE91CA7252214C9009D8269 /* CombinedProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombinedProvider.swift; sourceTree = ""; }; - 88E04750DB23687076871F5B /* Pods-Lockdown VPN Widget.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown VPN Widget.release.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown VPN Widget/Pods-Lockdown VPN Widget.release.xcconfig"; sourceTree = ""; }; + 89C1B1F2419FFC1E995F10BC /* Pods-LockdownIntents-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownIntents-settings-metadata.plist"; path = "Settings.bundle/Pods-LockdownIntents-settings-metadata.plist"; sourceTree = ""; }; 8DA68459884385F76BF86234 /* Pods-LockdownTests-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownTests-metadata.plist"; path = "Pods/Pods-LockdownTests-metadata.plist"; sourceTree = ""; }; 8ED8D7A5DFFEEA5E9BD7FD20 /* Pods-Lockdown VPN Widget-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown VPN Widget-settings-metadata.plist"; path = "LockdowniOS/Settings.bundle/Pods-Lockdown VPN Widget-settings-metadata.plist"; sourceTree = ""; }; 92D3DD81205F17D004056D79 /* Pods-Lockdown VPN Widget-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown VPN Widget-metadata.plist"; path = "Pods/Pods-Lockdown VPN Widget-metadata.plist"; sourceTree = ""; }; - 95899949475D15BB3DB63138 /* Pods-LockdownTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownTests/Pods-LockdownTests.debug.xcconfig"; sourceTree = ""; }; - A0BEB57F0C3C4F5746285E07 /* Pods-Lockdown VPN Widget.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown VPN Widget.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown VPN Widget/Pods-Lockdown VPN Widget.debug.xcconfig"; sourceTree = ""; }; A101106C202B9D4300807612 /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; A1141A111F46230500F54698 /* Lockdown.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Lockdown.app; sourceTree = BUILT_PRODUCTS_DIR; }; A1141A141F46230500F54698 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -621,7 +872,7 @@ A118F63E20B33FED009A64E7 /* TimerEx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimerEx.swift; sourceTree = ""; }; A118F63F20B33FED009A64E7 /* SpinerLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpinerLayer.swift; sourceTree = ""; }; A118F64020B33FED009A64E7 /* CGRectEx.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGRectEx.swift; sourceTree = ""; }; - A12186261FB8F691007058B3 /* SignupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignupViewController.swift; sourceTree = ""; }; + A12186261FB8F691007058B3 /* OldSignupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OldSignupViewController.swift; sourceTree = ""; }; A12229AA22C014CA00BFF624 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; A12473F31FE44284008493B8 /* Lockdown VPN Widget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Lockdown VPN Widget.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; A12473F61FE44285008493B8 /* VPNTodayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNTodayViewController.swift; sourceTree = ""; }; @@ -691,17 +942,32 @@ A1FCDA8922D3BA1900C928BC /* facebook_inc.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = facebook_inc.txt; sourceTree = ""; }; A1FCDA8C22D3C50A00C928BC /* email_opens.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = email_opens.txt; sourceTree = ""; }; A1FCDA9022D3D52C00C928BC /* facebook_inc_ipv6.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = facebook_inc_ipv6.txt; sourceTree = ""; }; - A2316A93257F196F84916806 /* Pods-Lockdown Firewall Widget.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown Firewall Widget.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown Firewall Widget/Pods-Lockdown Firewall Widget.debug.xcconfig"; sourceTree = ""; }; A75E57A0F35C50EA949FB1FE /* Pods-Lockdown-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown-settings-metadata.plist"; path = "LockdowniOS/Settings.bundle/Pods-Lockdown-settings-metadata.plist"; sourceTree = ""; }; - AE616CC875B5A9730B25D559 /* Pods_Lockdown_Firewall_Widget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown_Firewall_Widget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AFD87C46425342AFA2F65394 /* Pods-Lockdown VPN Widget.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown VPN Widget.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown VPN Widget/Pods-Lockdown VPN Widget.debug.xcconfig"; sourceTree = ""; }; B2AFAE1E2F56A1CA9EC153D4 /* Pods-LockdownTunnel-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownTunnel-metadata.plist"; path = "Pods/Pods-LockdownTunnel-metadata.plist"; sourceTree = ""; }; - BD91685BB7BEC612310EC3ED /* Pods_LockdownTunnel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LockdownTunnel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B52A28F4337A6E086CB1F96A /* Pods_Lockdown_Firewall_Widget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown_Firewall_Widget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BBFF5B209ADA1F1C8193F194 /* Pods_Lockdown_VPN_Widget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown_VPN_Widget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CB1223BE1CDCC2555FE71352 /* Pods-LockdownIntents.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LockdownIntents.debug.xcconfig"; path = "Pods/Target Support Files/Pods-LockdownIntents/Pods-LockdownIntents.debug.xcconfig"; sourceTree = ""; }; + D2EBAB60D2371A51A7E6A92C /* Pods-Lockdown Firewall Widget.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown Firewall Widget.release.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown Firewall Widget/Pods-Lockdown Firewall Widget.release.xcconfig"; sourceTree = ""; }; + DEA35788129374068081E375 /* Pods-Lockdown.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown/Pods-Lockdown.debug.xcconfig"; sourceTree = ""; }; + E2AF55B2F4F7EBC056DED63C /* Pods_LockdownTunnel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LockdownTunnel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E2ED1B8AB6C3D86D7147AF02 /* Pods-Lockdown.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown.release.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown/Pods-Lockdown.release.xcconfig"; sourceTree = ""; }; E4A025BF9012D4E6454AE1D6 /* Pods-Lockdown-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown-metadata.plist"; path = "Pods/Pods-Lockdown-metadata.plist"; sourceTree = ""; }; - ECA3DC3801B375B860C15978 /* Pods-Lockdown.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown/Pods-Lockdown.debug.xcconfig"; sourceTree = ""; }; - FA3DBA07D389FDA260B448B7 /* Pods-Lockdown.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown.release.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown/Pods-Lockdown.release.xcconfig"; sourceTree = ""; }; + EB84A575169097A4A77345F6 /* Pods-LockdownIntentsUI-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownIntentsUI-settings-metadata.plist"; path = "Settings.bundle/Pods-LockdownIntentsUI-settings-metadata.plist"; sourceTree = ""; }; + EEF3612A1027943CE53E6070 /* Pods-LockdownIntents-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-LockdownIntents-metadata.plist"; path = "Pods/Pods-LockdownIntents-metadata.plist"; sourceTree = ""; }; + FFE976725BA472E2F1CE7B83 /* Pods_LockdownTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LockdownTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 0B4B8AFB29412BD50060C072 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0B4B8B0029412BD50060C072 /* Intents.framework in Frameworks */, + 72251A33DE32085D77F6855E /* Pods_LockdownIntents.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3DBD57B822FD727900DE189F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -711,7 +977,7 @@ 3D94AAF022FD7BFA0012B0DE /* NetworkExtension.framework in Frameworks */, 3D92FF5F26CB8FA0008458C0 /* CocoaLumberjack.xcframework in Frameworks */, 3DBD57BC22FD727900DE189F /* NotificationCenter.framework in Frameworks */, - 3B392ABD6D5830F835A8F8F5 /* Pods_Lockdown_Firewall_Widget.framework in Frameworks */, + 5B535D3E36C811A6CDFC5F61 /* Pods_Lockdown_Firewall_Widget.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -719,7 +985,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - B05197BCB274B38A23A35CAF /* Pods_LockdownTests.framework in Frameworks */, + EF0E051F58B4991FA5262206 /* Pods_LockdownTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -747,7 +1013,7 @@ 3DAF907922EFD70200FB29E0 /* NetworkExtension.framework in Frameworks */, 3D201E7B26CC8C6000F2CC0E /* Yaml.xcframework in Frameworks */, 3D8A254C27487B35001B99E1 /* NEKit.framework in Frameworks */, - 2DDD126D8F073FC42E1C6A28 /* Pods_Lockdown.framework in Frameworks */, + F0FA63CE72E377E80B262592 /* Pods_Lockdown.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -760,7 +1026,7 @@ A12473F41FE44285008493B8 /* NotificationCenter.framework in Frameworks */, 3D92FF4026CB8F96008458C0 /* CocoaLumberjack.xcframework in Frameworks */, A1E78D13207BE58C007FAE70 /* CloudKit.framework in Frameworks */, - 39FD925FB5AC24FB6118A2E8 /* Pods_Lockdown_VPN_Widget.framework in Frameworks */, + 200B3ACD2F983ADCB1128C42 /* Pods_Lockdown_VPN_Widget.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -784,13 +1050,399 @@ 3D201EE326CC8CD400F2CC0E /* CocoaLumberjackSwift.xcframework in Frameworks */, A1FCDA6322C7616400C928BC /* NetworkExtension.framework in Frameworks */, 3D201E8B26CC8C6600F2CC0E /* lwip.xcframework in Frameworks */, - E82C3745DDABA3CDE9D1D776 /* Pods_LockdownTunnel.framework in Frameworks */, + FF6327EF056A4C0DCA5E8123 /* Pods_LockdownTunnel.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0B087B00291C1839006953DA /* EnableNotifications */ = { + isa = PBXGroup; + children = ( + 0B087B06291C184C006953DA /* EnableNotificationsViewController.xib */, + 0B087B08291C1852006953DA /* EnableNotificationsViewController.swift */, + ); + name = EnableNotifications; + sourceTree = ""; + }; + 0B1C8ECF28E44A6B00E9CA57 /* CommonUI */ = { + isa = PBXGroup; + children = ( + 0B2205C829142A6C002F2562 /* FloatingInput */, + 0B1C8ED028E44A7E00E9CA57 /* UIView */, + ); + path = CommonUI; + sourceTree = ""; + }; + 0B1C8ED028E44A7E00E9CA57 /* UIView */ = { + isa = PBXGroup; + children = ( + 0B1C8ED128E44A8800E9CA57 /* ProgressView.swift */, + 0B1FFD62293E16BF0027341C /* Separator.swift */, + ); + path = UIView; + sourceTree = ""; + }; + 0B2205C829142A6C002F2562 /* FloatingInput */ = { + isa = PBXGroup; + children = ( + 0B2205C629142601002F2562 /* FloatingTextInputTextField.swift */, + 0B2205C929142A84002F2562 /* TextBox.swift */, + 0B2205CB29142AA2002F2562 /* TextInputState.swift */, + 0B2205CD29142ACD002F2562 /* TextBoxLabel.swift */, + ); + path = FloatingInput; + sourceTree = ""; + }; + 0B28DEDA2913FA0200D35866 /* SignUpAndLogin */ = { + isa = PBXGroup; + children = ( + 0B28DEE02913FA3300D35866 /* ViewController */, + ); + name = SignUpAndLogin; + sourceTree = ""; + }; + 0B28DEE02913FA3300D35866 /* ViewController */ = { + isa = PBXGroup; + children = ( + 0B2205BD291416D4002F2562 /* SignUpViewController.xib */, + 0B2205C4291416DE002F2562 /* SignUpViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 0B3AB82B29350B2800B254C0 /* Business Logic */ = { + isa = PBXGroup; + children = ( + 0B3AB82D29350BCA00B254C0 /* Models */, + 0B3AB82E29350BD100B254C0 /* Services */, + ); + name = "Business Logic"; + sourceTree = ""; + }; + 0B3AB82D29350BCA00B254C0 /* Models */ = { + isa = PBXGroup; + children = ( + 0B4EF29F293A98F600A32554 /* LockdownUser.swift */, + ); + name = Models; + sourceTree = ""; + }; + 0B3AB82E29350BD100B254C0 /* Services */ = { + isa = PBXGroup; + children = ( + 0B1B6AC3293A9977003DCF72 /* UserService.swift */, + 0BECE944294B712500F3BFB8 /* CountdownDisplayService.swift */, + 0BECE94D294D11C600F3BFB8 /* PaywallService.swift */, + ); + name = Services; + sourceTree = ""; + }; + 0B3AB83329350C1400B254C0 /* DeleteAccount */ = { + isa = PBXGroup; + children = ( + 0B3AB83429350C2400B254C0 /* ViewController */, + ); + name = DeleteAccount; + sourceTree = ""; + }; + 0B3AB83429350C2400B254C0 /* ViewController */ = { + isa = PBXGroup; + children = ( + 0B1B6AC7293A9AF1003DCF72 /* DeleteMyAccountViewController.xib */, + 0B1B6AC8293A9AF1003DCF72 /* DeleteMyAccountViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 0B3AB83A29354B8500B254C0 /* Constants */ = { + isa = PBXGroup; + children = ( + 0B3AB83B29354B9100B254C0 /* LockdownGradient.swift */, + 0B564B67294368C00082C27C /* EmailAddress.swift */, + ); + path = Constants; + sourceTree = ""; + }; + 0B4B8B0129412BD50060C072 /* LockdownIntents */ = { + isa = PBXGroup; + children = ( + 0B4B8B5329420AA50060C072 /* LockdownIntents.entitlements */, + 0B4B8B0229412BD50060C072 /* IntentHandler.swift */, + 0B4B8B0429412BD50060C072 /* Info.plist */, + 0B4B8B232941352A0060C072 /* DisableVPNIntentHandler.swift */, + 0B4B8B25294135350060C072 /* EnableVPNIntentHandler.swift */, + ); + path = LockdownIntents; + sourceTree = ""; + }; + 0B4E081E28C8EBC400129620 /* Splashscreen */ = { + isa = PBXGroup; + children = ( + 0B4E082428C8EBCE00129620 /* ViewController */, + ); + name = Splashscreen; + sourceTree = ""; + }; + 0B4E082428C8EBCE00129620 /* ViewController */ = { + isa = PBXGroup; + children = ( + 0B4E082728C8EBED00129620 /* SplashscreenViewController.xib */, + 0B4E082528C8EBE000129620 /* SplashscreenViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 0B5CF1CF28B52A4600619E98 /* Services */ = { + isa = PBXGroup; + children = ( + 0BCD08A1293FDD0200DC6DD1 /* ConnectivityService */, + 0B1B6AC5293A99C1003DCF72 /* EmailComposable.swift */, + 0B84454D2940BDE300FA2F35 /* EmailValidatable.swift */, + 0BECE95C294F624B00F3BFB8 /* Keychainable.swift */, + 0B4B8B1F29412C9C0060C072 /* IntentService.swift */, + 0BECE9642950E27700F3BFB8 /* ProductPurchasable.swift */, + ); + path = Services; + sourceTree = ""; + }; + 0B6C410728BCEE4800DBE368 /* Presentation */ = { + isa = PBXGroup; + children = ( + 0B4E081E28C8EBC400129620 /* Splashscreen */, + 0B3AB83329350C1400B254C0 /* DeleteAccount */, + 0B9D69C328E1AA5700A3EFCE /* Onboarding */, + 0B28DEDA2913FA0200D35866 /* SignUpAndLogin */, + 0B087B00291C1839006953DA /* EnableNotifications */, + 0B6C410D28BCEE5200DBE368 /* Paywalls */, + ); + name = Presentation; + sourceTree = ""; + }; + 0B6C410D28BCEE5200DBE368 /* Paywalls */ = { + isa = PBXGroup; + children = ( + 0B6C411628BCEEED00DBE368 /* Model */, + 0BE113F3293A65800070DC51 /* View */, + 0BE5A1D628C5F38B00D180DC /* TablePaywall */, + 0B99BA55294A261600A47A45 /* ChristmasPaywall */, + ); + name = Paywalls; + sourceTree = ""; + }; + 0B6C411228BCEED200DBE368 /* ViewController */ = { + isa = PBXGroup; + children = ( + 0B1B6ACB293A9B1E003DCF72 /* TablePaywallViewController.xib */, + 0B1B6ACC293A9B1E003DCF72 /* TablePaywallViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 0B6C411628BCEEED00DBE368 /* Model */ = { + isa = PBXGroup; + children = ( + 0B3E5FCC28BFD5A500BAB0E7 /* AppStoreProductGroup.swift */, + 0B3AB8162935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift */, + 0B99BA4B294903F100A47A45 /* PaywallDisplayConfiguration.swift */, + ); + name = Model; + sourceTree = ""; + }; + 0B8062372860A195003EFF32 /* Core */ = { + isa = PBXGroup; + children = ( + 0BB5629129437C1300EEE4B5 /* PropertyWrappers */, + 0B3AB83A29354B8500B254C0 /* Constants */, + 0BA77E2228E45C8C007ECE45 /* Helpers */, + 0B1C8ECF28E44A6B00E9CA57 /* CommonUI */, + 0B5CF1CF28B52A4600619E98 /* Services */, + 0B80623D2860A19F003EFF32 /* Extensions */, + ); + path = Core; + sourceTree = ""; + }; + 0B80623D2860A19F003EFF32 /* Extensions */ = { + isa = PBXGroup; + children = ( + 0BECE9892951F3B800F3BFB8 /* String */, + 0BECF18128C6091D0092D430 /* UICollectionView+Dequeue.swift */, + 0BF1E99E28C67E1700ACD0FD /* NibLoadable.swift */, + 0BF1E9A728C696F900ACD0FD /* UIView+Ext.swift */, + 0B2205CF2915B093002F2562 /* UIViewController+Ext.swift */, + 0B6DC53F292E402A00DCB8E8 /* Font+Ext.swift */, + 0B3AB7E92934EFAD00B254C0 /* UIView+Corners.swift */, + 0BE113F9293A6B220070DC51 /* CALayer+Ext.swift */, + 0B1FFD5B293E15AA0027341C /* UIStackView+Ext.swift */, + 0BECE950294D1F8E00F3BFB8 /* Date+Ext.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 0B99BA54294A260800A47A45 /* Model */ = { + isa = PBXGroup; + children = ( + 0B1B6AD7293AA464003DCF72 /* PaywallTableFeature.swift */, + ); + name = Model; + sourceTree = ""; + }; + 0B99BA55294A261600A47A45 /* ChristmasPaywall */ = { + isa = PBXGroup; + children = ( + 0B99BA56294A264800A47A45 /* ViewController */, + ); + name = ChristmasPaywall; + sourceTree = ""; + }; + 0B99BA56294A264800A47A45 /* ViewController */ = { + isa = PBXGroup; + children = ( + 0B99BA60294A266E00A47A45 /* ChristmasPaywallViewController.xib */, + 0B99BA57294A266800A47A45 /* ChristmasPaywallViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 0B9D69C328E1AA5700A3EFCE /* Onboarding */ = { + isa = PBXGroup; + children = ( + 0B9D69CE28E1C0CF00A3EFCE /* Model */, + 0B9D69C928E1AA6100A3EFCE /* ViewController */, + ); + name = Onboarding; + sourceTree = ""; + }; + 0B9D69C928E1AA6100A3EFCE /* ViewController */ = { + isa = PBXGroup; + children = ( + 0B9D69CC28E1AB3100A3EFCE /* OnboardingViewController.xib */, + 0B9D69CA28E1AB2600A3EFCE /* OnboardingViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 0B9D69CE28E1C0CF00A3EFCE /* Model */ = { + isa = PBXGroup; + children = ( + 0B9D69CF28E1C0E700A3EFCE /* OnboardingPage.swift */, + 0BA77E2728E46F61007ECE45 /* OnboardingConfiguration.swift */, + 0B312AD32910411E00A218FB /* OnboardingPageContent.swift */, + ); + name = Model; + sourceTree = ""; + }; + 0B9D69D128E1C13500A3EFCE /* Resources */ = { + isa = PBXGroup; + children = ( + 0B9D69D228E1C18D00A3EFCE /* Videos */, + A1141A1B1F46230500F54698 /* Assets.xcassets */, + 0B99BA362948FE6300A47A45 /* LockdownIntents.intentdefinition */, + ); + path = Resources; + sourceTree = ""; + }; + 0B9D69D228E1C18D00A3EFCE /* Videos */ = { + isa = PBXGroup; + children = ( + 0B9D69D328E1C19E00A3EFCE /* Onboarding */, + ); + path = Videos; + sourceTree = ""; + }; + 0B9D69D328E1C19E00A3EFCE /* Onboarding */ = { + isa = PBXGroup; + children = ( + 0B9D69D428E1C1C700A3EFCE /* Onboarding_installation.mp4 */, + 0B0CCD422901B25300F5C632 /* Onboarding_launch_firewall.mp4 */, + 0B0CCD442901B28100F5C632 /* Onboarding_showcase_vpn.mp4 */, + 0B363A2628F56B6600E881BF /* Onboarding_blocklists.mp4 */, + 0BA77E2028E44FB7007ECE45 /* Onboarding_notifications.mp4 */, + ); + path = Onboarding; + sourceTree = ""; + }; + 0BA24F6A293DFD9D007FEA9B /* TablePaywallTableHeaderView */ = { + isa = PBXGroup; + children = ( + 0BA24F70293DFE28007FEA9B /* TablePaywallHeaderView.xib */, + 0BA24F72293DFE3B007FEA9B /* TablePaywallHeaderView.swift */, + ); + path = TablePaywallTableHeaderView; + sourceTree = ""; + }; + 0BA77E2228E45C8C007ECE45 /* Helpers */ = { + isa = PBXGroup; + children = ( + 0BA77E2328E45C94007ECE45 /* WeakObject.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + 0BB5629129437C1300EEE4B5 /* PropertyWrappers */ = { + isa = PBXGroup; + children = ( + 0BB5629729437C1B00EEE4B5 /* UserDefault.swift */, + 0BA77E2528E45F73007ECE45 /* ArrayRestrictable.swift */, + ); + path = PropertyWrappers; + sourceTree = ""; + }; + 0BCD08A1293FDD0200DC6DD1 /* ConnectivityService */ = { + isa = PBXGroup; + children = ( + 0BCD08A2293FDD0A00DC6DD1 /* ConnectionState.swift */, + 0BCD089F293FDCF400DC6DD1 /* ConnectivityService.swift */, + ); + path = ConnectivityService; + sourceTree = ""; + }; + 0BCD08A42940AE6A00DC6DD1 /* Core */ = { + isa = PBXGroup; + children = ( + 0BCD08A52940AE8600DC6DD1 /* ConnectivityServiceTests.swift */, + 0B84454F2940CE2A00FA2F35 /* EmailValidatableTests.swift */, + ); + path = Core; + sourceTree = ""; + }; + 0BE113F3293A65800070DC51 /* View */ = { + isa = PBXGroup; + children = ( + 0BA24F6A293DFD9D007FEA9B /* TablePaywallTableHeaderView */, + 0BE113FB293A946C0070DC51 /* TablePaywallTableRowView */, + ); + name = View; + sourceTree = ""; + }; + 0BE113FB293A946C0070DC51 /* TablePaywallTableRowView */ = { + isa = PBXGroup; + children = ( + 0BE113FC293A94930070DC51 /* TablePaywallFeatureRowView.xib */, + 0B1B6AD9293DFB50003DCF72 /* TablePaywallFeatureRowView.swift */, + ); + name = TablePaywallTableRowView; + sourceTree = ""; + }; + 0BE5A1D628C5F38B00D180DC /* TablePaywall */ = { + isa = PBXGroup; + children = ( + 0B99BA54294A260800A47A45 /* Model */, + 0B6C411228BCEED200DBE368 /* ViewController */, + ); + name = TablePaywall; + sourceTree = ""; + }; + 0BECE9892951F3B800F3BFB8 /* String */ = { + isa = PBXGroup; + children = ( + 0B80623E2860A1DB003EFF32 /* String+Localized.swift */, + 0BECE98A2951F3C300F3BFB8 /* String+URL.swift */, + ); + path = String; + sourceTree = ""; + }; 3D0711B922FE79FF00391C6E /* Why Trust */ = { isa = PBXGroup; children = ( @@ -950,7 +1602,7 @@ 3DBD57A422FBBA4600DE189F /* VPN */ = { isa = PBXGroup; children = ( - A12186261FB8F691007058B3 /* SignupViewController.swift */, + A12186261FB8F691007058B3 /* OldSignupViewController.swift */, 3DBD57B522FD00BB00DE189F /* SetRegionCell.swift */, 3DBD57B322FCFF2400DE189F /* SetRegionViewController.swift */, 3D5F5A0B23109ABB004C3860 /* WhatIsVpnViewController.swift */, @@ -999,12 +1651,12 @@ 7C0D11102473EDFD00A26E04 /* Services */ = { isa = PBXGroup; children = ( + 0BE9DD68285A59710023B555 /* OneTimeActions.swift */, + 0BE9DD6E285A59930023B555 /* BlockDayLog.swift */, 7C0D11112473EE2E00A26E04 /* DomainNameValidator.swift */, - 7C6619BB247810E2005E8BB1 /* BlockDayLog.swift */, - 7C3E8D20247D8057004B81D6 /* PushNotifications.swift */, - 7C1AE072247FD82A0000A7D3 /* PushNotificationsAuthorization.swift */, + 0BE9DD80285A59F20023B555 /* PushNotifications.swift */, + 0BE9DD86285A5A070023B555 /* PushNotificationsAuthorization.swift */, 7C1AE074247FE1FB0000A7D3 /* PushNotificationsAuthorizationUI.swift */, - 7C1AE079247FF87E0000A7D3 /* OneTimeActions.swift */, 7C44081A2539BCCE003FAD1E /* ProtectedFileAccess.swift */, ); name = Services; @@ -1013,6 +1665,7 @@ 7C0D111B2473FC7E00A26E04 /* LockdownTests */ = { isa = PBXGroup; children = ( + 0BCD08A42940AE6A00DC6DD1 /* Core */, 7C0D111C2473FC7E00A26E04 /* LockdownTests.swift */, 7C0D11242473FD6500A26E04 /* DomainNameValidatorTests.swift */, 7C0D111E2473FC7E00A26E04 /* Info.plist */, @@ -1098,10 +1751,10 @@ A12473F51FE44285008493B8 /* Today */, A1FCDA4222C0651300C928BC /* Lockdown Tunnel */, A1DBA19521B82F72008A9322 /* LICENSE.md */, - A1141A1B1F46230500F54698 /* Assets.xcassets */, A1FCDA8322CDE5ED00C928BC /* Block Lists */, 3D44377922DFB22600908CDC /* Fonts */, 7C9A936F251E1EC700DA5721 /* LockdownFirewallWidget */, + 0B4B8B0129412BD50060C072 /* LockdownIntents */, A1141A421F46233600F54698 /* Frameworks */, A17A6A2C202B44BB00657B9E /* Modified Pods */, 3DBD57CB22FD74D700DE189F /* Tests */, @@ -1120,6 +1773,7 @@ 3DBD57BB22FD727900DE189F /* Lockdown Firewall Widget.appex */, 7C0D111A2473FC7E00A26E04 /* LockdownTests.xctest */, 7C9A936A251E1EC700DA5721 /* LockdownFirewallWidgetExtension.appex */, + 0B4B8AFE29412BD50060C072 /* LockdownIntents.appex */, ); name = Products; sourceTree = ""; @@ -1127,10 +1781,14 @@ A1141A131F46230500F54698 /* LockdowniOS */ = { isa = PBXGroup; children = ( + 3DFD0F8422F0F773002A3F25 /* Main */, + 0B9D69D128E1C13500A3EFCE /* Resources */, + 0B3AB82B29350B2800B254C0 /* Business Logic */, + 0B6C410728BCEE4800DBE368 /* Presentation */, + 0B8062372860A195003EFF32 /* Core */, 7C422EAC25279755007F9C22 /* Screens */, 7C422E95252796E2007F9C22 /* Views */, 7C1AE07E248028E40000A7D3 /* Extensions */, - 3DFD0F8422F0F773002A3F25 /* Main */, 3DBD57A322FBB97D00DE189F /* Firewall */, 3D0711B922FE79FF00391C6E /* Why Trust */, 3DBD57A422FBBA4600DE189F /* VPN */, @@ -1168,11 +1826,14 @@ A1912FE91F58B2D00007F6D4 /* NotificationCenter.framework */, 7C9A936B251E1EC700DA5721 /* WidgetKit.framework */, 7C9A936D251E1EC700DA5721 /* SwiftUI.framework */, - 4CA426CE326B009F3E4493D1 /* Pods_Lockdown.framework */, - AE616CC875B5A9730B25D559 /* Pods_Lockdown_Firewall_Widget.framework */, - 6FE1DD136CBC73DC96B7009A /* Pods_Lockdown_VPN_Widget.framework */, - 1ACB305D330C07B959830513 /* Pods_LockdownTests.framework */, - BD91685BB7BEC612310EC3ED /* Pods_LockdownTunnel.framework */, + 0B4B8AFF29412BD50060C072 /* Intents.framework */, + 0B4B8B0A29412BD50060C072 /* IntentsUI.framework */, + 5087F67CEF771725FC357970 /* Pods_Lockdown.framework */, + B52A28F4337A6E086CB1F96A /* Pods_Lockdown_Firewall_Widget.framework */, + BBFF5B209ADA1F1C8193F194 /* Pods_Lockdown_VPN_Widget.framework */, + 20E68D70EBEDB6917C2C9994 /* Pods_LockdownIntents.framework */, + FFE976725BA472E2F1CE7B83 /* Pods_LockdownTests.framework */, + E2AF55B2F4F7EBC056DED63C /* Pods_LockdownTunnel.framework */, ); name = Frameworks; sourceTree = ""; @@ -1192,7 +1853,7 @@ A11E78A21F6A33C5007499CA /* Shared */ = { isa = PBXGroup; children = ( - 3DAF734C2768572300D97BB0 /* FirewallUtilities.swift */, + 0BE9DD74285A59B70023B555 /* FirewallUtilities.swift */, 3DCA4F2D22F190720017740D /* Client.swift */, 3DCA4F3022F190AE0017740D /* ClientModels.swift */, 3D94AB1122FE3A460012B0DE /* Environment.swift */, @@ -1201,9 +1862,9 @@ A1E748191F9108B6004B8021 /* SpeedTest.swift */, A1DBA18921B77C80008A9322 /* VPNController.swift */, A1DBA18521B77C66008A9322 /* VPNSubscription.swift */, - 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */, - 7CE91C582521D54F009D8269 /* UserDefaults.swift */, - 7CE91C702521D58C009D8269 /* Metrics.swift */, + 0BE9DD8C285A5A120023B555 /* UserDefaults.swift */, + 0BE9DD92285A5A200023B555 /* WhitelistUtilities.swift */, + 0BE9DD7A285A59E30023B555 /* Metrics.swift */, 7CE91C952521ED5E009D8269 /* VPNRegion.swift */, 7C0D11102473EDFD00A26E04 /* Services */, ); @@ -1310,16 +1971,21 @@ 2DF472CA81A935DEF14D7039 /* Pods-Lockdown Firewall Widget-settings-metadata.plist */, 8DA68459884385F76BF86234 /* Pods-LockdownTests-metadata.plist */, 2ADD2E8AC036859E49987E8B /* Pods-LockdownTests-settings-metadata.plist */, - ECA3DC3801B375B860C15978 /* Pods-Lockdown.debug.xcconfig */, - FA3DBA07D389FDA260B448B7 /* Pods-Lockdown.release.xcconfig */, - A2316A93257F196F84916806 /* Pods-Lockdown Firewall Widget.debug.xcconfig */, - 707A358300D4A3D33EFC8749 /* Pods-Lockdown Firewall Widget.release.xcconfig */, - A0BEB57F0C3C4F5746285E07 /* Pods-Lockdown VPN Widget.debug.xcconfig */, - 88E04750DB23687076871F5B /* Pods-Lockdown VPN Widget.release.xcconfig */, - 95899949475D15BB3DB63138 /* Pods-LockdownTests.debug.xcconfig */, - 0B014050F770126F5808AF28 /* Pods-LockdownTests.release.xcconfig */, - 549133AE36DE5B896F995988 /* Pods-LockdownTunnel.debug.xcconfig */, - 7435DF998DFB59C9808F1638 /* Pods-LockdownTunnel.release.xcconfig */, + EEF3612A1027943CE53E6070 /* Pods-LockdownIntents-metadata.plist */, + 89C1B1F2419FFC1E995F10BC /* Pods-LockdownIntents-settings-metadata.plist */, + EB84A575169097A4A77345F6 /* Pods-LockdownIntentsUI-settings-metadata.plist */, + DEA35788129374068081E375 /* Pods-Lockdown.debug.xcconfig */, + E2ED1B8AB6C3D86D7147AF02 /* Pods-Lockdown.release.xcconfig */, + 71855E12B7B5B939F0E2EB29 /* Pods-Lockdown Firewall Widget.debug.xcconfig */, + D2EBAB60D2371A51A7E6A92C /* Pods-Lockdown Firewall Widget.release.xcconfig */, + AFD87C46425342AFA2F65394 /* Pods-Lockdown VPN Widget.debug.xcconfig */, + 12578876980678A072282A2E /* Pods-Lockdown VPN Widget.release.xcconfig */, + CB1223BE1CDCC2555FE71352 /* Pods-LockdownIntents.debug.xcconfig */, + 777648CC444D5A906BBFC729 /* Pods-LockdownIntents.release.xcconfig */, + 1F90F5BA7CFAB9814D768F16 /* Pods-LockdownTests.debug.xcconfig */, + 489F7CCEAADF147B19320AAE /* Pods-LockdownTests.release.xcconfig */, + 628519CE6C4D40332EFBFB08 /* Pods-LockdownTunnel.debug.xcconfig */, + 569A4A1747E7C83C7F07C755 /* Pods-LockdownTunnel.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -1327,11 +1993,29 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 0B4B8AFD29412BD50060C072 /* LockdownIntents */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0B4B8B1C29412BD50060C072 /* Build configuration list for PBXNativeTarget "LockdownIntents" */; + buildPhases = ( + 8A6BAD8C56FAFF0F3160C71F /* [CP] Check Pods Manifest.lock */, + 0B4B8AFA29412BD50060C072 /* Sources */, + 0B4B8AFB29412BD50060C072 /* Frameworks */, + 0B4B8AFC29412BD50060C072 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LockdownIntents; + productName = LockdownIntents; + productReference = 0B4B8AFE29412BD50060C072 /* LockdownIntents.appex */; + productType = "com.apple.product-type.app-extension"; + }; 3DBD57BA22FD727900DE189F /* Lockdown Firewall Widget */ = { isa = PBXNativeTarget; buildConfigurationList = 3DBD57C722FD727900DE189F /* Build configuration list for PBXNativeTarget "Lockdown Firewall Widget" */; buildPhases = ( - FB6DB65AD7DDFC6CFF0AB663 /* [CP] Check Pods Manifest.lock */, + EC657542A18595AF7E0C7EF1 /* [CP] Check Pods Manifest.lock */, 3DBD57B722FD727900DE189F /* Sources */, 3DBD57B822FD727900DE189F /* Frameworks */, 3DBD57B922FD727900DE189F /* Resources */, @@ -1349,11 +2033,11 @@ isa = PBXNativeTarget; buildConfigurationList = 7C0D11232473FC7E00A26E04 /* Build configuration list for PBXNativeTarget "LockdownTests" */; buildPhases = ( - 395667F7EF55688E346D7A56 /* [CP] Check Pods Manifest.lock */, + 85639EE48B7A6DB2A148D31D /* [CP] Check Pods Manifest.lock */, 7C0D11162473FC7E00A26E04 /* Sources */, 7C0D11172473FC7E00A26E04 /* Frameworks */, 7C0D11182473FC7E00A26E04 /* Resources */, - 6B16CD066B7776C0EE46D41D /* [CP] Embed Pods Frameworks */, + CADCF2A9BF3F76AA4C2AF3DD /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1386,14 +2070,15 @@ isa = PBXNativeTarget; buildConfigurationList = A1141A391F46230600F54698 /* Build configuration list for PBXNativeTarget "Lockdown" */; buildPhases = ( - 64FE7C42B237022F592D9853 /* [CP] Check Pods Manifest.lock */, + 8769C5ADEAEA8A1FFB6C5E71 /* [CP] Check Pods Manifest.lock */, 3D94AEC12542A859005FDC0E /* ShellScript */, A1141A0D1F46230500F54698 /* Sources */, A1141A0E1F46230500F54698 /* Frameworks */, A1141A0F1F46230500F54698 /* Resources */, A18B79571F8C36460042A4EF /* Embed App Extensions */, 3DD3D09826CC8714002238E8 /* Embed Frameworks */, - 5A300A6E89DFE7D0A22A5136 /* [CP] Embed Pods Frameworks */, + 0B8062402860BF3F003EFF32 /* Swiftlint Script */, + F5A6A1B34414D2FCBEE6599F /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1403,6 +2088,7 @@ A1FCDA4822C0651300C928BC /* PBXTargetDependency */, 3DBD57C522FD727900DE189F /* PBXTargetDependency */, 7C9A9376251E1EC700DA5721 /* PBXTargetDependency */, + 0B4B8B1729412BD50060C072 /* PBXTargetDependency */, ); name = Lockdown; productName = TrustiOS; @@ -1413,7 +2099,7 @@ isa = PBXNativeTarget; buildConfigurationList = A12473FF1FE44285008493B8 /* Build configuration list for PBXNativeTarget "Lockdown VPN Widget" */; buildPhases = ( - 8DC19F20C31126BB8D4B1222 /* [CP] Check Pods Manifest.lock */, + F4D50C9EBF70B243476CFA9D /* [CP] Check Pods Manifest.lock */, A12473EF1FE44284008493B8 /* Sources */, A12473F01FE44284008493B8 /* Frameworks */, A12473F11FE44284008493B8 /* Resources */, @@ -1448,7 +2134,7 @@ isa = PBXNativeTarget; buildConfigurationList = A1FCDA4A22C0651300C928BC /* Build configuration list for PBXNativeTarget "LockdownTunnel" */; buildPhases = ( - 1883028BE10B38CE14B1FD85 /* [CP] Check Pods Manifest.lock */, + D3515DD495DCDFE3C9225BB5 /* [CP] Check Pods Manifest.lock */, A1FCDA3D22C0651300C928BC /* Sources */, A1FCDA3E22C0651300C928BC /* Frameworks */, A1FCDA3F22C0651300C928BC /* Resources */, @@ -1468,10 +2154,13 @@ A1141A091F46230500F54698 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1310; + LastSwiftUpdateCheck = 1410; LastUpgradeCheck = 1010; ORGANIZATIONNAME = "Confirmed Inc."; TargetAttributes = { + 0B4B8AFD29412BD50060C072 = { + CreatedOnToolsVersion = 14.1; + }; 3DBD57BA22FD727900DE189F = { CreatedOnToolsVersion = 10.3; DevelopmentTeam = V8J3Z26F6Z; @@ -1602,6 +2291,12 @@ fr, ja, es, + ar, + de, + ru, + vi, + "zh-Hant", + "zh-Hans", ); mainGroup = A1141A081F46230500F54698; productRefGroup = A1141A121F46230500F54698 /* Products */; @@ -1621,6 +2316,7 @@ 3DBD57BA22FD727900DE189F /* Lockdown Firewall Widget */, 7C0D11192473FC7E00A26E04 /* LockdownTests */, 7C9A9369251E1EC700DA5721 /* LockdownFirewallWidgetExtension */, + 0B4B8AFD29412BD50060C072 /* LockdownIntents */, ); }; /* End PBXProject section */ @@ -1664,6 +2360,16 @@ /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ + 0B4B8AFC29412BD50060C072 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FBD19586D73B1D8573C2AD98 /* Pods-LockdownIntents-metadata.plist in Resources */, + 0B99BA68294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */, + 510EA50D772A435C202CC370 /* Pods-LockdownIntents-settings-metadata.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3DBD57B922FD727900DE189F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1672,6 +2378,7 @@ 3D3BF4CE233D5E9100D0C482 /* Localizable.strings in Resources */, 3D94AB1022FE0CFB0012B0DE /* MainInterface.storyboard in Resources */, 3D5464D62303839500AE1F73 /* Settings.bundle in Resources */, + 0B99BA65294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */, 601BF3ED11EB7CBF95BF5720 /* Pods-Lockdown Firewall Widget-metadata.plist in Resources */, 388CD7581B88A7E496467546 /* Pods-Lockdown Firewall Widget-settings-metadata.plist in Resources */, ); @@ -1682,6 +2389,7 @@ buildActionMask = 2147483647; files = ( 4A86219093026DE70A097E79 /* Pods-LockdownTests-metadata.plist in Resources */, + 0B99BA66294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */, 78010EFC9ED40D77BD40C924 /* Pods-LockdownTests-settings-metadata.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1691,6 +2399,7 @@ buildActionMask = 2147483647; files = ( 7C0156582521C2F200670CB5 /* Montserrat-Light.ttf in Resources */, + 0B99BA67294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */, 7C0156542521C2F200670CB5 /* Montserrat-Medium.ttf in Resources */, 7C9A9373251E1EC700DA5721 /* Assets.xcassets in Resources */, 7C0156562521C2F200670CB5 /* Montserrat-SemiBold.ttf in Resources */, @@ -1705,21 +2414,30 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0BE113FD293A94930070DC51 /* TablePaywallFeatureRowView.xib in Resources */, + 0B0CCD452901B28200F5C632 /* Onboarding_showcase_vpn.mp4 in Resources */, 3D01D99F2481E42E003A710C /* general_ads.txt in Resources */, A1D85F0A207C562F00B766E0 /* adBlockListTwo.json in Resources */, + 0B9D69D528E1C1C700A3EFCE /* Onboarding_installation.mp4 in Resources */, + 0B2205BE291416D4002F2562 /* SignUpViewController.xib in Resources */, 3D01D99E2481E42B003A710C /* reporting.txt in Resources */, + 0B4E082828C8EBED00129620 /* SplashscreenViewController.xib in Resources */, A1D85F0B207C562F00B766E0 /* adBlockListThree.json in Resources */, A1159FCC207C228300DA4670 /* privacyBlockList.json in Resources */, A1159FCD207C228300DA4670 /* socialBlockList.json in Resources */, A1FCDA8722CDE8C000C928BC /* crypto_mining_ips.txt in Resources */, 3D752C342357FA3B00C163E4 /* SF-Pro-Rounded-Regular.otf in Resources */, + 0BA24F71293DFE28007FEA9B /* TablePaywallHeaderView.xib in Resources */, A1159FCE207C228300DA4670 /* adBlockList.json in Resources */, 3D44378522DFB22600908CDC /* Montserrat-Regular.ttf in Resources */, + 0BA77E2128E44FB7007ECE45 /* Onboarding_notifications.mp4 in Resources */, 3D44378122DFB22600908CDC /* Montserrat-Light.ttf in Resources */, + 0B99BA61294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */, 3DAA6B5322EA988F0018FC09 /* ransomware.txt in Resources */, 3D44378422DFB22600908CDC /* Montserrat-SemiBold.ttf in Resources */, 3D01D97B2480DCB3003A710C /* data_trackers.txt in Resources */, 3D44378022DFB22600908CDC /* Montserrat-Medium.ttf in Resources */, + 0B0CCD432901B25300F5C632 /* Onboarding_launch_firewall.mp4 in Resources */, A15F3C751F79DC8F00B07F03 /* LaunchScreen.storyboard in Resources */, A1FCDA8522CDE60800C928BC /* crypto_mining.txt in Resources */, A1FCDA8B22D3BA1900C928BC /* facebook_inc.txt in Resources */, @@ -1729,6 +2447,7 @@ 3D3BF4CC233D5E9100D0C482 /* Localizable.strings in Resources */, 3D752C352357FA3B00C163E4 /* SF-Pro-Rounded-Medium.otf in Resources */, 3DF5D75F2633B1E100F77D79 /* amazon_trackers.txt in Resources */, + 0B087B07291C184C006953DA /* EnableNotificationsViewController.xib in Resources */, 3D752C372357FA3B00C163E4 /* SF-Pro-Rounded-Semibold.otf in Resources */, 3D752C362357FA3B00C163E4 /* SF-Pro-Rounded-Bold.otf in Resources */, 3D44378222DFB22600908CDC /* Montserrat-Thin.ttf in Resources */, @@ -1736,17 +2455,21 @@ 3DAA6B4F22EA76420018FC09 /* clickbait.txt in Resources */, A1FCDA8D22D3C50A00C928BC /* email_opens.txt in Resources */, A1141A1C1F46230500F54698 /* Assets.xcassets in Resources */, + 0B363A2728F56B6600E881BF /* Onboarding_blocklists.mp4 in Resources */, 3D44378322DFB22600908CDC /* Montserrat-Bold.ttf in Resources */, A1FCDA9122D3D52C00C928BC /* facebook_inc_ipv6.txt in Resources */, 3D5F5A0823107C1E004C3860 /* game_ads.txt in Resources */, 3D5F5A0A23107EB8004C3860 /* snapchat_analytics.txt in Resources */, 3DCFE6FB244945A100EA9B35 /* marketing_beta.txt in Resources */, 3D0971DA22EBAD4C00CCD326 /* marketing.txt in Resources */, + 0B1B6ACD293A9B1E003DCF72 /* TablePaywallViewController.xib in Resources */, + 0B1B6AC9293A9AF1003DCF72 /* DeleteMyAccountViewController.xib in Resources */, 7C3EFA042486879800719D96 /* tracker_info.json in Resources */, A1141A1A1F46230500F54698 /* Main.storyboard in Resources */, 3D5464D323037CCA00AE1F73 /* Settings.bundle in Resources */, 20816D1FD569053C0994232B /* Pods-Lockdown-metadata.plist in Resources */, C9E66BB880A29A48D055FBFF /* Pods-Lockdown-settings-metadata.plist in Resources */, + 0B9D69CD28E1AB3100A3EFCE /* OnboardingViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1758,6 +2481,7 @@ 3D3BF4CD233D5E9100D0C482 /* Localizable.strings in Resources */, A12473FA1FE44285008493B8 /* MainInterface.storyboard in Resources */, 3D5464D42303839200AE1F73 /* Settings.bundle in Resources */, + 0B99BA63294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */, 90728B81560C790FD5A02A6B /* Pods-Lockdown VPN Widget-metadata.plist in Resources */, 1579100974C8086B190B35BB /* Pods-Lockdown VPN Widget-settings-metadata.plist in Resources */, ); @@ -1771,6 +2495,7 @@ A1D85F07207C4C8300B766E0 /* adBlockListTwo.json in Resources */, A1D85F09207C52A000B766E0 /* adBlockListThree.json in Resources */, A1159FCB207C201A00DA4670 /* privacyBlockList.json in Resources */, + 0B99BA62294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */, A1931CFE20791F5900E695EB /* adBlockList.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1781,6 +2506,7 @@ files = ( 3D5464D52303839400AE1F73 /* Settings.bundle in Resources */, 5666ABC4D0064E4669D1943F /* Pods-LockdownTunnel-metadata.plist in Resources */, + 0B99BA64294A266E00A47A45 /* ChristmasPaywallViewController.xib in Resources */, 5647ACFEBBAB001FAE27CAF9 /* Pods-LockdownTunnel-settings-metadata.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1788,7 +2514,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1883028BE10B38CE14B1FD85 /* [CP] Check Pods Manifest.lock */ = { + 0B8062402860BF3F003EFF32 /* Swiftlint Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1796,21 +2522,17 @@ inputFileListPaths = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "Swiftlint Script"; outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-LockdownTunnel-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "${PODS_ROOT}/SwiftLint/swiftlint\n"; }; - 395667F7EF55688E346D7A56 /* [CP] Check Pods Manifest.lock */ = { + 3D94AEC12542A859005FDC0E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1818,21 +2540,16 @@ inputFileListPaths = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-LockdownTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "if which bartycrouch > /dev/null; then\n bartycrouch update -x\n bartycrouch lint -x\nelse\n echo \"warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch\"\nfi\n"; }; - 3D94AEC12542A859005FDC0E /* ShellScript */ = { + 85639EE48B7A6DB2A148D31D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1840,54 +2557,43 @@ inputFileListPaths = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-LockdownTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which bartycrouch > /dev/null; then\n bartycrouch update -x\n bartycrouch lint -x\nelse\n echo \"warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch\"\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 5A300A6E89DFE7D0A22A5136 /* [CP] Embed Pods Frameworks */ = { + 8769C5ADEAEA8A1FFB6C5E71 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Lockdown/Pods-Lockdown-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/AwesomeSpotlightView/AwesomeSpotlightView.framework", - "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DynamicBlurView/DynamicBlurView.framework", - "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework", - "${BUILT_PRODUCTS_DIR}/NicoProgress/NicoProgress.framework", - "${BUILT_PRODUCTS_DIR}/PopupDialog/PopupDialog.framework", - "${BUILT_PRODUCTS_DIR}/PromiseKit/PromiseKit.framework", - "${BUILT_PRODUCTS_DIR}/RQShineLabel/RQShineLabel.framework", - "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", - "${BUILT_PRODUCTS_DIR}/SwiftMessages/SwiftMessages.framework", - "${BUILT_PRODUCTS_DIR}/SwiftyStoreKit/SwiftyStoreKit.framework", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AwesomeSpotlightView.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DynamicBlurView.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NicoProgress.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PopupDialog.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PromiseKit.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RQShineLabel.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftMessages.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyStoreKit.framework", + "$(DERIVED_FILE_DIR)/Pods-Lockdown-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Lockdown/Pods-Lockdown-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 64FE7C42B237022F592D9853 /* [CP] Check Pods Manifest.lock */ = { + 8A6BAD8C56FAFF0F3160C71F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1902,14 +2608,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Lockdown-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-LockdownIntents-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 6B16CD066B7776C0EE46D41D /* [CP] Embed Pods Frameworks */ = { + CADCF2A9BF3F76AA4C2AF3DD /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1927,7 +2633,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LockdownTests/Pods-LockdownTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 8DC19F20C31126BB8D4B1222 /* [CP] Check Pods Manifest.lock */ = { + D3515DD495DCDFE3C9225BB5 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1942,14 +2648,14 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Lockdown VPN Widget-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-LockdownTunnel-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - FB6DB65AD7DDFC6CFF0AB663 /* [CP] Check Pods Manifest.lock */ = { + EC657542A18595AF7E0C7EF1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1971,31 +2677,123 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + F4D50C9EBF70B243476CFA9D /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Lockdown VPN Widget-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F5A6A1B34414D2FCBEE6599F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Lockdown/Pods-Lockdown-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/AwesomeSpotlightView/AwesomeSpotlightView.framework", + "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework", + "${BUILT_PRODUCTS_DIR}/DynamicBlurView/DynamicBlurView.framework", + "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework", + "${BUILT_PRODUCTS_DIR}/NicoProgress/NicoProgress.framework", + "${BUILT_PRODUCTS_DIR}/PopupDialog/PopupDialog.framework", + "${BUILT_PRODUCTS_DIR}/PromiseKit/PromiseKit.framework", + "${BUILT_PRODUCTS_DIR}/RQShineLabel/RQShineLabel.framework", + "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", + "${BUILT_PRODUCTS_DIR}/SwiftMessages/SwiftMessages.framework", + "${BUILT_PRODUCTS_DIR}/SwiftyStoreKit/SwiftyStoreKit.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AwesomeSpotlightView.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DynamicBlurView.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/NicoProgress.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PopupDialog.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PromiseKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RQShineLabel.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftMessages.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyStoreKit.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Lockdown/Pods-Lockdown-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 0B4B8AFA29412BD50060C072 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0B4B8B4A2941387F0060C072 /* Environment.swift in Sources */, + 0B99BA2C2948EB7B00A47A45 /* PaywallTableFeature.swift in Sources */, + 0B99BA2E2948EBCC00A47A45 /* AccountUI.swift in Sources */, + 0B99BA2D2948EB8500A47A45 /* SubscriptionOfferPeriodUnit.swift in Sources */, + 0B4B8B242941352A0060C072 /* DisableVPNIntentHandler.swift in Sources */, + 0B99BA212948EB3E00A47A45 /* UserService.swift in Sources */, + 0B4B8B2F294136340060C072 /* UserDefaults.swift in Sources */, + 0B4B8B0329412BD50060C072 /* IntentHandler.swift in Sources */, + 0B99BA272948EB4C00A47A45 /* LockdownUser.swift in Sources */, + 0B99BA2A2948EB6500A47A45 /* Client.swift in Sources */, + 0B99BA292948EB5800A47A45 /* Shared.swift in Sources */, + 0B99BA53294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */, + 0BB39F302948AD7A002C9E57 /* String+Localized.swift in Sources */, + 0B99BA2B2948EB7300A47A45 /* AppStoreProductGroup.swift in Sources */, + 0B99BA282948EB5000A47A45 /* ClientModels.swift in Sources */, + 0B99BA322948EDFA00A47A45 /* VPNController.swift in Sources */, + 0B99BA342948FE6300A47A45 /* LockdownIntents.intentdefinition in Sources */, + 0B4B8B26294135350060C072 /* EnableVPNIntentHandler.swift in Sources */, + 0B4B8B512941388B0060C072 /* VPNRegion.swift in Sources */, + 0BECE952294D1F8E00F3BFB8 /* Date+Ext.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 3DBD57B722FD727900DE189F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3D94AAF422FDEAC80012B0DE /* WhitelistUtilities.swift in Sources */, - 7C1AE078247FE2010000A7D3 /* PushNotificationsAuthorization.swift in Sources */, - 7C6619BE247810EE005E8BB1 /* BlockDayLog.swift in Sources */, + 0BE9DD84285A59F20023B555 /* PushNotifications.swift in Sources */, 3D94AAF522FDEACD0012B0DE /* VPNController.swift in Sources */, + 0BE9DD72285A59930023B555 /* BlockDayLog.swift in Sources */, 3D94AAF122FDEAC00012B0DE /* Client.swift in Sources */, - 7C1AE07D247FF87F0000A7D3 /* OneTimeActions.swift in Sources */, - 3DAF73552768572300D97BB0 /* FirewallUtilities.swift in Sources */, 3D94AAF722FDEAD70012B0DE /* FirewallController.swift in Sources */, + 0BE9DD8A285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */, + 0BE9DD6C285A59710023B555 /* OneTimeActions.swift in Sources */, 3D94AAFD22FDEB460012B0DE /* VPNSubscription.swift in Sources */, + 0BDBDDEF293E2881009A1184 /* PaywallTableFeature.swift in Sources */, 7CE91C992521ED5E009D8269 /* VPNRegion.swift in Sources */, - 7CD52D83247EC18900D0530F /* PushNotifications.swift in Sources */, 3DBD57BF22FD727900DE189F /* FirewallTodayViewController.swift in Sources */, - 7CE91C682521D565009D8269 /* UserDefaults.swift in Sources */, + 0B99BA50294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */, + 0BE9DD78285A59B70023B555 /* FirewallUtilities.swift in Sources */, + 0BE9DD7E285A59E30023B555 /* Metrics.swift in Sources */, 3D94AB1422FE3BA20012B0DE /* Environment.swift in Sources */, - 3D94AAF822FDEADC0012B0DE /* Shared.swift in Sources */, + 0B3AB812293508DF00B254C0 /* AppStoreProductGroup.swift in Sources */, + 0BE9DD90285A5A120023B555 /* UserDefaults.swift in Sources */, + 0B3AB81B2935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */, + 0BE9DD96285A5A200023B555 /* WhitelistUtilities.swift in Sources */, + 0B8DBDB0286482D600831AC4 /* String+Localized.swift in Sources */, 3D94AAF222FDEAC20012B0DE /* ClientModels.swift in Sources */, - 7CE91C862521D5B7009D8269 /* Metrics.swift in Sources */, 7C44081E2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */, + 0BBEEF402948C3C600F4405E /* Shared.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2004,9 +2802,16 @@ buildActionMask = 2147483647; files = ( 7CD1435F248798D4009206A9 /* TrackerInfoTests.swift in Sources */, + 0B99BA51294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */, + 0B3AB81C2935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */, + 0BDBDDF0293E2881009A1184 /* PaywallTableFeature.swift in Sources */, 7C0D111D2473FC7E00A26E04 /* LockdownTests.swift in Sources */, 7CD52D81247E850D00D0530F /* SnapshotTests.swift in Sources */, + 0B3AB813293508E000B254C0 /* AppStoreProductGroup.swift in Sources */, + 0B8445512940CE4200FA2F35 /* EmailValidatableTests.swift in Sources */, + 0B8445472940AF2700FA2F35 /* ConnectivityServiceTests.swift in Sources */, 7C0D11252473FD6500A26E04 /* DomainNameValidatorTests.swift in Sources */, + 0B8DBDB1286482D700831AC4 /* String+Localized.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2014,18 +2819,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0BE9DD79285A59B70023B555 /* FirewallUtilities.swift in Sources */, 7C9A9384251E1F9C00DA5721 /* LoadingCircle.swift in Sources */, - 3DAF73642768586200D97BB0 /* WhitelistUtilities.swift in Sources */, + 0BDBDDF1293E2882009A1184 /* PaywallTableFeature.swift in Sources */, + 0BE9DD6D285A59710023B555 /* OneTimeActions.swift in Sources */, + 0BE9DD73285A59930023B555 /* BlockDayLog.swift in Sources */, + 0B8DBDB2286482D700831AC4 /* String+Localized.swift in Sources */, 7CE91CA8252214C9009D8269 /* CombinedProvider.swift in Sources */, + 0BE9DD8B285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */, 7C9A9371251E1EC700DA5721 /* LockdownFirewallWidget.swift in Sources */, - 3DAF73622768584500D97BB0 /* BlockDayLog.swift in Sources */, - 3DAF73612768584200D97BB0 /* PushNotifications.swift in Sources */, - 3DAF73632768584D00D97BB0 /* PushNotificationsAuthorization.swift in Sources */, - 3DAF73562768572300D97BB0 /* FirewallUtilities.swift in Sources */, + 0BE9DD91285A5A120023B555 /* UserDefaults.swift in Sources */, + 0B3AB81D2935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */, + 0B99BA52294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */, + 0B3AB814293508E000B254C0 /* AppStoreProductGroup.swift in Sources */, + 0BE9DD97285A5A200023B555 /* WhitelistUtilities.swift in Sources */, + 0BE9DD7F285A59E30023B555 /* Metrics.swift in Sources */, + 0BE9DD85285A59F20023B555 /* PushNotifications.swift in Sources */, 7CE91C9A2521ED5E009D8269 /* VPNRegion.swift in Sources */, - 7CE91C692521D566009D8269 /* UserDefaults.swift in Sources */, - 3DAF73602768583700D97BB0 /* OneTimeActions.swift in Sources */, - 7CE91C872521D5B8009D8269 /* Metrics.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2034,107 +2844,152 @@ buildActionMask = 2147483647; files = ( 3D94AB1222FE3A460012B0DE /* Environment.swift in Sources */, + 0BE113FA293A6B220070DC51 /* CALayer+Ext.swift in Sources */, + 0BECE9652950E27700F3BFB8 /* ProductPurchasable.swift in Sources */, A1DBA18A21B77C80008A9322 /* VPNController.swift in Sources */, 3DBD57B622FD00BC00DE189F /* SetRegionCell.swift in Sources */, 3D47CDB622F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallGridPulse.swift in Sources */, - 7C6619BC247810E2005E8BB1 /* BlockDayLog.swift in Sources */, 3D47CDD622F3C3F3003BD7F7 /* NVActivityIndicatorAnimationOrbit.swift in Sources */, 3D5561D4230B58F30062001D /* PrivacyPolicyViewController.swift in Sources */, 3D47CDBB22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScalePulseOut.swift in Sources */, + 0B99BA4C294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */, + 0BE9DD7B285A59E30023B555 /* Metrics.swift in Sources */, A118F64920B33FED009A64E7 /* CGRectEx.swift in Sources */, 3D47CDBF22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallZigZag.swift in Sources */, - 7C3E8D21247D8057004B81D6 /* PushNotifications.swift in Sources */, 3DBD57A222FBB0D900DE189F /* WebViewViewController.swift in Sources */, A118F64320B33FED009A64E7 /* FadeTransition.swift in Sources */, - 7CE91C592521D54F009D8269 /* UserDefaults.swift in Sources */, 3D47CDC822F3C3F3003BD7F7 /* NVActivityIndicatorAnimationCircleStrokeSpin.swift in Sources */, 3DF2455623A306DB00E46613 /* Loader.swift in Sources */, A118F64720B33FED009A64E7 /* SpinerLayer.swift in Sources */, + 0B1B6ACE293A9B1E003DCF72 /* TablePaywallViewController.swift in Sources */, + 0BA77E2428E45C94007ECE45 /* WeakObject.swift in Sources */, 3D47CDC522F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallRotateChase.swift in Sources */, + 0B9D69D028E1C0E700A3EFCE /* OnboardingPage.swift in Sources */, + 0B4E082628C8EBE000129620 /* SplashscreenViewController.swift in Sources */, A1EBEACD2097AE6E002B9087 /* M13CheckboxFadeController.swift in Sources */, + 0BA77E2828E46F61007ECE45 /* OnboardingConfiguration.swift in Sources */, 3D47CDCF22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationPacman.swift in Sources */, + 0B1B6ACA293A9AF1003DCF72 /* DeleteMyAccountViewController.swift in Sources */, 3D47CDC422F3C3F3003BD7F7 /* NVActivityIndicatorAnimationSquareSpin.swift in Sources */, - 7C1AE07A247FF87F0000A7D3 /* OneTimeActions.swift in Sources */, A1EBEAD72097AE6E002B9087 /* M13CheckboxAddRemovePathGenerator.swift in Sources */, + 0BE9DD93285A5A200023B555 /* WhitelistUtilities.swift in Sources */, 3D970DAD22EC149D00F9CC93 /* BlockLogCell.swift in Sources */, A1EBEACF2097AE6E002B9087 /* M13Checkbox.swift in Sources */, + 0B2205C729142601002F2562 /* FloatingTextInputTextField.swift in Sources */, + 0B80623F2860A1DB003EFF32 /* String+Localized.swift in Sources */, + 0B312AD42910411E00A218FB /* OnboardingPageContent.swift in Sources */, + 0B99BA332948FE6300A47A45 /* LockdownIntents.intentdefinition in Sources */, 3D47CDD522F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulseRise.swift in Sources */, 3D47CDCD22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScale.swift in Sources */, 7CAB283F254336230087AAF4 /* CustomNavigationView.swift in Sources */, + 0BF1E9A828C696F900ACD0FD /* UIView+Ext.swift in Sources */, A1EBEAD22097AE6E002B9087 /* M13CheckboxCheckPathGenerator.swift in Sources */, 3DCA4F3122F190AE0017740D /* ClientModels.swift in Sources */, 3DCFE6FA24493F9000EA9B35 /* marketing_beta.txt in Sources */, 3D0711B822FE79BE00391C6E /* WhyTrustViewController.swift in Sources */, A118F64520B33FED009A64E7 /* TimerEx.swift in Sources */, 7C422EAF252797A6007F9C22 /* AccountVC.swift in Sources */, + 0B99BA58294A266800A47A45 /* ChristmasPaywallViewController.swift in Sources */, 3D47CDD222F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallSpinFadeLoader.swift in Sources */, 3D9FC67723E503DF004122D3 /* EmailSignInViewController.swift in Sources */, + 0B2205CE29142ACD002F2562 /* TextBoxLabel.swift in Sources */, 3DCA4F3322F22CB40017740D /* HomeViewController.swift in Sources */, + 0B087B09291C1852006953DA /* EnableNotificationsViewController.swift in Sources */, 7CE91C962521ED5E009D8269 /* VPNRegion.swift in Sources */, + 0BECE945294B712500F3BFB8 /* CountdownDisplayService.swift in Sources */, 3DCA4F4122F252720017740D /* FirewallController.swift in Sources */, A1359FDA20AF6E32008C4BF7 /* LocalLogger.swift in Sources */, + 0B2205CC29142AA2002F2562 /* TextInputState.swift in Sources */, + 0B4EF2A0293A98F600A32554 /* LockdownUser.swift in Sources */, 3D47CDD122F3C3F3003BD7F7 /* NVActivityIndicatorAnimationCubeTransition.swift in Sources */, 7C422EA525279724007F9C22 /* Align.swift in Sources */, A118F64120B33FED009A64E7 /* TransitionSubmitButton.swift in Sources */, + 0BA24F73293DFE3B007FEA9B /* TablePaywallHeaderView.swift in Sources */, 3DBD57A622FBCD7A00DE189F /* WhitelistViewController.swift in Sources */, A1EBEAD82097AE6E002B9087 /* M13CheckboxBounceController.swift in Sources */, 3DCA4F2E22F190720017740D /* Client.swift in Sources */, + 0BE9DD81285A59F20023B555 /* PushNotifications.swift in Sources */, A101106D202B9D4300807612 /* BaseViewController.swift in Sources */, 7C0D11122473EE2E00A26E04 /* DomainNameValidator.swift in Sources */, + 0B3AB83C29354B9100B254C0 /* LockdownGradient.swift in Sources */, A1EBEADD2097AE6E002B9087 /* M13CheckboxStrokeController.swift in Sources */, A1EBEADA2097AE6E002B9087 /* M13CheckboxFillController.swift in Sources */, A1EBEAD42097AE6E002B9087 /* M13CheckboxPathGenerator.swift in Sources */, A1EBEACB2097AE6E002B9087 /* M13CheckboxDisclosurePathGenerator.swift in Sources */, 3D47CDC622F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulse.swift in Sources */, + 0B3AB8172935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */, + 0B860F6228FE9D06006B0AA7 /* BuildFile in Sources */, + 0BECE951294D1F8E00F3BFB8 /* Date+Ext.swift in Sources */, 3D47CDCB22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallGridBeat.swift in Sources */, 3D47CDCA22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationTriangleSkewSpin.swift in Sources */, 7C422EB72527A2D1007F9C22 /* MainTabBarViewController.swift in Sources */, + 0B564B68294368C00082C27C /* EmailAddress.swift in Sources */, 3D47CDC722F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBlank.swift in Sources */, + 0B3E5FCD28BFD5A500BAB0E7 /* AppStoreProductGroup.swift in Sources */, A1E7481A1F9108B6004B8021 /* SpeedTest.swift in Sources */, 3D970DAF22EC15D800F9CC93 /* BlockLogViewController.swift in Sources */, + 0B2205C5291416DE002F2562 /* SignUpViewController.swift in Sources */, + 0B1B6AC4293A9977003DCF72 /* UserService.swift in Sources */, A1EBEADB2097AE6E002B9087 /* M13CheckboxAnimationGenerator.swift in Sources */, 7CC8EFED254036050005054C /* FirewallRepair.swift in Sources */, + 0B9D69CB28E1AB2600A3EFCE /* OnboardingViewController.swift in Sources */, 3D47CDD422F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScalePulseOutRapid.swift in Sources */, - 7CE91C712521D58C009D8269 /* Metrics.swift in Sources */, - A12186271FB8F691007058B3 /* SignupViewController.swift in Sources */, + A12186271FB8F691007058B3 /* OldSignupViewController.swift in Sources */, A1EBEAD12097AE6E002B9087 /* M13Checkbox+IB.swift in Sources */, A1FCDA5F22C14EB800C928BC /* BlockListGroupCell.swift in Sources */, A1EBEACC2097AE6E002B9087 /* M13CheckboxDotController.swift in Sources */, + 0B1B6ADA293DFB50003DCF72 /* TablePaywallFeatureRowView.swift in Sources */, 7C1AE075247FE1FB0000A7D3 /* PushNotificationsAuthorizationUI.swift in Sources */, + 0BECF18228C6091D0092D430 /* UICollectionView+Dequeue.swift in Sources */, + 0BE9DD87285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */, A1EBEAD92097AE6E002B9087 /* M13CheckboxGestureRecognizer.swift in Sources */, A1FCDA5D22C1301A00C928BC /* BlockListGroupViewController.swift in Sources */, + 0B3AB7EA2934EFAD00B254C0 /* UIView+Corners.swift in Sources */, 3DBD57B022FC14CD00DE189F /* Shared.swift in Sources */, A154A07E215C78180010FFCC /* BlockListCell.swift in Sources */, 3D47CDCE22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScaleRippleMultiple.swift in Sources */, 3D47CDCC22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScaleMultiple.swift in Sources */, A1EBEADC2097AE6E002B9087 /* DefaultValues.swift in Sources */, A1EBEAD02097AE6E002B9087 /* M13CheckboxController.swift in Sources */, + 0B1FFD5C293E15AA0027341C /* UIStackView+Ext.swift in Sources */, 3D47CDB322F3C3F3003BD7F7 /* NVActivityIndicatorView.swift in Sources */, 3D47CDBD22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallZigZagDeflect.swift in Sources */, 3D47CDB022F3C3F3003BD7F7 /* NVActivityIndicatorPresenter.swift in Sources */, + 0BECE94E294D11C600F3BFB8 /* PaywallService.swift in Sources */, 3D47CDC322F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallClipRotatePulse.swift in Sources */, A1EBEAD52097AE6E002B9087 /* M13CheckboxRadioPathGenerator.swift in Sources */, 3D47CDB122F3C3F3003BD7F7 /* NVActivityIndicatorViewable.swift in Sources */, A154A080215C7A8C0010FFCC /* BlockListAddCell.swift in Sources */, + 0B1B6AD8293AA464003DCF72 /* PaywallTableFeature.swift in Sources */, + 0B1B6AC6293A99C1003DCF72 /* EmailComposable.swift in Sources */, + 0B2205CA29142A84002F2562 /* TextBox.swift in Sources */, 3D47CDB822F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallClipRotateMultiple.swift in Sources */, - 3DBD57A822FBD7A100DE189F /* WhitelistUtilities.swift in Sources */, + 0B6DC540292E402A00DCB8E8 /* Font+Ext.swift in Sources */, 3D9FC67923E521DE004122D3 /* ForgotPasswordViewController.swift in Sources */, 3D47CDC122F3C3F3003BD7F7 /* NVActivityIndicatorAnimationSemiCircleSpin.swift in Sources */, 3DBD57B422FCFF2500DE189F /* SetRegionViewController.swift in Sources */, + 0BCD08A3293FDD0A00DC6DD1 /* ConnectionState.swift in Sources */, 7C4D9BBB252C8748004175EA /* AccountUI.swift in Sources */, + 0B84454E2940BDE300FA2F35 /* EmailValidatable.swift in Sources */, 3D47CDBE22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScale.swift in Sources */, 7C44081B2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */, - 7C1AE073247FD82A0000A7D3 /* PushNotificationsAuthorization.swift in Sources */, 7C798A1A25409F8100A99695 /* Mailto.swift in Sources */, 3D47CDBC22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineSpinFadeLoader.swift in Sources */, 3D47CDBA22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallDoubleBounce.swift in Sources */, + 0BF1E99F28C67E1700ACD0FD /* NibLoadable.swift in Sources */, A1DBA18621B77C66008A9322 /* VPNSubscription.swift in Sources */, 3D0711BB22FE7B5100391C6E /* TitleViewController.swift in Sources */, 3D47CDB222F3C3F3003BD7F7 /* NVActivityIndicatorShape.swift in Sources */, + 0BE9DD69285A59710023B555 /* OneTimeActions.swift in Sources */, 7C422E97252796EE007F9C22 /* StaticTableView.swift in Sources */, + 0B2205D02915B093002F2562 /* UIViewController+Ext.swift in Sources */, 7C3EFA0224867DEE00719D96 /* TrackerInfo.swift in Sources */, + 0BB5629829437C1B00EEE4B5 /* UserDefault.swift in Sources */, A1EBEAD32097AE6E002B9087 /* M13CheckboxFlatController.swift in Sources */, + 0BECE98B2951F3C300F3BFB8 /* String+URL.swift in Sources */, 3D47CDC022F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallBeat.swift in Sources */, + 0B1FFD63293E16BF0027341C /* Separator.swift in Sources */, + 0BCD08A0293FDCF400DC6DD1 /* ConnectivityService.swift in Sources */, 3D47CDC222F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScaleRipple.swift in Sources */, A1EBEAD62097AE6E002B9087 /* M13CheckboxSpiralController.swift in Sources */, 3D47CDAF22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationDelegate.swift in Sources */, @@ -2142,16 +2997,22 @@ 3DBD57AC22FBDFE300DE189F /* WhitelistCell.swift in Sources */, 3DF2455423A2F8A400E46613 /* EmailSignUpViewController.swift in Sources */, 3D47CDB522F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallRotate.swift in Sources */, + 0BE9DD75285A59B70023B555 /* FirewallUtilities.swift in Sources */, + 0B4B8B2029412C9C0060C072 /* IntentService.swift in Sources */, 3D47CDC922F3C3F3003BD7F7 /* NVActivityIndicatorAnimationAudioEqualizer.swift in Sources */, + 0BA77E2628E45F73007ECE45 /* ArrayRestrictable.swift in Sources */, 3D47CDB722F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallClipRotate.swift in Sources */, 3D47CDD322F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScaleParty.swift in Sources */, + 0BE9DD6F285A59930023B555 /* BlockDayLog.swift in Sources */, A1141A151F46230500F54698 /* AppDelegate.swift in Sources */, 3DBD57AE22FBE04300DE189F /* WhitelistAddCell.swift in Sources */, 3D47CDB922F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallTrianglePath.swift in Sources */, A1EBEACE2097AE6E002B9087 /* M13CheckboxExpandController.swift in Sources */, + 0BE9DD8D285A5A120023B555 /* UserDefaults.swift in Sources */, 3D5F5A0C23109ABB004C3860 /* WhatIsVpnViewController.swift in Sources */, + 0B1C8ED228E44A8800E9CA57 /* ProgressView.swift in Sources */, + 0BECE95D294F624B00F3BFB8 /* Keychainable.swift in Sources */, A174CCAE22B15B1000F1B840 /* BlockListViewController.swift in Sources */, - 3DAF73522768572300D97BB0 /* FirewallUtilities.swift in Sources */, 3D47CDD022F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulseSync.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2160,24 +3021,29 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3DBD57A922FBD7A100DE189F /* WhitelistUtilities.swift in Sources */, - 7C1AE076247FE2000000A7D3 /* PushNotificationsAuthorization.swift in Sources */, - 7C6619BD247810EE005E8BB1 /* BlockDayLog.swift in Sources */, + 0BE9DD82285A59F20023B555 /* PushNotifications.swift in Sources */, 54F0B1A0273200B0002F3630 /* FirewallController.swift in Sources */, + 0BE9DD70285A59930023B555 /* BlockDayLog.swift in Sources */, A1DBA18B21B77C88008A9322 /* VPNController.swift in Sources */, 3DAF7C5722F456F2003C8F9C /* ClientModels.swift in Sources */, - 7C1AE07B247FF87F0000A7D3 /* OneTimeActions.swift in Sources */, - 3DAF73532768572300D97BB0 /* FirewallUtilities.swift in Sources */, - 3DBD57B122FC14CD00DE189F /* Shared.swift in Sources */, + 0BE9DD88285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */, + 0BE9DD6A285A59710023B555 /* OneTimeActions.swift in Sources */, + 0BDBDDED293E287C009A1184 /* PaywallTableFeature.swift in Sources */, 3DAF7C5622F4568C003C8F9C /* Client.swift in Sources */, 7CE91C972521ED5E009D8269 /* VPNRegion.swift in Sources */, - 7CD52D82247EC18800D0530F /* PushNotifications.swift in Sources */, 3D94AB1322FE3BA10012B0DE /* Environment.swift in Sources */, - 7CE91C602521D564009D8269 /* UserDefaults.swift in Sources */, + 0B99BA4E294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */, + 0BE9DD76285A59B70023B555 /* FirewallUtilities.swift in Sources */, + 0BE9DD7C285A59E30023B555 /* Metrics.swift in Sources */, + 0B3AB810293508DE00B254C0 /* AppStoreProductGroup.swift in Sources */, + 0BE9DD8E285A5A120023B555 /* UserDefaults.swift in Sources */, + 0B3AB8192935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */, + 0BE9DD94285A5A200023B555 /* WhitelistUtilities.swift in Sources */, A1DBA18E21B77C8E008A9322 /* VPNSubscription.swift in Sources */, + 0B8DBDAE286482D500831AC4 /* String+Localized.swift in Sources */, A12473F71FE44285008493B8 /* VPNTodayViewController.swift in Sources */, - 7CE91C7E2521D5B6009D8269 /* Metrics.swift in Sources */, 7C44081C2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */, + 0BBEEF422948C3D700F4405E /* Shared.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2185,6 +3051,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0B99BA4D294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */, + 0B3AB80A293508DD00B254C0 /* AppStoreProductGroup.swift in Sources */, + 0BDBDDE7293E287C009A1184 /* PaywallTableFeature.swift in Sources */, + 0B8DBDA8286482D500831AC4 /* String+Localized.swift in Sources */, + 0B3AB8182935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */, A1931D0020791F5900E695EB /* ContentBlockerRequestHandler.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2193,27 +3064,37 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7CE91C852521D5B7009D8269 /* Metrics.swift in Sources */, - 7C3E8D22247D8057004B81D6 /* PushNotifications.swift in Sources */, 3D94AB1522FE3BA40012B0DE /* Environment.swift in Sources */, - 3DBD57B222FC14CD00DE189F /* Shared.swift in Sources */, + 0BE9DD77285A59B70023B555 /* FirewallUtilities.swift in Sources */, A1FCDA4422C0651300C928BC /* PacketTunnelProvider.swift in Sources */, - 3DBD57AA22FBD7A100DE189F /* WhitelistUtilities.swift in Sources */, - 7C1AE077247FE2010000A7D3 /* PushNotificationsAuthorization.swift in Sources */, - 3DAF73542768572300D97BB0 /* FirewallUtilities.swift in Sources */, + 0BE9DD71285A59930023B555 /* BlockDayLog.swift in Sources */, + 0BE9DD95285A5A200023B555 /* WhitelistUtilities.swift in Sources */, + 0BE9DD89285A5A070023B555 /* PushNotificationsAuthorization.swift in Sources */, + 0BE9DD7D285A59E30023B555 /* Metrics.swift in Sources */, + 0BE9DD6B285A59710023B555 /* OneTimeActions.swift in Sources */, + 0B99BA4F294903F100A47A45 /* PaywallDisplayConfiguration.swift in Sources */, 7C44081D2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */, + 0B8DBDAF286482D600831AC4 /* String+Localized.swift in Sources */, + 0BBEEF602948C7E700F4405E /* Shared.swift in Sources */, + 0BDBDDEE293E2880009A1184 /* PaywallTableFeature.swift in Sources */, + 0B3AB811293508DF00B254C0 /* AppStoreProductGroup.swift in Sources */, + 0B3AB81A2935094B00B254C0 /* SubscriptionOfferPeriodUnit.swift in Sources */, 7CE91C982521ED5E009D8269 /* VPNRegion.swift in Sources */, + 0BE9DD8F285A5A120023B555 /* UserDefaults.swift in Sources */, 3DABDA0222F7DD7700480AAC /* ClientModels.swift in Sources */, - 7CE91C672521D565009D8269 /* UserDefaults.swift in Sources */, - 7C6619BF247810EF005E8BB1 /* BlockDayLog.swift in Sources */, - 7C1AE07C247FF87F0000A7D3 /* OneTimeActions.swift in Sources */, 3DABD9FD22F7961F00480AAC /* Client.swift in Sources */, + 0BE9DD83285A59F20023B555 /* PushNotifications.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 0B4B8B1729412BD50060C072 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0B4B8AFD29412BD50060C072 /* LockdownIntents */; + targetProxy = 0B4B8B1629412BD50060C072 /* PBXContainerItemProxy */; + }; 3DBD57C522FD727900DE189F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 3DBD57BA22FD727900DE189F /* Lockdown Firewall Widget */; @@ -2247,6 +3128,24 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 0B99BA362948FE6300A47A45 /* LockdownIntents.intentdefinition */ = { + isa = PBXVariantGroup; + children = ( + 0B99BA352948FE6300A47A45 /* Base */, + 0B99BA382948FE7600A47A45 /* en */, + 0B99BA3A2948FE7700A47A45 /* ar */, + 0B99BA3C2948FE7800A47A45 /* zh-Hans */, + 0B99BA3E2948FEBC00A47A45 /* zh-Hant */, + 0B99BA402948FEBD00A47A45 /* fr */, + 0B99BA422948FEBD00A47A45 /* de */, + 0B99BA442948FEBE00A47A45 /* ja */, + 0B99BA462948FEBF00A47A45 /* ru */, + 0B99BA482948FEBF00A47A45 /* es */, + 0B99BA4A2948FEC000A47A45 /* vi */, + ); + name = LockdownIntents.intentdefinition; + sourceTree = ""; + }; 3D3BF4D0233D5E9100D0C482 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -2254,6 +3153,12 @@ 3D896110253527B2006D8C12 /* fr */, 3DCBC8FF25425AB200446C98 /* ja */, 3DCBC90A25425BC900446C98 /* es */, + 0BF9014C2938BFE0001A2F2B /* ar */, + 0BF901512938BFF4001A2F2B /* de */, + 0BF901562938C008001A2F2B /* ru */, + 0BF9015B2938C015001A2F2B /* vi */, + 0BF901652938C026001A2F2B /* zh-Hant */, + 0B4B8AF92941079E0060C072 /* zh-Hans */, ); name = Localizable.strings; sourceTree = ""; @@ -2266,6 +3171,12 @@ 3DCBC90125425AB200446C98 /* ja */, 3DCBC90B25425BC900446C98 /* es */, 3DA14D3C255DF5CF00A3658E /* en */, + 0BF9014A2938BFE0001A2F2B /* ar */, + 0BF9014D2938BFF4001A2F2B /* de */, + 0BF901542938C008001A2F2B /* ru */, + 0BF901592938C015001A2F2B /* vi */, + 0BF901632938C025001A2F2B /* zh-Hant */, + 0B4B8AF72941079D0060C072 /* zh-Hans */, ); name = MainInterface.storyboard; sourceTree = ""; @@ -2278,6 +3189,12 @@ 3DCBC90025425AB200446C98 /* ja */, 3DCBC90925425BC900446C98 /* es */, 3DA14D34255DF56E00A3658E /* en */, + 0BF901492938BFE0001A2F2B /* ar */, + 0BF9014F2938BFF4001A2F2B /* de */, + 0BF901532938C008001A2F2B /* ru */, + 0BF901582938C015001A2F2B /* vi */, + 0BF901622938C025001A2F2B /* zh-Hant */, + 0B4B8AF62941079D0060C072 /* zh-Hans */, ); name = Main.storyboard; sourceTree = ""; @@ -2290,6 +3207,12 @@ 3DCBC90225425AB200446C98 /* ja */, 3DCBC90C25425BC900446C98 /* es */, 3DA14D3E255DF5D400A3658E /* en */, + 0BF9014B2938BFE0001A2F2B /* ar */, + 0BF901502938BFF4001A2F2B /* de */, + 0BF901552938C008001A2F2B /* ru */, + 0BF9015A2938C015001A2F2B /* vi */, + 0BF901642938C026001A2F2B /* zh-Hant */, + 0B4B8AF82941079D0060C072 /* zh-Hans */, ); name = MainInterface.storyboard; sourceTree = ""; @@ -2298,6 +3221,12 @@ isa = PBXVariantGroup; children = ( A15F3C741F79D90500B07F03 /* Base */, + 0BF901482938BFDF001A2F2B /* ar */, + 0BF9014E2938BFF4001A2F2B /* de */, + 0BF901522938C008001A2F2B /* ru */, + 0BF901572938C015001A2F2B /* vi */, + 0BF901612938C025001A2F2B /* zh-Hant */, + 0B4B8AF52941079D0060C072 /* zh-Hans */, ); name = LaunchScreen.storyboard; sourceTree = ""; @@ -2305,10 +3234,88 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 0B4B8B1D29412BD50060C072 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CB1223BE1CDCC2555FE71352 /* Pods-LockdownIntents.debug.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = LockdownIntents/LockdownIntents.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1060; + DEVELOPMENT_TEAM = V8J3Z26F6Z; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LockdownIntents/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LockdownIntents; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Confirmed Inc. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.6.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownIntents; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0B4B8B1E29412BD50060C072 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 777648CC444D5A906BBFC729 /* Pods-LockdownIntents.release.xcconfig */; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = LockdownIntents/LockdownIntents.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1060; + DEVELOPMENT_TEAM = V8J3Z26F6Z; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LockdownIntents/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LockdownIntents; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Confirmed Inc. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.6.1; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownIntents; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 3DBD57C822FD727900DE189F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A2316A93257F196F84916806 /* Pods-Lockdown Firewall Widget.debug.xcconfig */; + baseConfigurationReference = 71855E12B7B5B939F0E2EB29 /* Pods-Lockdown Firewall Widget.debug.xcconfig */; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -2316,7 +3323,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -2324,13 +3331,13 @@ ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Firewall Today/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Lockdown-Firewall-Today"; @@ -2343,8 +3350,9 @@ }; 3DBD57C922FD727900DE189F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 707A358300D4A3D33EFC8749 /* Pods-Lockdown Firewall Widget.release.xcconfig */; + baseConfigurationReference = D2EBAB60D2371A51A7E6A92C /* Pods-Lockdown Firewall Widget.release.xcconfig */; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -2352,7 +3360,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -2360,13 +3368,13 @@ ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Firewall Today/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Lockdown-Firewall-Today"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2378,8 +3386,9 @@ }; 7C0D11212473FC7E00A26E04 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 95899949475D15BB3DB63138 /* Pods-LockdownTests.debug.xcconfig */; + baseConfigurationReference = 1F90F5BA7CFAB9814D768F16 /* Pods-LockdownTests.debug.xcconfig */; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; @@ -2406,8 +3415,9 @@ }; 7C0D11222473FC7E00A26E04 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0B014050F770126F5808AF28 /* Pods-LockdownTests.release.xcconfig */; + baseConfigurationReference = 489F7CCEAADF147B19320AAE /* Pods-LockdownTests.release.xcconfig */; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; @@ -2436,6 +3446,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; @@ -2444,8 +3455,9 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; + FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = LockdownFirewallWidget/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -2454,7 +3466,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownFirewallWidget; @@ -2470,6 +3482,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; @@ -2478,8 +3491,9 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; + FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = LockdownFirewallWidget/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.0; @@ -2488,7 +3502,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownFirewallWidget; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2502,6 +3516,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2538,17 +3553,14 @@ GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); + GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=1"; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -2563,6 +3575,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; @@ -2603,7 +3616,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -2617,17 +3630,19 @@ }; A1141A3A1F46230600F54698 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = ECA3DC3801B375B860C15978 /* Pods-Lockdown.debug.xcconfig */; + baseConfigurationReference = DEA35788129374068081E375 /* Pods-Lockdown.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CODE_SIGN_ENTITLEMENTS = LockdowniOS/LockdowniOS.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -2637,12 +3652,12 @@ "COCOAPODS=1", ); INFOPLIST_FILE = "$(SRCROOT)/LockdowniOS/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -2655,17 +3670,19 @@ }; A1141A3B1F46230600F54698 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FA3DBA07D389FDA260B448B7 /* Pods-Lockdown.release.xcconfig */; + baseConfigurationReference = E2ED1B8AB6C3D86D7147AF02 /* Pods-Lockdown.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CODE_SIGN_ENTITLEMENTS = LockdowniOS/LockdowniOS.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; ENABLE_BITCODE = NO; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/iOS", @@ -2675,12 +3692,12 @@ "COCOAPODS=1", ); INFOPLIST_FILE = "$(SRCROOT)/LockdowniOS/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -2693,8 +3710,9 @@ }; A12474001FE44285008493B8 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A0BEB57F0C3C4F5746285E07 /* Pods-Lockdown VPN Widget.debug.xcconfig */; + baseConfigurationReference = AFD87C46425342AFA2F65394 /* Pods-Lockdown VPN Widget.debug.xcconfig */; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; @@ -2707,7 +3725,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -2715,13 +3733,13 @@ ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Today/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Lockdown-VPN-Today"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -2735,8 +3753,9 @@ }; A12474011FE44285008493B8 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 88E04750DB23687076871F5B /* Pods-Lockdown VPN Widget.release.xcconfig */; + baseConfigurationReference = 12578876980678A072282A2E /* Pods-Lockdown VPN Widget.release.xcconfig */; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_COMMA = YES; @@ -2749,7 +3768,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -2757,13 +3776,13 @@ ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Today/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Lockdown-VPN-Today"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -2778,23 +3797,24 @@ A1931D0620791F5900E695EB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "Lockdown Blocker/Lockdown Blocker.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Blocker/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Confirmed-Blocker"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2808,23 +3828,24 @@ A1931D0720791F5900E695EB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = "Lockdown Blocker/Lockdown Blocker.entitlements"; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Blocker/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Confirmed-Blocker"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2837,8 +3858,9 @@ }; A1FCDA4B22C0651300C928BC /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 549133AE36DE5B896F995988 /* Pods-LockdownTunnel.debug.xcconfig */; + baseConfigurationReference = 628519CE6C4D40332EFBFB08 /* Pods-LockdownTunnel.debug.xcconfig */; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -2846,7 +3868,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -2854,13 +3876,13 @@ ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Tunnel/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownTunnel; @@ -2873,8 +3895,9 @@ }; A1FCDA4C22C0651300C928BC /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7435DF998DFB59C9808F1638 /* Pods-LockdownTunnel.release.xcconfig */; + baseConfigurationReference = 569A4A1747E7C83C7F07C755 /* Pods-LockdownTunnel.release.xcconfig */; buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = NO; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -2882,7 +3905,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1060; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -2890,13 +3913,13 @@ ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Tunnel/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.3.3; + MARKETING_VERSION = 1.6.1; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownTunnel; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -2909,6 +3932,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 0B4B8B1C29412BD50060C072 /* Build configuration list for PBXNativeTarget "LockdownIntents" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0B4B8B1D29412BD50060C072 /* Debug */, + 0B4B8B1E29412BD50060C072 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 3DBD57C722FD727900DE189F /* Build configuration list for PBXNativeTarget "Lockdown Firewall Widget" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Debug.xcscheme b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Debug.xcscheme new file mode 100644 index 0000000..1d9cc69 --- /dev/null +++ b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Debug.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Release.xcscheme b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Release.xcscheme index 6bf0f4a..e74a274 100644 --- a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Release.xcscheme +++ b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Release.xcscheme @@ -23,7 +23,7 @@ @@ -88,7 +88,7 @@ + buildConfiguration = "Release"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/LockdowniOS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings index 0c67376..f9b0d7c 100644 --- a/LockdowniOS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ b/LockdowniOS.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -1,5 +1,8 @@ - + + PreviewsEnabled + + diff --git a/LockdowniOS/Account/EmailSignInViewController.swift b/LockdowniOS/Account/EmailSignInViewController.swift index 7285ab0..f5ad228 100644 --- a/LockdowniOS/Account/EmailSignInViewController.swift +++ b/LockdowniOS/Account/EmailSignInViewController.swift @@ -25,8 +25,7 @@ class EmailSignInViewController: BaseViewController, UITextFieldDelegate, Loadab setupToHideKeyboardOnTapOnView() } - func setupToHideKeyboardOnTapOnView() - { + func setupToHideKeyboardOnTapOnView() { let tap: UITapGestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(self.dismissKeyboard)) @@ -34,16 +33,14 @@ class EmailSignInViewController: BaseViewController, UITextFieldDelegate, Loadab view.addGestureRecognizer(tap) } - @objc func dismissKeyboard() - { + @objc private func dismissKeyboard() { view.endEditing(true) } func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if (textField == emailField) { + if textField == emailField { passwordField.becomeFirstResponder() - } - else if (textField == passwordField) { + } else if textField == passwordField { textField.resignFirstResponder() signIn() } @@ -52,7 +49,7 @@ class EmailSignInViewController: BaseViewController, UITextFieldDelegate, Loadab @IBAction func signIn() { guard let email = emailField.text, let password = passwordField.text else { - showPopupDialog(title: "Check Fields", message: "Email and password must not be empty", acceptButton: "Okay") + showPopupDialog(title: .localized("check_fields"), message: .localized("email_and_password_must_not_be_empty"), acceptButton: "Okay") return } @@ -60,23 +57,23 @@ class EmailSignInViewController: BaseViewController, UITextFieldDelegate, Loadab firstly { try Client.signInWithEmail(email: email, password: password) } - .done { (signin: SignIn) in + .done { (_: SignIn) in try setAPICredentials(email: email, password: password) setAPICredentialsConfirmed(confirmed: true) self.hideLoadingView() NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) - self.showPopupDialog(title: "Success! 🎉", message: "You've successfully signed in.", acceptButton: "Okay") { + self.showPopupDialog(title: .localized("Success! 🎉"), message: "You've successfully signed in.", acceptButton: "Okay") { self.presentingViewController?.dismiss(animated: true, completion: { // 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 + .then { (_: SubscriptionEvent) -> Promise in try Client.getKey() } .done { (getKey: GetKey) in try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) - if (getUserWantsVPNEnabled() == true) { + if getUserWantsVPNEnabled() { VPNController.shared.restart() } } @@ -94,7 +91,7 @@ class EmailSignInViewController: BaseViewController, UITextFieldDelegate, Loadab if let apiError = error as? ApiError { errorMessage = apiError.message } - self.showPopupDialog(title: "Error Signing In", message: errorMessage, acceptButton: "Okay") { + self.showPopupDialog(title: .localized("error_signing_in"), message: errorMessage, acceptButton: .localizedOkay) { } } } diff --git a/LockdowniOS/Account/EmailSignUpViewController.swift b/LockdowniOS/Account/EmailSignUpViewController.swift index b9d2792..8ef0c3a 100644 --- a/LockdowniOS/Account/EmailSignUpViewController.swift +++ b/LockdowniOS/Account/EmailSignUpViewController.swift @@ -15,7 +15,7 @@ import PromiseKit class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadable { struct Delegate { - var showSignIn: () -> () = { } + var showSignIn: () -> Void = { } } var delegate = Delegate() @@ -33,8 +33,7 @@ class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadab setupToHideKeyboardOnTapOnView() } - func setupToHideKeyboardOnTapOnView() - { + func setupToHideKeyboardOnTapOnView() { let tap: UITapGestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(self.dismissKeyboard)) @@ -42,14 +41,17 @@ class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadab view.addGestureRecognizer(tap) } - @objc func dismissKeyboard() - { + @objc private func dismissKeyboard() { view.endEditing(true) } @IBAction func passwordFieldDidChange(_ textField: UITextField) { - let attrStr = NSMutableAttributedString ( - string: NSLocalizedString("Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol.", comment: ""), + let passwordRequirements = """ +Password must be at least 8 characters, contain at least one uppercase letter, \ +one lowercase letter, one number, and one symbol. +""" + let attrStr = NSMutableAttributedString( + string: .localized(passwordRequirements), attributes: [ .font: UIFont(name: "Montserrat-Regular", size: 11) ?? UIFont.systemFont(ofSize: 11), .foregroundColor: UIColor.lightGray @@ -58,15 +60,16 @@ class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadab if let txt = passwordField.text { isPasswordValid = true attrStr.addAttributes(setupAttributeColor(if: (txt.count >= 8)), - range: findRange(in: attrStr.string, for: NSLocalizedString("at least 8 characters", comment: ""))) + range: findRange(in: attrStr.string, for: .localized("at least 8 characters"))) attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil)), - range: findRange(in: attrStr.string, for: NSLocalizedString("one uppercase letter", comment: ""))) + range: findRange(in: attrStr.string, for: .localized("one uppercase letter"))) attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil)), - range: findRange(in: attrStr.string, for: NSLocalizedString("one lowercase letter", comment: ""))) + range: findRange(in: attrStr.string, for: .localized("one lowercase letter"))) attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil)), - range: findRange(in: attrStr.string, for: NSLocalizedString("one number", comment: ""))) - attrStr.addAttributes(setupAttributeColor(if: ((txt.rangeOfCharacter(from: CharacterSet.symbols) != nil) || (txt.rangeOfCharacter(from: CharacterSet.punctuationCharacters) != nil))), - range: findRange(in: attrStr.string, for: NSLocalizedString("one symbol", comment: ""))) + range: findRange(in: attrStr.string, for: .localized("one number"))) + attrStr.addAttributes(setupAttributeColor(if: ((txt.rangeOfCharacter(from: CharacterSet.symbols) != nil) + || (txt.rangeOfCharacter(from: CharacterSet.punctuationCharacters) != nil))), + range: findRange(in: attrStr.string, for: .localized("one symbol"))) } else { isPasswordValid = false } @@ -75,10 +78,9 @@ class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadab } func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if (textField == emailField) { + if textField == emailField { passwordField.becomeFirstResponder() - } - else if (textField == passwordField) { + } else if textField == passwordField { textField.resignFirstResponder() createAccount() } @@ -96,18 +98,21 @@ class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadab } .catch { error in self.hideLoadingView() - if (self.popupErrorAsNSURLError(error)) { + if self.popupErrorAsNSURLError(error) { return - } - else if let apiError = error as? ApiError { + } else if let apiError = error as? ApiError { switch apiError.code { case kApiCodeEmailNotConfirmed: // This is the "correct" case for /signup, we are expecting "1" = email confirmation sent do { try setAPICredentials(email: self.emailField.text!, password: self.passwordField.text!) setAPICredentialsConfirmed(confirmed: false) + let message = """ +To finish signup, click the confirmation link in the email we just sent. \ +If you don't see it, check if it's stuck in your spam folder. +""" let popup = PopupDialog(title: "Confirm Your Email", - message: NSLocalizedString("To finish signup, click the confirmation link in the email we just sent. If you don't see it, check if it's stuck in your spam folder.", comment: ""), + message: .localized(message), image: nil, buttonAlignment: .horizontal, transitionStyle: .bounceDown, @@ -117,25 +122,26 @@ class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadab hideStatusBar: false, completion: nil) popup.addButtons([ - DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) { + DefaultButton(title: .localizedOkay, dismissOnTap: true) { self.hideLoadingView() self.presentingViewController?.dismiss(animated: true, completion: nil) } ]) self.present(popup, animated: true, completion: nil) NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) - } - catch { - self.showPopupDialog(title: "Error Saving Credentials", message: "Couldn't save credentials to local keychain. Please report this error to team@lockdownprivacy.com.", acceptButton: "Okay") + } catch { + self.showPopupDialog( + title: "Error Saving Credentials", + message: "Couldn't save credentials to local keychain. Please report this error to team@lockdownprivacy.com.", + acceptButton: "Okay") } default: _ = self.popupErrorAsApiError(error) } - } - else { - self.showPopupDialog(title: NSLocalizedString("Error Creating Email Account", comment: ""), + } else { + self.showPopupDialog(title: .localized("Error Creating Email Account"), message: "\(error)", - acceptButton: NSLocalizedString("Okay", comment: "")) + acceptButton: .localizedOkay) } } } @@ -159,10 +165,10 @@ class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadab if let range = baseString.localizedStandardRange(of: substring) { let startIndex = baseString.distance(from: baseString.startIndex, to: range.lowerBound) let length = substring.count - return NSMakeRange(startIndex, length) + return NSRange(location: startIndex, length: length) } else { print("Range does not exist in the base string.") - return NSMakeRange(0, 0) + return NSRange(location: 0, length: 0) } } diff --git a/LockdowniOS/Account/ForgotPasswordViewController.swift b/LockdowniOS/Account/ForgotPasswordViewController.swift index 4cabc81..0eb736d 100644 --- a/LockdowniOS/Account/ForgotPasswordViewController.swift +++ b/LockdowniOS/Account/ForgotPasswordViewController.swift @@ -11,7 +11,7 @@ import UIKit import PopupDialog import PromiseKit -class ForgotPasswordViewController: BaseViewController, UITextFieldDelegate, Loadable { +final class ForgotPasswordViewController: BaseViewController, UITextFieldDelegate, Loadable { @IBOutlet weak var emailField: UITextField! @@ -21,8 +21,7 @@ class ForgotPasswordViewController: BaseViewController, UITextFieldDelegate, Loa setupToHideKeyboardOnTapOnView() } - func setupToHideKeyboardOnTapOnView() - { + func setupToHideKeyboardOnTapOnView() { let tap: UITapGestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(self.dismissKeyboard)) @@ -30,13 +29,12 @@ class ForgotPasswordViewController: BaseViewController, UITextFieldDelegate, Loa view.addGestureRecognizer(tap) } - @objc func dismissKeyboard() - { + @objc private func dismissKeyboard() { view.endEditing(true) } func textFieldShouldReturn(_ textField: UITextField) -> Bool { - if (textField == emailField) { + if textField == emailField { textField.resignFirstResponder() submit() } @@ -45,16 +43,19 @@ class ForgotPasswordViewController: BaseViewController, UITextFieldDelegate, Loa @IBAction func submit() { guard let email = emailField.text else { - showPopupDialog(title: "Check Fields", message: "Email must not be empty", acceptButton: "Okay") + showPopupDialog(title: .localized("check_fields"), message: "Email must not be empty", acceptButton: .localizedOK) return } showLoadingView() firstly { try Client.forgotPassword(email: email) } - .done { (success: Bool) in + .done { _ in self.hideLoadingView() - self.showPopupDialog(title: "Check Email", message: "We've sent a reset password email to you. Be sure to check any spam/junk folders, in case it got stuck there.", acceptButton: "Okay") { + self.showPopupDialog( + title: .localized("check_email"), + message: .localized("we_have_sent_a_reset_password_email"), + acceptButton: .localizedOK) { self.dismiss(animated: true, completion: nil) } } @@ -64,8 +65,7 @@ class ForgotPasswordViewController: BaseViewController, UITextFieldDelegate, Loa if let apiError = error as? ApiError { errorMessage = apiError.message } - self.showPopupDialog(title: "Error Sending Reset Password Email", message: errorMessage, acceptButton: "Okay") { - } + self.showPopupDialog(title: "Error Sending Reset Password Email", message: errorMessage, acceptButton: .localizedOK) } } diff --git a/LockdowniOS/AccountUI.swift b/LockdowniOS/AccountUI.swift index 9292fff..7faf674 100644 --- a/LockdowniOS/AccountUI.swift +++ b/LockdowniOS/AccountUI.swift @@ -11,23 +11,4 @@ import UIKit enum AccountUI { static let accountStateDidChange = Notification.Name("AccountUIAccountStateDidChangeNotification") - - static func presentCreateAccount(on vc: UIViewController) { - let storyboard = UIStoryboard.main - let viewController = storyboard.instantiateViewController(withIdentifier: "emailSignUpViewController") as! EmailSignUpViewController - viewController.delegate.showSignIn = { [weak vc] in - if let strongVC = vc { - AccountUI.presentSignInToAccount(on: strongVC) - } - } - - vc.present(viewController, animated: true, completion: nil) - } - - static func presentSignInToAccount(on vc: UIViewController) { - let storyboard = UIStoryboard.main - let viewController = storyboard.instantiateViewController(withIdentifier: "emailSignInViewController") as! EmailSignInViewController - - vc.present(viewController, animated: true, completion: nil) - } } diff --git a/LockdowniOS/AccountVC.swift b/LockdowniOS/AccountVC.swift index fc1f906..2f87432 100644 --- a/LockdowniOS/AccountVC.swift +++ b/LockdowniOS/AccountVC.swift @@ -24,6 +24,7 @@ final class AccountViewController: BaseViewController, Loadable { tableView.anchors.edges.pin() tableView.separatorStyle = .singleLine tableView.cellLayoutMarginsFollowReadableWidth = true + tableView.showsVerticalScrollIndicator = false tableView.deselectsCellsAutomatically = true tableView.contentInset.top += 12 tableView.tableFooterView = UIView() @@ -37,10 +38,10 @@ final class AccountViewController: BaseViewController, Loadable { } } - @objc - func accountStateDidChange() { - assert(Thread.isMainThread) - self.reloadTable() + @objc private func accountStateDidChange() { + DispatchQueue.main.async { + self.reloadTable() + } } func reloadTable() { @@ -50,58 +51,66 @@ final class AccountViewController: BaseViewController, Loadable { } func createTable() { - var title = NSLocalizedString("⚠️ Not Signed In", comment: "") - var message: String? = NSLocalizedString("Sign up below to unlock benefits of a Lockdown account.", comment: "") - var firstButton = DefaultCell(title: NSLocalizedString("Sign Up | Sign In", comment: "")) { + // Remove top separator + tableView.tableHeaderView = UIView() + + var title: String = .localized("⚠️ Not Signed In") + var message: String? = .localized("Sign up below to unlock benefits of a Lockdown account.") + var firstButton = MakeDefaultCell(title: .localized("Sign Up | Sign In")) { // AccountViewController will update itself by observing // AccountUI.accountStateDidChange notification - AccountUI.presentCreateAccount(on: self) + let signUpViewController = SignUpViewController(mode: .signUp) + let idiom = UIScreen.main.traitCollection.userInterfaceIdiom + signUpViewController.modalPresentationStyle = idiom == .pad ? .pageSheet : .fullScreen + self.present(signUpViewController, animated: true) } firstButton.backgroundView = UIView() - firstButton.backgroundView?.backgroundColor = UIColor.tunnelsBlue + firstButton.backgroundView?.backgroundColor = .tunnelsBlue firstButton.label.textColor = UIColor.white if let apiCredentials = getAPICredentials() { message = apiCredentials.email if getAPICredentialsConfirmed() == true { - title = NSLocalizedString("Signed In", comment: "") - firstButton = DefaultCell(title: NSLocalizedString("Sign Out", comment: "")) { - let confirm = PopupDialog(title: NSLocalizedString("Sign Out?", comment: ""), - message: NSLocalizedString("You'll be signed out from this account.", comment: ""), - image: nil, - buttonAlignment: .horizontal, - transitionStyle: .bounceDown, - preferredWidth: 270, - tapGestureDismissal: true, - panGestureDismissal: false, - hideStatusBar: false, - completion: nil) + title = .localized("Signed In") + firstButton = MakeDefaultCell(title: .localizedSignOut) { + let confirm = PopupDialog(title: .localized("Sign Out?"), + message: .localized("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) { [unowned self] in - URLCache.shared.removeAllCachedResponses() - Client.clearCookies() - clearAPICredentials() - setAPICredentialsConfirmed(confirmed: false) - self.reloadTable() - self.showPopupDialog(title: NSLocalizedString("Success", comment: ""), message: NSLocalizedString("Signed out successfully.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) + DefaultButton(title: .localizedCancel, dismissOnTap: true) {}, + DefaultButton(title: .localizedSignOut, dismissOnTap: true) { [weak self] in + guard let self = self else { return } + URLCache.shared.removeAllCachedResponses() + Client.clearCookies() + clearAPICredentials() + setAPICredentialsConfirmed(confirmed: false) + self.reloadTable() + self.showPopupDialog( + title: .localized("Success"), + message: .localized("Signed out successfully."), + acceptButton: .localizedOkay) }, ]) self.present(confirm, animated: true, completion: nil) } firstButton.backgroundView?.backgroundColor = UIColor.clear firstButton.label.textColor = UIColor.systemRed - } - else { + } else { title = "⚠️ Email Not Confirmed" - firstButton = DefaultCell(title: NSLocalizedString("Confirm Email", comment: "")) { + firstButton = MakeDefaultCell(title: .localized("Confirm Email")) { self.showLoadingView() firstly { try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) } - .done { (signin: SignIn) in + .done { _ in self.hideLoadingView() // successfully signed in with no errors, show confirmation success setAPICredentialsConfirmed(confirmed: true) @@ -110,22 +119,29 @@ final class AccountViewController: BaseViewController, Loadable { firstly { () -> Promise in try Client.subscriptionEvent() } - .then { (result: SubscriptionEvent) -> Promise in + .then { _ -> Promise in try Client.getKey() } .done { (getKey: GetKey) in try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) - if (getUserWantsVPNEnabled() == true) { + if getUserWantsVPNEnabled() { 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)") + 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: ""), + let message = """ +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. +""" + let popup = PopupDialog(title: .localized("Success! 🎉"), + message: .localized(message), image: nil, buttonAlignment: .horizontal, transitionStyle: .bounceDown, @@ -135,16 +151,24 @@ final class AccountViewController: BaseViewController, Loadable { hideStatusBar: false, completion: nil) popup.addButtons([ - DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) { - self.reloadTable() + DefaultButton(title: .localizedOkay, dismissOnTap: true) { + self.reloadTable() } ]) self.present(popup, animated: true, completion: nil) } .catch { error in self.hideLoadingView() - let popup = PopupDialog(title: NSLocalizedString("Check Your Inbox", comment: ""), - message: "\(NSLocalizedString("To complete your signup, click the confirmation link we sent to", comment: "Used in To complete your signup, click the confirmation link we sent to you@gmail.com")) \(apiCredentials.email). \(NSLocalizedString("Be sure to check your spam folder in case it got stuck there.\n\nYou can also request a re-send of the confirmation.", comment: ""))", + let clickConfirmation: String = .localized( + "To complete your signup, click the confirmation link we sent to", + comment: "Used in To complete your signup, click the confirmation link we sent to you@gmail.com") + let checkSpam: String = .localized(""" +Be sure to check your spam folder in case it got stuck there. + +You can also request a re-send of the confirmation. +""") + let popup = PopupDialog(title: .localized("Check Your Inbox"), + message: "\(clickConfirmation) \(apiCredentials.email). \(checkSpam)", image: nil, buttonAlignment: .vertical, transitionStyle: .bounceDown, @@ -154,37 +178,38 @@ final class AccountViewController: BaseViewController, Loadable { hideStatusBar: false, completion: nil) popup.addButtons([ - DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {}, - DefaultButton(title: NSLocalizedString("Sign Out", comment: ""), dismissOnTap: true) { + DefaultButton(title: .localizedOkay, dismissOnTap: true) {}, + DefaultButton(title: .localizedSignOut, dismissOnTap: true) { URLCache.shared.removeAllCachedResponses() Client.clearCookies() clearAPICredentials() setAPICredentialsConfirmed(confirmed: false) self.reloadTable() - self.showPopupDialog(title: NSLocalizedString("Success", comment: ""), message: NSLocalizedString("Signed out successfully.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) + self.showPopupDialog( + title: .localized("Success"), + message: .localized("Signed out successfully."), + acceptButton: .localizedOkay) }, - DefaultButton(title: NSLocalizedString("Re-send", comment: ""), dismissOnTap: true) { + DefaultButton(title: .localized("Re-send"), dismissOnTap: true) { firstly { try Client.resendConfirmCode(email: apiCredentials.email) } .done { (success: Bool) in - var message = NSLocalizedString("Successfully re-sent your email confirmation to ", comment: "") + apiCredentials.email - if (success == false) { - message = NSLocalizedString("Failed to re-send email confirmation.", comment: "") + var message: String = .localized("Successfully re-sent your email confirmation to ") + apiCredentials.email + if !success { + message = .localized("Failed to re-send email confirmation.") } - self.showPopupDialog(title: "", message: message, acceptButton: NSLocalizedString("Okay", comment: "")) + self.showPopupDialog(title: "", message: message, acceptButton: .localizedOkay) } .catch { error in - if (self.popupErrorAsNSURLError(error)) { + if self.popupErrorAsNSURLError(error) { return - } - else if let apiError = error as? ApiError { + } else if let apiError = error as? ApiError { _ = self.popupErrorAsApiError(apiError) - } - else { - self.showPopupDialog(title: NSLocalizedString("Error Re-sending Email Confirmation", comment: ""), + } else { + self.showPopupDialog(title: .localized("Error Re-sending Email Confirmation"), message: "\(error)", - acceptButton: NSLocalizedString("Okay", comment: "")) + acceptButton: .localizedOkay) } } }, @@ -196,165 +221,28 @@ final class AccountViewController: BaseViewController, Loadable { } } - let upgradeButton = DefaultButtonCell(title: NSLocalizedString("Loading Plan", comment: "Shows when loading the details of a subscription plan")) { - self.performSegue(withIdentifier: "showUpgradePlanAccount", sender: self) + let upgradeButton = MakeDefaultButtonCell(title: .localized("View or Upgrade Plan")) { + BasePaywallService.shared.showPaywall(on: self) } - upgradeButton.startActivityIndicator() - upgradeButton.button.isEnabled = false - upgradeButton.selectionStyle = .none + upgradeButton.button.isEnabled = true + upgradeButton.selectionStyle = .default + upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue + upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - self.activePlans = [] - - // show the plan status/button - first check and sync email if it's confirmed, otherwise just use receipt - if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { - DDLogInfo("plan status: have confirmed API credentials, using them") - firstly { - try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) - } - .then { (signin: SignIn) -> Promise in - DDLogInfo("plan status: signin result: \(signin)") - return try Client.subscriptionEvent() - } - .then { (result: SubscriptionEvent) -> Promise<[Subscription]> in - DDLogInfo("plan status: subscriptionevent result: \(result)") - return try Client.activeSubscriptions() - }.ensure { - upgradeButton.stopActivityIndicator() - }.done { subscriptions in - DDLogInfo("active-subs: \(subscriptions)") - self.activePlans = subscriptions.map({ $0.planType }) - if let active = subscriptions.first { - if active.planType == .proAnnual { - upgradeButton.button.isEnabled = false - upgradeButton.selectionStyle = .none - upgradeButton.button.setTitle(NSLocalizedString("Plan: Annual Pro", comment: ""), for: UIControl.State()) - } else { - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue - upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("View or Upgrade Plan", comment: ""), for: UIControl.State()) - } - } else { - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue - upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("View Upgrade Options", comment: ""), for: UIControl.State()) - } - } - .catch { error in - if let apiError = error as? ApiError { - switch apiError.code { - case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue - upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("View Upgrade Options", comment:""), for: UIControl.State()) - case kApiCodeSandboxReceiptNotAllowed: - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue - upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("View Upgrade Options", comment:""), for: UIControl.State()) - default: - DDLogError("Error loading plan: API error code - \(apiError.code)") - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.button.setTitleColor(UIColor.systemRed, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("Error Loading Plan: Retry", comment: ""), for: UIControl.State()) - upgradeButton.onSelect { - self.reloadTable() - } - } - } else { - DDLogError("Error loading plan: Non-API Error - \(error.localizedDescription)") - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.button.setTitleColor(UIColor.systemRed, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("Error Loading Plan: Retry", comment: ""), for: UIControl.State()) - upgradeButton.onSelect { - self.reloadTable() - } - } - } - } - // not logged in via email, use receipt - else { - 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.button.isEnabled = false - upgradeButton.selectionStyle = .none - upgradeButton.button.setTitle(NSLocalizedString("Plan: Annual Pro", comment: ""), for: UIControl.State()) - } else { - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue - upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("View or Upgrade Plan", comment: ""), for: UIControl.State()) - } - } else { - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue - upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("View Upgrade Options", comment: ""), for: UIControl.State()) - } - }.catch { error in - DDLogError("Error reloading subscription: \(error.localizedDescription)") - if let apiError = error as? ApiError { - switch apiError.code { - case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue - upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("View Upgrade Options", comment:""), for: UIControl.State()) - case kApiCodeSandboxReceiptNotAllowed: - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.backgroundView?.backgroundColor = UIColor.tunnelsDarkBlue - upgradeButton.button.setTitleColor(UIColor.white, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("View Upgrade Options", comment:""), for: UIControl.State()) - default: - DDLogError("Error loading plan: API error code - \(apiError.code)") - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.button.setTitleColor(UIColor.systemRed, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("Error Loading Plan: Retry", comment: ""), for: UIControl.State()) - upgradeButton.onSelect { - self.reloadTable() - } - } - } else { - DDLogError("Error loading plan: Non-API Error - \(error.localizedDescription)") - upgradeButton.button.isEnabled = true - upgradeButton.selectionStyle = .default - upgradeButton.button.setTitleColor(UIColor.systemRed, for: UIControl.State()) - upgradeButton.button.setTitle(NSLocalizedString("Error Loading Plan: Retry", comment: ""), for: UIControl.State()) - upgradeButton.onSelect { - self.reloadTable() - } - } - } + if let currentSubscription = BaseUserService.shared.user.currentSubscription, + [.proAnnual, .proAnnualLTO].contains(currentSubscription.planType) { + upgradeButton.button.isEnabled = false + upgradeButton.selectionStyle = .none + upgradeButton.button.setTitle(.localized("Plan: Annual Pro"), for: UIControl.State()) } - let notificationsButton = DefaultCell(title: "", action: { }) + let notificationsButton = MakeDefaultCell(title: "", action: { }) - let updateNotificationButtonTitle = { (cell: _DefaultCell) in + let updateNotificationButtonTitle = { (cell: DefaultCell) in if PushNotifications.Authorization.getUserWantsNotificationsEnabled(forCategory: .weeklyUpdate) { - cell.label.text = NSLocalizedString("Notifications: On", comment: "") + cell.label.text = .localized("Notifications: On") } else { - cell.label.text = NSLocalizedString("Notifications: Off", comment: "") + cell.label.text = .localized("Notifications: Off") } } @@ -365,11 +253,23 @@ final class AccountViewController: BaseViewController, Loadable { PushNotifications.Authorization.setUserWantsNotificationsEnabled(false, forCategory: .weeklyUpdate) updateNotificationButtonTitle(notificationsButton) } else { - PushNotifications.Authorization.requestWeeklyUpdateAuthorization(presentingDialogOn: self).done { status in - DDLogInfo("New authorization status for push notifications: \(status)") - updateNotificationButtonTitle(notificationsButton) - }.catch { error in - DDLogError("Error updating notification authorization status: \(error.localizedDescription)") + UNUserNotificationCenter.current().getNotificationSettings { (settings) in + DispatchQueue.main.async { + switch settings.authorizationStatus { + case .denied, .notDetermined: + let enableNotificationsViewController = EnableNotificationsViewController { + updateNotificationButtonTitle(notificationsButton) + } + + enableNotificationsViewController.modalPresentationStyle = .fullScreen + self.present(enableNotificationsViewController, animated: true) + case .authorized: + PushNotifications.Authorization.setUserWantsNotificationsEnabled(true, forCategory: .weeklyUpdate) + updateNotificationButtonTitle(notificationsButton) + default: + break + } + } } } } @@ -383,14 +283,14 @@ final class AccountViewController: BaseViewController, Loadable { let titleLabel = UILabel() titleLabel.text = title - titleLabel.font = fontBold18 + titleLabel.font = .boldLockdownFont(size: 18) titleLabel.textAlignment = .center titleLabel.numberOfLines = 0 stack.addArrangedSubview(titleLabel) let messageLabel = UILabel() messageLabel.text = message - messageLabel.font = fontMedium18 + messageLabel.font = .mediumLockdownFont(size: 18) messageLabel.textAlignment = .center messageLabel.numberOfLines = 0 stack.addArrangedSubview(messageLabel) @@ -407,37 +307,42 @@ final class AccountViewController: BaseViewController, Loadable { } let otherCells = [ - DefaultCell(title: NSLocalizedString("Tutorial", comment: "")) { [unowned self] in + MakeDefaultCell(title: .localized("Tutorial")) { [unowned self] in self.startTutorial() }, - DefaultCell(title: NSLocalizedString("Why Trust Lockdown", comment: "")) { + MakeDefaultCell(title: .localized("Why Trust Lockdown")) { self.showWhyTrustPopup() }, - DefaultCell(title: NSLocalizedString("Privacy Policy", comment: "")) { + MakeDefaultCell(title: .localized("Privacy Policy")) { self.showPrivacyPolicyModal() }, - DefaultCell(title: NSLocalizedString("What is VPN?", comment: "")) { + MakeDefaultCell(title: .localized("What is VPN?")) { self.performSegue(withIdentifier: "showWhatIsVPN", sender: self) }, - DefaultCell(title: NSLocalizedString("Support | Feedback", comment: "")) { + MakeDefaultCell(title: .localized("Support | Feedback")) { + let message = """ +Remember to check our FAQs first, for answers to the most frequently asked questions. + +If it's not answered there, we're happy to provide support and take feedback by email. +""" self.showPopupDialog( title: nil, - message: NSLocalizedString("Remember to check our FAQs first, for answers to the most frequently asked questions.\n\nIf it's not answered there, we're happy to provide support and take feedback by email.", comment: ""), + message: .localized(message), buttons: [ - .custom(title: NSLocalizedString("View FAQs", comment: ""), completion: { + .custom(title: .localized("View FAQs"), completion: { self.showFAQsModal() }), - .custom(title: NSLocalizedString("Email Us", comment: ""), completion: { - self.emailTeam() + .custom(title: .localized("Email Us"), completion: { + self.composeEmail(.helpOrFeedback, attachments: [.diagnostics]) }), .cancel() ] ) }, - DefaultCell(title: NSLocalizedString("FAQs", comment: "")) { + MakeDefaultCell(title: .localized("FAQs")) { self.showFAQsModal() }, - DefaultCell(title: NSLocalizedString("Website", comment: "")) { + MakeDefaultCell(title: .localized("Website")) { self.showWebsiteModal() }, ] @@ -446,8 +351,14 @@ final class AccountViewController: BaseViewController, Loadable { tableView.addCell(cell) } + if let creds = getAPICredentials() { + tableView.addCell(MakeDefaultCell(title: .localized("delete_account"), color: .tunnelsWarning) { + self.deleteAccount(userEmail: creds.email) + }) + } + #if DEBUG - let fixVPNConfig = DefaultCell(title: "_Fix Firewall Config", action: { + let fixVPNConfig = MakeDefaultCell(title: "_Fix Firewall Config", action: { self.showFixFirewallConnectionDialog { FirewallController.shared.deleteConfigurationAndAddAgain() } @@ -457,7 +368,7 @@ final class AccountViewController: BaseViewController, Loadable { tableView.addRowCell { (cell) in cell.textLabel?.text = Bundle.main.versionString - cell.textLabel?.font = fontSemiBold17 + cell.textLabel?.font = .semiboldLockdownFont(size: 17) cell.textLabel?.textColor = UIColor.systemGray cell.textLabel?.textAlignment = .right @@ -470,7 +381,7 @@ final class AccountViewController: BaseViewController, Loadable { func startTutorial() { if let tabBarController = tabBarController as? MainTabBarController { tabBarController.selectedViewController = tabBarController.homeViewController - tabBarController.homeViewController.startTutorial() + tabBarController.homeViewController?.startTutorial() } } @@ -481,7 +392,7 @@ final class AccountViewController: BaseViewController, Loadable { vc.parentVC = (tabBarController as? MainTabBarController)?.homeViewController } case "showUpgradePlanAccount": - if let vc = segue.destination as? SignupViewController { + if let vc = segue.destination as? OldSignupViewController { vc.parentVC = self if activePlans.isEmpty { vc.mode = .newSubscription @@ -495,18 +406,28 @@ final class AccountViewController: BaseViewController, Loadable { } } +extension AccountViewController: EmailComposable { + private func deleteAccount(userEmail: String) { + let deleteAccountViewController = DeleteMyAccountViewController(userEmail: userEmail) + deleteAccountViewController.modalPresentationStyle = .formSheet + present(deleteAccountViewController, animated: true) + } +} + // MARK: - Helpers / Extensions -class _DefaultButtonCell: SelectableTableViewCell { +class DefaultButtonCell: SelectableTableViewCell { let button = UIButton(type: .system) } -func DefaultButtonCell(title: String, action: @escaping () -> ()) -> _DefaultButtonCell { - let cell = _DefaultButtonCell() +func MakeDefaultButtonCell(title: String, action: @escaping () -> Void) -> DefaultButtonCell { + let cell = DefaultButtonCell() cell.backgroundView = UIView() cell.button.setTitle(title, for: .normal) cell.button.isUserInteractionEnabled = false - cell.button.titleLabel?.font = fontSemiBold17 + cell.button.titleLabel?.font = .semiboldLockdownFont(size: 17) + cell.button.titleLabel?.adjustsFontSizeToFitWidth = true + cell.button.titleLabel?.lineBreakMode = .byClipping cell.button.tintColor = .tunnelsBlue cell.contentView.addSubview(cell.button) cell.button.anchors.height.equal(21) @@ -514,22 +435,22 @@ func DefaultButtonCell(title: String, action: @escaping () -> ()) -> _DefaultBut return cell.onSelect(callback: action) } -class _DefaultCell: SelectableTableViewCell { +class DefaultCell: SelectableTableViewCell { let label = UILabel() } -func DefaultCell(title: String, action: @escaping () -> ()) -> _DefaultCell { - let cell = _DefaultCell() +func MakeDefaultCell(title: String, color: UIColor = .tunnelsBlue, action: @escaping () -> Void) -> DefaultCell { + let cell = DefaultCell() cell.label.text = title - cell.label.font = fontSemiBold17 - cell.label.textColor = .tunnelsBlue + cell.label.font = .semiboldLockdownFont(size: 17) + cell.label.textColor = color cell.label.textAlignment = .center cell.contentView.addSubview(cell.label) cell.label.anchors.edges.marginsPin(insets: .init(top: 8, left: 0, bottom: 8, right: 0)) return cell.onSelect(callback: action) } -fileprivate extension _DefaultButtonCell { +fileprivate extension DefaultButtonCell { func startActivityIndicator() { let activity = UIActivityIndicatorView() diff --git a/LockdowniOS/Align.swift b/LockdowniOS/Align.swift index f87e5bc..6ae6641 100644 --- a/LockdowniOS/Align.swift +++ b/LockdowniOS/Align.swift @@ -148,7 +148,9 @@ internal extension Anchor where Type: AnchorType.Dimension { Constraints.constrain(self, anchor, constant: constant, relation: .equal) } - @discardableResult func greaterThanOrEqual(_ anchor: Anchor, constant: CGFloat = 0) -> NSLayoutConstraint { + @discardableResult func greaterThanOrEqual( + _ anchor: Anchor, + constant: CGFloat = 0) -> NSLayoutConstraint { Constraints.constrain(self, anchor, constant: constant, relation: .greaterThanOrEqual) } @@ -183,7 +185,10 @@ extension Anchor where Type: AnchorType.Edge { } /// Adds spacing between the current anchors. - @discardableResult internal func spacing(_ spacing: CGFloat, to anchor: Anchor, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + @discardableResult internal func spacing( + _ spacing: CGFloat, + to anchor: Anchor, + relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { let isInverted = (attribute == .bottom && anchor.attribute == .top) || (attribute == .right && anchor.attribute == .left) || (attribute == .trailing && anchor.attribute == .leading) @@ -244,26 +249,49 @@ internal struct AnchorCollectionEdges { internal typealias Axis = NSLayoutConstraint.Orientation #endif - @discardableResult internal func pin(to item2: LayoutItem? = nil, insets: EdgeInsets = .zero, axis: Axis? = nil, alignment: Alignmment = .fill) -> [NSLayoutConstraint] { + @discardableResult internal func pin( + to item2: LayoutItem? = nil, + insets: EdgeInsets = .zero, + axis: Axis? = nil, + alignment: Alignmment = .fill) -> [NSLayoutConstraint] { let item2 = item2 ?? item.superview! let left: NSLayoutConstraint.Attribute = isAbsolute ? .left : .leading let right: NSLayoutConstraint.Attribute = isAbsolute ? .right : .trailing var constraints = [NSLayoutConstraint]() func constrain(attribute: NSLayoutConstraint.Attribute, relation: NSLayoutConstraint.Relation, constant: CGFloat) { - constraints.append(Constraints.constrain(item: item, attribute: attribute, relatedBy: relation, toItem: item2, attribute: attribute, multiplier: 1, constant: constant)) + constraints.append(Constraints.constrain( + item: item, + attribute: attribute, + relatedBy: relation, + toItem: item2, + attribute: attribute, + multiplier: 1, + constant: constant)) } if axis == nil || axis == .horizontal { - constrain(attribute: left, relation: alignment.horizontal == .fill || alignment.horizontal == .leading ? .equal : .greaterThanOrEqual, constant: insets.left) - constrain(attribute: right, relation: alignment.horizontal == .fill || alignment.horizontal == .trailing ? .equal : .lessThanOrEqual, constant: -insets.right) + constrain( + attribute: left, + relation: alignment.horizontal == .fill || alignment.horizontal == .leading ? .equal : .greaterThanOrEqual, + constant: insets.left) + constrain( + attribute: right, + relation: alignment.horizontal == .fill || alignment.horizontal == .trailing ? .equal : .lessThanOrEqual, + constant: -insets.right) if alignment.horizontal == .center { constrain(attribute: .centerX, relation: .equal, constant: 0) } } if axis == nil || axis == .vertical { - constrain(attribute: .top, relation: alignment.vertical == .fill || alignment.vertical == .top ? .equal : .greaterThanOrEqual, constant: insets.top) - constrain(attribute: .bottom, relation: alignment.vertical == .fill || alignment.vertical == .bottom ? .equal : .lessThanOrEqual, constant: -insets.bottom) + constrain( + attribute: .top, + relation: alignment.vertical == .fill || alignment.vertical == .top ? .equal : .greaterThanOrEqual, + constant: insets.top) + constrain( + attribute: .bottom, + relation: alignment.vertical == .fill || alignment.vertical == .bottom ? .equal : .lessThanOrEqual, + constant: -insets.bottom) if alignment.vertical == .center { constrain(attribute: .centerY, relation: .equal, constant: 0) } @@ -315,12 +343,20 @@ internal struct AnchorCollectionSize { [width.equal(item.anchors.width * multiplier - insets.width), height.equal(item.anchors.height * multiplier - insets.height)] } - @discardableResult internal func greaterThanOrEqual(_ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1) -> [NSLayoutConstraint] { - [width.greaterThanOrEqual(item.anchors.width * multiplier - insets.width), height.greaterThanOrEqual(item.anchors.height * multiplier - insets.height)] + @discardableResult internal func greaterThanOrEqual( + _ item: Item, + insets: CGSize = .zero, + multiplier: CGFloat = 1) -> [NSLayoutConstraint] { + [width.greaterThanOrEqual(item.anchors.width * multiplier - insets.width), + height.greaterThanOrEqual(item.anchors.height * multiplier - insets.height)] } - @discardableResult internal func lessThanOrEqual(_ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1) -> [NSLayoutConstraint] { - [width.lessThanOrEqual(item.anchors.width * multiplier - insets.width), height.lessThanOrEqual(item.anchors.height * multiplier - insets.height)] + @discardableResult internal func lessThanOrEqual( + _ item: Item, + insets: CGSize = .zero, + multiplier: CGFloat = 1) -> [NSLayoutConstraint] { + [width.lessThanOrEqual(item.anchors.width * multiplier - insets.width), + height.lessThanOrEqual(item.anchors.height * multiplier - insets.height)] } } @@ -345,27 +381,67 @@ internal final class Constraints { } /// Creates and automatically installs a constraint. - fileprivate static func constrain(item item1: Any, attribute attr1: NSLayoutConstraint.Attribute, relatedBy relation: NSLayoutConstraint.Relation = .equal, toItem item2: Any? = nil, attribute attr2: NSLayoutConstraint.Attribute? = nil, multiplier: CGFloat = 1, constant: CGFloat = 0) -> NSLayoutConstraint { + fileprivate static func constrain( + item item1: Any, + attribute attr1: NSLayoutConstraint.Attribute, + relatedBy relation: NSLayoutConstraint.Relation = .equal, + toItem item2: Any? = nil, + attribute attr2: NSLayoutConstraint.Attribute? = nil, + multiplier: CGFloat = 1, + constant: CGFloat = 0) -> NSLayoutConstraint { + precondition(Thread.isMainThread, "Align APIs can only be used from the main thread") #if os(iOS) || os(tvOS) (item1 as? UIView)?.translatesAutoresizingMaskIntoConstraints = false #elseif os(macOS) (item1 as? NSView)?.translatesAutoresizingMaskIntoConstraints = false #endif - let constraint = NSLayoutConstraint(item: item1, attribute: attr1, relatedBy: relation, toItem: item2, attribute: attr2 ?? .notAnAttribute, multiplier: multiplier, constant: constant) + let constraint = NSLayoutConstraint( + item: item1, + attribute: attr1, + relatedBy: relation, + toItem: item2, + attribute: attr2 ?? .notAnAttribute, + multiplier: multiplier, + constant: constant) _install(constraint) return constraint } /// Creates and automatically installs a constraint between two anchors. - fileprivate static func constrain(_ lhs: Anchor, _ rhs: Anchor, constant: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - constrain(item: lhs.item, attribute: lhs.attribute, relatedBy: relation, toItem: rhs.item, attribute: rhs.attribute, multiplier: (multiplier / lhs.multiplier) * rhs.multiplier, constant: constant - lhs.offset + rhs.offset) + fileprivate static func constrain( + _ lhs: Anchor, + _ rhs: Anchor, + constant: CGFloat = 0, + multiplier: CGFloat = 1, + relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + constrain( + item: lhs.item, + attribute: lhs.attribute, + relatedBy: relation, + toItem: rhs.item, + attribute: rhs.attribute, + multiplier: (multiplier / lhs.multiplier) * rhs.multiplier, + constant: constant - lhs.offset + rhs.offset) } /// Creates and automatically installs a constraint between an anchor and /// a given item. - fileprivate static func constrain(_ lhs: Anchor, toItem item2: Any?, attribute attr2: NSLayoutConstraint.Attribute?, constant: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { - constrain(item: lhs.item, attribute: lhs.attribute, relatedBy: relation, toItem: item2, attribute: attr2, multiplier: multiplier / lhs.multiplier, constant: constant - lhs.offset) + fileprivate static func constrain( + _ lhs: Anchor, + toItem item2: Any?, + attribute attr2: NSLayoutConstraint.Attribute?, + constant: CGFloat = 0, + multiplier: CGFloat = 1, + relation: NSLayoutConstraint.Relation = .equal) -> NSLayoutConstraint { + constrain( + item: lhs.item, + attribute: lhs.attribute, + relatedBy: relation, + toItem: item2, + attribute: attr2, + multiplier: multiplier / lhs.multiplier, + constant: constant - lhs.offset) } private static var _stack = [Constraints]() // this is what enabled constraint auto-installing @@ -388,11 +464,20 @@ extension Constraints { self.init { closure(a.anchors, b.anchors) } } - @discardableResult internal convenience init(for a: A, _ b: B, _ c: C, _ closure: (LayoutAnchors, LayoutAnchors, LayoutAnchors) -> Void) { + @discardableResult internal convenience init( + for a: A, + _ b: B, + _ c: C, + _ closure: (LayoutAnchors, LayoutAnchors, LayoutAnchors) -> Void) { self.init { closure(a.anchors, b.anchors, c.anchors) } } - @discardableResult internal convenience init(for a: A, _ b: B, _ c: C, _ d: D, _ closure: (LayoutAnchors, LayoutAnchors, LayoutAnchors, LayoutAnchors) -> Void) { + @discardableResult internal convenience init( + for a: A, + _ b: B, + _ c: C, + _ d: D, + _ closure: (LayoutAnchors, LayoutAnchors, LayoutAnchors, LayoutAnchors) -> Void) { self.init { closure(a.anchors, b.anchors, c.anchors, d.anchors) } } } @@ -423,10 +508,16 @@ extension NSLayoutConstraint.Relation { extension EdgeInsets { fileprivate func inset(for attribute: NSLayoutConstraint.Attribute, edge: Bool = false) -> CGFloat { switch attribute { - case .top: return top; case .bottom: return edge ? -bottom : bottom - case .left, .leading: return left - case .right, .trailing: return edge ? -right : right - default: return 0 + case .top: + return top + case .bottom: + return edge ? -bottom : bottom + case .left, .leading: + return left + case .right, .trailing: + return edge ? -right : right + default: + return 0 } } } @@ -452,11 +543,17 @@ extension AnchorCollectionEdges { pin(to: item.superview!.safeAreaLayoutGuide, insets: insets, axis: axis, alignment: alignment) } - @discardableResult internal func readableContentPin(insets: EdgeInsets = .zero, axis: Axis? = nil, alignment: Alignmment = .fill) -> [NSLayoutConstraint] { + @discardableResult internal func readableContentPin( + insets: EdgeInsets = .zero, + axis: Axis? = nil, + alignment: Alignmment = .fill) -> [NSLayoutConstraint] { pin(to: item.superview!.readableContentGuide, insets: insets, axis: axis, alignment: alignment) } - @discardableResult internal func marginsPin(insets: EdgeInsets = .zero, axis: Axis? = nil, alignment: Alignmment = .fill) -> [NSLayoutConstraint] { + @discardableResult internal func marginsPin( + insets: EdgeInsets = .zero, + axis: Axis? = nil, + alignment: Alignmment = .fill) -> [NSLayoutConstraint] { pin(to: item.superview!.layoutMarginsGuide, insets: insets, axis: axis, alignment: alignment) } } diff --git a/LockdowniOS/AppDelegate.swift b/LockdowniOS/AppDelegate.swift index 8c44eb5..74a3649 100644 --- a/LockdowniOS/AppDelegate.swift +++ b/LockdowniOS/AppDelegate.swift @@ -5,20 +5,17 @@ // Copyright © 2018 Confirmed, Inc. All rights reserved. // -import UIKit +import AVFoundation +import BackgroundTasks +import CloudKit +import CocoaLumberjackSwift import NetworkExtension -import SwiftyStoreKit -import KeychainAccess import SafariServices import SwiftMessages -import StoreKit -import CloudKit -import CocoaLumberjackSwift +import SwiftyStoreKit import PopupDialog import PromiseKit -import UserNotifications import WidgetKit -import BackgroundTasks let fileLogger: DDFileLogger = DDFileLogger() @@ -28,7 +25,10 @@ let kHasShownTitlePage: String = "kHasShownTitlePage" class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - let noInternetMessageView = MessageView.viewFromNib(layout: .statusLine) + + private let connectivityService = ConnectivityService() + private let paywallService = BasePaywallService.shared + private let userService = BaseUserService.shared func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { @@ -49,51 +49,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Set up PopupDialog let dialogAppearance = PopupDialogDefaultView.appearance() - if #available(iOS 13.0, *) { - dialogAppearance.backgroundColor = .systemBackground - dialogAppearance.titleColor = .label - dialogAppearance.messageColor = .label - } else { - dialogAppearance.backgroundColor = .white - dialogAppearance.titleColor = .black - dialogAppearance.messageColor = .darkGray - } - dialogAppearance.titleFont = fontBold15 + dialogAppearance.backgroundColor = .systemBackground + dialogAppearance.titleColor = .label + dialogAppearance.messageColor = .label + dialogAppearance.titleFont = .boldLockdownFont(size: 15) dialogAppearance.titleTextAlignment = .center - dialogAppearance.messageFont = fontMedium15 + dialogAppearance.messageFont = .mediumLockdownFont(size: 15) dialogAppearance.messageTextAlignment = .center let buttonAppearance = DefaultButton.appearance() - if #available(iOS 13.0, *) { - buttonAppearance.buttonColor = .systemBackground - buttonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1) - } - else { - buttonAppearance.buttonColor = .clear - buttonAppearance.separatorColor = UIColor(white: 0.9, alpha: 1) - } - buttonAppearance.titleFont = fontSemiBold17 + buttonAppearance.buttonColor = .systemBackground + buttonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1) + buttonAppearance.titleFont = .semiboldLockdownFont(size: 17) buttonAppearance.titleColor = UIColor.tunnelsBlue let dynamicButtonAppearance = DynamicButton.appearance() - if #available(iOS 13.0, *) { - dynamicButtonAppearance.buttonColor = .systemBackground - dynamicButtonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1) - } - else { - dynamicButtonAppearance.buttonColor = .clear - dynamicButtonAppearance.separatorColor = UIColor(white: 0.9, alpha: 1) - } - dynamicButtonAppearance.titleFont = fontSemiBold17 + dynamicButtonAppearance.buttonColor = .systemBackground + dynamicButtonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1) + dynamicButtonAppearance.titleFont = .semiboldLockdownFont(size: 17) dynamicButtonAppearance.titleColor = UIColor.tunnelsBlue let cancelButtonAppearance = CancelButton.appearance() - if #available(iOS 13.0, *) { - cancelButtonAppearance.buttonColor = .systemBackground - cancelButtonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1) - } - else { - cancelButtonAppearance.buttonColor = .clear - cancelButtonAppearance.separatorColor = UIColor(white: 0.9, alpha: 1) - } - cancelButtonAppearance.titleFont = fontSemiBold17 + cancelButtonAppearance.buttonColor = .systemBackground + cancelButtonAppearance.separatorColor = UIColor(white: 0.2, alpha: 1) + cancelButtonAppearance.titleFont = .semiboldLockdownFont(size: 17) cancelButtonAppearance.titleColor = UIColor.lightGray // Lockdown default lists @@ -102,26 +78,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Whitelist default domains setupLockdownWhitelistedDomains() - // Show indicator at top when internet not reachable - reachability?.whenReachable = { reachability in - SwiftMessages.hide() - } - reachability?.whenUnreachable = { _ in - DDLogInfo("Internet not reachable") - self.noInternetMessageView.backgroundView.backgroundColor = UIColor.orange - self.noInternetMessageView.bodyLabel?.textColor = UIColor.white - self.noInternetMessageView.configureContent(body: NSLocalizedString("No Internet Connection", comment: "")) - var noInternetMessageViewConfig = SwiftMessages.defaultConfig - noInternetMessageViewConfig.presentationContext = .window(windowLevel: UIWindow.Level(rawValue: 0)) - noInternetMessageViewConfig.preferredStatusBarStyle = .lightContent - noInternetMessageViewConfig.duration = .forever - SwiftMessages.show(config: noInternetMessageViewConfig, view: self.noInternetMessageView) - } - do { - try reachability?.startNotifier() - } catch { - DDLogError("Unable to start reachability notifier") - } + IntentService.donateIntents() + + connectivityService.startObservingConnectivity() // Content Blocker SFContentBlockerManager.reloadContentBlocker( withIdentifier: "com.confirmed.lockdown.Confirmed-Blocker") { (_ error: Error?) -> Void in @@ -134,7 +93,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { VPNSubscription.cacheLocalizedPrices() SwiftyStoreKit.completeTransactions(atomically: true) { purchases in for purchase in purchases { - DDLogInfo("LAUNCH: Processing Purchase\n\(purchase)"); + DDLogInfo("LAUNCH: Processing Purchase\n\(purchase)") if purchase.transaction.transactionState == .purchased || purchase.transaction.transactionState == .restored { if purchase.needsFinishTransaction { DDLogInfo("Finishing transaction for purchase: \(purchase)") @@ -145,40 +104,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } // Periodically check if the firewall is functioning correctly - every 2.5 hours - if #available(iOS 13.0, *) { - DDLogInfo("BGTask: Registering BGTask id \(FirewallRepair.identifier)") - BGTaskScheduler.shared.register(forTaskWithIdentifier: FirewallRepair.identifier, using: nil) { task in - DDLogInfo("BGTask: Task starting") - FirewallRepair.handleAppRefresh(task) - } - } - else { - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) + DDLogInfo("BGTask: Registering BGTask id \(FirewallRepair.identifier)") + BGTaskScheduler.shared.register(forTaskWithIdentifier: FirewallRepair.identifier, using: nil) { task in + DDLogInfo("BGTask: Task starting") + FirewallRepair.handleAppRefresh(task) } // WORKAROUND: allows the widget to toggle VPN application.registerForRemoteNotifications() setupWidgetToggleWorkaround() - // If not yet agreed to privacy policy, set initial view controller to TitleViewController - if (defaults.bool(forKey: kHasShownTitlePage) == false) { - // TODO: removed this check because this was causing crashes possibly due to Locale - // don't show onboarding page for anyone who installed before Aug 16th - // let formatter = DateFormatter() - // formatter.dateFormat = "yyyy/MM/dd HH:mm" - // let tutorialCutoffDate = formatter.date(from: "2019/08/16 00:00")!.timeIntervalSince1970; - // if let appInstall = appInstallDate, appInstall.timeIntervalSince1970 < tutorialCutoffDate { - // print("Not showing onboarding page, installation epoch \(appInstall.timeIntervalSince1970)") - // } - // else { - DDLogInfo("Showing onboarding page") - self.window = UIWindow(frame: UIScreen.main.bounds) - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let viewController = storyboard.instantiateViewController(withIdentifier: "titleViewController") as! TitleViewController - self.window?.rootViewController = viewController - self.window?.makeKeyAndVisible() - // } - } + window = UIWindow(frame: UIScreen.main.bounds) + window?.rootViewController = SplashscreenViewController() + window?.makeKeyAndVisible() + + _ = try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers) return true } @@ -189,6 +129,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func applicationDidBecomeActive(_ application: UIApplication) { + showRedemptionCodeResultIfNeeded() + DDLogInfo("applicationDidBecomeActive") PacketTunnelProviderLogs.flush() updateMetrics(.resetIfNeeded, rescheduleNotifications: .always) @@ -232,6 +174,53 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return .portrait } + + private func showRedemptionCodeResultIfNeeded() { + DDLogInfo("After code redemption, paywall context is: \(paywallService.context).") + guard paywallService.context == .redeemOfferCode else { return } + paywallService.context = .normal + DDLogInfo("Going further, making context \(paywallService.context).") + + DDLogInfo("After code redemption, searching topViewController.") + let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first + guard var topController = keyWindow?.rootViewController else { return } + while let presentedViewController = topController.presentedViewController { + topController = presentedViewController + } + DDLogInfo("topViewController is: \(String(describing: type(of: topController)))") + + guard let topController = topController as? BaseViewController else { + DDLogInfo("topViewController is not of type BaseViewController.") + return + } + + let previousSubscription = userService.user.currentSubscription + DDLogInfo("Previous subscription is: \(String(describing: previousSubscription))") + + (topController as? Loadable)?.showLoadingView() + userService.updateUserSubscription { newSubscription in + DDLogInfo("Updated subscription is: \(String(describing: previousSubscription))") + (topController as? Loadable)?.hideLoadingView() + + if previousSubscription?.planType == newSubscription?.planType { + DDLogInfo("Subscription is the same, showing failure popup.") + topController.showPopupDialog(title: .localized("cannot_redeem_code"), + message: .localized("the_code_could_not_be_redeemed"), + acceptButton: .localizedOK, + tapGestureDismissal: false, + panGestureDismissal: false) + } else { + DDLogInfo("Subscription is different, showing success popup.") + topController.showPopupDialog(title: .localized("congratulations_excl"), + message: .localized("the_code_was_successfully_redeemed"), + acceptButton: .localizedOK, + tapGestureDismissal: false, + panGestureDismissal: false) { [weak topController] in + topController?.dismiss(animated: true) + } + } + } + } // MARK: - WIDGET TOGGLE WORKAROUND func setupWidgetToggleWorkaround() { @@ -240,17 +229,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { clearDatabaseForRecord(recordName: kCloseFirewallTunnelRecord) clearDatabaseForRecord(recordName: kRestartFirewallTunnelRecord) let privateDatabase = CKContainer(identifier: kICloudContainer).privateCloudDatabase - privateDatabase.fetchAllSubscriptions(completionHandler: { subscriptions, error in + privateDatabase.fetchAllSubscriptions(completionHandler: { _, _ in // always set up cloudkit subscriptions - no downside to doing it // if error == nil, let subs = subscriptions { -//// for sub in subs { -//// print("deleting sub: \(sub.subscriptionID)") -//// privateDatabase.delete(withSubscriptionID: sub.subscriptionID, completionHandler: { -//// result, error in -//// print("result: \(result)") -//// }) -//// } -//// return +// for sub in subs { +// print("deleting sub: \(sub.subscriptionID)") +// privateDatabase.delete(withSubscriptionID: sub.subscriptionID, completionHandler: { +// result, error in +// print("result: \(result)") +// }) +// } +// return // var isSubscribedToOpen = false // var isSubscribedToClose = false // var isSubscribedToRestart = false @@ -294,16 +283,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { notificationInfo.shouldBadge = false notificationInfo.category = categoryName subscription.notificationInfo = notificationInfo - privateDatabase.save(subscription, - completionHandler: ({returnRecord, error in + privateDatabase.save(subscription) { _, error in if let err = error { DDLogInfo("Could not save CloudKit subscription (not signed in?) \(err)") } else { - DispatchQueue.main.async() { + DispatchQueue.main.async { DDLogInfo("Successfully saved CloudKit subscription") } } - })) + } } func clearDatabaseForRecord(recordName: String) { @@ -316,7 +304,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DDLogError("Error querying for CKRecordType: \(recordName) - \(err)") } for aRecord in record! { - privateDatabase.delete(withRecordID: aRecord.recordID, completionHandler: { (recordID, error) in + privateDatabase.delete(withRecordID: aRecord.recordID, completionHandler: { _, _ in DDLogInfo("Deleting record \(aRecord.recordID)") }) } @@ -324,20 +312,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func application(_ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable : Any], + didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { DDLogInfo("Receiving remote notification") if let aps = userInfo["aps"] as? NSDictionary { if let message = aps["category"] as? NSString { if message.contains(kCloseFirewallTunnelRecord) { FirewallController.shared.setEnabled(false, isUserExplicitToggle: true, completion: { _ in }) - } - else if message.contains(kOpenFirewallTunnelRecord) { + } else if message.contains(kOpenFirewallTunnelRecord) { FirewallController.shared.setEnabled(true, isUserExplicitToggle: true, completion: { _ in }) - } - else if message.contains(kRestartFirewallTunnelRecord) { - FirewallController.shared.restart(completion: { - error in + } else if message.contains(kRestartFirewallTunnelRecord) { + FirewallController.shared.restart(completion: { error in if error != nil { DDLogError("Error restarting firewall on RemoteNotification: \(error!)") } @@ -353,7 +338,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { }) } - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { guard let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true), let host = components.host else { @@ -361,9 +346,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return false } - if (host == "resetsuccessful") { - let popup = PopupDialog(title: "Password Reset Successfully", - message: "Please Sign In With Your New Password", + if host == "resetsuccessful" { + let popup = PopupDialog(title: .localized("password_reset_successfully"), + message: .localized("please_sign_in_with_new_password"), image: nil, buttonAlignment: .horizontal, transitionStyle: .bounceDown, @@ -373,55 +358,47 @@ class AppDelegate: UIResponder, UIApplicationDelegate { hideStatusBar: false, completion: nil) popup.addButtons([ - DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) { + DefaultButton(title: .localizedOkay, dismissOnTap: true) { if let hvc = self.getCurrentViewController() as? HomeViewController { - AccountUI.presentSignInToAccount(on: hvc) + let signUpViewController = SignUpViewController(mode: .login) + hvc.present(signUpViewController, animated: true) } } ]) self.getCurrentViewController()?.present(popup, animated: true, completion: nil) return true - } - - else if (host == "changeVPNregion") { + } else if host == "changeVPNregion" { if let home = self.getCurrentViewController() as? HomeViewController { home.showSetRegion(self) } - } - - else if (host == "showMetrics") { + } else if host == "showMetrics" { if let home = self.getCurrentViewController() as? HomeViewController { home.showBlockLog(self) } - } - - else if (host == "toggleFirewall") { + } else if host == "toggleFirewall" { if let home = self.getCurrentViewController() as? HomeViewController { home.toggleFirewall(self) } - } - - else if (host == "toggleVPN") { + } else if host == "toggleVPN" { if let home = self.getCurrentViewController() as? HomeViewController { home.toggleVPN(self) } - } - - else if (host == "emailconfirmed") { + } else if host == "emailconfirmed" { // test the stored login guard let apiCredentials = getAPICredentials() else { - let popup = PopupDialog(title: "Error", - message: NSLocalizedString("No stored API credentials found. Please contact team@lockdownprivacy.com about this error.", comment: ""), - image: nil, - buttonAlignment: .horizontal, - transitionStyle: .bounceDown, - preferredWidth: 270, - tapGestureDismissal: true, - panGestureDismissal: false, - hideStatusBar: false, - completion: nil) + let popup = PopupDialog( + title: "Error", + message: .localized("No stored API credentials found. Please contact team@lockdownprivacy.com about this error."), + 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) {} + DefaultButton(title: .localizedOkay, dismissOnTap: true) {} ]) getCurrentViewController()?.present(popup, animated: true, completion: nil) return true @@ -429,19 +406,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { firstly { try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) } - .done { (signin: SignIn) in + .done { (_: SignIn) in // 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 + .then { (_: SubscriptionEvent) -> Promise in try Client.getKey() } .done { (getKey: GetKey) in try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) - if (getUserWantsVPNEnabled() == true) { + if getUserWantsVPNEnabled() == true { VPNController.shared.restart() } } @@ -449,8 +426,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // 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: ""), + let message = """ +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. +""" + let popup = PopupDialog(title: .localized("Success! 🎉"), + message: .localized(message), image: nil, buttonAlignment: .horizontal, transitionStyle: .bounceDown, @@ -460,7 +441,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { hideStatusBar: false, completion: nil) popup.addButtons([ - DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {} + DefaultButton(title: .localizedOkay, dismissOnTap: true) {} ]) self.getCurrentViewController()?.present(popup, animated: true, completion: nil) DispatchQueue.main.async { @@ -474,8 +455,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { errorMessage = apiError.message } + let confirmError: String = .localized("Error while trying to confirm your account:") + let persistsHelp: String = .localized("If this persists, please contact team@lockdownprivacy.com.") let popup = PopupDialog(title: "Error Confirming Account", - message: "\(NSLocalizedString("Error while trying to confirm your account:", comment: "")) \(errorMessage). \(NSLocalizedString("If this persists, please contact team@lockdownprivacy.com.", comment: ""))", + message: "\(confirmError) \(errorMessage). \(persistsHelp)", image: nil, buttonAlignment: .horizontal, transitionStyle: .bounceDown, @@ -485,7 +468,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { hideStatusBar: false, completion: nil) popup.addButtons([ - DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {} + DefaultButton(title: .localizedOkay, dismissOnTap: true) {} ]) self.getCurrentViewController()?.present(popup, animated: true, completion: nil) } @@ -517,7 +500,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var currentController: UIViewController! = rootController // Each ViewController keeps track of the view it has presented, so we // can move from the head to the tail, which will always be the current view - while( currentController.presentedViewController != nil ) { + while currentController.presentedViewController != nil { currentController = currentController.presentedViewController } return currentController @@ -536,11 +519,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } extension AppDelegate: UNUserNotificationCenterDelegate { - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + func userNotificationCenter( + _ center: UNUserNotificationCenter, willPresent + notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert, .sound]) } - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { let identifier = PushNotifications.Identifier(rawValue: response.notification.request.identifier) if identifier.isWeeklyUpdate { showUpdateBlockListsFlow() @@ -572,7 +561,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { private func showUpdatingBlockListsLoader() { let activity = ActivityData( - message: NSLocalizedString("Updating Block Lists", comment: ""), + message: .localized("Updating Block Lists"), messageFont: UIFont(name: "Montserrat-Bold", size: 18), type: .ballSpinFadeLoader, backgroundColor: UIColor(red: 0, green: 0, blue: 0, alpha: 0.7) @@ -586,11 +575,10 @@ extension AppDelegate: UNUserNotificationCenterDelegate { private func showBlockListsUpdatedPopup() { let popup = PopupDialog( - title: NSLocalizedString("Update Success", comment: ""), + title: .localized("Update Success"), message: "You're now protected against the latest trackers. 🎉" ) - popup.addButton(DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: - true, action: nil)) + popup.addButton(DefaultButton(title: .localizedOkay, dismissOnTap: true, action: nil)) self.getCurrentViewController()?.present(popup, animated: true, completion: nil) } } diff --git a/LockdowniOS/AppStoreProductGroup.swift b/LockdowniOS/AppStoreProductGroup.swift new file mode 100644 index 0000000..649bcfa --- /dev/null +++ b/LockdowniOS/AppStoreProductGroup.swift @@ -0,0 +1,34 @@ +// +// AppStoreProductGroup.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/10/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import UIKit + +enum AppStoreProductGroup: Int, CaseIterable { + case firewallAndVpn + case pro + + func hasFeature(_ feature: PaywallTableFeature) -> Bool { + switch self { + case .firewallAndVpn: + return [ + .stopsAdsAndTrackers, + .blocksBadware, + .accessCuratedBlockLists, + .buildCustomBlockLists, + .stopsBrowsingHistoryTracking, + .anonymizingBrowsing, + .hidesLocation, + .letsYouChangeIP, + .appForPhoneAndIpad + ].contains(feature) + case .pro: + return [PaywallTableFeature].allDefaultFeatures.contains(feature) + } + } +} diff --git a/LockdowniOS/Base.lproj/LaunchScreen.storyboard b/LockdowniOS/Base.lproj/LaunchScreen.storyboard index 7f26b01..27d7f29 100644 --- a/LockdowniOS/Base.lproj/LaunchScreen.storyboard +++ b/LockdowniOS/Base.lproj/LaunchScreen.storyboard @@ -1,11 +1,18 @@ - - + + - + + + + + + Montserrat-SemiBold + + @@ -18,7 +25,30 @@ - + + + + + + + + + + + + + + + + + + + @@ -26,4 +56,13 @@ + + + + + + + + + diff --git a/LockdowniOS/Base.lproj/Main.storyboard b/LockdowniOS/Base.lproj/Main.storyboard index 5e8db78..d5c2a30 100644 --- a/LockdowniOS/Base.lproj/Main.storyboard +++ b/LockdowniOS/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -73,14 +73,14 @@ - + - + - - - - - - - - - - - - - - - - - - - - + + + + - + + - + + + - - + + + + - + @@ -1836,10 +1897,15 @@ + + + + + @@ -1848,7 +1914,7 @@ - + @@ -1911,18 +1977,13 @@ - - - - - @@ -1932,7 +1993,6 @@ - @@ -1953,7 +2013,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/ComplexPaywallViewController.xib b/LockdowniOS/ComplexPaywallViewController.xib new file mode 100644 index 0000000..faab35e --- /dev/null +++ b/LockdowniOS/ComplexPaywallViewController.xib @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + Montserrat-Regular + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/Core/CommonUI/FloatingInput/FloatingTextInputTextField.swift b/LockdowniOS/Core/CommonUI/FloatingInput/FloatingTextInputTextField.swift new file mode 100644 index 0000000..8343425 --- /dev/null +++ b/LockdowniOS/Core/CommonUI/FloatingInput/FloatingTextInputTextField.swift @@ -0,0 +1,165 @@ +// +// FloatingTextField.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/3/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +open class FloatingTextInputTextField: UITextField { + + /// Container with additional labels + let textBox = TextBox() + + private var rightViews = [TextInputState: UIView]() + private var borderLayer: CALayer? + + @IBInspectable open var title: String? { + get { return textBox.title } + set { textBox.title = newValue } + } + + open var titleFont: UIFont? { + get { return textBox.titleLabel.font } + set { textBox.titleLabel.font = newValue } + } + + @IBInspectable open var titleColor: UIColor? { + get { return textBox.titleColor } + set { textBox.titleColor = newValue } + } + + open var placeholderFont: UIFont? { + get { return textBox.placeholderFont } + set { textBox.placeholderFont = newValue } + } + + @IBInspectable open var placeholderColor: UIColor? { + get { return textBox.placeholderLabel.textColor } + set { textBox.placeholderLabel.textColor = newValue } + } + + // MARK: - Init + + override public init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + open func commonInit() { + if let text = super.placeholder { + super.placeholder = nil + placeholder = text + } + setUpTextBoxConstraints() + setupActions() + updateState(animated: false) + adjustsFontForContentSizeCategory = true + rightViewMode = .always + layer.masksToBounds = true + layer.borderColor = UIColor.fromHex("#00ADE7").cgColor + corners = .continuous(8) + } + + // MARK: - Public + + @objc open func clear() { + if delegate?.textFieldShouldClear?(self) == false { return } + super.text = nil // в `self.text` обновление текста происходит без анимации + updateState(animated: true) + sendActions(for: .editingChanged) + } + + open func setRigthView(_ view: UIView?, for state: TextInputState) { + rightViews[state] = view + updateState(animated: false) + } + + open func rigthView(for state: TextInputState) -> UIView? { + return rightViews[state] + } + + // MARK: - UITextInput + + // If font size of placeholder and of text in UITextField are different, + // the caret height (and hence that of the whole textField) will be changing. + // To avoid this, we equal the caret height to placeholderLabel font size. + override open func caretRect(for position: UITextPosition) -> CGRect { + var rect = super.caretRect(for: position) + rect.size.height = textBox.placeholderLabel.font.lineHeight + return rect + } + + // MARK: - UITextField + + override open var text: String? { + didSet { updateState(animated: false) } + } + + override open var placeholder: String? { + get { return textBox.placeholderLabel.text } + set { textBox.placeholderLabel.text = newValue } + } + + override open func editingRect(forBounds bounds: CGRect) -> CGRect { + let rect = super.editingRect(forBounds: bounds) + return rect.inset(by: textBox.editingTextInsets).integral + } + + override open func placeholderRect(forBounds bounds: CGRect) -> CGRect { + let rect = super.editingRect(forBounds: bounds) + return rect.inset(by: textBox.editingTextInsets).integral + } + + override open func textRect(forBounds bounds: CGRect) -> CGRect { + let rect = super.textRect(forBounds: bounds) + return rect.inset(by: textBox.editingTextInsets).integral + } + + override open func rightViewRect(forBounds bounds: CGRect) -> CGRect { + return super.rightViewRect(forBounds: bounds.inset(by: layoutMargins)) + } + + // MARK: - UIView + + override open func layoutMarginsDidChange() { + super.layoutMarginsDidChange() + textBox.layoutMargins = layoutMargins + } + + // MARK: - Private + + private func setupActions() { + [.editingDidBegin, .editingChanged, .editingDidEnd].forEach { + addTarget(self, action: #selector(textDidEditing), for: $0) + } + } + + @objc private func textDidEditing() { + updateState(animated: true) + } + + private func updateState(animated: Bool) { + let state = TextInputState(hasText: hasText, firstResponder: isFirstResponder) + rightView = rigthView(for: state) + textBox.setState(state, animated: animated) + } + + private func setUpTextBoxConstraints() { + addSubview(textBox) + textBox.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + textBox.topAnchor.constraint(equalTo: topAnchor), + textBox.leadingAnchor.constraint(equalTo: leadingAnchor), + textBox.trailingAnchor.constraint(equalTo: trailingAnchor), + textBox.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } +} diff --git a/LockdowniOS/Core/CommonUI/FloatingInput/TextBox.swift b/LockdowniOS/Core/CommonUI/FloatingInput/TextBox.swift new file mode 100644 index 0000000..8f91f20 --- /dev/null +++ b/LockdowniOS/Core/CommonUI/FloatingInput/TextBox.swift @@ -0,0 +1,170 @@ +// +// TextBox.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/3/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +internal final class TextBox: UIView { + + private let leftTextMargin: CGFloat = 16 + + private(set) var state = TextInputState.placeholder + + var title: String? { + didSet { + titleLabel.text = title + titlePlaceholderLabel.text = title + } + } + + var titleColor: UIColor? { + didSet { + titleLabel.textColor = titleColor + titlePlaceholderLabel.textColor = titleColor + } + } + + var placeholderFont: UIFont? = .regularLockdownFont(size: 17) { + didSet { + titlePlaceholderLabel.font = placeholderFont + placeholderLabel.font = placeholderFont + } + } + + let titleLabel: UILabel = TextBoxLabel(fontSize: UIFont.smallSystemFontSize) + let titlePlaceholderLabel: UILabel = TextBoxLabel() + let placeholderLabel: UILabel = TextBoxLabel() + + private let titleBottomSpace: CGFloat = 2 + private let placeholderBottom: CGFloat = 6 + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + commonInit() + } + + // MARK: - Internal + + var editingTextInsets: UIEdgeInsets { + return UIEdgeInsets( + top: titleLabel.font.lineHeight + titleBottomSpace, + left: leftTextMargin, + bottom: 0, + right: leftTextMargin) + } + + func setState(_ newState: TextInputState, animated: Bool) { + let oldSate = state + state = newState + let isAnimated = animated && window != nil && frame != .zero + + switch (oldSate, newState, isAnimated) { + case (_, .empty, true): + moveTitleDown() + case (.empty, .placeholder, true): + moveTitleUp() + default: + stateDidUpdate() + } + } + + // MARK: - Private + + private func commonInit() { + isUserInteractionEnabled = false + let subviews = [ + titleLabel, + titlePlaceholderLabel, + placeholderLabel + ] + for subview in subviews { + subview.translatesAutoresizingMaskIntoConstraints = false + subview.isUserInteractionEnabled = false + addSubview(subview) + } + setupConstraints() + // debug() + } + + private func setupConstraints() { + layoutMargins = .zero + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 8), + titleLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: leftTextMargin), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.trailingAnchor), + + titlePlaceholderLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + titlePlaceholderLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor, constant: leftTextMargin), + titlePlaceholderLabel.trailingAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.trailingAnchor), + + placeholderLabel.topAnchor.constraint(equalTo: titlePlaceholderLabel.topAnchor), + placeholderLabel.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), + placeholderLabel.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor) + ]) + } + + private func stateDidUpdate() { + updateTitle() + updatePlaceholder() + } + + private func updateTitle() { + switch state { + case .empty: + titleLabel.isHidden = true + titlePlaceholderLabel.isHidden = false + case .text, .placeholder, .textInput: + titleLabel.isHidden = false + titlePlaceholderLabel.isHidden = true + } + } + + private func updatePlaceholder() { + placeholderLabel.alpha = (state == .placeholder) ? 1 : 0 + } + + private func moveTitleDown() { + titlePlaceholderLabel.transform = transform( + from: titleLabel.frame, + to: titlePlaceholderLabel.frame) + animateTitles() + } + + private func moveTitleUp() { + titleLabel.transform = transform( + from: titlePlaceholderLabel.frame, + to: titleLabel.frame) + animateTitles() + } + + private func animateTitles() { + updateTitle() + UIView.animate(withDuration: 0.25) { + self.titleLabel.transform = .identity + self.titlePlaceholderLabel.transform = .identity + self.updatePlaceholder() + } + } + + private func transform(from source: CGRect, to destination: CGRect) -> CGAffineTransform { + let scaleX = source.width / destination.width + let scaleY = source.height / destination.height + + let translationX = source.origin.x - destination.origin.x - (destination.width * (1.0 - scaleX) / 2) + let translationY = source.origin.y - destination.origin.y - (destination.height * (1.0 - scaleY) / 2) + + return CGAffineTransform(translationX: translationX, y: translationY).scaledBy(x: scaleX, y: scaleY) + } +} diff --git a/LockdowniOS/Core/CommonUI/FloatingInput/TextBoxLabel.swift b/LockdowniOS/Core/CommonUI/FloatingInput/TextBoxLabel.swift new file mode 100644 index 0000000..85d9566 --- /dev/null +++ b/LockdowniOS/Core/CommonUI/FloatingInput/TextBoxLabel.swift @@ -0,0 +1,42 @@ +// +// TextBoxLabel.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/3/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class TextBoxLabel: UILabel { + + private var savesHeight = true + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + convenience init(fontSize: CGFloat, savesHeight: Bool = true) { + self.init(frame: .zero) + font = .systemFont(ofSize: fontSize) + self.savesHeight = savesHeight + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func commonInit() { + adjustsFontForContentSizeCategory = true + } + + override var intrinsicContentSize: CGSize { + let size = super.intrinsicContentSize + + if savesHeight { + return CGSize(width: size.width, height: font.lineHeight) + } + return size + } +} diff --git a/LockdowniOS/Core/CommonUI/FloatingInput/TextInputState.swift b/LockdowniOS/Core/CommonUI/FloatingInput/TextInputState.swift new file mode 100644 index 0000000..498b245 --- /dev/null +++ b/LockdowniOS/Core/CommonUI/FloatingInput/TextInputState.swift @@ -0,0 +1,35 @@ +// +// TextInputState.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/3/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +/// Text field state. +/// +/// - empty: no text. +/// - text: contains text. +/// - placeholder: the field is focused, but no text yet. +/// - textInput: inputting text. +public enum TextInputState { + case empty + case text + case placeholder + case textInput + + public init(hasText: Bool, firstResponder: Bool) { + switch (hasText, firstResponder) { + case (false, false): + self = .empty + case (true, false): + self = .text + case (false, true): + self = .placeholder + case (true, true): + self = .textInput + } + } +} diff --git a/LockdowniOS/Core/CommonUI/UIView/ProgressView.swift b/LockdowniOS/Core/CommonUI/UIView/ProgressView.swift new file mode 100644 index 0000000..85b934f --- /dev/null +++ b/LockdowniOS/Core/CommonUI/UIView/ProgressView.swift @@ -0,0 +1,70 @@ +// +// ProgressView.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/28/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class ProgressView: UIView { + + var progress: Float = 0 { + didSet { + updateProgressView() + } + } + + var progressTintColor: UIColor = .white { + didSet { + updateProgressView() + } + } + + var progressBackgroundColor: UIColor = .white.withAlphaComponent(0.25) { + didSet { + updateProgressView() + } + } + + private let progressView = UIView() + private var progressViewWidthConstraint: NSLayoutConstraint? + + init() { + super.init(frame: .zero) + + clipsToBounds = true + + addSubview(progressView) + + progressView.translatesAutoresizingMaskIntoConstraints = false + progressView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true + progressView.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true + progressView.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true + progressViewWidthConstraint = progressView.widthAnchor.constraint(equalToConstant: 0) + progressViewWidthConstraint?.isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + + updateProgressView() + corners = .continuous(bounds.midY) + } + + func updateProgressView() { + backgroundColor = progressBackgroundColor + progressView.backgroundColor = progressTintColor + + progressViewWidthConstraint?.constant = bounds.width * CGFloat(progress) + } + + private func updateColors() { + backgroundColor = progressBackgroundColor + } +} diff --git a/LockdowniOS/Core/CommonUI/UIView/Separator.swift b/LockdowniOS/Core/CommonUI/UIView/Separator.swift new file mode 100644 index 0000000..4b5b8e0 --- /dev/null +++ b/LockdowniOS/Core/CommonUI/UIView/Separator.swift @@ -0,0 +1,35 @@ +// +// Separator.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/5/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class Separator: UIView { + + var height: CGFloat { + didSet { + heightConstraint?.constant = height + } + } + + private var heightConstraint: NSLayoutConstraint? + + init(height: CGFloat = 1) { + self.height = height + + super.init(frame: .zero) + + backgroundColor = .lightGray.withAlphaComponent(0.3) + + heightConstraint = heightAnchor.constraint(equalToConstant: height) + heightConstraint?.isActive = true + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/LockdowniOS/Core/Constants/EmailAddress.swift b/LockdowniOS/Core/Constants/EmailAddress.swift new file mode 100644 index 0000000..f029123 --- /dev/null +++ b/LockdowniOS/Core/Constants/EmailAddress.swift @@ -0,0 +1,14 @@ +// +// EmailAddress.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/9/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +enum EmailAddress { + static let team = "team@lockdownprivacy.com" + static let terms = "terms@lockdownprivacy.com" +} diff --git a/LockdowniOS/Core/Constants/LockdownGradient.swift b/LockdowniOS/Core/Constants/LockdownGradient.swift new file mode 100644 index 0000000..a4192ce --- /dev/null +++ b/LockdowniOS/Core/Constants/LockdownGradient.swift @@ -0,0 +1,37 @@ +// +// LockdownGradient.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/28/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +enum LockdownGradient { + case lightBlue + case onboardingBlue + case onboardingPurple + case ltoButtonOnHomePage + case custom([CGColor]) + + var colors: [CGColor] { + switch self { + case .lightBlue: + return [ + UIColor.fromHex("#00B6F3").cgColor, + UIColor.fromHex("#0092CC").cgColor, + UIColor.fromHex("#0083B7").cgColor + ] + case .onboardingBlue: + return [UIColor.fromHex("#1188E4").cgColor, UIColor.fromHex("#076BB8").cgColor] + case .onboardingPurple: + return [UIColor.fromHex("#AA68FE").cgColor, UIColor.fromHex("#671AC9").cgColor] + case .ltoButtonOnHomePage: + return [UIColor.fromHex("#FFFFFF00").withAlphaComponent(0).cgColor, + UIColor.fromHex("#FFFFFF4D").withAlphaComponent(0.3).cgColor] + case .custom(let colors): + return colors + } + } +} diff --git a/LockdowniOS/Core/Extensions/CALayer+Ext.swift b/LockdowniOS/Core/Extensions/CALayer+Ext.swift new file mode 100644 index 0000000..a758b27 --- /dev/null +++ b/LockdowniOS/Core/Extensions/CALayer+Ext.swift @@ -0,0 +1,29 @@ +// +// CALayer+Ext.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/2/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +extension CALayer { + + // MARK: - Animations + + func pause() { + let pausedTime: CFTimeInterval = convertTime(CACurrentMediaTime(), from: nil) + speed = 0.0 + timeOffset = pausedTime + } + + func resume() { + let pausedTime: CFTimeInterval = timeOffset + speed = 1.0 + timeOffset = 0.0 + beginTime = 0.0 + let timeSincePause: CFTimeInterval = convertTime(CACurrentMediaTime(), from: nil) - pausedTime + beginTime = timeSincePause + } +} diff --git a/LockdowniOS/Core/Extensions/Date+Ext.swift b/LockdowniOS/Core/Extensions/Date+Ext.swift new file mode 100644 index 0000000..1f81302 --- /dev/null +++ b/LockdowniOS/Core/Extensions/Date+Ext.swift @@ -0,0 +1,32 @@ +// +// Date+Ext.swift +// LockdowniOS +// +// Created by Alexander Parshakov on 12/17/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +extension Date { + + static let xmasStart: Date = .from(year: 2022, month: 12, day: 10) + static let xmasEnd: Date = .from(year: 2023, month: 1, day: 6) + + static let halloweenStart: Date = .from(year: 2023, month: 10, day: 20) + static let halloweenEnd: Date = .from(year: 2023, month: 10, day: 31) + + static let thanksgivingStart: Date = .from(year: 2023, month: 11, day: 23) + static let thanksgivingEnd: Date = .from(year: 2023, month: 11, day: 30) + + static func from(year: Int, month: Int, day: Int) -> Date { + var dateComponents = DateComponents() + dateComponents.year = year + dateComponents.month = month + dateComponents.day = day + dateComponents.timeZone = TimeZone(abbreviation: "GMT") + + let userCalendar = Calendar(identifier: .gregorian) // since the components above (like year 1980) are for Gregorian + return userCalendar.date(from: dateComponents) ?? Date() + } +} diff --git a/LockdowniOS/Core/Extensions/Font+Ext.swift b/LockdowniOS/Core/Extensions/Font+Ext.swift new file mode 100644 index 0000000..5cd1ff6 --- /dev/null +++ b/LockdowniOS/Core/Extensions/Font+Ext.swift @@ -0,0 +1,28 @@ +// +// Font+Ext.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/23/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +extension UIFont { + + static func regularLockdownFont(size: CGFloat) -> UIFont { + return UIFont(name: "Montserrat-Regular", size: size) ?? .systemFont(ofSize: size, weight: .regular) + } + + static func mediumLockdownFont(size: CGFloat) -> UIFont { + return UIFont(name: "Montserrat-Medium", size: size) ?? .systemFont(ofSize: size, weight: .medium) + } + + static func semiboldLockdownFont(size: CGFloat) -> UIFont { + return UIFont(name: "Montserrat-SemiBold", size: size) ?? .systemFont(ofSize: size, weight: .semibold) + } + + static func boldLockdownFont(size: CGFloat) -> UIFont { + return UIFont(name: "Montserrat-Bold", size: size) ?? .systemFont(ofSize: size, weight: .bold) + } +} diff --git a/LockdowniOS/Core/Extensions/NibLoadable.swift b/LockdowniOS/Core/Extensions/NibLoadable.swift new file mode 100644 index 0000000..fcccd80 --- /dev/null +++ b/LockdowniOS/Core/Extensions/NibLoadable.swift @@ -0,0 +1,38 @@ +// +// NibLoadable.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/5/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import UIKit + +public protocol NibLoadable: AnyObject { + /// The nib file to use to load a new instance of the View designed in a XIB + static var nib: UINib { get } +} + +// MARK: Default implementation +public extension NibLoadable { + /// By default, use the nib which have the same name as the name of the class, + /// and located in the bundle of that class + static var nib: UINib { + return UINib(nibName: String(describing: self), bundle: Bundle(for: self)) + } +} + +// MARK: Support for instantiation from NIB +public extension NibLoadable where Self: UIView { + /** + Returns a `UIView` object instantiated from nib + - returns: A `NibLoadable`, `UIView` instance + */ + static func loadFromNib() -> Self { + guard let view = nib.instantiate(withOwner: nil, options: nil).first as? Self else { + fatalError("The nib \(nib) expected its root view to be of type \(self)") + } + return view + } +} diff --git a/LockdowniOS/Core/Extensions/String/String+Localized.swift b/LockdowniOS/Core/Extensions/String/String+Localized.swift new file mode 100644 index 0000000..fc096e7 --- /dev/null +++ b/LockdowniOS/Core/Extensions/String/String+Localized.swift @@ -0,0 +1,26 @@ +// +// String+Localized.swift +// Lockdown +// +// Created by Alexander Parshakov on 6/20/22. +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +public extension String { + + static func localized(_ key: String, comment: String = "") -> String { + return NSLocalizedString(key, comment: comment) + } + + func localized(comment: String = "", inserting arguments: CVarArg...) -> String { + let localizedString = NSLocalizedString(self, comment: comment) + return String(format: localizedString, arguments: arguments) + } + + static let localizedSignOut = NSLocalizedString("Sign Out", comment: "") + static let localizedCancel = NSLocalizedString("Cancel", comment: "") + static let localizedOK = NSLocalizedString("OK", comment: "") + static let localizedOkay = NSLocalizedString("Okay", comment: "") +} diff --git a/LockdowniOS/Core/Extensions/String/String+URL.swift b/LockdowniOS/Core/Extensions/String/String+URL.swift new file mode 100644 index 0000000..246a597 --- /dev/null +++ b/LockdowniOS/Core/Extensions/String/String+URL.swift @@ -0,0 +1,14 @@ +// +// String+URL.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/20/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +extension String { + static let lockdownUrlTerms = "https://lockdownprivacy.com/terms" + static let lockdownUrlPrivacy = "https://lockdownprivacy.com/privacy" +} diff --git a/LockdowniOS/Core/Extensions/UICollectionView+Dequeue.swift b/LockdowniOS/Core/Extensions/UICollectionView+Dequeue.swift new file mode 100644 index 0000000..2243c73 --- /dev/null +++ b/LockdowniOS/Core/Extensions/UICollectionView+Dequeue.swift @@ -0,0 +1,19 @@ +// +// UICollectionView+Dequeue.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/5/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +protocol Dequeuable: UIView { + static var dequeuableId: String { get } +} + +extension UICollectionView { + func dequeueCell(for: T, indexPath: IndexPath) -> T? where T : Dequeuable { + return dequeueReusableCell(withReuseIdentifier: T.dequeuableId, for: indexPath) as? T + } +} diff --git a/LockdowniOS/Core/Extensions/UIStackView+Ext.swift b/LockdowniOS/Core/Extensions/UIStackView+Ext.swift new file mode 100644 index 0000000..5bfff64 --- /dev/null +++ b/LockdowniOS/Core/Extensions/UIStackView+Ext.swift @@ -0,0 +1,16 @@ +// +// UIStackView+Ext.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/5/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +extension UIStackView { + + func clear() { + arrangedSubviews.forEach { $0.removeFromSuperview() } + } +} diff --git a/LockdowniOS/Core/Extensions/UIView+Corners.swift b/LockdowniOS/Core/Extensions/UIView+Corners.swift new file mode 100644 index 0000000..ccf1a19 --- /dev/null +++ b/LockdowniOS/Core/Extensions/UIView+Corners.swift @@ -0,0 +1,61 @@ +// +// UIView+Corners.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/28/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +/// Better set cornerRadius using this property for smooth corners +enum Corners { + /// Preferable (for smoothness) + case continuous(CGFloat) + + case circular(CGFloat) +} + +extension Corners { + var radius: CGFloat { + switch self { + case .circular(let radius), .continuous(let radius): + return radius + } + } +} + +extension UIView { + var corners: Corners { + get { + let radius = layer.cornerRadius + return layer.cornerCurve == .continuous ? .continuous(radius) : .circular(radius) + } + set { + switch newValue { + case .circular: + layer.cornerCurve = .circular + case .continuous: + layer.cornerCurve = .continuous + } + layer.cornerRadius = newValue.radius + } + } +} + +extension CALayer { + var corners: Corners { + get { + return cornerCurve == .continuous ? .continuous(cornerRadius) : .circular(cornerRadius) + } + set { + switch newValue { + case .circular: + cornerCurve = .circular + case .continuous: + cornerCurve = .continuous + } + cornerRadius = newValue.radius + } + } +} diff --git a/LockdowniOS/Core/Extensions/UIView+Ext.swift b/LockdowniOS/Core/Extensions/UIView+Ext.swift new file mode 100644 index 0000000..e8a3ccb --- /dev/null +++ b/LockdowniOS/Core/Extensions/UIView+Ext.swift @@ -0,0 +1,133 @@ +// +// UIView+Ext.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/6/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import UIKit + +extension UIView { + + // MARK: - Gradient + + @discardableResult + func applyGradient(_ gradient: LockdownGradient, corners: Corners = .continuous(0)) -> CAGradientLayer { + let gradientLayer = CAGradientLayer() + gradientLayer.colors = gradient.colors + gradientLayer.startPoint = CGPoint(x: 0, y: 0) + gradientLayer.endPoint = CGPoint(x: 0, y: 1) + gradientLayer.frame = bounds + gradientLayer.corners = corners + + self.corners = corners + + layer.sublayers?.first(where: { $0 is CAGradientLayer })?.removeFromSuperlayer() + layer.insertSublayer(gradientLayer, at: 0) + + return gradientLayer + } + + // MARK: - Animation + + func showAnimatedPress(duration: Double = 0.1, withScale scale: CGFloat = 0.95, completion: (() -> Void)? = nil) { + isUserInteractionEnabled = false + UIView.animate(withDuration: duration, animations: { + self.transformToScale(scale) + }) { (_) in + UIView.animate(withDuration: duration, animations: { + self.transformToIdentity() + }) { (_) in + self.isUserInteractionEnabled = true + completion?() + } + } + } + + func transformToIdentity() { + self.transform = CGAffineTransform.identity + } + + private func transformToScale(_ scale: CGFloat) { + transform = CGAffineTransform(scaleX: scale, y: scale) + } + + func fadeIn(duration: CGFloat, completion: (() -> Void)? = nil) { + self.alpha = 0 + UIView.animate(withDuration: duration, animations: { + self.alpha = 1 + }, completion: { isContinued in + guard isContinued else { return } + completion?() + }) + } + + func fadeOut(duration: CGFloat, completion: (() -> Void)? = nil) { + UIView.animate(withDuration: duration, animations: { + self.alpha = 0 + }, completion: { isContinued in + guard isContinued else { return } + completion?() + }) + } + + func fadeOutAsDiminished(duration: CGFloat, delay: CGFloat = 0, completion: (() -> Void)? = nil) { + UIView.animate(withDuration: duration, delay: delay, animations: { + self.transform = CGAffineTransform(scaleX: 0.01, y: 0.01) + self.alpha = 0 + }, completion: { isContinued in + guard isContinued else { return } + completion?() + }) + } + + func animateBorderWidth(toValue: CGFloat, duration: Double = 0.4, color: UIColor = .fromHex("#00ADE7")) { + let animation = CABasicAnimation(keyPath: "borderWidth") + animation.fromValue = layer.borderWidth + animation.toValue = toValue + animation.duration = duration + layer.borderColor = color.cgColor + layer.add(animation, forKey: "Width") + layer.borderWidth = toValue + } + + // MARK: - Shadow + + func dropShadow() { + layer.masksToBounds = false + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.12 + layer.shadowOffset = CGSize(width: 1, height: 1) + layer.shadowRadius = layer.cornerRadius + layer.shadowPath = UIBezierPath(rect: self.bounds).cgPath + layer.shouldRasterize = true + layer.rasterizationScale = UIScreen.main.scale + } +} + +extension UIColor { + + static func fromHex(_ hex: String) -> UIColor { + var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + + if cString.hasPrefix("#") { + cString.remove(at: cString.startIndex) + } + + if (cString.count) != 6 { + return UIColor.gray + } + + var rgbValue:UInt64 = 0 + Scanner(string: cString).scanHexInt64(&rgbValue) + + return UIColor( + red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, + green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(rgbValue & 0x0000FF) / 255.0, + alpha: CGFloat(1.0) + ) + } +} diff --git a/LockdowniOS/Core/Extensions/UIViewController+Ext.swift b/LockdowniOS/Core/Extensions/UIViewController+Ext.swift new file mode 100644 index 0000000..6f75701 --- /dev/null +++ b/LockdowniOS/Core/Extensions/UIViewController+Ext.swift @@ -0,0 +1,30 @@ +// +// UIViewController+Ext.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/4/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +extension UIViewController { + + // MARK: - Common Animation + func transition(with view: UIView, + duration: CGFloat = 0.4, + options: UIView.AnimationOptions = [.transitionCrossDissolve], + completion: @escaping () -> Void) { + UIView.transition(with: view, duration: duration, options: options) { + completion() + } + } + + // MARK: - Dark Mode + + var isDarkMode: Bool { traitCollection.userInterfaceStyle == .dark } + + // MARK: - Idiom + + var isPad: Bool { traitCollection.userInterfaceIdiom == .pad } +} diff --git a/LockdowniOS/Core/Helpers/WeakObject.swift b/LockdowniOS/Core/Helpers/WeakObject.swift new file mode 100644 index 0000000..70c31a2 --- /dev/null +++ b/LockdowniOS/Core/Helpers/WeakObject.swift @@ -0,0 +1,16 @@ +// +// WeakObject.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/28/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +final class WeakObject { + + weak var object : T? + + init (_ object: T) { + self.object = object + } +} diff --git a/LockdowniOS/Core/PropertyWrappers/ArrayRestrictable.swift b/LockdowniOS/Core/PropertyWrappers/ArrayRestrictable.swift new file mode 100644 index 0000000..7ae86b0 --- /dev/null +++ b/LockdowniOS/Core/PropertyWrappers/ArrayRestrictable.swift @@ -0,0 +1,26 @@ +// +// ArrayRestrictable.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/28/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +@propertyWrapper +struct PositiveAndArrayRestricted { + + var arrayCount: Int = 0 + + private var value: Int + + var wrappedValue: Int { + get { return max(0, value) } + set { value = min(newValue, arrayCount - 1) } + } + + init(defaultValue: Int) { + value = defaultValue + } +} diff --git a/LockdowniOS/Core/PropertyWrappers/UserDefault.swift b/LockdowniOS/Core/PropertyWrappers/UserDefault.swift new file mode 100644 index 0000000..11a6a8d --- /dev/null +++ b/LockdowniOS/Core/PropertyWrappers/UserDefault.swift @@ -0,0 +1,47 @@ +// +// UserDefault.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/6/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import Foundation + +@propertyWrapper +struct UserDefault { + private let key: String + private let defaultValue: T + private let userDefaults: UserDefaults + + init(_ key: String, defaultValue: T, userDefaults: UserDefaults = UserDefaults(suiteName: "group.com.confirmed")!) { + self.key = key + self.defaultValue = defaultValue + self.userDefaults = userDefaults + } + + var wrappedValue: T { + get { + let value = userDefaults.object(forKey: key) as? T ?? defaultValue + DDLogInfo("Reading UserDefaults.\(key) of value \(value).") + return value + } + set { + DDLogInfo("Setting UserDefaults.\(key) value as \(newValue).") + userDefaults.set(newValue, forKey: key) + } + } +} + +extension UserDefaults { + + @UserDefault("homeScreenLastPaywallDisplayDate", defaultValue: Date()) + static var lastPaywallDisplayDate + + @UserDefault("hasSeenPaywallOnHomeScreen", defaultValue: false) + static var hasSeenPaywallOnHomeScreen + + @UserDefault("hasSeenLTO", defaultValue: false) + static var hasSeenLTO +} diff --git a/LockdowniOS/Core/Services/ConnectivityService/ConnectionState.swift b/LockdowniOS/Core/Services/ConnectivityService/ConnectionState.swift new file mode 100644 index 0000000..a37ee2e --- /dev/null +++ b/LockdowniOS/Core/Services/ConnectivityService/ConnectionState.swift @@ -0,0 +1,31 @@ +// +// ConnectionState.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/6/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +enum ConnectionState { + case unknown, satisfied, restrictedCellular, noConnection + + var errorMessage: String? { + switch self { + case .unknown, .satisfied: + return nil + case .restrictedCellular: + return "Enable Cellular Data for Lockdown" + case .noConnection: + return .localized("No Internet Connection") + } + } + + var color: UIColor { + if self == .noConnection { + return .orange + } + return .red + } +} diff --git a/LockdowniOS/Core/Services/ConnectivityService/ConnectivityService.swift b/LockdowniOS/Core/Services/ConnectivityService/ConnectivityService.swift new file mode 100644 index 0000000..8ed2f68 --- /dev/null +++ b/LockdowniOS/Core/Services/ConnectivityService/ConnectivityService.swift @@ -0,0 +1,86 @@ +// +// ConnectivityService.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/6/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CoreTelephony +import SwiftMessages +import Network +import NetworkExtension + +final class ConnectivityService { + + let noInternetMessageView = MessageView.viewFromNib(layout: .statusLine) + + private var connectionState: ConnectionState = .unknown { + didSet { showConnectionErrorIfNeeded() } + } + + private var cellularDataRestrictedState = CTCellularDataRestrictedState.restrictedStateUnknown { + didSet { showConnectionErrorIfNeeded() } + } + + private var isCellularDataRestricted: Bool { cellularDataRestrictedState == .restricted } + + init(connectionState: ConnectionState = .unknown) { + self.connectionState = connectionState + } + + func startObservingConnectivity() { + startMonitoringCellularRestriction() + startMonitoringConnection() + } + + func showConnectionErrorIfNeeded() { + // When VPN is in a transitioning state (.disconnecting, .connecting), internet is temporarily cut off. + // We should not show any warning in this case. + guard ![.disconnecting, .connecting].contains(NEVPNManager.shared().connection.status) else { return } + + DispatchQueue.main.async { + if let errorMessage = self.connectionState.errorMessage { + self.showBanner(text: errorMessage, color: self.connectionState.color) + } else { + SwiftMessages.hideAll() + } + } + } + + // MARK: - Private Methods + + private func startMonitoringCellularRestriction() { + let cellularState = CTCellularData.init() + cellularState.cellularDataRestrictionDidUpdateNotifier = { (dataRestrictedState) in + self.cellularDataRestrictedState = dataRestrictedState + } + } + + private func startMonitoringConnection() { + let monitor = NWPathMonitor() + monitor.pathUpdateHandler = { path in + if path.status == .satisfied { + self.connectionState = .satisfied + } else if path.usesInterfaceType(.cellular), self.isCellularDataRestricted { + self.connectionState = .restrictedCellular + } else { + self.connectionState = .noConnection + } + } + let queue = DispatchQueue(label: "Monitor") + monitor.start(queue: queue) + } + + private func showBanner(text: String, color: UIColor) { + noInternetMessageView.backgroundView.backgroundColor = color + noInternetMessageView.bodyLabel?.textColor = .white + noInternetMessageView.configureContent(body: text) + + var noInternetMessageViewConfig = SwiftMessages.defaultConfig + noInternetMessageViewConfig.presentationContext = .window(windowLevel: .init(rawValue: 0)) + noInternetMessageViewConfig.preferredStatusBarStyle = .lightContent + noInternetMessageViewConfig.duration = .forever + SwiftMessages.show(config: noInternetMessageViewConfig, view: noInternetMessageView) + } +} diff --git a/LockdowniOS/Core/Services/EmailComposable.swift b/LockdowniOS/Core/Services/EmailComposable.swift new file mode 100644 index 0000000..c25dcbf --- /dev/null +++ b/LockdowniOS/Core/Services/EmailComposable.swift @@ -0,0 +1,152 @@ +// +// EmailComposable.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/2/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import Foundation +import MessageUI + +protocol EmailComposable: MFMailComposeViewControllerDelegate { + func composeEmail(_ email: Email, to recipient: String, errorBody: String?, attachments: [EmailAttachment]) +} + +extension EmailComposable where Self: BaseViewController { + + func composeEmail(_ email: Email, + to recipient: String = EmailAddress.team, + errorBody: String? = nil, + attachments: [EmailAttachment] = []) { + writeUserLogs() + + var message = email.body + if let errorBody = errorBody { + message += "\n\nError Details: " + errorBody + } + message += email.needsFurtherUserInput ? "\n\n\n" : "" + + if MFMailComposeViewController.canSendMail() { + let composeVC = MFMailComposeViewController() + composeVC.mailComposeDelegate = self + composeVC.setToRecipients([recipient]) + composeVC.setSubject(email.subject) + composeVC.setMessageBody(message, isHTML: false) + + attachments.forEach { + composeVC.addAttachmentData($0.data, mimeType: $0.mimeType, fileName: $0.fileName) + } + present(composeVC, animated: true) + } else { + guard let mailtoURL = Mailto.generateURL(recipient: recipient, subject: email.subject, body: message) else { + DDLogError("Failed to generate mailto url") + return + } + + UIApplication.shared.open(mailtoURL, options: [:]) { (success) in + guard !success else { return } + self.showPopupDialog( + title: .localized("Couldn't Find Your Email Client"), + message: .localized("Please make sure you have added an e-mail account to your iOS device and try again."), + acceptButton: .localizedOK) + } + } + } + + private func writeUserLogs() { + DDLogInfo("") + DDLogInfo("UserId: \(keychain[kVPNCredentialsId] ?? "No User ID")") + DDLogInfo("UserReceipt: \(keychain[kVPNCredentialsKeyBase64] ?? "No User Receipt")") + + if Client.hasValidCookie() { + DDLogInfo("Has loaded cookie.") + } + DDLogInfo("") + PacketTunnelProviderLogs.flush() + DDLogInfo("") + } +} + +enum Email { + case helpOrFeedback + case deleteAccount(email: String, userId: String) + case termsAndPrivacyPolicy + case blockingImprovementIdeas + case custom(subject: String, body: String) + + var subject: String { + var appendString = "" + if getUserWantsVPNEnabled() { + appendString += " - S" + } + switch self { + case .helpOrFeedback: + return "Lockdown Question or Feedback (iOS \(Bundle.main.versionString))" + appendString + case .deleteAccount: + return "Delete Account" + case .termsAndPrivacyPolicy: + return "Lockdown Privacy Policy Question or Feedback (iOS \(Bundle.main.versionString))" + appendString + case .blockingImprovementIdeas: + return "Lockdown Blocking Improvement Ideas (iOS \(Bundle.main.versionString))" + case .custom(let subject, _): + return subject + } + } + + var body: String { + switch self { + case .helpOrFeedback: + return "Hi, my question or feedback for Lockdown is: " + case .deleteAccount(let email, let userId): + return "Please, delete my entire account record, along with associated personal data.\n\nDeletion credentials: \n\(email)\n\(userId)" + case .termsAndPrivacyPolicy: + return "Hi, my question or feedback for Lockdown Privacy Policy is: " + case .blockingImprovementIdeas: + return "Hi, my blocking improvement ideas for Lockdown Privacy are: " + case .custom(_, let body): + return body + } + } + + /// Defines whether the e-mail form expects user to type some additional info below the email body. + /// If yes, 3 new lines will be inserted. + var needsFurtherUserInput: Bool { + switch self { + case .helpOrFeedback, .termsAndPrivacyPolicy, .blockingImprovementIdeas, .custom: + return true + case .deleteAccount: + return false + } + } +} + +enum EmailAttachment { + case diagnostics + + var data: Data { + switch self { + case .diagnostics: + let attachmentData = NSMutableData() + for logFileData in logFileDataArray { + attachmentData.append(logFileData as Data) + } + return attachmentData as Data + } + } + + var mimeType: String { + switch self { + case .diagnostics: + return "text/plain" + } + } + + var fileName: String { + switch self { + case .diagnostics: + return "diagnostics.txt" + } + } +} diff --git a/LockdowniOS/Core/Services/EmailValidatable.swift b/LockdowniOS/Core/Services/EmailValidatable.swift new file mode 100644 index 0000000..83cf92e --- /dev/null +++ b/LockdowniOS/Core/Services/EmailValidatable.swift @@ -0,0 +1,64 @@ +// +// EmailValidatable.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/7/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +protocol EmailValidatable: AnyObject { + func errorValidatingEmail(_ email: String?) -> EmailValidationError? +} + +extension EmailValidatable { + func errorValidatingEmail(_ email: String?) -> EmailValidationError? { + guard let email, !email.isEmpty else { return .notFilledIn } + + if email.lowercased().hasSuffix(".con") { + return .enteredConInsteadOfCom + } + + // looks for links in this case, URL (email format) as in "mailto:test@example.com" + guard let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + return .noValidEmailAddressDetected + } + + // set the range and get all of the matches using NSDataDetector + let range = NSRange(location: 0, length: email.count) + let allMatches = dataDetector.matches(in: email, options: [], range: range) + + // if there is exactly 1 email address (with the mailto link) + if allMatches.count == 1, allMatches.first?.url?.absoluteString.contains("mailto:") == true { + // no email error detected + return nil + } else if allMatches.count > 1 { + return .tooManyEmailAddressesEntered + } + + return .noValidEmailAddressDetected + } +} + +enum EmailValidationError: Error { + case notFilledIn + case enteredConInsteadOfCom + case noValidEmailAddressDetected + case tooManyEmailAddressesEntered +} + +extension EmailValidationError: LocalizedError { + var errorDescription: String? { + switch self { + case .notFilledIn: + return .localized("please_fill_in_your_email") + case .enteredConInsteadOfCom: + return .localized("you_entered_con_instead_of_com") + case .noValidEmailAddressDetected: + return .localized("no_valid_email_address_detected") + case .tooManyEmailAddressesEntered: + return .localized("too_many_email_addresses_entered") + } + } +} diff --git a/LockdowniOS/Core/Services/IntentService.swift b/LockdowniOS/Core/Services/IntentService.swift new file mode 100644 index 0000000..2cef0e5 --- /dev/null +++ b/LockdowniOS/Core/Services/IntentService.swift @@ -0,0 +1,39 @@ +// +// IntentService.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/7/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import Intents + +final class IntentService { + + static func donateIntents() { + INPreferences.requestSiriAuthorization { status in + switch status { + case .authorized: + DDLogInfo("User allowed access to Siri.") + + self.donateDisableVPNIntent() + self.donateEnableVPNIntent() + default: + DDLogInfo("User denied access to Siri.") + } + } + } + + private static func donateDisableVPNIntent() { + let intent = DisableVPNIntent() + let interaction = INInteraction(intent: intent, response: nil) + interaction.donate(completion: nil) + } + + private static func donateEnableVPNIntent() { + let intent = EnableVPNIntent() + let interaction = INInteraction(intent: intent, response: nil) + interaction.donate(completion: nil) + } +} diff --git a/LockdowniOS/Core/Services/Keychainable.swift b/LockdowniOS/Core/Services/Keychainable.swift new file mode 100644 index 0000000..f888b01 --- /dev/null +++ b/LockdowniOS/Core/Services/Keychainable.swift @@ -0,0 +1,40 @@ +// +// Keychainable.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/18/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import KeychainAccess + +protocol Keychainable: AnyObject { + func saveKeychainBool(_ item: KeychainBoolItem, _ bool: Bool, keychain: Keychain) + func readKeychainBool(_ item: KeychainBoolItem, keychain: Keychain) -> Bool +} + +extension Keychainable { + private static var defaultKeychain: Keychain { Keychain(service: "com.confirmed.tunnels").synchronizable(true) } + + func saveKeychainBool(_ item: KeychainBoolItem, _ bool: Bool, keychain: Keychain = Self.defaultKeychain) { + do { + try keychain.set(String(bool), key: item.rawValue) + } catch let error { + print(error) + } + } + + func readKeychainBool(_ item: KeychainBoolItem, keychain: Keychain = Self.defaultKeychain) -> Bool { + do { + let value: String = try keychain.get(item.rawValue) ?? "" + return Bool(value) ?? false + } catch let error { + print(error) + return false + } + } + } + + enum KeychainBoolItem: String { + case hasSeenXmasLTO + } diff --git a/LockdowniOS/Core/Services/Paywalling/FreemiumMigrationService.swift b/LockdowniOS/Core/Services/Paywalling/FreemiumMigrationService.swift new file mode 100644 index 0000000..efdbf31 --- /dev/null +++ b/LockdowniOS/Core/Services/Paywalling/FreemiumMigrationService.swift @@ -0,0 +1,50 @@ +// +// FreemiumMigrationService.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/8/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import TPInAppReceipt + +protocol FreemiumMigrationService: AnyObject { + var freemiumMigrationAppVersion: Int { get set } + + var isMigratedUser: Bool { get } + + func register(freemiumMigrationAppVersion: Int) +} + +extension FreemiumMigrationService { + func register(freemiumMigrationAppVersion: Int) { + self.freemiumMigrationAppVersion = freemiumMigrationAppVersion + } +} + +final class BaseFreemiumMigrationService: FreemiumMigrationService { + + static let shared = BaseFreemiumMigrationService() + + var freemiumMigrationAppVersion: Int = 1000 + + var isMigratedUser: Bool { + do { + let receipt = try InAppReceipt() + try receipt.validate() + + let originalBuildVersion = Int(receipt.originalAppVersion) + + if let originalBuildVersion = originalBuildVersion, + originalBuildVersion < freemiumMigrationAppVersion { + return true + } + + return false + + } catch { + return false + } + } +} diff --git a/LockdowniOS/Core/Services/ProductPurchasable.swift b/LockdowniOS/Core/Services/ProductPurchasable.swift new file mode 100644 index 0000000..2c43ca4 --- /dev/null +++ b/LockdowniOS/Core/Services/ProductPurchasable.swift @@ -0,0 +1,245 @@ +// +// ProductPurchasable.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/19/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import Foundation +import PromiseKit +import StoreKit + +protocol ProductPurchasable: Loadable { + func purchaseProduct(withId productId: String, onSuccess: (() -> Void)?, onFailure: (() -> Void)?) + func restorePurchases() +} + +extension ProductPurchasable where Self: BaseViewController { + func purchaseProduct(withId productId: String = VPNSubscription.selectedProductId, + onSuccess: (() -> Void)? = nil, + onFailure: (() -> Void)? = nil) { + VPNSubscription.selectedProductId = productId + showLoadingView() + + VPNSubscription.purchase( + succeeded: { + BaseUserService.shared.updateUserSubscription { [weak self] _ in + self?.hideLoadingView() + self?.handleSuccessfulPurchase() + onSuccess?() + } + }, + errored: { error in + onFailure?() + self.hideLoadingView() + DDLogError("Start Trial Failed: \(error)") + + if let skError = error as? SKError { + var errorText = "" + switch skError.code { + case .unknown: + errorText = .localized("Unknown error. Please contact support at team@lockdownprivacy.com.") + case .clientInvalid: + errorText = .localized("Not allowed to make the payment") + case .paymentCancelled: + errorText = .localized("Payment was cancelled") + case .paymentInvalid: + errorText = .localized("The purchase identifier was invalid") + case .paymentNotAllowed: + errorText = .localized(""" +Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. \ +Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again. +""") + case .storeProductNotAvailable: + errorText = .localized("The product is not available in the current storefront") + case .cloudServicePermissionDenied: + errorText = .localized("Access to cloud service information is not allowed") + case .cloudServiceNetworkConnectionFailed: + errorText = .localized("Could not connect to the network") + case .cloudServiceRevoked: + errorText = .localized("User has revoked permission to use this cloud service") + default: + errorText = skError.localizedDescription + } + self.showPopupDialog(title: .localized("Error Starting Trial"), message: errorText, acceptButton: .localizedOkay) + } else if self.popupErrorAsNSURLError(error) { + return + } else if self.popupErrorAsApiError(error) { + return + } else { + self.showPopupDialog( + title: .localized("Error Starting Trial"), + message: .localized("Please contact team@lockdownprivacy.com.\n\nError details:\n") + "\(error)", + acceptButton: .localizedOkay) + } + }) + } + + func restorePurchases() { + showLoadingView() + + firstly { + try Client.signIn(forceRefresh: true) + } + .then { _ in + try Client.getKey() + } + .done { _ in + // we were able to get key, so subscription is valid -- follow pathway from HomeViewController to associate this with the email account if there is one + let presentingViewController = self.presentingViewController as? HomeViewController + BaseUserService.shared.updateUserSubscription { [weak self] _ in + self?.hideLoadingView() + self?.dismiss(animated: true, completion: { + if presentingViewController != nil { + presentingViewController?.toggleVPN("me") + } else { + VPNController.shared.setEnabled(true) + } + }) + } + } + .catch { error in + self.hideLoadingView() + DDLogError("Restore Failed: \(error)") + if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: + // now try email if it exists + if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { + DDLogInfo("restore: have confirmed API credentials, using them") + self.showLoadingView() + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .then { (signin: SignIn) -> Promise in + DDLogInfo("restore: signin result: \(signin)") + return try Client.getKey() + } + .done { (getKey: GetKey) in + BaseUserService.shared.updateUserSubscription { [weak self] _ in + self?.hideLoadingView() + + do { + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + } catch { + DDLogInfo("restore: setting VPN creds with ID and Dismissing: \(getKey.id)") + let presentingViewController = self?.presentingViewController as? HomeViewController + self?.dismiss(animated: true, completion: { + if presentingViewController != nil { + presentingViewController?.toggleVPN("me") + } else { + VPNController.shared.setEnabled(true) + } + }) + } + } + } + .catch { error in + self.hideLoadingView() + DDLogError("restore: Error doing restore with email-login: \(error)") + if self.popupErrorAsNSURLError(error) { + return + } else if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: + self.showPopupDialog(title: .localized("No Active Subscription"), + message: .localized(""" +Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, \ +you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com +"""), + acceptButton: .localizedOK) + default: + _ = self.popupErrorAsApiError(error) + } + } + } + } else { + let message = """ +Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, \ +you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com +""" + self.showPopupDialog(title: .localized("No Active Subscription"), + message: .localized(message), + acceptButton: .localizedOK) + } + default: + let pleaseEmail: String = .localized("Please email team@lockdownprivacy.com with the following Error Code ") + self.showPopupDialog( + title: .localized("Error Restoring Subscription"), + message: pleaseEmail + "\(apiError.code) : \(apiError.message)", + acceptButton: .localizedOK) + } + } else { + let message: String = .localized(""" +Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with +the following error message: \ + +""") + self.showPopupDialog(title: .localized("Error Restoring Subscription"), message: message + "\(error)", acceptButton: .localizedOK) + } + } + } + + // MARK: - Handling Purchase Results + + private func handleSuccessfulPurchase() { + dismiss(animated: true, completion: { + // force refresh receipt, and sync with email if it exists, activate VPNte + if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { + DDLogInfo("purchase complete: syncing with confirmed email") + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .then { (signin: SignIn) -> Promise in + DDLogInfo("purchase complete: signin result: \(signin)") + return try Client.subscriptionEvent(forceRefresh: true) + } + .then { (result: SubscriptionEvent) -> Promise in + DDLogInfo("purchase complete: subscriptionevent result: \(result)") + return try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + DDLogInfo("purchase complete: setting VPN creds with ID: \(getKey.id)") + VPNController.shared.setEnabled(true) + } + .catch { error in + DDLogError("purchase complete: Error: \(error)") + if self.popupErrorAsNSURLError("Error activating Secure Tunnel: \(error)") { + return + } else if let apiError = error as? ApiError { + switch apiError.code { + default: + _ = self.popupErrorAsApiError("API Error activating Secure Tunnel: \(error)") + } + } + } + } else { + firstly { + try Client.signIn(forceRefresh: true) // this will fetch and set latest receipt, then submit to API to get cookie + } + .then { _ 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 + DDLogError("purchase complete - no email: Error: \(error)") + if self.popupErrorAsNSURLError("Error activating Secure Tunnel: \(error)") { + return + } else if let apiError = error as? ApiError { + switch apiError.code { + default: + _ = self.popupErrorAsApiError("API Error activating Secure Tunnel: \(error)") + } + } + } + } + }) + } +} diff --git a/LockdowniOS/CountdownDisplayService.swift b/LockdowniOS/CountdownDisplayService.swift new file mode 100644 index 0000000..49e373d --- /dev/null +++ b/LockdowniOS/CountdownDisplayService.swift @@ -0,0 +1,156 @@ +// +// CountdownDisplayService.swift +// LockdowniOS +// +// Created by Alexander Parshakov on 12/15/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import Foundation +import UIKit + +@objc protocol CountdownDisplayDelegate: AnyObject { + func didFinishCountdown() +} + +protocol CountdownDisplayService: AnyObject { + + var seconds: TimeInterval { get set } + + var delegates: [WeakObject?] { get set } + + func startUpdating(hourLabel: UILabel?, minuteLabel: UILabel?, secondLabel: UILabel) + + func startUpdating(button: UIButton) + + func pauseUpdating() + + func stopAndRemoveLTO() +} + +final class BaseCountdownDisplayService: CountdownDisplayService { + + static let shared: CountdownDisplayService = BaseCountdownDisplayService(seconds: 60) + + private weak var button: UIButton? + private weak var hourLabel: UILabel? + private weak var minuteLabel: UILabel? + private weak var secondLabel: UILabel? + + var seconds: TimeInterval + + var delegates: [WeakObject?] = [] + + private var timer: Timer? + + private init(seconds: TimeInterval) { + self.seconds = seconds + } + + func startUpdating(hourLabel: UILabel? = nil, minuteLabel: UILabel? = nil, secondLabel: UILabel) { + self.hourLabel = hourLabel + self.minuteLabel = minuteLabel + self.secondLabel = secondLabel + + timer?.invalidate() + runTimer { + DDLogInfo("Started updating LTO labels.") + self.updateLabels() + } + } + + func startUpdating(button: UIButton) { + self.button = button + + timer?.invalidate() + runTimer { + DDLogInfo("Started updating LTO button.") + DispatchQueue.main.async { + self.updateButton() + } + } + } + + func pauseUpdating() { + timer?.invalidate() + timer = nil + } + + func stopAndRemoveLTO() { + BasePaywallService.shared.context = .normal + DDLogInfo("Finished countdown. Changing context to \(BasePaywallService.shared.context).") + + DDLogInfo("Notifying all delegates.") + self.delegates.forEach { $0?.object?.didFinishCountdown() } + + DDLogInfo("Clearing singleton references.") + self.clearAllData() + } + + private func runTimer(updateUI: @escaping () -> Void) { + forceUpdate() + + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in + guard let self else { return } + + if self.seconds > 0 { + self.seconds -= 1 + updateUI() + } else { + self.stopAndRemoveLTO() + } + } + } + + private func forceUpdate() { + updateLabels() + updateButton() + } + + private func updateButton() { + let minutes = self.timeString(for: .minute) + let seconds = self.timeString(for: .second) + + let timeString = minutes + ":" + seconds + + button?.setTitle(timeString, for: .normal) + } + + private func updateLabels() { + DispatchQueue.main.async { + self.hourLabel?.text = self.timeString(for: .hour) + self.minuteLabel?.text = self.timeString(for: .minute) + self.secondLabel?.text = self.timeString(for: .second) + } + } + + private func timeString(for component: CountdownDisplayComponent) -> String { + let time: Int + + switch component { + case .hour: + time = Int(seconds) / 3600 + case .minute: + time = Int(seconds) / 60 % 60 + case .second: + time = Int(seconds) % 60 + } + + return String(format:"%02i", time) + } + + private func clearAllData() { + pauseUpdating() + + delegates = [] + button = nil + hourLabel = nil + minuteLabel = nil + secondLabel = nil + } +} + +enum CountdownDisplayComponent { + case hour, minute, second +} diff --git a/LockdowniOS/CustomNavigationView.swift b/LockdowniOS/CustomNavigationView.swift index 29d8200..9fda5fc 100644 --- a/LockdowniOS/CustomNavigationView.swift +++ b/LockdowniOS/CustomNavigationView.swift @@ -16,16 +16,16 @@ final class CustomNavigationView: UIView { } } - var buttonTitle: String = NSLocalizedString("SAVE", comment: "") { + var buttonTitle: String = .localized("SAVE") { didSet { button.setTitle(buttonTitle, for: .normal) } } - private(set) var buttonCallback: () -> () = { } + private(set) var buttonCallback: () -> Void = { } @discardableResult - func onButtonPressed(_ callback: @escaping () -> ()) -> CustomNavigationView { + func onButtonPressed(_ callback: @escaping () -> Void) -> CustomNavigationView { buttonCallback = callback return self } @@ -47,7 +47,7 @@ final class CustomNavigationView: UIView { addSubview(titleView) titleView.textAlignment = .left titleView.text = title - titleView.font = fontMedium17 + titleView.font = .mediumLockdownFont(size: 17) titleView.anchors.leading.marginsPin(inset: 20) titleView.anchors.width.greaterThanOrEqual(220) @@ -55,7 +55,7 @@ final class CustomNavigationView: UIView { titleView.anchors.top.pin(inset: 18) addSubview(button) - button.titleLabel?.font = fontBold13 + button.titleLabel?.font = .boldLockdownFont(size: 13) button.contentHorizontalAlignment = .trailing button.tintColor = .confirmedBlue button.setTitle(buttonTitle, for: .normal) diff --git a/LockdowniOS/DeleteMyAccountViewController.swift b/LockdowniOS/DeleteMyAccountViewController.swift new file mode 100644 index 0000000..6610054 --- /dev/null +++ b/LockdowniOS/DeleteMyAccountViewController.swift @@ -0,0 +1,67 @@ +// +// DeleteMyAccountViewController.swift +// Lockdown +// +// Created by Alexander Parshakov on 10/17/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import MessageUI +import UIKit + +final class DeleteMyAccountViewController: BaseViewController { + + @IBOutlet private var titleLabel: UILabel! + @IBOutlet private var bodyLabel: UILabel! + + @IBOutlet private var proceedButton: UIButton! + @IBOutlet private var exitButton: UIButton! + + private let userEmail: String + + init(userEmail: String) { + self.userEmail = userEmail + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupTexts() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + proceedButton.corners = .continuous(proceedButton.bounds.midY) + } + + override func actionUponEmailComposeClosure() { + dismiss(animated: true) + } + + private func setupTexts() { + titleLabel.text = .localized("delete_my_account") + bodyLabel.text = .localized("by_proceeding_you_will_submit_request_for_deleting_account") + + proceedButton.setTitle(.localized("proceed"), for: .normal) + exitButton.setTitle(.localizedCancel, for: .normal) + } + + @IBAction private func didTapProceed(_ sender: Any) { + let userId = keychain[kVPNCredentialsId] ?? "No userId" + composeEmail(.deleteAccount(email: userEmail, userId: userId)) + } + + @IBAction private func didTapExit(_ sender: Any) { + dismiss(animated: true) + } +} + +extension DeleteMyAccountViewController: EmailComposable {} diff --git a/LockdowniOS/DeleteMyAccountViewController.xib b/LockdowniOS/DeleteMyAccountViewController.xib new file mode 100644 index 0000000..fe2e9bf --- /dev/null +++ b/LockdowniOS/DeleteMyAccountViewController.xib @@ -0,0 +1,103 @@ + + + + + + + + + + + + + Montserrat-Bold + + + Montserrat-Medium + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/EnableNotificationsViewController.swift b/LockdowniOS/EnableNotificationsViewController.swift new file mode 100644 index 0000000..9e66981 --- /dev/null +++ b/LockdowniOS/EnableNotificationsViewController.swift @@ -0,0 +1,146 @@ +// +// EnableNotificationsViewController.swift +// Lockdown +// +// Created by Alexander Parshakov on 11/9/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import UIKit + +final class EnableNotificationsViewController: UIViewController { + + @IBOutlet private var stayInLoopLabel: UILabel! + @IBOutlet private var descriptionLabel: UILabel! + + @IBOutlet private var imageBackgroundView: UIView! + @IBOutlet private var enableNotificationsButton: UIButton! + @IBOutlet private var maybeLaterButton: UIButton! + + private var onAgreed: (() -> Void)? + + init(onAgreed: (() -> Void)? = nil) { + self.onAgreed = onAgreed + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupTexts() + updateImageBackgroundView() + + navigationController?.navigationBar.topItem?.backButtonTitle = "" + navigationController?.navigationBar.tintColor = .label + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + enableNotificationsButton.applyGradient(.lightBlue, corners: .continuous(enableNotificationsButton.bounds.midY)) + } + + @IBAction private func didTapEnableNotifications(_ sender: Any) { + OneTimeActions.markAsSeen(.newEnableNotificationsController) + + enableNotificationsButton.showAnimatedPress { [weak self] in + self?.askForNotificationsPermission() + } + } + + @IBAction private func didTapMaybeLater(_ sender: Any) { + OneTimeActions.markAsSeen(.newEnableNotificationsController) + + if presentingViewController != nil { + dismiss(animated: true) + } else { + switchToMainAppScreen() + } + } + + private func setupTexts() { + stayInLoopLabel.text = .localized("stay_in_the_loop") + descriptionLabel.text = .localized("once_a_week_helpful_reminders") + + enableNotificationsButton.setTitle(.localized("enable_notifications"), for: .normal) + maybeLaterButton.setTitle(.localized("maybe_later"), for: .normal) + } + + private func updateImageBackgroundView() { + imageBackgroundView.corners = .continuous(26) + imageBackgroundView.backgroundColor = .fromHex("0366DA").withAlphaComponent(isDarkMode ? 0.15 : 0.05) + } +} + +extension EnableNotificationsViewController { + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + updateImageBackgroundView() + } +} + +extension EnableNotificationsViewController { + private func askForNotificationsPermission() { + PushNotifications.Authorization.setUserWantsNotificationsEnabled(true, forCategory: .weeklyUpdate) + + UNUserNotificationCenter.current().getNotificationSettings { (settings) in + switch settings.authorizationStatus { + case .authorized, .notDetermined: + + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (isSuccess, error) in + if let error { + DDLogWarn(error.localizedDescription) + } else if isSuccess { + DDLogInfo("Successfully authorized notifications.") + } + + // If we have a presenting view controller, then it was shown from Account Tab. + // Otherwise, it was shown from onboarding/signup. + DispatchQueue.main.async { + if self.presentingViewController != nil { + self.dismiss(animated: true) { + if isSuccess { + self.onAgreed?() + } else { + PushNotifications.Authorization.setUserWantsNotificationsEnabled(false, forCategory: .weeklyUpdate) + } + } + } else { + self.switchToMainAppScreen() + + if isSuccess { + self.onAgreed?() + } else { + PushNotifications.Authorization.setUserWantsNotificationsEnabled(false, forCategory: .weeklyUpdate) + } + } + } + } + case .denied: + PushNotifications.Authorization.setUserWantsNotificationsEnabled(false, forCategory: .weeklyUpdate) + DispatchQueue.main.async { + PushNotifications.Authorization.showGoToSettingsPopup(on: self) {} + } + default: + break + } + } + } + + private func switchToMainAppScreen() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + guard let keyWindow else { return } + self.transition(with: keyWindow, duration: 1, options: [.preferredFramesPerSecond60, .transitionFlipFromLeft]) { + keyWindow.rootViewController = UIStoryboard.main.instantiateViewController(withIdentifier: "MainTabBarController") + keyWindow.makeKeyAndVisible() + } + } + } +} diff --git a/LockdowniOS/EnableNotificationsViewController.xib b/LockdowniOS/EnableNotificationsViewController.xib new file mode 100644 index 0000000..b7aa57b --- /dev/null +++ b/LockdowniOS/EnableNotificationsViewController.xib @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + Montserrat-Regular + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/FirewallRepair.swift b/LockdowniOS/FirewallRepair.swift index 703b7e8..bd73151 100644 --- a/LockdowniOS/FirewallRepair.swift +++ b/LockdowniOS/FirewallRepair.swift @@ -65,23 +65,19 @@ enum FirewallRepair { } static func reschedule() { - if #available(iOS 13.0, *) { - DDLogInfo("BGTask: Cancelling all task requests") - BGTaskScheduler.shared.cancelAllTaskRequests() - - let timeIntervalSeconds: Double = 3600 - - let request = BGAppRefreshTaskRequest(identifier: FirewallRepair.identifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: timeIntervalSeconds) - DDLogInfo("BGTask: Scheduling app refresh id: \(FirewallRepair.identifier), earliest date \(Date(timeIntervalSinceNow: TimeInterval(timeIntervalSeconds)))") - do { - try BGTaskScheduler.shared.submit(request) - } catch { - DDLogError("Could not schedule app refresh: \(error)") - } - } - else { - DDLogInfo("BGTask: Not iOS 13, not calling reschedule") + DDLogInfo("BGTask: Cancelling all task requests") + BGTaskScheduler.shared.cancelAllTaskRequests() + + let timeIntervalSeconds: Double = 3600 + + let request = BGAppRefreshTaskRequest(identifier: FirewallRepair.identifier) + request.earliestBeginDate = Date(timeIntervalSinceNow: timeIntervalSeconds) + let earliestDate = Date(timeIntervalSinceNow: TimeInterval(timeIntervalSeconds)) + DDLogInfo("BGTask: Scheduling app refresh id: \(FirewallRepair.identifier), earliest date \(earliestDate)") + do { + try BGTaskScheduler.shared.submit(request) + } catch { + DDLogError("Could not schedule app refresh: \(error)") } } @@ -97,26 +93,24 @@ enum FirewallRepair { DDLogInfo("Repair \(context): refreshed manager") DDLogInfo("Repair \(context): userWantsFirewallEnabled \(getUserWantsFirewallEnabled())") DDLogInfo("Repair \(context): firewallStatus \(FirewallController.shared.status())") - if getUserWantsFirewallEnabled() && (FirewallController.shared.status() == .connected || FirewallController.shared.status() == .invalid || FirewallController.shared.status() == .disconnected) { + if getUserWantsFirewallEnabled() && [.connected, .invalid, .disconnected].contains(FirewallController.shared.status()) { DDLogInfo("Repair \(context): user wants enabled") - if (appHasJustBeenUpgradedOrIsNewInstall()) { + if appHasJustBeenUpgradedOrIsNewInstall() { DDLogInfo("Repair \(context): APP UPGRADED, REFRESHING DEFAULT BLOCK LISTS, WHITELISTS, RESTARTING FIREWALL") setupFirewallDefaultBlockLists() setupLockdownWhitelistedDomains() - FirewallController.shared.restart(completion: { - error in + FirewallController.shared.restart(completion: { error in if error != nil { DDLogError("Error restarting firewall on \(context): \(error!)") } completion(.repairAttempted) }) - } - else { + } else { Client.getBlockedDomainTest().done { - DDLogError("Repair \(context): Repair Fetch Test Failed: Connected to \(testFirewallDomain) even though it's supposed to be blocked") + DDLogError("Repair \(context): Repair Fetch Test Failed: Connected to \(testFirewallDomain) " + + "even though it's supposed to be blocked") DDLogError("Repair \(context): Doing repair") - FirewallController.shared.restart(completion: { - error in + FirewallController.shared.restart(completion: { error in if error != nil { DDLogError("Repair \(context): Error restarting firewall on \(context): \(error!)") } @@ -127,15 +121,13 @@ enum FirewallRepair { let nsError = error as NSError if nsError.domain == NSURLErrorDomain { DDLogInfo("Repair \(context): Successful blocking of \(testFirewallDomain) with NSURLErrorDomain error: \(nsError)") - } - else { + } else { DDLogInfo("Repair \(context): Successful blocking of \(testFirewallDomain), but seeing non-NSURLErrorDomain error: \(error)") } completion(.repairAttempted) } } - } - else { + } else { completion(.noAction) } }) diff --git a/LockdowniOS/HomeViewController.swift b/LockdowniOS/HomeViewController.swift index ab588fd..c2f8026 100644 --- a/LockdowniOS/HomeViewController.swift +++ b/LockdowniOS/HomeViewController.swift @@ -19,7 +19,7 @@ import AwesomeSpotlightView class CircularView: UIView { override func layoutSubviews() { super.layoutSubviews() - self.layer.cornerRadius = self.bounds.size.width * 0.50 + corners = .continuous(bounds.midX) } @IBInspectable var shadowUIColor: UIColor? { @@ -40,7 +40,8 @@ class CircularView: UIView { let kHasSeenEmailSignup = "hasSeenEmailSignup" -class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Loadable { +// swiftlint:disable type_body_length +class HomeViewController: BaseViewController, Loadable { let kHasViewedTutorial = "hasViewedTutorial" let kHasSeenInitialFirewallConnectedDialog = "hasSeenInitialFirewallConnectedDialog11" @@ -62,7 +63,7 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load @IBOutlet weak var dailyMetrics: UILabel? @IBOutlet weak var weeklyMetrics: UILabel? @IBOutlet weak var allTimeMetrics: UILabel? - var metricsTimer : Timer? + var metricsTimer: Timer? @IBOutlet weak var firewallSettingsButton: UIButton! @IBOutlet weak var firewallViewLogButton: UIButton! @IBOutlet weak var firewallShareButton: UIButton! @@ -77,6 +78,11 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load @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() { @@ -88,47 +94,48 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load metricsTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(updateMetrics), userInfo: nil, repeats: true) metricsTimer?.fire() } - firewallViewLogButton.layer.cornerRadius = 8 + firewallViewLogButton.corners = .continuous(8) firewallViewLogButton.layer.maskedCorners = [.layerMinXMaxYCorner] - firewallSettingsButton.layer.cornerRadius = 8 + firewallSettingsButton.corners = .continuous(8) firewallSettingsButton.layer.maskedCorners = [.layerMaxXMaxYCorner] updateVPNButtonWithStatus(status: VPNController.shared.status()) //updateIP() - vpnWhitelistButton.layer.cornerRadius = 8 + vpnWhitelistButton.corners = .continuous(8) vpnWhitelistButton.layer.maskedCorners = [.layerMinXMaxYCorner] - vpnSetRegionButton.layer.cornerRadius = 8 + 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) { + if VPNController.shared.status() == .connected { firstly { try Client.signIn() } - .done { (signin: SignIn) in + .done { _ in // successfully signed in with no subscription errors, do nothing } .catch { error in - if (self.popupErrorAsNSURLError(error)) { + if self.popupErrorAsNSURLError(error) { return - } - else if let apiError = error as? ApiError { + } 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) - }) + 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: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""), + } else { + self.showPopupDialog(title: .localized("Error Signing In To Verify Subscription"), message: "\(error)", - acceptButton: "Okay") + acceptButton: .localizedOkay) } } } @@ -150,58 +157,65 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load // Used for debugging signup //performSegue(withIdentifier: "showSignup", sender: nil) - if (defaults.bool(forKey: kHasViewedTutorial) == false) { + if defaults.bool(forKey: kHasViewedTutorial) == false { startTutorial() - } - else if (defaults.bool(forKey: kHasSeenEmailSignup) == false) { - AccountUI.presentCreateAccount(on: self) + } 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 } - 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) { + if getTotalMetrics() > 1000 && defaults.bool(forKey: kHasSeenShare) == false { 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) + 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: 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]) + 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.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75) spotlightView.enableArrowDown = true - spotlightView.textLabelFont = fontMedium16 - spotlightView.labelSpacing = 24; + spotlightView.textLabelFont = .mediumLockdownFont(size: 16) + spotlightView.labelSpacing = 24 spotlightView.delegate = self self.view.addSubview(spotlightView) spotlightView.start() }, - DefaultButton(title: NSLocalizedString("Next", comment: ""), dismissOnTap: true) { + 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) { @@ -223,6 +237,7 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load } coordinator.animate { [unowned self] _ in + self.updateLtoButtonFrame() self.updateStackViewAxis(basedOn: size) } completion: { (_) in return @@ -242,10 +257,10 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load hideStatusBar: true, completion: nil) - let getEnhancedPrivacyButton = DefaultButton(title: NSLocalizedString("1 Week Free", comment: ""), dismissOnTap: true) { - self.performSegue(withIdentifier: "showSignup", sender: self) + let getEnhancedPrivacyButton = DefaultButton(title: .localized("1 Week Free"), dismissOnTap: true) { + self.showPaywallIfNoSubscription() } - let laterButton = CancelButton(title: NSLocalizedString("Skip Trial", comment: ""), dismissOnTap: true) { } + let laterButton = CancelButton(title: .localized("Skip Trial"), dismissOnTap: true) { } popup.addButtons([laterButton, getEnhancedPrivacyButton]) @@ -256,32 +271,32 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load @objc func tunnelStatusDidChange(_ notification: Notification) { // Firewall if let tunnelProviderSession = notification.object as? NETunnelProviderSession { - DDLogInfo("VPNStatusDidChange as NETunnelProviderSession with status: \(tunnelProviderSession.status.description)"); - if (!getUserWantsFirewallEnabled()) { + DDLogInfo("VPNStatusDidChange as NETunnelProviderSession with status: \(tunnelProviderSession.status.description)") + if !getUserWantsFirewallEnabled() { updateFirewallButtonWithStatus(status: .disconnected) - } - else { + } else { updateFirewallButtonWithStatus(status: tunnelProviderSession.status) - if (tunnelProviderSession.status == .connected && defaults.bool(forKey: kHasSeenInitialFirewallConnectedDialog) == false) { + 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: "")) + 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); + 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"); + } else { + DDLogInfo("VPNStatusDidChange neither TunnelProviderSession nor NEVPNConnection") } } @@ -290,16 +305,47 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load func startTutorial() { var spotlights: [AwesomeSpotlight] = [] 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 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 s7 = AwesomeSpotlight(withRect: getRectForView(tabBarButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: NSLocalizedString("To see this tutorial again, open the Account tab.", comment: "")) + 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) } @@ -307,10 +353,10 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load spotlight: spotlights) spotlightView.accessibilityIdentifier = "tutorial" spotlightView.cutoutRadius = 8 - spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); + spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75) spotlightView.enableArrowDown = true - spotlightView.textLabelFont = fontMedium16 - spotlightView.labelSpacing = 24; + spotlightView.textLabelFont = .mediumLockdownFont(size: 16) + spotlightView.labelSpacing = 24 spotlightView.delegate = self tabBarController?.view.addSubview(spotlightView) spotlightView.start() @@ -322,12 +368,6 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load } defaults.set(true, forKey: kHasViewedTutorial) - if getAPICredentials() != nil { - // already has email signup pending or confirmed, don't show create account - } - else { - AccountUI.presentCreateAccount(on: self) - } } @IBAction func shareFirewallMetricsTapped(_ sender: Any) { @@ -337,7 +377,9 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load 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) @@ -348,32 +390,58 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load 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 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" + formatter.setLocalizedDateFormatFromTemplate("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) + "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 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 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: 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) - + 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: 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: ""), + title: .localized("Share Your Stats"), + message: .localized(message), image: image, buttonAlignment: .horizontal, transitionStyle: .bounceDown, @@ -383,17 +451,23 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load hideStatusBar: true, completion: nil) - let cancelButton = CancelButton(title: NSLocalizedString("Cancel", comment: ""), dismissOnTap: true) { } + let cancelButton = CancelButton(title: .localizedCancel, dismissOnTap: true) { } - let shareButton = DefaultButton(title: NSLocalizedString("Next", comment: ""), dismissOnTap: true) { - let shareText = "\(NSLocalizedString("I blocked", comment: "Used in the sentence: I blocked 500 trackers with Lockdown.")) \(thousandsFormatter.string(for: getTotalMetrics())!)\(NSLocalizedString(" trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com.", comment: "Used in the sentence: I blocked 500 trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com."))" + 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 = { (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: "Used as a button text in a popup. Like 'OK' except more excited.")) + 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 = [ UIActivity.ActivityType.assignToContact, UIActivity.ActivityType.addToReadingList, UIActivity.ActivityType.openInIBooks, UIActivity.ActivityType.postToVimeo, UIActivity.ActivityType.print ] + vc.excludedActivityTypes = [.assignToContact, .addToReadingList, .openInIBooks, .postToVimeo, .print] if let popoverPC = vc.popoverPresentationController { popoverPC.sourceView = self.firewallShareButton @@ -419,9 +493,10 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load } @IBAction func toggleFirewall(_ sender: Any) { - if (defaults.bool(forKey: kHasAgreedToFirewallPrivacyPolicy) == false) { + if defaults.bool(forKey: kHasAgreedToFirewallPrivacyPolicy) == false { let storyboard = UIStoryboard(name: "Main", bundle: nil) - let viewController = storyboard.instantiateViewController(withIdentifier: "firewallPrivacyPolicyViewController") as! PrivacyPolicyViewController + 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) @@ -430,7 +505,10 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load if getIsCombinedBlockListEmpty() { FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) - self.showPopupDialog(title: NSLocalizedString("No Block Lists Enabled", comment: ""), message: NSLocalizedString("Please tap Block List and enable at least one block list to activate Firewall.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) + 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 } @@ -448,13 +526,13 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load updateFirewallButtonWithStatus(status: .disconnecting) FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) case .connecting, .disconnecting, .reasserting: - break; + break } } func ensureFirewallWorkingAfterEnabling(waitingSeconds: TimeInterval) { FirewallController.shared.existingManagerCount { (count) in - if let count = count, count > 0 { + if count ?? 0 > 0 { DispatchQueue.main.asyncAfter(deadline: .now() + waitingSeconds) { DDLogInfo("\(waitingSeconds) seconds passed, checking if Firewall is enabled") guard getUserWantsFirewallEnabled() else { @@ -472,7 +550,6 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load case .connected: // all good DDLogInfo("Firewall is connected, no action") - break case .disconnected, .invalid: // we suppose that the connection is somehow broken, trying to fix DDLogInfo("Firewall is not connected even though it should be, attempting to fix") @@ -504,62 +581,72 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load toggleCircle: firewallToggleCircle, toggleAnimatedCircle: firewallToggleAnimatedCircle, button: firewallButton, - prefixText: NSLocalizedString("Firewall", comment: "").uppercased()) + prefixText: .localized("Firewall").uppercased()) } - func updateToggleButtonWithStatus(lastStatus: NEVPNStatus?, newStatus: NEVPNStatus, activeLabel: UILabel, toggleCircle: UIButton, toggleAnimatedCircle: NVActivityIndicatorView, button: UIButton, prefixText: String) { + // 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() { + 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: "").uppercased())" + 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 = NSLocalizedString("Activating", comment: "").uppercased() + 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)\(NSLocalizedString(" Off", comment: "").uppercased())" + 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 = NSLocalizedString("Deactivating", comment: "").uppercased() + activeLabel.text = .localized("Deactivating").uppercased() activeLabel.backgroundColor = .lightGray toggleCircle.isHidden = true toggleAnimatedCircle.color = .lightGray toggleAnimatedCircle.startAnimating() button.tintColor = .lightGray case .reasserting: - break; + 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 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.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75) spotlightView.enableArrowDown = true - spotlightView.textLabelFont = fontMedium16 - spotlightView.labelSpacing = 24; + spotlightView.textLabelFont = .mediumLockdownFont(size: 16) + spotlightView.labelSpacing = 24 view.addSubview(spotlightView) spotlightView.start() } @@ -587,7 +674,7 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load toggleCircle: vpnToggleCircle, toggleAnimatedCircle: vpnToggleAnimatedCircle, button: vpnButton, - prefixText: NSLocalizedString("Tunnel", comment: "").uppercased()) + prefixText: .localized("Tunnel").uppercased()) } @IBAction func toggleVPN(_ sender: Any) { @@ -623,10 +710,9 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load .catch { error in DDLogError("Error doing email-login -> subscription-event: \(error)") self.updateVPNButtonWithStatus(status: .disconnected) - if (self.popupErrorAsNSURLError(error)) { + if self.popupErrorAsNSURLError(error) { return - } - else if let apiError = error as? ApiError { + } else if let apiError = error as? ApiError { switch apiError.code { case kApiCodeInvalidAuth, kApiCodeIncorrectLogin: let confirm = PopupDialog(title: "Incorrect Login", @@ -640,34 +726,39 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load hideStatusBar: false, completion: nil) confirm.addButtons([ - DefaultButton(title: NSLocalizedString("Cancel", comment: ""), dismissOnTap: true) { + DefaultButton(title: .localizedCancel, dismissOnTap: true) { }, - DefaultButton(title: NSLocalizedString("Sign Out", comment: ""), dismissOnTap: true) { + DefaultButton(title: .localizedSignOut, dismissOnTap: true) { URLCache.shared.removeAllCachedResponses() Client.clearCookies() clearAPICredentials() setAPICredentialsConfirmed(confirmed: false) - self.showPopupDialog(title: "Success", message: "Signed out successfully.", acceptButton: NSLocalizedString("Okay", comment: "")) + self.showPopupDialog( + title: .localized("Success"), + message: .localized("Signed out successfully."), + acceptButton: .localizedOkay) }, ]) self.present(confirm, animated: true, completion: nil) case kApiCodeNoSubscriptionInReceipt: - self.performSegue(withIdentifier: "showSignup", sender: self) + self.showPaywallIfNoSubscription() 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) - }) + 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 { + } else { firstly { try Client.signIn() // this will fetch and set latest receipt, then submit to API to get cookie } - .then { (signin: SignIn) -> Promise in + .then { _ -> 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() } @@ -677,37 +768,44 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load } .catch { error in self.updateVPNButtonWithStatus(status: .disconnected) - if (self.popupErrorAsNSURLError(error)) { + + if self.popupErrorAsNSURLError(error) { return - } - else if let apiError = error as? ApiError { + } else if let apiError = error as? ApiError { switch apiError.code { case kApiCodeNoSubscriptionInReceipt: - self.performSegue(withIdentifier: "showSignup", sender: self) + self.showPaywallIfNoSubscription() 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) - }) + 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) { + 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.showPopupDialog(title: NSLocalizedString("Apple Outage", comment: ""), message: "There is currently an outage at Apple which is preventing Secure Tunnel from activating. This will likely by resolved by Apple soon, and we apologize for this issue in the meantime." + NSLocalizedString("\n\n If this error persists, please contact team@lockdownprivacy.com.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) - } - } - else { + } else { _ = self.popupErrorAsApiError(error) } } - } - else { - self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""), + } else { + self.showPopupDialog(title: .localized("Error Signing In To Verify Subscription"), message: "\(error)", - acceptButton: NSLocalizedString("Okay", comment: "")) + acceptButton: .localizedOkay) } } } @@ -742,7 +840,7 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load vc.parentVC = self } case "showUpgradePlan": - if let vc = segue.destination as? SignupViewController { + if let vc = segue.destination as? OldSignupViewController { if activePlans.isEmpty { vc.mode = .newSubscription } else { @@ -766,23 +864,24 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load DDLogInfo("Incrementing Rating Count: " + String(ratingCount)) defaults.set(ratingCount, forKey: ratingCountKey) // not testflight - if (isTestFlight) { + 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 { + 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) { + 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) { + if ratingTriggered >= 3 { DDLogInfo("Rating Check: Skipping - ratingTriggered greater or equal to 3: \(ratingTriggered)") return } @@ -792,10 +891,9 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Load SKStoreReviewController.requestReview() } } - } -class LockdownCustomActivityItemProvider : UIActivityItemProvider { +class LockdownCustomActivityItemProvider: UIActivityItemProvider { let shareText: String @@ -804,20 +902,44 @@ class LockdownCustomActivityItemProvider : UIActivityItemProvider { super.init(placeholderItem: text) } - override func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { - if let type = activityType { - switch type { - case UIActivity.ActivityType.postToTwitter: + 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 } } - 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 { @@ -845,15 +967,13 @@ fileprivate extension PopupDialogButton { } final class DynamicButton: PopupDialogButton { - var onTap: ((DynamicButton) -> ())? + var onTap: ((DynamicButton) -> Void)? override var buttonAction: PopupDialogButton.PopupDialogButtonAction? { - get { - if let onTap = onTap { - return { [weak self] in if let value = self { return onTap(value) } } - } else { - return nil - } + if let onTap = onTap { + return { [weak self] in if let value = self { return onTap(value) } } + } else { + return nil } } } @@ -878,3 +998,95 @@ extension NEVPNStatus: CustomStringConvertible { } } + +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) + } + } +} diff --git a/LockdowniOS/Info.plist b/LockdowniOS/Info.plist index e282c35..0ccc5d7 100644 --- a/LockdowniOS/Info.plist +++ b/LockdowniOS/Info.plist @@ -49,6 +49,13 @@ We use this to automatically select the best region for the VPN. NSPhotoLibraryAddUsageDescription You asked to save an image to your photos. This does not give access to read your photos. + NSSiriUsageDescription + Lockdown Siri support is needed for Shortcuts functionality. + NSUserActivityTypes + + DisableVPNIntent + EnableVPNIntent + UIAppFonts Montserrat-Bold.ttf diff --git a/LockdowniOS/Loader.swift b/LockdowniOS/Loader.swift index f4491d6..347035b 100644 --- a/LockdowniOS/Loader.swift +++ b/LockdowniOS/Loader.swift @@ -16,13 +16,13 @@ protocol Loadable { } final class LoadingView: UIView { - private let activityIndicatorView = UIActivityIndicatorView(style: .whiteLarge) + private let activityIndicatorView = UIActivityIndicatorView(style: .large) override func layoutSubviews() { super.layoutSubviews() backgroundColor = UIColor.black.withAlphaComponent(0.6) - layer.cornerRadius = 5 + layer.corners = .continuous(12) if activityIndicatorView.superview == nil { addSubview(activityIndicatorView) @@ -39,8 +39,8 @@ final class LoadingView: UIView { } } -fileprivate struct Constants { - fileprivate static let loadingViewTag = 63342 +private struct Constants { + static let loadingViewTag = 63342 } extension Loadable where Self: UIViewController { @@ -48,6 +48,7 @@ extension Loadable where Self: UIViewController { func showLoadingView() { let loadingView = LoadingView() view.addSubview(loadingView) + view.isUserInteractionEnabled = false loadingView.translatesAutoresizingMaskIntoConstraints = false loadingView.widthAnchor.constraint(equalToConstant: 100).isActive = true @@ -60,6 +61,7 @@ extension Loadable where Self: UIViewController { } func hideLoadingView() { + view.isUserInteractionEnabled = true view.subviews.forEach { subview in if subview.tag == Constants.loadingViewTag { subview.removeFromSuperview() diff --git a/LockdowniOS/LocalLogger.swift b/LockdowniOS/LocalLogger.swift index cbcf9c1..5fdb71c 100644 --- a/LockdowniOS/LocalLogger.swift +++ b/LockdowniOS/LocalLogger.swift @@ -9,18 +9,16 @@ import Foundation import CocoaLumberjackSwift var logFileDataArray: [NSData] { - get { - let logFilePaths = fileLogger.logFileManager.sortedLogFilePaths as [String] - var logFileDataArray = [NSData]() - for logFilePath in logFilePaths { - let fileURL = NSURL(fileURLWithPath: logFilePath) - if let logFileData = try? NSData(contentsOf: fileURL as URL, options: NSData.ReadingOptions.mappedIfSafe) { - // Insert at front to reverse the order, so that oldest logs appear first. - logFileDataArray.insert(logFileData, at: 0) - } + let logFilePaths = fileLogger.logFileManager.sortedLogFilePaths as [String] + var logFileDataArray = [NSData]() + for logFilePath in logFilePaths { + let fileURL = NSURL(fileURLWithPath: logFilePath) + if let logFileData = try? NSData(contentsOf: fileURL as URL, options: NSData.ReadingOptions.mappedIfSafe) { + // Insert at front to reverse the order, so that oldest logs appear first. + logFileDataArray.insert(logFileData, at: 0) } - return logFileDataArray } + return logFileDataArray } func setupLocalLogger() { diff --git a/LockdowniOS/LockdownPaywallOptionAdvantageView.swift b/LockdowniOS/LockdownPaywallOptionAdvantageView.swift new file mode 100644 index 0000000..21bd790 --- /dev/null +++ b/LockdowniOS/LockdownPaywallOptionAdvantageView.swift @@ -0,0 +1,25 @@ +// +// LockdownPaywallOptionAdvantageView.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/5/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import UIKit + +final class LockdownPaywallOptionAdvantageView: UIView, NibLoadable { + + @IBOutlet private var advantageLabel: UILabel! + + static func make(text: String) -> LockdownPaywallOptionAdvantageView { + let view = LockdownPaywallOptionAdvantageView.loadFromNib() + + view.advantageLabel.text = text + view.advantageLabel.preferredMaxLayoutWidth = view.bounds.size.width + + return view + } + +} diff --git a/LockdowniOS/LockdownPaywallOptionAdvantageView.xib b/LockdowniOS/LockdownPaywallOptionAdvantageView.xib new file mode 100644 index 0000000..b751b25 --- /dev/null +++ b/LockdowniOS/LockdownPaywallOptionAdvantageView.xib @@ -0,0 +1,72 @@ + + + + + + + + + + + + + Montserrat-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/LockdownPaywallOptionCell.swift b/LockdowniOS/LockdownPaywallOptionCell.swift new file mode 100644 index 0000000..1c0e5da --- /dev/null +++ b/LockdowniOS/LockdownPaywallOptionCell.swift @@ -0,0 +1,75 @@ +// +// LockdownPaywallOptionCell.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/5/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import UIKit + +final class LockdownPaywallOptionCell: UICollectionViewCell, Dequeuable { + + @IBOutlet private var gradientView: UIView! + + @IBOutlet private var titleLabel: UILabel! + @IBOutlet private var priceLabel: UILabel! + @IBOutlet private var afterTrialLabel: UILabel! + + static var dequeuableId: String = .init(describing: LockdownPaywallOptionCell.self) + + private var gradientLayer: CAGradientLayer? + + func configure(with offer: SubscriptionOffer) { + titleLabel.attributedText = offer.productGroup.localizedTitle(for: .defaultPaywall) + priceLabel.text = offer.pricePerPeriod + afterTrialLabel.text = offer.afterTrialPeriodText + } + + func toggleSelect(isSelected: Bool, animated: Bool) { + if isSelected { + gradientView.layer.borderWidth = 0 + gradientLayer = gradientView.applyGradient( + colors: [ + hexStringToUIColor(hex: "#00B6F3").cgColor, + hexStringToUIColor(hex: "#0099CD").cgColor + ], + cornerRadius: gradientView.layer.cornerRadius) + + [titleLabel, priceLabel, afterTrialLabel].forEach { + $0?.textColor = .white + } + if animated { + showAnimatedPress() + } + } else { + gradientView.layer.borderWidth = 1 + if #available(iOS 13.0, *) { + gradientView.backgroundColor = .systemBackground + afterTrialLabel.textColor = .secondaryLabel + [titleLabel, priceLabel].forEach { + $0?.textColor = .label + } + } else { + gradientView.backgroundColor = .white + afterTrialLabel.textColor = .lightGray + [titleLabel, priceLabel].forEach { + $0?.textColor = .black + } + } + } + } + + override func prepareForReuse() { + gradientLayer?.removeFromSuperlayer() + gradientLayer = nil + } + + override func awakeFromNib() { + super.awakeFromNib() + + gradientView.layer.cornerRadius = 8 + gradientView.layer.borderColor = UIColor.lightGray.cgColor + } +} diff --git a/LockdowniOS/LockdownPaywallOptionCell.xib b/LockdowniOS/LockdownPaywallOptionCell.xib new file mode 100644 index 0000000..4347d0f --- /dev/null +++ b/LockdowniOS/LockdownPaywallOptionCell.xib @@ -0,0 +1,88 @@ + + + + + + + + + + + Montserrat-Regular + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/LockdownPaywallViewController.xib b/LockdowniOS/LockdownPaywallViewController.xib new file mode 100644 index 0000000..e7892b4 --- /dev/null +++ b/LockdowniOS/LockdownPaywallViewController.xib @@ -0,0 +1,291 @@ + + + + + + + + + + + + + Montserrat-Bold + + + Montserrat-Medium + + + Montserrat-Regular + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/LockdownUser.swift b/LockdowniOS/LockdownUser.swift new file mode 100644 index 0000000..69ed8d1 --- /dev/null +++ b/LockdowniOS/LockdownUser.swift @@ -0,0 +1,15 @@ +// +// LockdownUser.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/2/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +final class LockdownUser { + private(set) var currentSubscription: Subscription? + + func updateSubscription(to newSubscription: Subscription?) { + currentSubscription = newSubscription + } +} diff --git a/LockdowniOS/LockdowniOS.entitlements b/LockdowniOS/LockdowniOS.entitlements index 8919c5e..fb3e249 100644 --- a/LockdowniOS/LockdowniOS.entitlements +++ b/LockdowniOS/LockdowniOS.entitlements @@ -28,6 +28,8 @@ allow-vpn + com.apple.developer.siri + com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.application-groups diff --git a/LockdowniOS/MainTabBarViewController.swift b/LockdowniOS/MainTabBarViewController.swift index 359698d..2d40fc5 100644 --- a/LockdowniOS/MainTabBarViewController.swift +++ b/LockdowniOS/MainTabBarViewController.swift @@ -10,13 +10,11 @@ import UIKit final class MainTabBarController: UITabBarController { - var homeViewController: HomeViewController { - return viewControllers![0] as! HomeViewController - } + var homeViewController: HomeViewController? { viewControllers![0] as? HomeViewController } - var accountViewController: AccountViewController { - let navigation = viewControllers![1] as! UINavigationController - return navigation.viewControllers[0] as! AccountViewController + var accountViewController: AccountViewController? { + guard let navigation = viewControllers![1] as? UINavigationController else { return nil } + return navigation.viewControllers[0] as? AccountViewController } var accountTabBarButton: UIView? { diff --git a/LockdowniOS/OldSignupViewController.swift b/LockdowniOS/OldSignupViewController.swift new file mode 100644 index 0000000..4e7c569 --- /dev/null +++ b/LockdowniOS/OldSignupViewController.swift @@ -0,0 +1,511 @@ +// +// SignupViewController.swift +// Tunnels +// +// Copyright © 2019 Confirmed Inc. All rights reserved. +// + +import UIKit +import SwiftyStoreKit +import NetworkExtension +import PromiseKit +import CocoaLumberjackSwift +import StoreKit + +final class OldSignupViewController: BaseViewController, Loadable { + + // MARK: - VARIABLES + + var parentVC: UIViewController? + + enum Mode { + case newSubscription + case upgrade(active: [Subscription.PlanType]) + } + + var mode = Mode.newSubscription + + @IBOutlet var monthlyPlanContainer: UIView! + @IBOutlet var monthlyPlanCheckbox: M13Checkbox! + + @IBOutlet var monthlyProPlanContainer: UIView! + @IBOutlet var monthlyProPlanCheckbox: M13Checkbox! + + @IBOutlet var annualPlanContainer: UIView! + @IBOutlet var annualPlanCheckbox: M13Checkbox! + + @IBOutlet var annualProPlanContainer: UIView! + @IBOutlet var annualProPlanCheckbox: M13Checkbox! + + @IBOutlet var upgradeSubscriptionHintLabel: UILabel! + + @IBOutlet var startTrialButton: TKTransitionSubmitButton! + @IBOutlet var pricingSubtitle: UILabel! + + @IBOutlet var restorePurchasesButton: TKTransitionSubmitButton! + + override func viewDidLoad() { + super.viewDidLoad() + VPNSubscription.cacheLocalizedPrices() + switch mode { + case .newSubscription: + selectAnnual() + upgradeSubscriptionHintLabel.isHidden = true + case .upgrade(active: let activeSubscriptions): + configureWithActiveSubscriptions(activeSubscriptions) + restorePurchasesButton.isHidden = true + } + } + + var disabledCheckboxes: Set = [] + + @objc func selectMonthly() { + animatedSetChecked(monthlyPlanCheckbox, .checked) + animatedSetChecked(monthlyProPlanCheckbox, .unchecked) + animatedSetChecked(annualPlanCheckbox, .unchecked) + animatedSetChecked(annualProPlanCheckbox, .unchecked) + VPNSubscription.selectedProductId = VPNSubscription.productIdMonthly + updatePricingSubtitle() + } + + @objc func selectMonthlyPro() { + animatedSetChecked(monthlyPlanCheckbox, .unchecked) + animatedSetChecked(monthlyProPlanCheckbox, .checked) + animatedSetChecked(annualPlanCheckbox, .unchecked) + animatedSetChecked(annualProPlanCheckbox, .unchecked) + VPNSubscription.selectedProductId = VPNSubscription.productIdMonthlyPro + updatePricingSubtitle() + } + + @IBAction func monthlyTapped(_ sender: Any) { + selectMonthly() + } + + @IBAction func monthlyProTapped(_ sender: Any) { + selectMonthlyPro() + } + + @objc func selectAnnual() { + animatedSetChecked(monthlyPlanCheckbox, .unchecked) + animatedSetChecked(monthlyProPlanCheckbox, .unchecked) + animatedSetChecked(annualPlanCheckbox, .checked) + animatedSetChecked(annualProPlanCheckbox, .unchecked) + VPNSubscription.selectedProductId = VPNSubscription.productIdAnnual + updatePricingSubtitle() + } + + @IBAction func annualTapped(_ sender: Any) { + selectAnnual() + } + + @objc func selectAnnualPro() { + animatedSetChecked(monthlyPlanCheckbox, .unchecked) + animatedSetChecked(monthlyProPlanCheckbox, .unchecked) + animatedSetChecked(annualPlanCheckbox, .unchecked) + animatedSetChecked(annualProPlanCheckbox, .checked) + VPNSubscription.selectedProductId = VPNSubscription.productIdAnnualPro + updatePricingSubtitle() + } + + @IBAction func annualProTapped(_ sender: Any) { + selectAnnualPro() + } + + @objc func updatePricingSubtitle() { + let context: VPNSubscription.SubscriptionContext = { + switch mode { + case .newSubscription: + return .new + case .upgrade: + return .upgrade + } + }() + + if monthlyPlanCheckbox.checkState == .checked, monthlyPlanCheckbox.isEnabled { + pricingSubtitle.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthly, for: context) + } else if annualPlanCheckbox.checkState == .checked, annualPlanCheckbox.isEnabled { + pricingSubtitle.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnual, for: context) + } else if annualProPlanCheckbox.checkState == .checked, annualProPlanCheckbox.isEnabled { + pricingSubtitle.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnualPro, for: context) + } else if monthlyProPlanCheckbox.checkState == .checked, monthlyProPlanCheckbox.isEnabled { + pricingSubtitle.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthlyPro, for: context) + } + } + + @IBAction func dismissSignUpScreen() { + self.view.endEditing(true) + self.dismiss(animated: true, completion: {}) + } + + func toggleStartTrialButton(_ enabled: Bool) { + if enabled { + UIView.transition(with: self.pricingSubtitle, + duration: 0.15, + options: .transitionCrossDissolve, + animations: { + self.pricingSubtitle.alpha = 1.0 + }) + startTrialButton.isUserInteractionEnabled = true + startTrialButton.setOriginalState() + startTrialButton.layer.cornerRadius = 4 + unblockUserInteraction() + } else { + UIView.transition(with: self.pricingSubtitle, + duration: 0.15, + options: .transitionCrossDissolve, + animations: { + self.pricingSubtitle.alpha = 0.0 + }) + startTrialButton.isUserInteractionEnabled = false + startTrialButton.startLoadingAnimation() + blockUserInteraction() + } + } + + @IBAction func startTrial (_ sender: UIButton) { + toggleStartTrialButton(false) + VPNSubscription.purchase( + succeeded: { + self.dismiss(animated: true, completion: { + if let presentingViewController = self.parentVC as? AccountViewController { + presentingViewController.reloadTable() + } + // force refresh receipt, and sync with email if it exists, activate VPNte + if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { + DDLogInfo("purchase complete: syncing with confirmed email") + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .then { (signin: SignIn) -> Promise in + DDLogInfo("purchase complete: signin result: \(signin)") + return try Client.subscriptionEvent(forceRefresh: true) + } + .then { (result: SubscriptionEvent) -> Promise in + DDLogInfo("purchase complete: subscriptionevent result: \(result)") + return try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + DDLogInfo("purchase complete: setting VPN creds with ID: \(getKey.id)") + VPNController.shared.setEnabled(true) + } + .catch { error in + DDLogError("purchase complete: Error: \(error)") + if self.popupErrorAsNSURLError("Error activating Secure Tunnel: \(error)") { + return + } else if let apiError = error as? ApiError { + switch apiError.code { + default: + _ = self.popupErrorAsApiError("API Error activating Secure Tunnel: \(error)") + } + } + } + } else { + firstly { + try Client.signIn(forceRefresh: true) // this will fetch and set latest receipt, then submit to API to get cookie + } + .then { _ 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 + DDLogError("purchase complete - no email: Error: \(error)") + if self.popupErrorAsNSURLError("Error activating Secure Tunnel: \(error)") { + return + } else if let apiError = error as? ApiError { + switch apiError.code { + default: + _ = self.popupErrorAsApiError("API Error activating Secure Tunnel: \(error)") + } + } + } + } + }) + }, + errored: { error in + self.toggleStartTrialButton(true) + DDLogError("Start Trial Failed: \(error)") + + if let skError = error as? SKError { + var errorText = "" + switch skError.code { + case .unknown: + errorText = .localized("Unknown error. Please contact support at team@lockdownprivacy.com.") + case .clientInvalid: + errorText = .localized("Not allowed to make the payment") + case .paymentCancelled: + errorText = .localized("Payment was cancelled") + case .paymentInvalid: + errorText = .localized("The purchase identifier was invalid") + case .paymentNotAllowed: + errorText = .localized(""" +Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. \ +Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again. +""") + case .storeProductNotAvailable: + errorText = .localized("The product is not available in the current storefront") + case .cloudServicePermissionDenied: + errorText = .localized("Access to cloud service information is not allowed") + case .cloudServiceNetworkConnectionFailed: + errorText = .localized("Could not connect to the network") + case .cloudServiceRevoked: + errorText = .localized("User has revoked permission to use this cloud service") + default: + errorText = skError.localizedDescription + } + self.showPopupDialog(title: .localized("Error Starting Trial"), message: errorText, acceptButton: .localizedOkay) + } else if self.popupErrorAsNSURLError(error) { + return + } else if self.popupErrorAsApiError(error) { + return + } else { + self.showPopupDialog( + title: .localized("Error Starting Trial"), + message: .localized("Please contact team@lockdownprivacy.com.\n\nError details:\n") + "\(error)", + acceptButton: .localizedOkay) + } + }) + } + + func toggleRestorePurchasesButton(_ enabled: Bool) { + if enabled { + restorePurchasesButton.isUserInteractionEnabled = true + restorePurchasesButton.setOriginalState() + restorePurchasesButton.layer.cornerRadius = 4 + unblockUserInteraction() + } else { + restorePurchasesButton.isUserInteractionEnabled = false + restorePurchasesButton.startLoadingAnimation() + blockUserInteraction() + } + } + + @IBAction func restorePurchases(_ sender: Any) { + //toggleRestorePurchasesButton(false) + firstly { + try Client.signIn(forceRefresh: true) + } + .then { _ in + try Client.getKey() + } + .done { _ in + // we were able to get key, so subscription is valid -- follow pathway from HomeViewController to associate this with the email account if there is one + let presentingViewController = self.presentingViewController as? HomeViewController + self.dismiss(animated: true, completion: { + if presentingViewController != nil { + presentingViewController?.toggleVPN("me") + } else { + VPNController.shared.setEnabled(true) + } + }) + } + .catch { error in + self.toggleRestorePurchasesButton(true) + DDLogError("Restore Failed: \(error)") + if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: + // now try email if it exists + if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { + DDLogInfo("restore: have confirmed API credentials, using them") + self.toggleRestorePurchasesButton(false) + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .then { (signin: SignIn) -> Promise in + DDLogInfo("restore: signin result: \(signin)") + return try Client.getKey() + } + .done { (getKey: GetKey) in + self.toggleRestorePurchasesButton(true) + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + DDLogInfo("restore: setting VPN creds with ID and Dismissing: \(getKey.id)") + let presentingViewController = self.presentingViewController as? HomeViewController + self.dismiss(animated: true, completion: { + if presentingViewController != nil { + presentingViewController?.toggleVPN("me") + } else { + VPNController.shared.setEnabled(true) + } + }) + } + .catch { error in + self.toggleRestorePurchasesButton(true) + DDLogError("restore: Error doing restore with email-login: \(error)") + if self.popupErrorAsNSURLError(error) { + return + } else if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: + self.showPopupDialog(title: .localized("No Active Subscription"), + message: .localized(""" +Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, \ +you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com +"""), + acceptButton: .localizedOK) + default: + _ = self.popupErrorAsApiError(error) + } + } + } + } else { + let message = """ +Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, \ +you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com +""" + self.showPopupDialog(title: .localized("No Active Subscription"), + message: .localized(message), + acceptButton: .localizedOK) + } + default: + let pleaseEmail: String = .localized("Please email team@lockdownprivacy.com with the following Error Code ") + self.showPopupDialog( + title: .localized("Error Restoring Subscription"), + message: pleaseEmail + "\(apiError.code) : \(apiError.message)", + acceptButton: .localizedOK) + } + } else { + let message: String = .localized(""" +Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with +the following error message: \ + +""") + self.showPopupDialog(title: .localized("Error Restoring Subscription"), message: message + "\(error)", acceptButton: .localizedOK) + } + } + } + + @IBAction func openPrivacyPolicy (_ sender: Any) { + self.showPrivacyPolicyModal() + } + + @IBAction func openTermsAndConditions (_ sender: Any) { + self.showTermsModal() + } + + @IBAction func cancelButtonPressed (_ sender: Any) { + self.dismiss(animated: true, completion: nil) + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + self.view.endEditing(true) + } + +} + +extension OldSignupViewController { + + // MARK: - Functions for "Upgrade Plan" mode + + private func configureWithActiveSubscriptions(_ activePlans: [Subscription.PlanType]) { + startTrialButton.setTitle(.localized("Upgrade Plan"), for: .normal) + + for plan in activePlans { + configureForUnavailable(plan, reason: .purchased) + if let unavailableToUpgrade = plan.unavailableToUpgrade { + for unavailable in unavailableToUpgrade where unavailable != plan { + configureForUnavailable(unavailable, reason: .lowerTierThanPurchased) + } + } + } + + selectAnnualOrFirstAvailable() + } + + enum UnavailableReason { + case purchased + case lowerTierThanPurchased + } + + private func configureForUnavailable(_ subscriptionPlanType: Subscription.PlanType, reason: UnavailableReason) { + switch subscriptionPlanType { + case .monthly: + markAsUnavailable(monthlyPlanCheckbox, plan: subscriptionPlanType, reason: reason, container: monthlyPlanContainer) + case .proMonthly: + markAsUnavailable(monthlyProPlanCheckbox, plan: subscriptionPlanType, reason: reason, container: monthlyProPlanContainer) + case .annual: + markAsUnavailable(annualPlanCheckbox, plan: subscriptionPlanType, reason: reason, container: annualPlanContainer) + case .proAnnual: + markAsUnavailable(annualProPlanCheckbox, plan: subscriptionPlanType, reason: reason, container: annualProPlanContainer) + default: + break + } + } + + private func markAsUnavailable(_ checkbox: M13Checkbox, plan: Subscription.PlanType, reason: UnavailableReason, container: UIView) { + disabledCheckboxes.insert(checkbox) + checkbox.isEnabled = false + + switch reason { + case .lowerTierThanPurchased: + container.isHidden = true + case .purchased: + container.alpha = 0.4 + checkbox.setCheckState(.checked, animated: false) + checkbox.tintColor = UIColor.gray + checkbox.secondaryTintColor = UIColor.gray + checkbox.isEnabled = false + checkbox.accessibilityLabel = Accessibility.currentPlanCheckboxLabel(for: plan) + checkbox.accessibilityHint = Accessibility.currentPlanCheckboxHint() + } + } + + private func animatedSetChecked(_ checkbox: M13Checkbox, _ state: M13Checkbox.CheckState) { + if disabledCheckboxes.contains(checkbox) { + return + } else { + checkbox.setCheckState(state, animated: true) + } + } + + func selectAnnualOrFirstAvailable() { + if disabledCheckboxes.contains(annualPlanCheckbox) { + // Annual is unavailable, find first available option to select + if monthlyPlanCheckbox.isEnabled { + selectMonthly() + } else if annualPlanCheckbox.isEnabled { + selectAnnual() + } else if monthlyProPlanCheckbox.isEnabled { + selectMonthlyPro() + } else if annualProPlanCheckbox.isEnabled { + selectAnnualPro() + } else { + selectAnnualPro() + startTrialButton.isEnabled = false + startTrialButton.backgroundColor = UIColor.gray + startTrialButton.isUserInteractionEnabled = false + startTrialButton.alpha = 0.5 + pricingSubtitle.alpha = 0.3 + } + } else { + selectAnnual() + } + } +} + +extension OldSignupViewController { + enum Accessibility { + static func currentPlanCheckboxLabel(for plan: Subscription.PlanType) -> String? { + switch plan { + case .monthly: + return .localized("\"iOS Monthly\" is your current plan") + case .proMonthly: + return .localized("\"Pro Monthly\" is your current plan") + case .annual: + return .localized("\"iOS Annual\" is your current plan") + case .proAnnual: + return .localized("\"Pro Annual\" is your current plan") + default: + return "\"\(plan.rawValue)\" is your current plan" + } + } + + static func currentPlanCheckboxHint() -> String? { + return nil + } + } +} diff --git a/LockdowniOS/OnboardingConfiguration.swift b/LockdowniOS/OnboardingConfiguration.swift new file mode 100644 index 0000000..b7f9ce5 --- /dev/null +++ b/LockdowniOS/OnboardingConfiguration.swift @@ -0,0 +1,23 @@ +// +// OnboardingConfiguration.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/28/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +struct OnboardingConfiguration { + + /// How often the onboarding page progress bar is updated. + let timerInterval: TimeInterval + + /// How long the onboarding page text and gradient are updated. + let contentAnimationDuration: Double + + init(timerInterval: TimeInterval = 0.05, contentAnimationDuration: Double = 0.3) { + self.timerInterval = timerInterval + self.contentAnimationDuration = contentAnimationDuration + } +} diff --git a/LockdowniOS/OnboardingPage.swift b/LockdowniOS/OnboardingPage.swift new file mode 100644 index 0000000..77a7e2d --- /dev/null +++ b/LockdowniOS/OnboardingPage.swift @@ -0,0 +1,92 @@ +// +// OnboardingPage.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/26/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import AVFoundation +import Foundation + +struct OnboardingPage { + let title: String + let description: String + let content: OnboardingPageContent + let gradient: LockdownGradient + + var videoDuration: Float { + guard case .video(let videoTitle) = content else { return 0 } + guard let videoPath = Bundle.main.path(forResource: videoTitle, ofType: "mp4") else { return 0 } + + let videoUrl = URL(fileURLWithPath: videoPath) + let asset = AVAsset(url: videoUrl) + let duration = asset.duration + return Float(CMTimeGetSeconds(duration)) + } + + // MARK: - Factory + + static let featuredByApple: Self = { + return OnboardingPage(title: .localized("onboarding_simple_and_powerful"), + description: .localized("onboarding_lockdown_stops_trackers"), + content: .image(title: "Onboarding_apple_logo", duration: 5, presentationType: .fadeIn), + gradient: .onboardingBlue) + }() + + static let installation: Self = { + return OnboardingPage(title: .localized("onboarding_worlds_first_on_device"), + description: .localized("onboarding_firewall_and_vpn_work_across_all_apps"), + content: .video(title: "Onboarding_installation"), + gradient: .onboardingPurple) + }() + + static let easyProtection: Self = { + return OnboardingPage(title: .localized("onboarding_staying_protected_is_easy_with_lockdown"), + description: .localized("onboarding_firewall_and_vpn_work_across_all_apps"), + content: .video(title: "Onboarding_launch_firewall"), + gradient: .onboardingPurple) + }() + + static let anonymousWithVPN: Self = { + return OnboardingPage(title: .localized("onboarding_remain_anonymous_with_secure_tunnel_vpn"), + description: .localized("onboarding_no_logs_fast_and_fully_audited"), + content: .video(title: "Onboarding_showcase_vpn"), + gradient: .onboardingBlue) + }() + + static let blocklists: Self = { + return OnboardingPage(title: .localized("onboarding_take_control_of_who_you_block"), + description: .localized("onboarding_when_using_our_intuitive_blocklists"), + content: .video(title: "Onboarding_blocklists"), + gradient: .onboardingPurple) + }() + + static let summaryAndUpdates: Self = { + return OnboardingPage(title: .localized("onboarding_stay_protected"), + description: .localized("onboarding_get_weekly_summary_and_blocklist_updates"), + content: .video(title: "Onboarding_notifications"), + gradient: .onboardingBlue) + }() + + static let manyTrackersBlocked: Self = { + return OnboardingPage(title: .localized("onboarding_over_1_billion_trackers_blocked"), + description: .localized("onboarding_used_by_more_than_1_million"), + content: .image(title: "Onboarding_blocked_tweet", duration: 10, presentationType: .zoomOutAndMoveToLeft), + gradient: .onboardingPurple) + }() +} + +extension Array where Element == OnboardingPage { + static let defaultPages: Self = { + return [ + .featuredByApple, + .installation, + .easyProtection, + .anonymousWithVPN, + .blocklists, + .summaryAndUpdates, + .manyTrackersBlocked + ] + }() +} diff --git a/LockdowniOS/OnboardingPageContent.swift b/LockdowniOS/OnboardingPageContent.swift new file mode 100644 index 0000000..b4be8eb --- /dev/null +++ b/LockdowniOS/OnboardingPageContent.swift @@ -0,0 +1,66 @@ +// +// OnboardingPageContent.swift +// Lockdown +// +// Created by Alexander Parshakov on 10/31/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import UIKit + +typealias ImageViewAnimationCompletion = ((UIImageView) -> Void)? + +enum OnboardingPageContent { + case image(title: String, duration: Float, presentationType: OnboardingPageImagePresentationType) + case video(title: String) +} + +enum OnboardingPageImagePresentationType { + case fadeIn + case zoomOutAndMoveToLeft + + var animation: ImageViewAnimationCompletion { + switch self { + case .fadeIn: + return { imageView in + imageView.prepareForReuse() + imageView.fadeIn(duration: 1.2) + } + case .zoomOutAndMoveToLeft: + return { imageView in + imageView.prepareForReuse() + imageView.fadeIn(duration: 1.2) { + imageView.fadeOutAsDiminished(duration: 1.2, delay: 2) { + + imageView.changeToAllTweetsImage() + imageView.transform = CGAffineTransform(scaleX: 1.8, y: 1.8) + imageView.fadeIn(duration: 0.8) + + UIView.animate(withDuration: 5, delay: 0, animations: { + imageView.center = .init(x: imageView.center.x - 100, y: imageView.center.y) + }, completion: { isContinued in + guard isContinued else { return } + imageView.fadeOut(duration: 0.6) + }) + } + } + } + } + } +} + +extension UIImageView { + func prepareForReuse() { + layer.removeAllAnimations() + transformToIdentity() + } +} + +private extension UIImageView { + func changeToAllTweetsImage() { + if let image = UIImage(named: "Onboarding_tweets") { + self.image = image + } + } +} diff --git a/LockdowniOS/OnboardingViewController.swift b/LockdowniOS/OnboardingViewController.swift new file mode 100644 index 0000000..445437d --- /dev/null +++ b/LockdowniOS/OnboardingViewController.swift @@ -0,0 +1,453 @@ +// +// OnboardingViewController.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/26/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import AVFoundation +import Foundation +import UIKit + +final class OnboardingViewController: UIViewController, CAAnimationDelegate { + + @IBOutlet private var progressBarStackView: UIStackView! + + @IBOutlet private var infoLabelStackView: UIStackView! + @IBOutlet private var titleLabel: UILabel! + @IBOutlet private var descriptionLabel: UILabel! + + @IBOutlet private var videoContentBorderView: UIView! + @IBOutlet private var videoContentView: UIView! + @IBOutlet private var imageView: UIImageView! + + @IBOutlet private var leftTapGestureView: UIView! + @IBOutlet private var rightTapGestureView: UIView! + + @IBOutlet private var signUpButton: UIButton! + @IBOutlet private var loginButton: UIButton! + + @IBOutlet private var videoContentVerticalCenterConstraint: NSLayoutConstraint! + @IBOutlet private var imageContentLeadingConstraint: NSLayoutConstraint! + @IBOutlet private var imageContentTrailingConstraint: NSLayoutConstraint! + + private var progressViews: [WeakObject] = [] + private var gradientLayer: CAGradientLayer? + private var avPlayerLayer: AVPlayerLayer? + private var avPlayer: AVPlayer? + + /// Defining if page progress should be continued upon viewWillAppear. + private var hasDisappearedBefore = false + + private let configuration = OnboardingConfiguration() + private var progressBarTimer: Timer? + private var currentTimerDuration: Float = 0 + + private var currentProgressPercentage: Float { + switch currentPage.content { + case .image(_, let duration, _): + return currentTimerDuration / duration + case .video: + return currentTimerDuration / currentPage.videoDuration + } + } + + private let pages: [OnboardingPage] + private var currentPage: OnboardingPage { pages[currentPageIndex] } + + @PositiveAndArrayRestricted(defaultValue: 0) + private var currentPageIndex { + didSet { + DispatchQueue.main.async { + self.updatePage() + } + } + } + + init(pages: [OnboardingPage] = .defaultPages) { + self.pages = pages + _currentPageIndex.arrayCount = pages.count + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupProgressViews() + updatePage() + setupTexts() + setupButtons() + setupSideTapGestures() + + configureContentSizingForIdiom() + + NotificationCenter.default.addObserver(self, + selector: #selector(freezePageProgress), + name: UIApplication.didEnterBackgroundNotification, + object: nil) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + view.layoutIfNeeded() + + gradientLayer?.frame = view.bounds + avPlayerLayer?.frame = videoContentView.bounds + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + navigationController?.navigationBar.tintColor = .label + navigationController?.navigationBar.barStyle = .default + navigationController?.setNavigationBarHidden(true, animated: animated) + + if hasDisappearedBefore { + continuePageProgress() + hasDisappearedBefore = false + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + navigationController?.setNavigationBarHidden(false, animated: animated) + + freezePageProgress() + hasDisappearedBefore = true + } + + @IBAction private func didTapSignUp(_ sender: Any) { + OneTimeActions.markAsSeen(.newFancyOnboarding) + signUpButton.showAnimatedPress { [weak self] in + let signUpViewController = SignUpViewController(mode: .signUp) + self?.navigationController?.pushViewController(signUpViewController, animated: true) + } + } + + @IBAction private func didTapLogin(_ sender: Any) { + OneTimeActions.markAsSeen(.newFancyOnboarding) + loginButton.showAnimatedPress { [weak self] in + let signUpViewController = SignUpViewController(mode: .login) + self?.navigationController?.pushViewController(signUpViewController, animated: true) + } + } + + // MARK: - Private Methods + + private func setupProgressViews() { + progressBarStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } + progressBarStackView.layoutIfNeeded() + + for _ in pages { + let progressView = ProgressView() + progressBarStackView.addArrangedSubview(progressView) + progressViews.append(WeakObject(progressView)) + + progressView.heightAnchor.constraint(equalToConstant: isPad ? 6 : 4).isActive = true + } + } + + private func setupTexts() { + signUpButton.setTitle(.localized("onboarding_sign_up"), for: .normal) + loginButton.setTitle(.localized("onboarding_login"), for: .normal) + + // https://stackoverflow.com/questions/46200027/uilabel-wrong-word-wrap-in-ios-11/ + // To prevent UIKit from moving the penultimate word to the line with the last orphaned word. + titleLabel.lineBreakStrategy = [] + } + + private func setupButtons() { + [signUpButton, loginButton].forEach { + $0?.dropShadow() + $0?.corners = .continuous(16) + $0?.titleLabel?.adjustsFontSizeToFitWidth = true + $0?.contentEdgeInsets = .init(top: 0, left: 12, bottom: 0, right: 12) + $0?.clipsToBounds = true + } + } + + private func configureContentSizingForIdiom() { + if UIDevice.current.userInterfaceIdiom == .pad { + // Make video centered on iPad + videoContentVerticalCenterConstraint.priority = .required + + // Lower priority of side constraints, so the =< width constraint in XIB is prioritized. + // This gives the imageView on iPad [limited] space to grow. + // As a result, the image doesn't look overly big on large iPads. + imageContentLeadingConstraint.priority = .defaultHigh + imageContentTrailingConstraint.priority = .defaultHigh + } else { + videoContentVerticalCenterConstraint.priority = .defaultHigh + imageContentLeadingConstraint.priority = .required + imageContentTrailingConstraint.priority = .required + } + } + + private func updatePage() { + UIView.transition(with: infoLabelStackView, + duration: configuration.contentAnimationDuration, + options: .transitionCrossDissolve) { [weak self] in + guard let self = self else { return } + self.titleLabel.text = self.currentPage.title + self.descriptionLabel.text = self.currentPage.description + } + + updateGradient() + updateContent() + } + + private func updateContent() { + imageView.prepareForReuse() + switch currentPage.content { + case .image(let title, _, let presentationType): + switchContentVisibility(isVideoContent: false) + showImage(title: title, animation: presentationType.animation) + case .video(let videoTitle): + switchContentVisibility(isVideoContent: true) + showVideo(title: videoTitle) + } + view.setNeedsLayout() + + startTimer() + } + + private func switchContentVisibility(isVideoContent: Bool) { + imageView.isHidden = isVideoContent + videoContentView.isHidden = !isVideoContent + videoContentBorderView.isHidden = !isVideoContent + } + + private func showImage(title: String, animation: ImageViewAnimationCompletion) { + imageView.image = UIImage(named: title) ?? UIImage() + imageView.prepareForReuse() + DispatchQueue.main.async { + animation?(self.imageView) + } + } + + private func showVideo(title: String) { + guard let videoPath = Bundle.main.path(forResource: title, ofType: "mp4") else { return } + + avPlayerLayer?.removeFromSuperlayer() + avPlayerLayer = nil + + videoContentView.corners = .continuous(12) + videoContentBorderView.corners = .continuous(12) + + let videoUrl = URL(fileURLWithPath: videoPath) + let avPlayer = AVPlayer(url: videoUrl) + let avPlayerLayer = AVPlayerLayer(player: avPlayer) + avPlayerLayer.videoGravity = .resizeAspectFill + avPlayerLayer.corners = .continuous(12) + self.avPlayerLayer = avPlayerLayer + self.avPlayer = avPlayer + + videoContentView.layer.addSublayer(avPlayerLayer) + avPlayer.play() + } + + @objc private func updateProgressView() { + let currentProgressView = progressViews[currentPageIndex] + currentTimerDuration += Float(configuration.timerInterval) + currentProgressView.object?.progress = currentProgressPercentage + + if currentProgressPercentage >= 1.0 { + stopTimer() + switchToNextPage() + } + } + + private func startTimer() { + progressBarTimer = Timer.scheduledTimer(timeInterval: configuration.timerInterval, + target: self, + selector: #selector(updateProgressView), + userInfo: nil, + repeats: true) + } + + private func stopTimer(keepProgress: Bool = false) { + if !keepProgress { + currentTimerDuration = 0 + } + progressBarTimer?.invalidate() + progressBarTimer = nil + } + + @objc private func continuePageProgress() { + guard progressBarTimer == nil else { return } + // Removing observer of returning to app since it makes no sense when we are in the app. + NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) + + DispatchQueue.main.async { + if case .image = self.currentPage.content { + // If it's an image, we have a complete restart of content displaying. + self.updateContent() + } else { + // If it is a video, we continue playing it from where we stopped + // and restart the timer for progress view updating. + self.avPlayer?.play() + self.startTimer() + } + } + } + + @objc private func freezePageProgress() { + // Adding observer only here so continuePageProgress isn't called additionally in the beginning. + // This observer will be added upon leaving the app and removed upon returning. + NotificationCenter.default.addObserver(self, + selector: #selector(continuePageProgress), + name: UIApplication.didBecomeActiveNotification, + object: nil) + + // Freezing progress depending on the type of content. + DispatchQueue.main.async { + if case .image = self.currentPage.content { + // If it is an image, we will have to completely restart animation upon returning. + // For smoothness, we hide the image before leaving the app. + self.stopTimer() + self.imageView.isHidden = true + } else { + // If it is a video, we will just continue playing it upon returning, hence we pause and keep progress. + self.stopTimer(keepProgress: true) + self.avPlayer?.pause() + } + } + } +} + +// MARK: - Gesture Recognizers +extension OnboardingViewController { + + private func setupSideTapGestures() { + let leftTapGesture = UITapGestureRecognizer(target: self, action: #selector(switchToPreviousPage)) + leftTapGestureView.addGestureRecognizer(leftTapGesture) + + let rightTapGesture = UITapGestureRecognizer(target: self, action: #selector(switchToNextPage)) + rightTapGestureView.addGestureRecognizer(rightTapGesture) + + let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress)) + longPressGesture.minimumPressDuration = 0.3 + view.addGestureRecognizer(longPressGesture) + + view.bringSubviewToFront(leftTapGestureView) + view.bringSubviewToFront(rightTapGestureView) + } + + @objc private func switchToPreviousPage() { + progressViews.resetProgress(at: currentPageIndex) + currentPageIndex -= 1 + progressViews.resetProgress(at: currentPageIndex) + + stopTimer() + } + + @objc private func switchToNextPage() { + currentTimerDuration = 0 + progressBarTimer?.invalidate() + + if currentPageIndex == pages.count - 1 { + imageView.layer.removeAllAnimations() + progressViews.resetProgresses() + currentPageIndex = 0 + } else { + UIView.performWithoutAnimation { + self.progressViews[currentPageIndex].object?.progress = 1 + self.progressViews[currentPageIndex].object?.layoutIfNeeded() + } + currentPageIndex += 1 + } + } + + @objc private func handleLongPress(gestureReconizer: UILongPressGestureRecognizer) { + if gestureReconizer.state == .ended { + resumeContentAfterLongPress() + fadeSecondaryElements(visible: true) + } else if gestureReconizer.state != .changed { + pauseContentOnLongPress() + fadeSecondaryElements(visible: false) + } + } + + private func pauseContentOnLongPress() { + DispatchQueue.main.async { + self.stopTimer(keepProgress: true) + + if case .image = self.currentPage.content { + self.imageView.layer.pause() + } else { + self.avPlayer?.pause() + } + } + } + + private func resumeContentAfterLongPress() { + DispatchQueue.main.async { + self.startTimer() + + if case .image = self.currentPage.content { + self.imageView.layer.resume() + } else { + self.avPlayer?.play() + } + } + } + + private func fadeSecondaryElements(visible: Bool) { + UIView.animate(withDuration: 0.2) { + [self.progressBarStackView, + self.signUpButton, + self.loginButton + ].forEach { + $0?.alpha = visible ? 1 : 0 + } + } + } +} + +// MARK: - Gradient Update +extension OnboardingViewController { + private func updateGradient() { + if gradientLayer == nil { + gradientLayer = view.applyGradient(currentPage.gradient) + } + + animateGradientToNextColor() + } + + private func animateGradientToNextColor() { + let fromColors = gradientLayer?.colors + let toColors: [CGColor] = currentPage.gradient.colors + gradientLayer?.colors = currentPage.gradient.colors + + let animation: CABasicAnimation = CABasicAnimation(keyPath: "colors") + + animation.fromValue = fromColors + animation.toValue = toColors + animation.duration = configuration.contentAnimationDuration + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + gradientLayer?.add(animation, forKey:"animateGradient") + } +} + +private extension Array where Element == WeakObject { + func resetProgresses() { + forEach { $0.object?.progress = 0 } + } + + func resetProgress(at index: Int) { + let progressBar = self[index] + UIView.performWithoutAnimation { + progressBar.object?.progress = 0 + progressBar.object?.layoutIfNeeded() + } + } +} diff --git a/LockdowniOS/OnboardingViewController.xib b/LockdowniOS/OnboardingViewController.xib new file mode 100644 index 0000000..15c58e2 --- /dev/null +++ b/LockdowniOS/OnboardingViewController.xib @@ -0,0 +1,240 @@ + + + + + + + + + + + + + Montserrat-Bold + + + Montserrat-Regular + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/PaywallDisplayConfiguration.swift b/LockdowniOS/PaywallDisplayConfiguration.swift new file mode 100644 index 0000000..dde9d7a --- /dev/null +++ b/LockdowniOS/PaywallDisplayConfiguration.swift @@ -0,0 +1,38 @@ +// +// PaywallDisplayConfiguration.swift +// LockdowniOS +// +// Created by Alexander Parshakov on 12/13/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +struct PaywallDisplayConfiguration { + + /// For how long it's necessary to wait to show the paywall again. + let lapseDuration: Int + + /// In what units the time is calculated. + let lapseUnit: Calendar.Component + + static let `default`: Self = { + #if DEBUG + return .init(lapseDuration: 20, lapseUnit: .second) + #else + return .init(lapseDuration: 72, lapseUnit: .hour) + #endif + }() +} + +extension Calendar { + static func hasExceededLapse(between firstDate: Date, and secondDate: Date, for configuration: PaywallDisplayConfiguration = .default) -> Bool { + let components = Calendar.current.dateComponents([configuration.lapseUnit], + from: firstDate, + to: secondDate) + + guard let lapse = components.value(for: configuration.lapseUnit) else { return false } + + return abs(lapse) > configuration.lapseDuration + } +} diff --git a/LockdowniOS/PaywallService.swift b/LockdowniOS/PaywallService.swift new file mode 100644 index 0000000..d7ff237 --- /dev/null +++ b/LockdowniOS/PaywallService.swift @@ -0,0 +1,115 @@ +// +// PaywallService.swift +// LockdowniOS +// +// Created by Alexander Parshakov on 12/16/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import StoreKit +import UIKit + +protocol PaywallService: AnyObject { + var context: PaywallContext { get set } + + func showPaywall(on vc: UIViewController, forceSpecialOffer: Bool) + + func showRedemptionSheet() +} + +final class BasePaywallService: PaywallService { + + var context: PaywallContext = .normal + + var countdownDisplayService: CountdownDisplayService = BaseCountdownDisplayService.shared + + static let shared = BasePaywallService() + + private init() {} + + func showPaywall(on vc: UIViewController, forceSpecialOffer: Bool = false) { + defer { + UserDefaults.lastPaywallDisplayDate = Date() + } + + // We show special offer only if all these conditions work: + // 1. We closed the main paywall just now OR want to force-show the special offer, + // 2. Today's date is within special offer period by optionally initializing it. + if context == .followUpLimitedTimeOffer || forceSpecialOffer, let specialOffer = LimitedTimeOffer() { + DDLogInfo("Preparing to show special offer named: \(specialOffer)") + switch specialOffer { + case .xmas: + let christmasPaywallViewController = ChristmasPaywallViewController(countdownDisplayService: countdownDisplayService) + if let delegate = vc as? LTOViewControllerCloseDelegate { + christmasPaywallViewController.delegate = delegate + } + christmasPaywallViewController.modalPresentationStyle = .overFullScreen + vc.present(christmasPaywallViewController, animated: true) + DDLogInfo("Showing LTO screen from: \(String(describing: type(of: vc)))") + default: + break + } + countdownDisplayService.delegates.append(WeakObject(self)) + } else { + let paywall = TablePaywallViewController() + paywall.modalPresentationStyle = vc.isPad ? .pageSheet : .overFullScreen + + if let delegate = vc as? PaywallViewControllerCloseDelegate { + paywall.delegate = delegate + } + + vc.definesPresentationContext = true + vc.present(paywall, animated: true) { + vc.definesPresentationContext = false + } + } + } + + func showRedemptionSheet() { + guard #available(iOS 14.0, *) else { return } + SKPaymentQueue.default().presentCodeRedemptionSheet() + context = .redeemOfferCode + DDLogInfo("Showing redemption sheet, changing context to \(context)") + } +} + +extension BasePaywallService: CountdownDisplayDelegate { + func didFinishCountdown() { + countdownDisplayService.pauseUpdating() + context = .normal + } +} + +extension BasePaywallService: Keychainable {} + +enum PaywallContext { + /// The context that shows a normal paywall without LTO discounts. + case normal + + /// The context that shows an LTO paywall with a holiday discount + /// Only after an ordinary paywall has been closed. + case followUpLimitedTimeOffer + + /// When user is in the process of code redemption after calling presentCodeRedemptionSheet() method. + case redeemOfferCode +} + +enum LimitedTimeOffer { + case halloween, thanksgiving, xmas + + init?(for date: Date = Date()) { + let currentDate = Date.from(year: Calendar.current.component(.year, from: Date()), + month: Calendar.current.component(.month, from: Date()), + day: Calendar.current.component(.day, from: Date())) + if (.xmasStart ... .xmasEnd).contains(currentDate) { + self = .xmas + } else if (.halloweenStart ... .halloweenEnd).contains(currentDate) { + self = .halloween + } else if (.thanksgivingStart ... .thanksgivingEnd).contains(currentDate) { + self = .thanksgiving + } else { + return nil + } + } +} diff --git a/LockdowniOS/PaywallTableFeature.swift b/LockdowniOS/PaywallTableFeature.swift new file mode 100644 index 0000000..be08125 --- /dev/null +++ b/LockdowniOS/PaywallTableFeature.swift @@ -0,0 +1,90 @@ +// +// PaywallTableFeature.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/3/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation + +/// Used for features in the table of TablePaywallViewController +struct PaywallTableFeature { + + let body: String + + let isAvailableForFree: Bool + + let isAvailableOnVPN: Bool + + /// All features are available on PRO, so the default value is true. + let isAvailableOnPro: Bool + + init(body: String, isAvailableForFree: Bool, isAvailableOnVPN: Bool, isAvailableOnPro: Bool = true) { + self.body = body + self.isAvailableForFree = isAvailableForFree + self.isAvailableOnVPN = isAvailableOnVPN + self.isAvailableOnPro = isAvailableOnPro + } + + // MARK: - Features + + static let stopsAdsAndTrackers: Self = { + .init(body: .localized("stops_ads_and_hidden_trackers"), isAvailableForFree: true, isAvailableOnVPN: true) + }() + + static let blocksBadware: Self = { + .init(body: .localized("blocks_badware_in_all_your_apps"), isAvailableForFree: true, isAvailableOnVPN: true) + }() + + static let accessCuratedBlockLists: Self = { + .init(body: .localized("access_our_curated_block_lists"), isAvailableForFree: true, isAvailableOnVPN: true) + }() + + static let buildCustomBlockLists: Self = { + .init(body: .localized("build_your_custom_block_lists"), isAvailableForFree: true, isAvailableOnVPN: true) + }() + + static let stopsBrowsingHistoryTracking: Self = { + .init(body: .localized("stops_browsing_history_tracking"), isAvailableForFree: false, isAvailableOnVPN: true) + }() + + static let anonymizingBrowsing: Self = { + .init(body: .localized("anonymizing_your_browsing"), isAvailableForFree: false, isAvailableOnVPN: true) + }() + + static let hidesLocation: Self = { + .init(body: .localized("hides_your_location_and_ip"), isAvailableForFree: false, isAvailableOnVPN: true) + }() + + static let letsYouChangeIP: Self = { + .init(body: .localized("lets_you_change_your_ip_to_other_region"), isAvailableForFree: false, isAvailableOnVPN: true) + }() + + static let appForPhoneAndIpad: Self = { + .init(body: .localized("app_for_iphone_plus_ipad"), isAvailableForFree: false, isAvailableOnVPN: true) + }() + + static let appForMac: Self = { + .init(body: .localized("app_for_mac"), isAvailableForFree: false, isAvailableOnVPN: false) + }() +} + +extension PaywallTableFeature: Equatable {} + +extension Array where Element == PaywallTableFeature { + static let allDefaultFeatures: Self = { + return [ + .stopsAdsAndTrackers, + .blocksBadware, + .accessCuratedBlockLists, + .buildCustomBlockLists, + .stopsBrowsingHistoryTracking, + .anonymizingBrowsing, + .hidesLocation, + .letsYouChangeIP, + .appForPhoneAndIpad, + .appForMac + ] + }() +} diff --git a/LockdowniOS/PrivacyPolicyViewController.swift b/LockdowniOS/PrivacyPolicyViewController.swift index ee6ce3e..7edcfac 100644 --- a/LockdowniOS/PrivacyPolicyViewController.swift +++ b/LockdowniOS/PrivacyPolicyViewController.swift @@ -22,7 +22,7 @@ class PrivacyPolicyViewController: BaseViewController { @IBOutlet weak var privacyPolicyWrap: UIView! @IBOutlet weak var whyTrustButton: UIButton! - var parentVC: HomeViewController? = nil + var parentVC: HomeViewController? var privacyPolicyKey = kHasAgreedToFirewallPrivacyPolicy override func viewDidLoad() { @@ -32,12 +32,11 @@ class PrivacyPolicyViewController: BaseViewController { @IBAction func getStartedTapped(_ sender: Any) { defaults.set(true, forKey: privacyPolicyKey) - if (privacyPolicyKey == kHasAgreedToFirewallPrivacyPolicy) { + if privacyPolicyKey == kHasAgreedToFirewallPrivacyPolicy { self.dismiss(animated: true, completion: { self.parentVC?.toggleFirewall(self) }) - } - else { + } else { self.dismiss(animated: true, completion: { self.parentVC?.toggleVPN(self) }) diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/Contents.json new file mode 100644 index 0000000..3ae77e6 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ads.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ads-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ads-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads-1.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads-1.png new file mode 100644 index 0000000..0d9c199 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads-2.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads-2.png new file mode 100644 index 0000000..0d9c199 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads.png new file mode 100644 index 0000000..0d9c199 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ads_icon.imageset/ads.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/Contents.json new file mode 100644 index 0000000..01e1bfa --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pngegg.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pngegg-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pngegg-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-1.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-1.png new file mode 100644 index 0000000..420ad0e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-2.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-2.png new file mode 100644 index 0000000..420ad0e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg.png new file mode 100644 index 0000000..420ad0e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/clickbait_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/clickbait_icon.imageset/Contents.json new file mode 100644 index 0000000..628377c --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/clickbait_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "upside-down.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/clickbait_icon.imageset/upside-down.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/clickbait_icon.imageset/upside-down.png new file mode 100644 index 0000000..5c9fdb7 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/clickbait_icon.imageset/upside-down.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/crypto_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/crypto_icon.imageset/Contents.json new file mode 100644 index 0000000..6cd5098 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/crypto_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bitcoin_icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/crypto_icon.imageset/bitcoin_icon.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/crypto_icon.imageset/bitcoin_icon.png new file mode 100644 index 0000000..7665f3e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/crypto_icon.imageset/bitcoin_icon.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/email_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/email_icon.imageset/Contents.json new file mode 100644 index 0000000..dfa6a66 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/email_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "email.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/email_icon.imageset/email.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/email_icon.imageset/email.png new file mode 100644 index 0000000..a655d4f Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/email_icon.imageset/email.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_icon.imageset/Contents.json new file mode 100644 index 0000000..248447a --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "facebook.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_icon.imageset/facebook.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_icon.imageset/facebook.png new file mode 100644 index 0000000..998a033 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_icon.imageset/facebook.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Contents.json new file mode 100644 index 0000000..32f8535 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "facebook_white_icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_white_icon.imageset/facebook_white_icon.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_white_icon.imageset/facebook_white_icon.png new file mode 100644 index 0000000..16262e0 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/facebook_white_icon.imageset/facebook_white_icon.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/Contents.json new file mode 100644 index 0000000..7c88ab0 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "game_ads.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "game_ads_icon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "game_ads_icon-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads.png new file mode 100644 index 0000000..b8ae567 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-1.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-1.png new file mode 100644 index 0000000..b8ae567 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-2.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-2.png new file mode 100644 index 0000000..b8ae567 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/Contents.json new file mode 100644 index 0000000..afbe6f7 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "google-1.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "google-2.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "google.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google-1.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google-1.png new file mode 100644 index 0000000..35e7062 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google-2.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google-2.png new file mode 100644 index 0000000..35e7062 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google.png new file mode 100644 index 0000000..35e7062 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/google_icon.imageset/google.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/marketing_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/marketing_icon.imageset/Contents.json new file mode 100644 index 0000000..30e09a5 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/marketing_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "marketing.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/marketing_icon.imageset/marketing.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/marketing_icon.imageset/marketing.png new file mode 100644 index 0000000..f065691 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/marketing_icon.imageset/marketing.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/ransomware_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ransomware_icon.imageset/Contents.json new file mode 100644 index 0000000..8072c85 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ransomware_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ransomware.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/ransomware_icon.imageset/ransomware.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ransomware_icon.imageset/ransomware.png new file mode 100644 index 0000000..410d304 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/ransomware_icon.imageset/ransomware.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/Contents.json new file mode 100644 index 0000000..803383c --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "report.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "report-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "report-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report-1.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report-1.png new file mode 100644 index 0000000..1a2deff Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report-2.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report-2.png new file mode 100644 index 0000000..1a2deff Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report.png new file mode 100644 index 0000000..1a2deff Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/reporting_icon.imageset/report.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/Contents.json new file mode 100644 index 0000000..0a10d3c --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ghost_1f47b.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ghost_1f47b-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ghost_1f47b-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-1.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-1.png new file mode 100644 index 0000000..c936ed8 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-2.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-2.png new file mode 100644 index 0000000..c936ed8 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b.png new file mode 100644 index 0000000..c936ed8 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/Contents.json new file mode 100644 index 0000000..36425c7 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "user-data-icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "user-data-icon-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "user-data-icon-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-1.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-1.png new file mode 100644 index 0000000..6e82dc6 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-2.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-2.png new file mode 100644 index 0000000..6e82dc6 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon.png b/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon.png new file mode 100644 index 0000000..6e82dc6 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/ComparisonTableXmark.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/ComparisonTableXmark.imageset/Contents.json new file mode 100644 index 0000000..c234ec3 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/ComparisonTableXmark.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "close.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/ComparisonTableXmark.imageset/close.pdf b/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/ComparisonTableXmark.imageset/close.pdf new file mode 100644 index 0000000..c4b8ac9 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/ComparisonTableXmark.imageset/close.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Complex Paywall Table Images/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Confirmed Blue.colorset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Confirmed Blue.colorset/Contents.json new file mode 100644 index 0000000..82be4f6 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Confirmed Blue.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE7", + "green" : "0xAC", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/DivideLine.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/DivideLine.imageset/Contents.json new file mode 100644 index 0000000..170ead1 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/DivideLine.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DivideLine.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/DivideLine.imageset/DivideLine.pdf b/LockdowniOS/Resources/Assets.xcassets/DivideLine.imageset/DivideLine.pdf new file mode 100644 index 0000000..9ba1f85 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/DivideLine.imageset/DivideLine.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/Contents.json new file mode 100644 index 0000000..4e536a9 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "notification-icon@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "notification-icon@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-small@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "icon-small@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "icon-40@3x.png", + "scale" : "3x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "icon.png", + "scale" : "1x" + }, + { + "size" : "57x57", + "idiom" : "iphone", + "filename" : "icon@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "notification-icon~ipad.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "notification-icon~ipad@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-small.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "icon-small@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "icon-small-50.png", + "scale" : "1x" + }, + { + "size" : "50x50", + "idiom" : "ipad", + "filename" : "icon-small-50@2x.png", + "scale" : "2x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "icon-72.png", + "scale" : "1x" + }, + { + "size" : "72x72", + "idiom" : "ipad", + "filename" : "icon-72@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "icon-76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "icon-83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "ios-marketing.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40.png new file mode 100644 index 0000000..21ee194 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40@2x.png new file mode 100644 index 0000000..85672f2 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40@3x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40@3x.png new file mode 100644 index 0000000..5612228 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-40@3x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-60@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-60@2x.png new file mode 100644 index 0000000..5612228 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-60@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-60@3x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-60@3x.png new file mode 100644 index 0000000..f7f04ce Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-60@3x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-72.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-72.png new file mode 100644 index 0000000..86c1499 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-72.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-72@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-72@2x.png new file mode 100644 index 0000000..1491580 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-72@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-76.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-76.png new file mode 100644 index 0000000..dabe5f3 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-76.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-76@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-76@2x.png new file mode 100644 index 0000000..e30497b Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-76@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-83.5@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-83.5@2x.png new file mode 100644 index 0000000..ac5dac6 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-83.5@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small-50.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small-50.png new file mode 100644 index 0000000..c38d67d Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small-50.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small-50@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small-50@2x.png new file mode 100644 index 0000000..8629ef4 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small-50@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small.png new file mode 100644 index 0000000..4945e8d Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small@2x.png new file mode 100644 index 0000000..1b1fd09 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small@3x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small@3x.png new file mode 100644 index 0000000..2a496e6 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon-small@3x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon.png new file mode 100644 index 0000000..65664e2 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon@2x.png new file mode 100644 index 0000000..309eb66 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/icon@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/ios-marketing.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/ios-marketing.png new file mode 100644 index 0000000..382af77 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/ios-marketing.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon@2x.png new file mode 100644 index 0000000..21ee194 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon@3x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon@3x.png new file mode 100644 index 0000000..3f0fc86 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon@3x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon~ipad.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon~ipad.png new file mode 100644 index 0000000..df89249 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon~ipad.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon~ipad@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon~ipad@2x.png new file mode 100644 index 0000000..21ee194 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/AppIcon-B.appiconset/notification-icon~ipad@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/Contents.json new file mode 100644 index 0000000..43fd22e --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "newicon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newicon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "newicon-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon-1.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon-1.png new file mode 100644 index 0000000..02f1664 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon-2.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon-2.png new file mode 100644 index 0000000..02f1664 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon.png new file mode 100644 index 0000000..02f1664 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/ios-marketing-B.imageset/newicon.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/Contents.json new file mode 100644 index 0000000..0f70272 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "launchscreen.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "launchscreen-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "launchscreen-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen-1.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen-1.png new file mode 100644 index 0000000..e3261d4 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen-2.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen-2.png new file mode 100644 index 0000000..e3261d4 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen.png new file mode 100644 index 0000000..e3261d4 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/launchscreen.imageset/launchscreen.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/Contents.json new file mode 100644 index 0000000..43fd22e --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "newicon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "newicon-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "newicon-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon-1.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon-1.png new file mode 100644 index 0000000..02f1664 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon-2.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon-2.png new file mode 100644 index 0000000..02f1664 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon.png b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon.png new file mode 100644 index 0000000..02f1664 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icon-Alt/lockdown_icon_appstore-B.imageset/newicon.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..28d9cc7 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/Contents.json @@ -0,0 +1,158 @@ +{ + "images" : [ + { + "filename" : "notification-icon@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "notification-icon@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "icon-small.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "icon-small@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-small@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "icon.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "icon@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "icon-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "icon-60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "notification-icon~ipad.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "notification-icon~ipad@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "icon-small.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "icon-small@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "icon-40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "icon-40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "icon-small-50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "icon-small-50@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "icon-72.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "72x72" + }, + { + "filename" : "icon-72@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "72x72" + }, + { + "filename" : "icon-76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "icon-76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "icon-83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "ios-marketing.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40.png new file mode 100644 index 0000000..df37be7 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 0000000..e32b637 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40@3x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 0000000..cc0ab7e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-40@3x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-60@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 0000000..cc0ab7e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-60@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-60@3x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 0000000..805fcc8 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-60@3x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-72.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-72.png new file mode 100644 index 0000000..b4c5244 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-72.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-72@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-72@2x.png new file mode 100644 index 0000000..ef55962 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-72@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-76.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-76.png new file mode 100644 index 0000000..beb0e66 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-76.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-76@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 0000000..e20ae82 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-76@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-83.5@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 0000000..a829af4 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small-50.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small-50.png new file mode 100644 index 0000000..766512b Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small-50.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small-50@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small-50@2x.png new file mode 100644 index 0000000..9b5f1b4 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small-50@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small.png new file mode 100644 index 0000000..a234b36 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small@2x.png new file mode 100644 index 0000000..c26bed0 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small@3x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small@3x.png new file mode 100644 index 0000000..173686b Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon-small@3x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon.png new file mode 100644 index 0000000..91e2ffe Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon@2x.png new file mode 100644 index 0000000..4eb75dd Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/icon@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/ios-marketing.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/ios-marketing.png new file mode 100644 index 0000000..8a0250e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/ios-marketing.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon@2x.png new file mode 100644 index 0000000..df37be7 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon@3x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon@3x.png new file mode 100644 index 0000000..d77b643 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon@3x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon~ipad.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon~ipad.png new file mode 100644 index 0000000..be9e6b9 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon~ipad.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon~ipad@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon~ipad@2x.png new file mode 100644 index 0000000..df37be7 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/AppIcon.appiconset/notification-icon~ipad@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/BlueLock.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/BlueLock.imageset/Contents.json new file mode 100644 index 0000000..c0e7b39 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/BlueLock.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/BlueLock.imageset/Lock.pdf b/LockdowniOS/Resources/Assets.xcassets/Icons/BlueLock.imageset/Lock.pdf new file mode 100644 index 0000000..87608e3 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/BlueLock.imageset/Lock.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/Checkmark.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/Checkmark.imageset/Contents.json new file mode 100644 index 0000000..215a875 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/Checkmark.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "check.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/Checkmark.imageset/check.pdf b/LockdowniOS/Resources/Assets.xcassets/Icons/Checkmark.imageset/check.pdf new file mode 100644 index 0000000..95af0b7 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/Checkmark.imageset/check.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/GreenShield.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/GreenShield.imageset/Contents.json new file mode 100644 index 0000000..379b270 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/GreenShield.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "shield.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/GreenShield.imageset/shield.pdf b/LockdowniOS/Resources/Assets.xcassets/Icons/GreenShield.imageset/shield.pdf new file mode 100644 index 0000000..ace67ad Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/GreenShield.imageset/shield.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/SplashScreenLogo.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/SplashScreenLogo.imageset/Contents.json new file mode 100644 index 0000000..c0e7b39 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/SplashScreenLogo.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/SplashScreenLogo.imageset/Lock.pdf b/LockdowniOS/Resources/Assets.xcassets/Icons/SplashScreenLogo.imageset/Lock.pdf new file mode 100644 index 0000000..cf8f3fd Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/SplashScreenLogo.imageset/Lock.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/Xmark.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/Xmark.imageset/Contents.json new file mode 100644 index 0000000..51c4b1e --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/Xmark.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Xmark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/Xmark.imageset/Xmark.pdf b/LockdowniOS/Resources/Assets.xcassets/Icons/Xmark.imageset/Xmark.pdf new file mode 100644 index 0000000..aef3e0f Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/Xmark.imageset/Xmark.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/Contents.json new file mode 100644 index 0000000..6d39844 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "icon_512x512@2x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_512x512@2x-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_512x512@2x-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x-1.png b/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x-1.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x-2.png b/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x-2.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/ios-marketing.imageset/icon_512x512@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/Contents.json new file mode 100644 index 0000000..6d39844 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "icon_512x512@2x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "icon_512x512@2x-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icon_512x512@2x-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x-1.png b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x-1.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x-2.png b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x-2.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x.png b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon.imageset/icon_512x512@2x.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/Contents.json new file mode 100644 index 0000000..e43a222 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "ios-marketing.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ios-marketing-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ios-marketing-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing-1.png b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing-1.png new file mode 100644 index 0000000..8a0250e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing-2.png b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing-2.png new file mode 100644 index 0000000..8a0250e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing.png b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing.png new file mode 100644 index 0000000..8a0250e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Icons/lockdown_icon_appstore.imageset/ios-marketing.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/LTO/ArrowRight.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/LTO/ArrowRight.imageset/Contents.json new file mode 100644 index 0000000..d303da0 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/LTO/ArrowRight.imageset/Contents.json @@ -0,0 +1,17 @@ +{ + "images" : [ + { + "filename" : "arrow-right.pdf", + "idiom" : "universal", + "language-direction" : "left-to-right" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/LTO/ArrowRight.imageset/arrow-right.pdf b/LockdowniOS/Resources/Assets.xcassets/LTO/ArrowRight.imageset/arrow-right.pdf new file mode 100644 index 0000000..c96f151 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/LTO/ArrowRight.imageset/arrow-right.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/LTO/ChristmasLTOBackground.imageset/BG.pdf b/LockdowniOS/Resources/Assets.xcassets/LTO/ChristmasLTOBackground.imageset/BG.pdf new file mode 100644 index 0000000..1e23d87 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/LTO/ChristmasLTOBackground.imageset/BG.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/LTO/ChristmasLTOBackground.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/LTO/ChristmasLTOBackground.imageset/Contents.json new file mode 100644 index 0000000..4d8b457 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/LTO/ChristmasLTOBackground.imageset/Contents.json @@ -0,0 +1,17 @@ +{ + "images" : [ + { + "filename" : "BG.pdf", + "idiom" : "universal", + "language-direction" : "left-to-right" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/LTO/Contents.json b/LockdowniOS/Resources/Assets.xcassets/LTO/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/LTO/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Onboarding/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_apple_logo.imageset/Apple Logo@3x.pdf b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_apple_logo.imageset/Apple Logo@3x.pdf new file mode 100644 index 0000000..c9dde0d Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_apple_logo.imageset/Apple Logo@3x.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_apple_logo.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_apple_logo.imageset/Contents.json new file mode 100644 index 0000000..881f704 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_apple_logo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Apple Logo@3x.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_blocked_tweet.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_blocked_tweet.imageset/Contents.json new file mode 100644 index 0000000..839c2a8 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_blocked_tweet.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Video-3.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_blocked_tweet.imageset/Video-3.pdf b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_blocked_tweet.imageset/Video-3.pdf new file mode 100644 index 0000000..98f9ba0 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_blocked_tweet.imageset/Video-3.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_tweets.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_tweets.imageset/Contents.json new file mode 100644 index 0000000..96884ec --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_tweets.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Onboarding_tweets.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_tweets.imageset/Onboarding_tweets.pdf b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_tweets.imageset/Onboarding_tweets.pdf new file mode 100644 index 0000000..dd06588 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/Onboarding/Onboarding_tweets.imageset/Onboarding_tweets.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/Panel Background.colorset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Panel Background.colorset/Contents.json new file mode 100644 index 0000000..dc38378 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Panel Background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "colors" : [ + { + "idiom" : "universal", + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "1.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000" + } + } + }, + { + "idiom" : "universal", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0x1C", + "alpha" : "1.000", + "blue" : "0x1E", + "green" : "0x1C" + } + } + } + ] +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/Panel Secondary Background.colorset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Panel Secondary Background.colorset/Contents.json new file mode 100644 index 0000000..ec3e8d4 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Panel Secondary Background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.196", + "green" : "0.183", + "red" : "0.182" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/Power Button Shadow Color.colorset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/Power Button Shadow Color.colorset/Contents.json new file mode 100644 index 0000000..742124e --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/Power Button Shadow Color.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.250", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/SignUp/Contents.json b/LockdowniOS/Resources/Assets.xcassets/SignUp/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/SignUp/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/SignUp/SignUpKey.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/SignUp/SignUpKey.imageset/Contents.json new file mode 100644 index 0000000..6990dc3 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/SignUp/SignUpKey.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Frame 367.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/SignUp/SignUpKey.imageset/Frame 367.png b/LockdowniOS/Resources/Assets.xcassets/SignUp/SignUpKey.imageset/Frame 367.png new file mode 100644 index 0000000..6dc9c77 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/SignUp/SignUpKey.imageset/Frame 367.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/SignUp/WhiteCheckmarkOnDark.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/SignUp/WhiteCheckmarkOnDark.imageset/Contents.json new file mode 100644 index 0000000..51c67b5 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/SignUp/WhiteCheckmarkOnDark.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "Frame 306.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/SignUp/WhiteCheckmarkOnDark.imageset/Frame 306.pdf b/LockdowniOS/Resources/Assets.xcassets/SignUp/WhiteCheckmarkOnDark.imageset/Frame 306.pdf new file mode 100644 index 0000000..be159f8 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/SignUp/WhiteCheckmarkOnDark.imageset/Frame 306.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/WhiteLock.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/WhiteLock.imageset/Contents.json new file mode 100644 index 0000000..a065ff3 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/WhiteLock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "WhiteLock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/WhiteLock.imageset/WhiteLock.pdf b/LockdowniOS/Resources/Assets.xcassets/WhiteLock.imageset/WhiteLock.pdf new file mode 100644 index 0000000..aa88745 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/WhiteLock.imageset/WhiteLock.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/Contents.json new file mode 100644 index 0000000..76ff159 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "bluecircle.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "bluecircle-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "bluecircle-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle-1.png b/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle-1.png new file mode 100644 index 0000000..27ee167 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle-2.png b/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle-2.png new file mode 100644 index 0000000..27ee167 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle.png b/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle.png new file mode 100644 index 0000000..27ee167 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/blue_circle.imageset/bluecircle.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/checkmark.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/checkmark.imageset/Contents.json new file mode 100644 index 0000000..e20b6aa --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/checkmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "check.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/checkmark.imageset/check.pdf b/LockdowniOS/Resources/Assets.xcassets/checkmark.imageset/check.pdf new file mode 100644 index 0000000..1bf4335 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/checkmark.imageset/check.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/globe.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/globe.imageset/Contents.json new file mode 100644 index 0000000..053d53f --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/globe.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "earth-globe-americas_1f30e.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "earth-globe-americas_1f30e-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "earth-globe-americas_1f30e-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-1.png b/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-1.png new file mode 100644 index 0000000..51b5b4f Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-2.png b/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-2.png new file mode 100644 index 0000000..51b5b4f Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e.png b/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e.png new file mode 100644 index 0000000..51b5b4f Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/Contents.json new file mode 100644 index 0000000..812609e --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "IMG_0005.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "IMG_0004.png", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "IMG_0005-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "IMG_0004-1.png", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "IMG_0005-2.png", + "scale" : "3x" + }, + { + "idiom" : "universal", + "filename" : "IMG_0004-2.png", + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004-1.png b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004-1.png new file mode 100644 index 0000000..b571c9e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004-2.png b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004-2.png new file mode 100644 index 0000000..b571c9e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004.png b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004.png new file mode 100644 index 0000000..b571c9e Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0004.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005-1.png b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005-1.png new file mode 100644 index 0000000..d29c36b Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005-2.png b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005-2.png new file mode 100644 index 0000000..d29c36b Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005.png b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005.png new file mode 100644 index 0000000..d29c36b Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/notification_example.imageset/IMG_0005.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/power.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/power.imageset/Contents.json new file mode 100644 index 0000000..ad2ec5f --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/power.imageset/Contents.json @@ -0,0 +1,18 @@ +{ + "images" : [ + { + "filename" : "power.pdf", + "idiom" : "universal", + "language-direction" : "left-to-right" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "compression-type" : "lossless", + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdowniOS/Resources/Assets.xcassets/power.imageset/power.pdf b/LockdowniOS/Resources/Assets.xcassets/power.imageset/power.pdf new file mode 100644 index 0000000..6f539f6 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/power.imageset/power.pdf differ diff --git a/LockdowniOS/Resources/Assets.xcassets/power_button_padded.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/power_button_padded.imageset/Contents.json new file mode 100644 index 0000000..e5cbb33 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/power_button_padded.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "power_button_padded.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/power_button_padded.imageset/power_button_padded.png b/LockdowniOS/Resources/Assets.xcassets/power_button_padded.imageset/power_button_padded.png new file mode 100644 index 0000000..f6b9d30 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/power_button_padded.imageset/power_button_padded.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/share.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/share.imageset/Contents.json new file mode 100644 index 0000000..bfbe1c1 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/share.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "share.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/share.imageset/share.png b/LockdowniOS/Resources/Assets.xcassets/share.imageset/share.png new file mode 100644 index 0000000..3f83fa6 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/share.imageset/share.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/website_icon.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/website_icon.imageset/Contents.json new file mode 100644 index 0000000..030c738 --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/website_icon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "website_icon.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/website_icon.imageset/website_icon.png b/LockdowniOS/Resources/Assets.xcassets/website_icon.imageset/website_icon.png new file mode 100644 index 0000000..138a4f2 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/website_icon.imageset/website_icon.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/Contents.json b/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/Contents.json new file mode 100644 index 0000000..c0cc94e --- /dev/null +++ b/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "whyTrustLockdown.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "whyTrustLockdown-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "whyTrustLockdown-2.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-1.png b/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-1.png new file mode 100644 index 0000000..f31ba04 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-1.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-2.png b/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-2.png new file mode 100644 index 0000000..f31ba04 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-2.png differ diff --git a/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown.png b/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown.png new file mode 100644 index 0000000..f31ba04 Binary files /dev/null and b/LockdowniOS/Resources/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown.png differ diff --git a/LockdowniOS/Resources/Base.lproj/LockdownIntents.intentdefinition b/LockdowniOS/Resources/Base.lproj/LockdownIntents.intentdefinition new file mode 100644 index 0000000..cdbe9dd --- /dev/null +++ b/LockdowniOS/Resources/Base.lproj/LockdownIntents.intentdefinition @@ -0,0 +1,209 @@ + + + + + INEnums + + INIntentDefinitionModelVersion + 1.2 + INIntentDefinitionNamespace + 1KWK8L + INIntentDefinitionSystemVersion + 22A400 + INIntentDefinitionToolsBuildVersion + 14B47b + INIntentDefinitionToolsVersion + 14.1 + INIntents + + + INIntentCategory + generic + INIntentConfigurable + + INIntentDefaultImageName + lockdown_icon + INIntentDescription + Turns off Lockdown's Secure Tunnel (VPN) + INIntentDescriptionID + T5pK8z + INIntentManagedParameterCombinations + + + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Disable VPN + INIntentParameterCombinationTitleID + RTZHc5 + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + DisableVPN + INIntentParameterCombinations + + + + INIntentParameterCombinationIsPrimary + + INIntentParameterCombinationSubtitle + Turns off Lockdown's VPN + INIntentParameterCombinationSubtitleID + UeXEBO + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Disable VPN + INIntentParameterCombinationTitleID + LE8gnO + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeConciseFormatString + VPN successfully disabled! + INIntentResponseCodeConciseFormatStringID + aMNv5r + INIntentResponseCodeFormatString + VPN successfully disabled! + INIntentResponseCodeFormatStringID + 4ANXnv + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeConciseFormatString + Sorry, VPN is already disabled. + INIntentResponseCodeConciseFormatStringID + bOKxTL + INIntentResponseCodeFormatString + Sorry, VPN is already disabled. + INIntentResponseCodeFormatStringID + bBQS0s + INIntentResponseCodeName + failure + + + + INIntentTitle + Disable VPN + INIntentTitleID + idl2ou + INIntentType + Custom + INIntentVerb + Do + + + INIntentCategory + generic + INIntentConfigurable + + INIntentDefaultImageName + lockdown_icon + INIntentDescription + Turns on Lockdown's Secure Tunnel (VPN) + INIntentDescriptionID + HpId4o + INIntentManagedParameterCombinations + + + + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Enable VPN + INIntentParameterCombinationTitleID + qT3LPw + INIntentParameterCombinationUpdatesLinked + + + + INIntentName + EnableVPN + INIntentParameterCombinations + + + + INIntentParameterCombinationIsPrimary + + INIntentParameterCombinationSubtitle + Turns on Lockdown's VPN + INIntentParameterCombinationSubtitleID + FeCNfk + INIntentParameterCombinationSupportsBackgroundExecution + + INIntentParameterCombinationTitle + Enable VPN + INIntentParameterCombinationTitleID + RscSJF + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeConciseFormatString + VPN successfully enabled! + INIntentResponseCodeConciseFormatStringID + 1t7xfA + INIntentResponseCodeFormatString + VPN successfully enabled! + INIntentResponseCodeFormatStringID + PvV4bc + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeConciseFormatString + Sorry, VPN is already enabled. + INIntentResponseCodeConciseFormatStringID + LXbqDZ + INIntentResponseCodeFormatString + Sorry, VPN is already enabled. + INIntentResponseCodeFormatStringID + e4VeOj + INIntentResponseCodeName + failure + + + INIntentResponseCodeConciseFormatString + VPN is not available. Subscribe to one of our plans to access VPN. + INIntentResponseCodeConciseFormatStringID + 8X2Bt8 + INIntentResponseCodeFormatString + VPN is not available. Subscribe to one of our plans to access VPN. + INIntentResponseCodeFormatStringID + EwLFUJ + INIntentResponseCodeName + notVpnSubscriber + + + INIntentResponseLastParameterTag + 1 + + INIntentTitle + Enable VPN + INIntentTitleID + C99L7t + INIntentType + Custom + INIntentVerb + Do + + + INTypes + + + diff --git a/LockdowniOS/Resources/Videos/Onboarding/Onboarding_blocklists.mp4 b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_blocklists.mp4 new file mode 100644 index 0000000..a7704a3 Binary files /dev/null and b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_blocklists.mp4 differ diff --git a/LockdowniOS/Resources/Videos/Onboarding/Onboarding_installation.mp4 b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_installation.mp4 new file mode 100644 index 0000000..fa07288 Binary files /dev/null and b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_installation.mp4 differ diff --git a/LockdowniOS/Resources/Videos/Onboarding/Onboarding_launch_firewall.mp4 b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_launch_firewall.mp4 new file mode 100644 index 0000000..3ba6991 Binary files /dev/null and b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_launch_firewall.mp4 differ diff --git a/LockdowniOS/Resources/Videos/Onboarding/Onboarding_notifications.mp4 b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_notifications.mp4 new file mode 100644 index 0000000..7f66675 Binary files /dev/null and b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_notifications.mp4 differ diff --git a/LockdowniOS/Resources/Videos/Onboarding/Onboarding_showcase_vpn.mp4 b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_showcase_vpn.mp4 new file mode 100644 index 0000000..c181c2d Binary files /dev/null and b/LockdowniOS/Resources/Videos/Onboarding/Onboarding_showcase_vpn.mp4 differ diff --git a/LockdowniOS/Resources/ar.lproj/LockdownIntents.strings b/LockdowniOS/Resources/ar.lproj/LockdownIntents.strings new file mode 100644 index 0000000..7b4a773 --- /dev/null +++ b/LockdowniOS/Resources/ar.lproj/LockdownIntents.strings @@ -0,0 +1,39 @@ +"1t7xfA" = "تم تفعيل الVPN!"; + +"4ANXnv" = "تم إيقاف الVPN!"; + +"8X2Bt8" = "الVPN غير متوفر. إشترك الأن للحصول على خدمة الVPN."; + +"C99L7t" = "تفعيل الVPN"; + +"EwLFUJ" = "الVPN غير متوفر. إشترك الأن للحصول على خدمة الVPN."; + +"FeCNfk" = "يفعل VPN Lockdown"; + +"HpId4o" = "يفعل VPN Lockdown"; + +"LE8gnO" = "إيقاف الVPN"; + +"LXbqDZ" = "الVPN مفعّل"; + +"PvV4bc" = "تم تفعيل الVPN!"; + +"RTZHc5" = "تم إيقاف الVPN!"; + +"RscSJF" = "تفعيل الVPN"; + +"T5pK8z" = "يوقف VPN Lockdown"; + +"UeXEBO" = "يوقف VPN Lockdown"; + +"aMNv5r" = "تم إيقاف الVPN!"; + +"bBQS0s" = "الVPN متوقف"; + +"bOKxTL" = "الVPN متوقف"; + +"e4VeOj" = "الVPN مفعّل"; + +"idl2ou" = "أوقف الVPN"; + +"qT3LPw" = "فعّل الVPN"; diff --git a/LockdowniOS/Resources/de.lproj/LockdownIntents.strings b/LockdowniOS/Resources/de.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/de.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/Resources/en.lproj/LockdownIntents.strings b/LockdowniOS/Resources/en.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/en.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/Resources/es.lproj/LockdownIntents.strings b/LockdowniOS/Resources/es.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/es.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/Resources/fr.lproj/LockdownIntents.strings b/LockdowniOS/Resources/fr.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/fr.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/Resources/ja.lproj/LockdownIntents.strings b/LockdowniOS/Resources/ja.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/ja.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/Resources/ru.lproj/LockdownIntents.strings b/LockdowniOS/Resources/ru.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/ru.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/Resources/vi.lproj/LockdownIntents.strings b/LockdowniOS/Resources/vi.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/vi.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/Resources/zh-Hans.lproj/LockdownIntents.strings b/LockdowniOS/Resources/zh-Hans.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/zh-Hans.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/Resources/zh-Hant.lproj/LockdownIntents.strings b/LockdowniOS/Resources/zh-Hant.lproj/LockdownIntents.strings new file mode 100644 index 0000000..0e95783 --- /dev/null +++ b/LockdowniOS/Resources/zh-Hant.lproj/LockdownIntents.strings @@ -0,0 +1,40 @@ +"1t7xfA" = "VPN successfully enabled!"; + +"4ANXnv" = "VPN successfully disabled!"; + +"8X2Bt8" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"C99L7t" = "Enable VPN"; + +"EwLFUJ" = "VPN is not available. Subscribe to one of our plans to access VPN."; + +"FeCNfk" = "Turns on Lockdown's VPN"; + +"HpId4o" = "Turns on Lockdown's Secure Tunnel (VPN)"; + +"LE8gnO" = "Disable VPN"; + +"LXbqDZ" = "Sorry, VPN is already enabled."; + +"PvV4bc" = "VPN successfully enabled!"; + +"RTZHc5" = "Disable VPN"; + +"RscSJF" = "Enable VPN"; + +"T5pK8z" = "Turns off Lockdown's Secure Tunnel (VPN)"; + +"UeXEBO" = "Turns off Lockdown's VPN"; + +"aMNv5r" = "VPN successfully disabled!"; + +"bBQS0s" = "Sorry, VPN is already disabled."; + +"bOKxTL" = "Sorry, VPN is already disabled."; + +"e4VeOj" = "Sorry, VPN is already enabled."; + +"idl2ou" = "Disable VPN"; + +"qT3LPw" = "Enable VPN"; + diff --git a/LockdowniOS/SetRegionViewController.swift b/LockdowniOS/SetRegionViewController.swift index 8878eee..8a195a1 100644 --- a/LockdowniOS/SetRegionViewController.swift +++ b/LockdowniOS/SetRegionViewController.swift @@ -24,7 +24,7 @@ class SetRegionViewController: BaseViewController, UITableViewDataSource, UITabl dismiss(animated: true, completion: nil) } - //MARK: - TABLE VIEW + // MARK: - TABLE VIEW func numberOfSections(in tableView: UITableView) -> Int { return 1 @@ -67,14 +67,15 @@ class SetRegionViewController: BaseViewController, UITableViewDataSource, UITabl func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let vpnRegion = vpnRegions[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: "setRegionCell", for: indexPath) as! SetRegionCell + guard let cell = tableView.dequeueReusableCell(withIdentifier: "setRegionCell", for: indexPath) as? SetRegionCell else { + return UITableViewCell() + } cell.regionFlag.text = vpnRegion.regionFlagEmoji cell.regionName.text = vpnRegion.regionDisplayName if vpnRegion.serverPrefix == getSavedVPNRegion().serverPrefix { cell.regionSelected.isHidden = false - } - else { + } else { cell.regionSelected.isHidden = true } diff --git a/LockdowniOS/SignUpViewController.xib b/LockdowniOS/SignUpViewController.xib new file mode 100644 index 0000000..9f7f2ae --- /dev/null +++ b/LockdowniOS/SignUpViewController.xib @@ -0,0 +1,349 @@ + + + + + + + + + + + + + Montserrat-Bold + + + Montserrat-Medium + + + Montserrat-Regular + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/SignupViewController.swift b/LockdowniOS/SignupViewController.swift index 85c5014..7f84cd6 100644 --- a/LockdowniOS/SignupViewController.swift +++ b/LockdowniOS/SignupViewController.swift @@ -1,503 +1,519 @@ // -// SignupViewController.swift -// Tunnels -// -// Copyright © 2019 Confirmed Inc. All rights reserved. +// SignUpViewController.swift +// Lockdown // +// Created by Alexander Parshakov on 11/3/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// -import UIKit -import SwiftyStoreKit -import NetworkExtension -import PromiseKit import CocoaLumberjackSwift -import StoreKit - -class SignupViewController: BaseViewController { +import Foundation +import PopupDialog +import PromiseKit +import UIKit - //MARK: - VARIABLES - - var parentVC: UIViewController? +final class SignUpViewController: BaseViewController, Loadable { - enum Mode { - case newSubscription - case upgrade(active: [Subscription.PlanType]) - } + @IBOutlet private var closeButtonSpacer: UIView! + @IBOutlet private var closeButton: UIButton! - var mode = Mode.newSubscription + @IBOutlet private var upperLabelsStackView: UIStackView! + @IBOutlet private var welcomeLabel: UILabel! + @IBOutlet private var descriptionLabel: UILabel! - @IBOutlet var monthlyPlanContainer: UIView! - @IBOutlet var monthlyPlanCheckbox: M13Checkbox! + @IBOutlet private var emailTextField: FloatingTextInputTextField! + @IBOutlet private var passwordTextField: FloatingTextInputTextField! + @IBOutlet private var passwordValidationLabel: UILabel! - @IBOutlet var monthlyProPlanContainer: UIView! - @IBOutlet var monthlyProPlanCheckbox: M13Checkbox! + @IBOutlet private var advantageLabelOne: UILabel! + @IBOutlet private var advantageLabelTwo: UILabel! + @IBOutlet private var advantageLabelThree: UILabel! - @IBOutlet var annualPlanContainer: UIView! - @IBOutlet var annualPlanCheckbox: M13Checkbox! + @IBOutlet private var alternativeActionButton: UIButton! + @IBOutlet private var mainButton: UIButton! - @IBOutlet var annualProPlanContainer: UIView! - @IBOutlet var annualProPlanCheckbox: M13Checkbox! + @IBOutlet private var byContinuingLabel: UILabel! + @IBOutlet private var termsOfServiceLabel: UIButton! + @IBOutlet private var andLabel: UILabel! + @IBOutlet private var privacyPolicyLabel: UIButton! - @IBOutlet var upgradeSubscriptionHintLabel: UILabel! - - @IBOutlet var startTrialButton: TKTransitionSubmitButton! - @IBOutlet var pricingSubtitle: UILabel! - - @IBOutlet var restorePurchasesButton: TKTransitionSubmitButton! + private var signUpButtonGradientLayer: CAGradientLayer? - override func viewDidLoad() { - super.viewDidLoad() - VPNSubscription.cacheLocalizedPrices() - switch mode { - case .newSubscription: - selectAnnual() - upgradeSubscriptionHintLabel.isHidden = true - case .upgrade(active: let activeSubscriptions): - configureWithActiveSubscriptions(activeSubscriptions) - restorePurchasesButton.isHidden = true + private var mode: AuthenticationViewControllerMode { + didSet { + DispatchQueue.main.async { + self.updateScreen() + } } } - var disabledCheckboxes: Set = [] + private var textFieldBorderWidth: CGFloat { isDarkMode ? 2.5 : 2 } + private var isPasswordValid = false + private var didAutofillTextfield = false + + /// For cases when user is focused on a textfield and swipes back to the previous screen. + private var isDisappearing = false - @objc func selectMonthly() { - animatedSetChecked(monthlyPlanCheckbox, .checked) - animatedSetChecked(monthlyProPlanCheckbox, .unchecked) - animatedSetChecked(annualPlanCheckbox, .unchecked) - animatedSetChecked(annualProPlanCheckbox, .unchecked) - VPNSubscription.selectedProductId = VPNSubscription.productIdMonthly - updatePricingSubtitle() + init(mode: AuthenticationViewControllerMode) { + self.mode = mode + + super.init(nibName: nil, bundle: nil) } - @objc func selectMonthlyPro() { - animatedSetChecked(monthlyPlanCheckbox, .unchecked) - animatedSetChecked(monthlyProPlanCheckbox, .checked) - animatedSetChecked(annualPlanCheckbox, .unchecked) - animatedSetChecked(annualProPlanCheckbox, .unchecked) - VPNSubscription.selectedProductId = VPNSubscription.productIdMonthlyPro - updatePricingSubtitle() + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } - @IBAction func monthlyTapped(_ sender: Any) { - selectMonthly() + override func viewDidLoad() { + super.viewDidLoad() + + setupTextFields() + setupSkipButton() + setupSignUpButton() + + setupTexts() + + setupGestureRecognizers() + + updateScreen() + + navigationController?.navigationBar.topItem?.backButtonTitle = "" + navigationController?.navigationBar.tintColor = .label } - @IBAction func monthlyProTapped(_ sender: Any) { - selectMonthlyPro() + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + isDisappearing = false } - @objc func selectAnnual() { - animatedSetChecked(monthlyPlanCheckbox, .unchecked) - animatedSetChecked(monthlyProPlanCheckbox, .unchecked) - animatedSetChecked(annualPlanCheckbox, .checked) - animatedSetChecked(annualProPlanCheckbox, .unchecked) - VPNSubscription.selectedProductId = VPNSubscription.productIdAnnual - updatePricingSubtitle() + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + isDisappearing = true } - @IBAction func annualTapped(_ sender: Any) { - selectAnnual() + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + mainButton.corners = .continuous(mainButton.bounds.midY) + signUpButtonGradientLayer?.corners = mainButton.corners + signUpButtonGradientLayer?.frame = mainButton.bounds } - @objc func selectAnnualPro() { - animatedSetChecked(monthlyPlanCheckbox, .unchecked) - animatedSetChecked(monthlyProPlanCheckbox, .unchecked) - animatedSetChecked(annualPlanCheckbox, .unchecked) - animatedSetChecked(annualProPlanCheckbox, .checked) - VPNSubscription.selectedProductId = VPNSubscription.productIdAnnualPro - updatePricingSubtitle() + @IBAction private func didTapCloseButton(_ sender: Any) { + dismiss(animated: true) } - @IBAction func annualProTapped(_ sender: Any) { - selectAnnualPro() + @IBAction private func didTapAlternativeActionButton(_ sender: Any) { + switch mode { + case .login: + // Forgot password + guard let forgotPasswordViewController = UIStoryboard.main.instantiateViewController(withIdentifier: "ForgotPasswordViewController") + as? ForgotPasswordViewController else { return } + forgotPasswordViewController.modalPresentationStyle = .fullScreen + present(forgotPasswordViewController, animated: true) + case .signUp: + // I already have an account, change to login + mode = .login + } } - @objc func updatePricingSubtitle() { - let context: VPNSubscription.SubscriptionContext = { - switch mode { - case .newSubscription: - return .new - case .upgrade: - return .upgrade + @IBAction private func didTapMainButton() { + mainButton.showAnimatedPress { [weak self] in + guard let self else { return } + switch self.mode { + case .signUp: + self.createAccount() + case .login: + self.login() } - }() - - if monthlyPlanCheckbox.checkState == .checked, monthlyPlanCheckbox.isEnabled { - pricingSubtitle.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthly, for: context) - } - else if annualPlanCheckbox.checkState == .checked, annualPlanCheckbox.isEnabled { - pricingSubtitle.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnual, for: context) - } - else if annualProPlanCheckbox.checkState == .checked, annualProPlanCheckbox.isEnabled { - pricingSubtitle.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnualPro, for: context) - } - else if monthlyProPlanCheckbox.checkState == .checked, monthlyProPlanCheckbox.isEnabled { - pricingSubtitle.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthlyPro, for: context) } } - @IBAction func dismissSignUpScreen() { - self.view.endEditing(true) - self.dismiss(animated: true, completion: {}) + @IBAction private func didTapTermsOfService(_ sender: Any) { + showModalWebView(title: .localized("terms_of_service"), urlString: .lockdownUrlTerms) + } + + @IBAction private func didTapPrivacyPolicy(_ sender: Any) { + showModalWebView(title: .localized("Privacy Policy"), urlString: .lockdownUrlPrivacy) } - func toggleStartTrialButton(_ enabled: Bool) { - if (enabled) { - UIView.transition(with: self.pricingSubtitle, - duration: 0.15, - options: .transitionCrossDissolve, - animations: { - self.pricingSubtitle.alpha = 1.0 - }) - startTrialButton.isUserInteractionEnabled = true - startTrialButton.setOriginalState() - startTrialButton.layer.cornerRadius = 4 - unblockUserInteraction() + private func setupTextFields() { + [emailTextField, passwordTextField].forEach { + $0?.titleFont = .regularLockdownFont(size: 13) + $0?.placeholderFont = .regularLockdownFont(size: 17) + $0?.titleColor = .gray + $0?.textColor = .black + $0?.corners = .continuous(8) + $0?.delegate = self + + if traitCollection.layoutDirection == .rightToLeft { + $0?.textAlignment = .right + $0?.semanticContentAttribute = .forceRightToLeft + } } - else { - UIView.transition(with: self.pricingSubtitle, - duration: 0.15, - options: .transitionCrossDissolve, - animations: { - self.pricingSubtitle.alpha = 0.0 - }) - startTrialButton.isUserInteractionEnabled = false - startTrialButton.startLoadingAnimation() - blockUserInteraction() + emailTextField.addTarget(self, action: #selector(updateMainButtonState), for: .editingChanged) + passwordTextField.addTarget(self, action: #selector(validatePassword), for: .editingChanged) + } + + private func setupSkipButton() { + let skipButton = UIBarButtonItem(title: .localized("skip"), style: .plain, target: self, action: #selector(proceedToEnableNotifications)) + + [.normal, .highlighted].forEach { + skipButton.setTitleTextAttributes([ + .font: UIFont.regularLockdownFont(size: 17), + .underlineStyle: NSUnderlineStyle.single.rawValue + ], for: $0) } + navigationItem.rightBarButtonItem = skipButton + + closeButtonSpacer.isHidden = navigationController != nil + closeButton.isHidden = navigationController != nil } - @IBAction func startTrial (_ sender: UIButton) { - toggleStartTrialButton(false) - VPNSubscription.purchase ( - succeeded: { - self.dismiss(animated: true, completion: { - if let presentingViewController = self.parentVC as? AccountViewController { - presentingViewController.reloadTable() - } - // force refresh receipt, and sync with email if it exists, activate VPNte - if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { - DDLogInfo("purchase complete: syncing with confirmed email") - firstly { - try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) - } - .then { (signin: SignIn) -> Promise in - DDLogInfo("purchase complete: signin result: \(signin)") - return try Client.subscriptionEvent(forceRefresh: true) - } - .then { (result: SubscriptionEvent) -> Promise in - DDLogInfo("purchase complete: subscriptionevent result: \(result)") - return try Client.getKey() - } - .done { (getKey: GetKey) in - try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) - DDLogInfo("purchase complete: setting VPN creds with ID: \(getKey.id)") - VPNController.shared.setEnabled(true) - } - .catch { error in - DDLogError("purchase complete: Error: \(error)") - if (self.popupErrorAsNSURLError("Error activating Secure Tunnel: \(error)")) { - return - } - else if let apiError = error as? ApiError { - switch apiError.code { - default: - _ = self.popupErrorAsApiError("API Error activating Secure Tunnel: \(error)") - } - } - } - } - else { - firstly { - try Client.signIn(forceRefresh: true) // 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 - DDLogError("purchase complete - no email: Error: \(error)") - if (self.popupErrorAsNSURLError("Error activating Secure Tunnel: \(error)")) { - return - } - else if let apiError = error as? ApiError { - switch apiError.code { - default: - _ = self.popupErrorAsApiError("API Error activating Secure Tunnel: \(error)") - } - } - } - } - }) - }, - errored: { error in - self.toggleStartTrialButton(true) - DDLogError("Start Trial Failed: \(error)") - - if let skError = error as? SKError { - var errorText = "" - switch skError.code { - case .unknown: errorText = NSLocalizedString("Unknown error. Please contact support at team@lockdownprivacy.com.", comment: "") - case .clientInvalid: errorText = NSLocalizedString("Not allowed to make the payment", comment: "") - case .paymentCancelled: errorText = NSLocalizedString("Payment was cancelled", comment: "") - case .paymentInvalid: errorText = NSLocalizedString("The purchase identifier was invalid", comment: "") - case .paymentNotAllowed: errorText = NSLocalizedString("Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again.", comment: "") - case .storeProductNotAvailable: errorText = NSLocalizedString("The product is not available in the current storefront", comment: "") - case .cloudServicePermissionDenied: errorText = NSLocalizedString("Access to cloud service information is not allowed", comment: "") - case .cloudServiceNetworkConnectionFailed: errorText = NSLocalizedString("Could not connect to the network", comment: "") - case .cloudServiceRevoked: errorText = NSLocalizedString("User has revoked permission to use this cloud service", comment: "") - default: errorText = skError.localizedDescription - } - self.showPopupDialog(title: NSLocalizedString("Error Starting Trial", comment: ""), - message: errorText, - acceptButton: "Okay") - } - else if (self.popupErrorAsNSURLError(error)) { - return - } - else if (self.popupErrorAsApiError(error)) { - return - } - else { - self.showPopupDialog(title: NSLocalizedString("Error Starting Trial", comment: ""), - message: NSLocalizedString("Please contact team@lockdownprivacy.com.\n\nError details:\n", comment: "") + "\(error)", - acceptButton: "Okay") - } - }) + @objc private func proceedToEnableNotifications() { + let enableNotificationsViewController = EnableNotificationsViewController() + navigationController?.pushViewController(enableNotificationsViewController, animated: true) } - func toggleRestorePurchasesButton(_ enabled: Bool) { - if (enabled) { - restorePurchasesButton.isUserInteractionEnabled = true - restorePurchasesButton.setOriginalState() - restorePurchasesButton.layer.cornerRadius = 4 - unblockUserInteraction() - } - else { - restorePurchasesButton.isUserInteractionEnabled = false - restorePurchasesButton.startLoadingAnimation() - blockUserInteraction() - } + private func setupSignUpButton() { + signUpButtonGradientLayer = mainButton.applyGradient(.lightBlue) + updateMainButtonState() } - @IBAction func restorePurchases(_ sender: Any) { - //toggleRestorePurchasesButton(false) - firstly { - try Client.signIn(forceRefresh: true) - } - .then { (signin: SignIn) -> Promise in - try Client.getKey() - } - .done { (getKey: GetKey) in - // we were able to get key, so subscription is valid -- follow pathway from HomeViewController to associate this with the email account if there is one - let presentingViewController = self.presentingViewController as? HomeViewController - self.dismiss(animated: true, completion: { - if presentingViewController != nil { - presentingViewController?.toggleVPN("me") - } - else { - VPNController.shared.setEnabled(true) - } - }) - } - .catch { error in - self.toggleRestorePurchasesButton(true) - DDLogError("Restore Failed: \(error)") - if let apiError = error as? ApiError { - switch apiError.code { - case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: - // now try email if it exists - if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { - DDLogInfo("restore: have confirmed API credentials, using them") - self.toggleRestorePurchasesButton(false) - firstly { - try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) - } - .then { (signin: SignIn) -> Promise in - DDLogInfo("restore: signin result: \(signin)") - return try Client.getKey() - } - .done { (getKey: GetKey) in - self.toggleRestorePurchasesButton(true) - try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) - DDLogInfo("restore: setting VPN creds with ID and Dismissing: \(getKey.id)") - let presentingViewController = self.presentingViewController as? HomeViewController - self.dismiss(animated: true, completion: { - if presentingViewController != nil { - presentingViewController?.toggleVPN("me") - } - else { - VPNController.shared.setEnabled(true) - } - }) - } - .catch { error in - self.toggleRestorePurchasesButton(true) - DDLogError("restore: Error doing restore with email-login: \(error)") - if (self.popupErrorAsNSURLError(error)) { - return - } - else if let apiError = error as? ApiError { - switch apiError.code { - case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: - self.showPopupDialog(title: NSLocalizedString("No Active Subscription", comment: ""), - message: NSLocalizedString("Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com", comment: ""), - acceptButton: NSLocalizedString("OK", comment: "")) - default: - _ = self.popupErrorAsApiError(error) - } - } - } - } - else { - self.showPopupDialog(title: NSLocalizedString("No Active Subscription", comment: ""), - message: NSLocalizedString("Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com", comment: ""), - acceptButton: NSLocalizedString("OK", comment: "")) - } - default: - self.showPopupDialog(title: NSLocalizedString("Error Restoring Subscription", comment: ""), - message: NSLocalizedString("Please email team@lockdownprivacy.com with the following Error Code ", comment: "") + "\(apiError.code) : \(apiError.message)", - acceptButton: NSLocalizedString("OK", comment: "")) - } - } - else { - self.showPopupDialog(title: NSLocalizedString("Error Restoring Subscription", comment: ""), - message: NSLocalizedString("Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with the following error message: ", comment: "") + "\(error)", - acceptButton: NSLocalizedString("OK", comment: "")) - } - } + private func setupTexts() { + welcomeLabel.text = .localized("welcome") + descriptionLabel.text = .localized("already_have_account_login") + emailTextField.title = .localized("email") + passwordTextField.title = .localized("password") + + advantageLabelOne.text = .localized("access_our_curated_block_lists_and_build_your_own") + advantageLabelTwo.text = .localized("access_lockdown_across_all_your_devices") + advantageLabelThree.text = .localized("firewall_and_vpn_support") + + mainButton.setTitle(.localized("onboarding_sign_up"), for: .normal) + + byContinuingLabel.text = .localized("by_continuing_you_agree_with_our") + privacyPolicyLabel.setTitle(.localized("Privacy Policy"), for: .normal) + andLabel.text = .localized("and") + termsOfServiceLabel.setTitle(.localized("terms_of_service"), for: .normal) } - @IBAction func openPrivacyPolicy (_ sender: Any) { - self.showPrivacyPolicyModal() + private func setupGestureRecognizers() { + let tapOutsideTextfields = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + view.addGestureRecognizer(tapOutsideTextfields) } - @IBAction func openTermsAndConditions (_ sender: Any) { - self.showTermsModal() + @objc private func dismissKeyboard() { + view.endEditing(true) } +} + +extension SignUpViewController { - @IBAction func cancelButtonPressed (_ sender: Any) { - self.dismiss(animated: true, completion: nil) + private func updateScreen() { + switch mode { + case .signUp: + updateForSignUp() + case .login: + updateForLogin() + } } - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - self.view.endEditing(true) + private func updateForSignUp() { + // So that the textfields won't trigger autofill suggestions + // https://developer.apple.com/forums/thread/108085 + emailTextField.textContentType = .oneTimeCode + passwordTextField.textContentType = .oneTimeCode + + transition(with: upperLabelsStackView) { [weak self] in + self?.descriptionLabel.text = .localized("sign_up_to_access_new_block_lists_from_trackers") + } + alternativeActionButton.setTitle(.localized("already_have_account_login"), for: .normal) } + private func updateForLogin() { + emailTextField.textContentType = .username + passwordTextField.textContentType = .password + + validatePassword() + + transition(with: upperLabelsStackView) { [weak self] in + self?.descriptionLabel.text = .localized("already_have_account_login_below") + } + transition(with: alternativeActionButton) { [weak self] in + self?.alternativeActionButton.setTitle(.localized("forgot_password"), for: .normal) + } + transition(with: mainButton) { [weak self] in + self?.mainButton.setTitle(.localized("onboarding_login"), for: .normal) + } + } } -extension SignupViewController { +extension SignUpViewController: UITextFieldDelegate, EmailValidatable { - // MARK: - Functions for "Upgrade Plan" mode - - private func configureWithActiveSubscriptions(_ activePlans: [Subscription.PlanType]) { - startTrialButton.setTitle(NSLocalizedString("Upgrade Plan", comment: ""), for: .normal) + func textField(_ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String) -> Bool { + // If the range is {0,0} and the string count > 1, then user copy paste text or used password autofill. + didAutofillTextfield = range == NSRange(location: 0, length: 0) && string.count > 1 && mode == .login - for plan in activePlans { - configureForUnavailable(plan, reason: .purchased) - if let unavailableToUpgrade = plan.unavailableToUpgrade { - for unavailable in unavailableToUpgrade where unavailable != plan { - configureForUnavailable(unavailable, reason: .lowerTierThanPurchased) - } + return true + } + + func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + // If autofilled, don't show keyboard + if mode == .login && didAutofillTextfield { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.didAutofillTextfield = false } + + DispatchQueue.main.async { + self.view.endEditing(true) + + // Color somehow falls back to default, fixing it + textField.textColor = .black + } + + return false } - selectAnnualOrFirstAvailable() - } - - enum UnavailableReason { - case purchased - case lowerTierThanPurchased - } - - private func configureForUnavailable(_ subscriptionPlanType: Subscription.PlanType, reason: UnavailableReason) { - switch subscriptionPlanType { - case .monthly: - markAsUnavailable(monthlyPlanCheckbox, plan: subscriptionPlanType, reason: reason, container: monthlyPlanContainer) - case .proMonthly: - markAsUnavailable(monthlyProPlanCheckbox, plan: subscriptionPlanType, reason: reason, container: monthlyProPlanContainer) - case .annual: - markAsUnavailable(annualPlanCheckbox, plan: subscriptionPlanType, reason: reason, container: annualPlanContainer) - case .proAnnual: - markAsUnavailable(annualProPlanCheckbox, plan: subscriptionPlanType, reason: reason, container: annualProPlanContainer) - default: - break + textField.animateBorderWidth(toValue: textFieldBorderWidth) + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if textField == emailTextField { + emailTextField.animateBorderWidth(toValue: 0) + passwordTextField.becomeFirstResponder() + } else { + passwordTextField.animateBorderWidth(toValue: textFieldBorderWidth) + dismissKeyboard() } + + return true } - private func markAsUnavailable(_ checkbox: M13Checkbox, plan: Subscription.PlanType, reason: UnavailableReason, container: UIView) { - disabledCheckboxes.insert(checkbox) - checkbox.isEnabled = false + func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) { + guard !isDisappearing else { return } - switch reason { - case .lowerTierThanPurchased: - container.isHidden = true - case .purchased: - container.alpha = 0.4 - checkbox.setCheckState(.checked, animated: false) - checkbox.tintColor = UIColor.gray - checkbox.secondaryTintColor = UIColor.gray - checkbox.isEnabled = false - checkbox.accessibilityLabel = Accessibility.currentPlanCheckboxLabel(for: plan) - checkbox.accessibilityHint = Accessibility.currentPlanCheckboxHint() + if textField == passwordTextField, textField.text?.count ?? 0 > 0, !isPasswordValid { + textField.animateBorderWidth(toValue: textFieldBorderWidth, color: .tunnelsWarning) + } else if textField == emailTextField, let emailError = errorValidatingEmail(emailTextField.text) { + showPopupDialog(title: .localized("invalid_email_address"), + message: emailError.localizedDescription, + transitionStyle: .fadeIn, + acceptButton: .localizedOkay) + textField.animateBorderWidth(toValue: textFieldBorderWidth, color: .tunnelsWarning) + } else { + textField.animateBorderWidth(toValue: 0) } } - private func animatedSetChecked(_ checkbox: M13Checkbox, _ state: M13Checkbox.CheckState) { - if disabledCheckboxes.contains(checkbox) { + @objc private func validatePassword() { + updateMainButtonState() + + guard mode == .signUp else { + passwordTextField.animateBorderWidth(toValue: 0) + passwordValidationLabel.isHidden = true + isPasswordValid = true return + } + + let passwordRequirements = """ +Password must be at least 8 characters, contain at least one uppercase letter, \ +one lowercase letter, one number, and one symbol. +""" + let attrStr = NSMutableAttributedString( + string: .localized(passwordRequirements), + attributes: [ + .font: UIFont.mediumLockdownFont(size: 13), + .foregroundColor: UIColor.lightGray + ]) + + if let txt = passwordTextField.text { + isPasswordValid = true + attrStr.addAttributes(setupAttributeColor(if: (txt.count >= 8)), + range: findRange(in: attrStr.string, for: .localized("at least 8 characters"))) + attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil)), + range: findRange(in: attrStr.string, for: .localized("one uppercase letter"))) + attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil)), + range: findRange(in: attrStr.string, for: .localized("one lowercase letter"))) + attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.decimalDigits) != nil)), + range: findRange(in: attrStr.string, for: .localized("one number"))) + attrStr.addAttributes(setupAttributeColor(if: ((txt.rangeOfCharacter(from: CharacterSet.symbols) != nil) + || (txt.rangeOfCharacter(from: CharacterSet.punctuationCharacters) != nil))), + range: findRange(in: attrStr.string, for: .localized("one symbol"))) + } else { + isPasswordValid = false + } + + passwordValidationLabel.attributedText = attrStr + passwordValidationLabel.isHidden = isPasswordValid + + updateMainButtonState() + } + + private func setupAttributeColor(if isValid: Bool) -> [NSAttributedString.Key: Any] { + if isValid { + return [NSAttributedString.Key.foregroundColor: UIColor.lightGray] } else { - checkbox.setCheckState(state, animated: true) + isPasswordValid = false + return [NSAttributedString.Key.foregroundColor: UIColor.red] } } - func selectAnnualOrFirstAvailable() { - if disabledCheckboxes.contains(annualPlanCheckbox) { - // Annual is unavailable, find first available option to select - if monthlyPlanCheckbox.isEnabled { - selectMonthly() - } else if annualPlanCheckbox.isEnabled { - selectAnnual() - } else if monthlyProPlanCheckbox.isEnabled { - selectMonthlyPro() - } else if annualProPlanCheckbox.isEnabled { - selectAnnualPro() - } else { - selectAnnualPro() - startTrialButton.isEnabled = false - startTrialButton.backgroundColor = UIColor.gray - startTrialButton.isUserInteractionEnabled = false - startTrialButton.alpha = 0.5 - pricingSubtitle.alpha = 0.3 - } + private func findRange(in baseString: String, for substring: String) -> NSRange { + if let range = baseString.localizedStandardRange(of: substring) { + let startIndex = baseString.distance(from: baseString.startIndex, to: range.lowerBound) + let length = substring.count + return NSRange(location: startIndex, length: length) + } else { + print("Range does not exist in the base string.") + return NSRange(location: 0, length: 0) + } + } + + @objc private func updateMainButtonState() { + if mode == .signUp { + mainButton.isUserInteractionEnabled = emailTextField.text?.count ?? 0 > 0 && isPasswordValid } else { - selectAnnual() + mainButton.isUserInteractionEnabled = emailTextField.text?.count ?? 0 > 0 && passwordTextField.text?.count ?? 0 > 0 } + mainButton.isEnabled = mainButton.isUserInteractionEnabled + signUpButtonGradientLayer?.isHidden = !mainButton.isUserInteractionEnabled } } -extension SignupViewController { - enum Accessibility { - static func currentPlanCheckboxLabel(for plan: Subscription.PlanType) -> String? { - switch plan { - case .monthly: - return NSLocalizedString("\"iOS Monthly\" is your current plan", comment: "") - case .proMonthly: - return NSLocalizedString("\"Pro Monthly\" is your current plan", comment: "") - case .annual: - return NSLocalizedString("\"iOS Annual\" is your current plan", comment: "") - case .proAnnual: - return NSLocalizedString("\"Pro Annual\" is your current plan", comment: "") - default: - return "\"\(plan.rawValue)\" is your current plan" +// MARK: - Backend +extension SignUpViewController { + + private func createAccount() { + // Do /signup (do subscription-event later, user needs to confirm email first though) + showLoadingView() + + // TODO: client side preliminary password fields, email validation - server does additional checking later + + firstly { + try Client.signup(email: self.emailTextField.text ?? "", password: self.passwordTextField.text ?? "") + } + .catch { error in + self.hideLoadingView() + if self.popupErrorAsNSURLError(error) { + return + } else if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeEmailNotConfirmed: + // This is the "correct" case for /signup, we are expecting "1" = email confirmation sent + do { + try setAPICredentials(email: self.emailTextField.text!, password: self.passwordTextField.text!) + setAPICredentialsConfirmed(confirmed: false) + let message = """ +To finish signup, click the confirmation link in the email we just sent. \ +If you don't see it, check if it's stuck in your spam folder. +""" + let popup = PopupDialog(title: .localized("confirm_your_email"), + message: .localized(message), + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + popup.addButtons([ + DefaultButton(title: .localizedOkay, dismissOnTap: true) { + self.hideLoadingView() + self.proceedToEnableNotifications() + } + ]) + self.present(popup, animated: true, completion: nil) + NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) + } catch { + self.showPopupDialog( + title: "Error Saving Credentials", + message: "Couldn't save credentials to local keychain. Please report this error to team@lockdownprivacy.com.", + acceptButton: .localizedOkay) + } + default: + _ = self.popupErrorAsApiError(error) + } + } else { + self.showPopupDialog(title: .localized("Error Creating Email Account"), + message: "\(error)", + acceptButton: .localizedOkay) } } + } + + private func login() { + guard let email = emailTextField.text, let password = passwordTextField.text else { + showPopupDialog(title: .localized("check_fields"), message: .localized("email_and_password_must_not_be_empty"), acceptButton: .localizedOkay) + return + } - static func currentPlanCheckboxHint() -> String? { - return nil + showLoadingView() + firstly { + try Client.signInWithEmail(email: email, password: password) + } + .done { (_: SignIn) in + try setAPICredentials(email: email, password: password) + setAPICredentialsConfirmed(confirmed: true) + self.hideLoadingView() + NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) + self.showPopupDialog(title: .localized("Success! 🎉"), + message: .localized("you_have_successfully_sign_in"), + acceptButton: .localizedOkay, + tapGestureDismissal: false, + panGestureDismissal: false) { + if let navigationController = self.navigationController { + let enableNotificationsViewController = EnableNotificationsViewController() + navigationController.isNavigationBarHidden = true + navigationController.setViewControllers([enableNotificationsViewController], animated: true) + self.processSuccessfulLogin() + } else { + self.presentingViewController?.dismiss(animated: true) { + self.processSuccessfulLogin() + } + } + } + } + .catch { error in + self.hideLoadingView() + var errorMessage = error.localizedDescription + if let apiError = error as? ApiError { + errorMessage = apiError.message + } + self.showPopupDialog( + title: .localized("error_signing_in"), + message: errorMessage, + transitionStyle: .zoomIn, + acceptButton: .localizedOkay) {} + } + } + + private func processSuccessfulLogin() { + // logged in and confirmed - update this email with the receipt and refresh VPN credentials + firstly { () -> Promise in + try Client.subscriptionEvent() + } + .then { (_: SubscriptionEvent) -> Promise in + try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + if getUserWantsVPNEnabled() { + 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)") } } } + +enum AuthenticationViewControllerMode { + case login, signUp +} diff --git a/LockdowniOS/SplashscreenViewController.swift b/LockdowniOS/SplashscreenViewController.swift new file mode 100644 index 0000000..6d0267a --- /dev/null +++ b/LockdowniOS/SplashscreenViewController.swift @@ -0,0 +1,51 @@ +// +// SplashscreenViewController.swift +// Lockdown +// +// Created by Alexander Parshakov on 9/7/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import AppTrackingTransparency +import UIKit + +final class SplashscreenViewController: BaseViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + BaseUserService.shared.updateUserSubscription { [weak self] _ in + DispatchQueue.main.async { + self?.dismiss() + } + } + } + + private func dismiss() { + dismiss(animated: false) { + let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + + // Do not show onboarding if user has seen the previous onboarding or has already seen this new one + if OneTimeActions.hasSeen(.welcomeScreen) == false, + OneTimeActions.hasSeen(.newFancyOnboarding) == false { + let onboardingViewController = OnboardingViewController() + let navigation = UINavigationController(rootViewController: onboardingViewController) + keyWindow?.rootViewController = navigation + } else { + keyWindow?.rootViewController = UIStoryboard.main.instantiateViewController(withIdentifier: "MainTabBarController") + } + keyWindow?.makeKeyAndVisible() + } + } + + private func askForPermissionToTrack(completion: @escaping () -> Void) { + guard #available(iOS 14, *) else { + completion() + return + } + + ATTrackingManager.requestTrackingAuthorization(completionHandler: { _ in + completion() + }) + } +} diff --git a/LockdowniOS/SplashscreenViewController.xib b/LockdowniOS/SplashscreenViewController.xib new file mode 100644 index 0000000..c6f0cc2 --- /dev/null +++ b/LockdowniOS/SplashscreenViewController.xib @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/StaticTableView.swift b/LockdowniOS/StaticTableView.swift index 4b6c5ba..4f1d503 100644 --- a/LockdowniOS/StaticTableView.swift +++ b/LockdowniOS/StaticTableView.swift @@ -30,7 +30,7 @@ final class StaticTableView: UITableView { } @discardableResult - func addRow(insert: Insert = .last, _ configure: (UIView) -> ()) -> SelectableTableViewCell { + func addRow(insert: Insert = .last, _ configure: (UIView) -> Void) -> SelectableTableViewCell { let cell = SelectableTableViewCell() cell.selectionStyle = .none cell.backgroundColor = nil @@ -40,7 +40,7 @@ final class StaticTableView: UITableView { } @discardableResult - func addRowCell(insert: Insert = .last, _ configure: (UITableViewCell) -> ()) -> SelectableTableViewCell { + func addRowCell(insert: Insert = .last, _ configure: (UITableViewCell) -> Void) -> SelectableTableViewCell { let cell = SelectableTableViewCell() cell.selectionStyle = .none configure(cell) @@ -90,28 +90,28 @@ final class StaticTableView: UITableView { } class SelectableTableViewCell: UITableViewCell { - var selectionCallback: () -> () = { } - var deletionCallback: (() -> ())? + var selectionCallback: () -> Void = {} + var deletionCallback: (() -> Void)? enum Action { case toggleCheckmark } @discardableResult - func onSelect(callback: @escaping () -> ()) -> Self { + func onSelect(callback: @escaping () -> Void) -> Self { selectionStyle = .default selectionCallback = callback return self } @discardableResult - func onSwipeToDelete(callback: @escaping () -> ()) -> Self { + func onSwipeToDelete(callback: @escaping () -> Void) -> Self { deletionCallback = callback return self } @discardableResult - func onSelect(_ action: Action, callback: @escaping () -> () = { }) -> Self { + func onSelect(_ action: Action, callback: @escaping () -> Void = {}) -> Self { selectionCallback = { [unowned self] in switch action { case .toggleCheckmark: @@ -181,7 +181,7 @@ extension StaticTableView: UITableViewDelegate { } extension UIViewController { - func addTableView(_ tableView: UITableView, layout: (UIView) -> ()) { + func addTableView(_ tableView: UITableView, layout: (UIView) -> Void) { // adding UITableView as UITableViewController will enable // UIKit's own "scroll to text field when keyboard appears" let tableViewController = StaticTableViewController() diff --git a/LockdowniOS/SubscriptionOfferPeriodUnit.swift b/LockdowniOS/SubscriptionOfferPeriodUnit.swift new file mode 100644 index 0000000..afcbf45 --- /dev/null +++ b/LockdowniOS/SubscriptionOfferPeriodUnit.swift @@ -0,0 +1,11 @@ +// +// SubscriptionOfferPeriodUnit.swift +// LockdowniOS +// +// Created by Alexander Parshakov on 11/28/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +enum SubscriptionOfferPeriodUnit { + case year, month, week, day, unknown +} diff --git a/LockdowniOS/TablePaywallFeatureRowView.swift b/LockdowniOS/TablePaywallFeatureRowView.swift new file mode 100644 index 0000000..3f816fc --- /dev/null +++ b/LockdowniOS/TablePaywallFeatureRowView.swift @@ -0,0 +1,55 @@ +// +// TablePaywallFeatureRowView.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/5/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class TablePaywallFeatureRowView: UIView, NibLoadable { + + @IBOutlet private var featureLabel: UILabel! + + @IBOutlet private var freeLevelIcon: UIImageView! + @IBOutlet private var vpnLevelIcon: UIImageView! + @IBOutlet private var proLevelIcon: UIImageView! + + static func make(feature: PaywallTableFeature, isHighlighted: Bool) -> TablePaywallFeatureRowView { + let view = TablePaywallFeatureRowView.loadFromNib() + + view.setupFeatureText(for: feature, isHighlighted: isHighlighted) + view.setupIcons(for: feature) + + return view + } + + private func setupFeatureText(for feature: PaywallTableFeature, isHighlighted: Bool) { + featureLabel.text = feature.body + featureLabel.textColor = isHighlighted ? .label : .gray + + let fontSize: CGFloat = UIScreen.main.traitCollection.userInterfaceIdiom == .pad ? 16 : 11 + featureLabel.font = .semiboldLockdownFont(size: fontSize) + } + + private func setupIcons(for feature: PaywallTableFeature) { + freeLevelIcon.image = imageForAvailabilityStatus(feature.isAvailableForFree) + freeLevelIcon.tintColor = colorForAvailabilityStatus(feature.isAvailableForFree) + + vpnLevelIcon.image = imageForAvailabilityStatus(feature.isAvailableOnVPN) + vpnLevelIcon.tintColor = colorForAvailabilityStatus(feature.isAvailableOnVPN) + + proLevelIcon.image = imageForAvailabilityStatus(feature.isAvailableOnPro) + proLevelIcon.tintColor = colorForAvailabilityStatus(feature.isAvailableOnPro) + } + + private func imageForAvailabilityStatus(_ isAvailable: Bool) -> UIImage { + let image = (isAvailable ? UIImage(named: "Checkmark") : UIImage(named: "ComparisonTableXmark")) + return image?.withRenderingMode(.alwaysTemplate) ?? UIImage() + } + + private func colorForAvailabilityStatus(_ isAvailable: Bool) -> UIColor { + return isAvailable ? .confirmedBlue : .gray + } +} diff --git a/LockdowniOS/TablePaywallFeatureRowView.xib b/LockdowniOS/TablePaywallFeatureRowView.xib new file mode 100644 index 0000000..c4160a2 --- /dev/null +++ b/LockdowniOS/TablePaywallFeatureRowView.xib @@ -0,0 +1,95 @@ + + + + + + + + + + + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/TablePaywallTableHeaderView/TablePaywallHeaderView.swift b/LockdowniOS/TablePaywallTableHeaderView/TablePaywallHeaderView.swift new file mode 100644 index 0000000..0a27885 --- /dev/null +++ b/LockdowniOS/TablePaywallTableHeaderView/TablePaywallHeaderView.swift @@ -0,0 +1,32 @@ +// +// TablePaywallHeaderView.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/5/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class TablePaywallHeaderView: UIView, NibLoadable { + + @IBOutlet private var featureTitleLabel: UILabel! + @IBOutlet private var freeLevelTitleLabel: UILabel! + @IBOutlet private var vpnLevelTitleLabel: UILabel! + @IBOutlet private var proLevelTitleLabel: UILabel! + + static func make() -> TablePaywallHeaderView { + let view = TablePaywallHeaderView.loadFromNib() + + view.featureTitleLabel.text = .localized("feature_table_column") + view.freeLevelTitleLabel.text = .localized("feature_level_table_column") + + let fontSize: CGFloat = UIScreen.main.traitCollection.userInterfaceIdiom == .pad ? 16 : 11 + view.featureTitleLabel.font = .semiboldLockdownFont(size: fontSize) + view.freeLevelTitleLabel.font = .semiboldLockdownFont(size: fontSize) + view.vpnLevelTitleLabel.font = .semiboldLockdownFont(size: fontSize) + view.proLevelTitleLabel.font = .semiboldLockdownFont(size: fontSize) + + return view + } +} diff --git a/LockdowniOS/TablePaywallTableHeaderView/TablePaywallHeaderView.xib b/LockdowniOS/TablePaywallTableHeaderView/TablePaywallHeaderView.xib new file mode 100644 index 0000000..8d00d48 --- /dev/null +++ b/LockdowniOS/TablePaywallTableHeaderView/TablePaywallHeaderView.xib @@ -0,0 +1,97 @@ + + + + + + + + + + + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/TablePaywallViewController.swift b/LockdowniOS/TablePaywallViewController.swift new file mode 100644 index 0000000..33b78cd --- /dev/null +++ b/LockdowniOS/TablePaywallViewController.swift @@ -0,0 +1,369 @@ +// +// TablePaywallViewController.swift +// Lockdown +// +// Created by Alexander Parshakov on 8/29/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import Foundation +import PromiseKit +import StoreKit +import UIKit + +protocol PaywallViewControllerCloseDelegate: AnyObject { + func didClosePaywall() +} + +final class TablePaywallViewController: BaseViewController, Loadable { + + @IBOutlet private var restorePurchaseLabel: UILabel! + + @IBOutlet private var titleLabel: UILabel! + + @IBOutlet private var featureStackView: UIStackView! + + @IBOutlet private var chooseYourPlanLabel: UILabel! + @IBOutlet private var segmentedControl: UISegmentedControl! + + @IBOutlet private var annualOfferView: UIView! + @IBOutlet private var annualOfferTitleLabel: UILabel! + @IBOutlet private var annualOfferSubtitleLabel: UILabel! + @IBOutlet private var saveMoneyContainerView: UIView! + @IBOutlet private var saveMoneyLabel: UILabel! + + @IBOutlet private var monthlyOfferView: UIView! + @IBOutlet private var monthlyOfferTitleLabel: UILabel! + @IBOutlet private var monthlyOfferSubtitleLabel: UILabel! + + @IBOutlet private var shadowView: UIView! + @IBOutlet private var continueButton: UIButton! + @IBOutlet private var privacyPolicyLabel: UILabel! + @IBOutlet private var redeemCodeLabel: UILabel! + @IBOutlet private var termsOfServiceLabel: UILabel! + + @IBOutlet private var offerStackViewBottomConstraint: NSLayoutConstraint! + @IBOutlet private var scrollViewBottomConstraint: NSLayoutConstraint! + + private var annualOfferGestureRecognizer: UITapGestureRecognizer? + private var monthlyOfferGestureRecognizer: UITapGestureRecognizer? + + private var continueButtonGradientLayer: CAGradientLayer? + + private let paywallTableFeatures: [PaywallTableFeature] + private let existingSubscription: Subscription? + private let paywallService: PaywallService + + weak var delegate: PaywallViewControllerCloseDelegate? + + private var currentGroup: AppStoreProductGroup = .pro { + didSet { + updateScreenState() + } + } + private var currentPeriod: SubscriptionOfferPeriodUnit = .year { + didSet { + updateScreenState() + } + } + + init(paywallTableFeatures: [PaywallTableFeature] = .allDefaultFeatures, + existingSubscription: Subscription? = BaseUserService.shared.user.currentSubscription, + paywallService: PaywallService = BasePaywallService.shared) { + self.paywallTableFeatures = paywallTableFeatures + self.existingSubscription = existingSubscription + self.paywallService = paywallService + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + VPNSubscription.cacheLocalizedPrices() + + setupUI() + updateScreenState() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + saveMoneyContainerView.corners = .continuous(saveMoneyContainerView.bounds.midY) + + continueButton.applyGradient(.lightBlue, corners: .continuous(continueButton.bounds.midY)) + } + + deinit { + delegate?.didClosePaywall() + + if UserDefaults.hasSeenLTO == false { + paywallService.context = .followUpLimitedTimeOffer + } + } + + @IBAction private func didTapClose(_ sender: Any) { + dismiss(animated: true) + } + + @IBAction private func didSelectSegment(_ sender: Any) { + currentGroup = segmentedControl.selectedSegmentIndex == 0 ? .firewallAndVpn : .pro + updateScreenState() + } + + @IBAction private func didTapContinue(_ sender: Any) { + continueButton.showAnimatedPress { [weak self] in + self?.updateChosenOffer() + self?.purchaseProduct() + } + } + + // MARK: - Private Methods + + private func setupUI() { + setupTexts() + updateFeatureStackView() + setupSegmentedControl() + updateOfferViews() + setupShadowFromContinue() + updateViewsForIpad() + setupGestureRecognizers() + } + + private func updateFeatureStackView() { + featureStackView.spacing = isPad ? 6 : 3 + featureStackView.clear() + + featureStackView.addArrangedSubview(TablePaywallHeaderView.make()) + + paywallTableFeatures.forEach { + featureStackView.addArrangedSubview(Separator(height: isPad ? 2 : 1)) + + let rowView = TablePaywallFeatureRowView.make(feature: $0, isHighlighted: currentGroup.hasFeature($0)) + featureStackView.addArrangedSubview(rowView) + } + } + + private func setupSegmentedControl() { + segmentedControl.selectedSegmentIndex = currentGroup == .firewallAndVpn ? 0 : 1 + + if UIScreen.main.traitCollection.userInterfaceIdiom == .pad { + segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white, .font: UIFont.boldLockdownFont(size: 15)], for: .selected) + segmentedControl.setTitleTextAttributes([.font: UIFont.mediumLockdownFont(size: 16)], for: .normal) + } else { + segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white, .font: UIFont.boldLockdownFont(size: 13)], for: .selected) + segmentedControl.setTitleTextAttributes([.font: UIFont.mediumLockdownFont(size: 13)], for: .normal) + } + + segmentedControl.setTitle("VPN", forSegmentAt: 0) + segmentedControl.setTitle("PRO", forSegmentAt: 1) + } + + private func setupTexts() { + restorePurchaseLabel.text = .localized("restore_purchase") + + titleLabel.text = .localized("start_trial_now_to_get_full_access") + chooseYourPlanLabel.text = .localized("CHOOSE_YOUR_PLAN") + + annualOfferTitleLabel.text = .localized("plan_annual") + monthlyOfferTitleLabel.text = .localized("plan_monthly") + + continueButton.setTitle(.localized("continue"), for: .normal) + + privacyPolicyLabel.text = .localized("Privacy Policy") + if #available(iOS 14.0, *) { + redeemCodeLabel.isHidden = false + redeemCodeLabel.text = .localized("redeem_code") + } + termsOfServiceLabel.text = .localized("terms_of_service") + + updateSaveMoneyLabel() + } + + private func updateSaveMoneyLabel() { + saveMoneyContainerView.isHidden = false + let localizationKey = currentGroup == .firewallAndVpn ? "save_annual_vpn_subscription" : "save_annual_pro_subscription" + saveMoneyLabel.text = .localized(localizationKey) + } + + private func updateOfferViews() { + [annualOfferView, monthlyOfferView].forEach { $0?.recolorToDefault() } + + [annualOfferTitleLabel, annualOfferSubtitleLabel, monthlyOfferTitleLabel, monthlyOfferSubtitleLabel].forEach { + $0?.textColor = .confirmedBlue + } + + // Highlighting the view for currently selected period + let currentOfferView = currentPeriod == .year ? annualOfferView : monthlyOfferView + let currentTitleLabel = currentPeriod == .year ? annualOfferTitleLabel : monthlyOfferTitleLabel + let currentSubtitleLabel = currentPeriod == .year ? annualOfferSubtitleLabel : monthlyOfferSubtitleLabel + + currentOfferView?.backgroundColor = .confirmedBlue + currentTitleLabel?.textColor = .white + currentSubtitleLabel?.textColor = .white + } + + private func setupShadowFromContinue() { + shadowView.layer.shadowColor = UIColor.systemBackground.cgColor + shadowView.layer.masksToBounds = false + shadowView.layer.shadowOffset = CGSize(width: 0, height : -20) + shadowView.layer.shadowPath = UIBezierPath(rect: shadowView.bounds).cgPath + shadowView.layer.shadowOpacity = 1 + shadowView.layer.shadowRadius = 25 + } + + private func updateViewsForIpad() { + guard UIScreen.main.traitCollection.userInterfaceIdiom == .pad else { return } + + restorePurchaseLabel.font = restorePurchaseLabel.font.withSize(16) + chooseYourPlanLabel.font = chooseYourPlanLabel.font.withSize(24) + + annualOfferTitleLabel.font = annualOfferTitleLabel.font.withSize(20) + annualOfferSubtitleLabel.font = annualOfferSubtitleLabel.font.withSize(18) + saveMoneyLabel.font = saveMoneyLabel.font.withSize(17) + monthlyOfferTitleLabel.font = monthlyOfferTitleLabel.font.withSize(20) + monthlyOfferSubtitleLabel.font = monthlyOfferSubtitleLabel.font.withSize(18) + + privacyPolicyLabel.font = privacyPolicyLabel.font.withSize(16) + redeemCodeLabel.font = redeemCodeLabel.font.withSize(16) + termsOfServiceLabel.font = termsOfServiceLabel.font.withSize(16) + + scrollViewBottomConstraint.constant = 20 + offerStackViewBottomConstraint.constant = 50 + } + + private func setupGestureRecognizers() { + let restorePurchaseTap = UITapGestureRecognizer(target: self, action: #selector(restorePurchase)) + restorePurchaseLabel.isUserInteractionEnabled = true + restorePurchaseLabel.addGestureRecognizer(restorePurchaseTap) + + let annualTap = UITapGestureRecognizer(target: self, action: #selector(choseAnnualOffer)) + annualOfferView.addGestureRecognizer(annualTap) + annualOfferGestureRecognizer = annualTap + + let monthlyTap = UITapGestureRecognizer(target: self, action: #selector(choseMonthlyOffer)) + monthlyOfferView.addGestureRecognizer(monthlyTap) + monthlyOfferGestureRecognizer = monthlyTap + + let privacyPolicyTap = UITapGestureRecognizer(target: self, action: #selector(showPrivacyPolicy)) + privacyPolicyLabel.isUserInteractionEnabled = true + privacyPolicyLabel.addGestureRecognizer(privacyPolicyTap) + + let redeemCodeTap = UITapGestureRecognizer(target: self, action: #selector(showRedeemCode)) + redeemCodeLabel.isUserInteractionEnabled = true + redeemCodeLabel.addGestureRecognizer(redeemCodeTap) + + let termsOfServiceTap = UITapGestureRecognizer(target: self, action: #selector(showTermsOfService)) + termsOfServiceLabel.isUserInteractionEnabled = true + termsOfServiceLabel.addGestureRecognizer(termsOfServiceTap) + } + + @objc private func choseAnnualOffer() { + currentPeriod = .year + } + + @objc private func choseMonthlyOffer() { + currentPeriod = .month + } + + @objc private func showPrivacyPolicy() { + showModalWebView(title: .localized("Privacy Policy"), urlString: .lockdownUrlPrivacy) + } + + @objc private func showRedeemCode() { + paywallService.showRedemptionSheet() + } + + @objc private func showTermsOfService() { + showModalWebView(title: .localized("terms_of_service"), urlString: .lockdownUrlTerms) + } + + private func updateScreenState() { + // If period and group of existing subscription (plan) are selected, + // we must force-select the period of the other plan. + if existingSubscription?.isSubscription(in: currentGroup, of: currentPeriod) == true { + currentPeriod = currentPeriod == .year ? .month : .year + } + + updateFeatureStackView() + updateSaveMoneyLabel() + updateOfferViews() + updatePriceSubtitles() + + annualOfferGestureRecognizer?.isEnabled = true + monthlyOfferGestureRecognizer?.isEnabled = true + + guard let existingSubscription, existingSubscription.correspondingProductGroup == currentGroup else { return } + + let isAnnual = [.proAnnual, .annual, .proAnnualLTO].contains(existingSubscription.planType) + + // If existing subscription is annual, no need to show the save-label. + saveMoneyContainerView.isHidden = isAnnual + + let offerView = isAnnual ? annualOfferView : monthlyOfferView + let subtitleLabel = isAnnual ? annualOfferSubtitleLabel : monthlyOfferSubtitleLabel + let gestureRecognizer = isAnnual ? annualOfferGestureRecognizer : monthlyOfferGestureRecognizer + + offerView?.layer.borderWidth = 0 + subtitleLabel?.text = .localized("current_plan") + gestureRecognizer?.isEnabled = false + } + + private func updateChosenOffer() { + switch currentGroup { + case .firewallAndVpn: + if currentPeriod == .year { + VPNSubscription.selectedProductId = VPNSubscription.productIdAnnual + } else { + VPNSubscription.selectedProductId = VPNSubscription.productIdMonthly + } + case .pro: + if currentPeriod == .year { + VPNSubscription.selectedProductId = VPNSubscription.productIdAnnualPro + } else { + VPNSubscription.selectedProductId = VPNSubscription.productIdMonthlyPro + } + } + } + + private func updatePriceSubtitles() { + switch currentGroup { + case .firewallAndVpn: + monthlyOfferSubtitleLabel.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthly, for: .new) + annualOfferSubtitleLabel.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnual, for: .new) + case .pro: + monthlyOfferSubtitleLabel.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthlyPro, for: .new) + annualOfferSubtitleLabel.text = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnualPro, for: .new) + } + } +} + +extension TablePaywallViewController { + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + shadowView.layer.shadowColor = UIColor.systemBackground.cgColor + } +} + +// MARK: - Purchase and Restore + +extension TablePaywallViewController: ProductPurchasable { + @objc private func restorePurchase() { + restorePurchases() + } +} + +private extension UIView { + func recolorToDefault() { + corners = .continuous(10) + layer.borderWidth = 2 + layer.borderColor = UIColor.confirmedBlue.cgColor + + backgroundColor = .systemBackground + } +} diff --git a/LockdowniOS/TablePaywallViewController.xib b/LockdowniOS/TablePaywallViewController.xib new file mode 100644 index 0000000..fc851d2 --- /dev/null +++ b/LockdowniOS/TablePaywallViewController.xib @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + Montserrat-Bold + + + Montserrat-Regular + + + Montserrat-SemiBold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/TitleViewController.swift b/LockdowniOS/TitleViewController.swift index 5fb9842..341b50a 100644 --- a/LockdowniOS/TitleViewController.swift +++ b/LockdowniOS/TitleViewController.swift @@ -50,7 +50,7 @@ class TitleViewController: BaseViewController { self.titleLabel.shine(animated: true, completion: { UIView.animate(withDuration: 1.5, animations: { self.descriptionLabel.alpha = 1 - }, completion: { e in + }, completion: { _ in UIView.animate(withDuration: 1.5, animations: { self.getStartedButton.alpha = 1 self.whyTrustButton.alpha = 1 @@ -67,7 +67,7 @@ class TitleViewController: BaseViewController { defaults.set(true, forKey: kHasShownTitlePage) PushNotifications.Authorization.requestWeeklyUpdateAuthorization(presentingDialogOn: self).done { _ in - OneTimeActions.markAsSeen(.notificationAuthorizationRequestPopup) + OneTimeActions.markAsSeen(.welcomeScreen) self.performSegue(withIdentifier: "getStartedTapped", sender: self) }.catch { error in DDLogWarn(error.localizedDescription) diff --git a/LockdowniOS/TrackerInfo.swift b/LockdowniOS/TrackerInfo.swift index 17b12d9..dfe8344 100644 --- a/LockdowniOS/TrackerInfo.swift +++ b/LockdowniOS/TrackerInfo.swift @@ -10,8 +10,8 @@ import Foundation import CocoaLumberjackSwift struct TrackerInfo: Decodable { - private var trackerIds: [String : String] - private var descriptions: [String : TrackerDescription] + private var trackerIds: [String: String] + private var descriptions: [String: TrackerDescription] func description(forDomain domain: String) -> TrackerDescription? { if let trackerId = trackerIds[domain] { @@ -53,9 +53,14 @@ final class TrackerInfoRegistry { let userBlocked = getUserBlockedDomains() if userBlocked.keys.contains(domain) { - return TrackerDescription(title: domain, description: NSLocalizedString("You blocked this domain in your custom blocked domains.", comment: "")) + return TrackerDescription(title: domain, description: .localized("You blocked this domain in your custom blocked domains.")) } else if let match = userBlocked.keys.first(where: { domain.hasSuffix($0) }) { - return TrackerDescription(title: domain, description: "\(NSLocalizedString("You blocked", comment: "Used in: You blocked tracker.com in your custom blocked domains.")) \(match) \(NSLocalizedString("in your custom blocked domains.", comment: "Used in: You blocked tracker.com in your custom blocked domains."))") + let youBlocked: String = .localized("You blocked", comment: "Used in: You blocked tracker.com in your custom blocked domains.") + let customDomains: String = .localized("in your custom blocked domains.", + comment: "Used in: You blocked tracker.com in your custom blocked domains.") + return TrackerDescription( + title: domain, + description: "\(youBlocked) \(match) \(customDomains)") } let blocked = getLockdownBlockedDomains().lockdownDefaults @@ -68,11 +73,12 @@ final class TrackerInfoRegistry { let groupsFormatted = TrackerInfoRegistry.formatList(strings: groups) if !groups.isEmpty { - return TrackerDescription(title: domain, - description: "\(domain) \(NSLocalizedString("is a part of", comment: "Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list")) \(groupsFormatted) \(NSLocalizedString("block list", comment: "Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list"))") + let partOf: String = .localized("is a part of", comment: "Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list") + let blocklist: String = .localized("block list", comment: "Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list") + return TrackerDescription(title: domain, description: "\(domain) \(partOf) \(groupsFormatted) \(blocklist)") } - return TrackerDescription(title: NSLocalizedString("No Info Found", comment: ""), description: NSLocalizedString("No additional information on this blocked domain found.", comment: "")) + return TrackerDescription(title: .localized("No Info Found"), description: .localized("No additional information on this blocked domain found.")) } static private func formatList(strings: [String]) -> String { @@ -85,7 +91,7 @@ final class TrackerInfoRegistry { let last = strings.removeLast() return strings - .joined(separator: ", ") + NSLocalizedString(" and ", comment: "Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com") + last + .joined(separator: ", ") + .localized(" and ", comment: "Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com") + last } private func getTrackerInfoDoc() throws -> TrackerInfo { diff --git a/LockdowniOS/UserService.swift b/LockdowniOS/UserService.swift new file mode 100644 index 0000000..4aaf432 --- /dev/null +++ b/LockdowniOS/UserService.swift @@ -0,0 +1,76 @@ +// +// UserService.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/2/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import PromiseKit + +protocol UserService: AnyObject { + var user: LockdownUser { get } + + func updateUserSubscription(completion: @escaping (Subscription?) -> Void) +} + +final class BaseUserService: UserService { + + static let shared: UserService = BaseUserService() + + var user = LockdownUser() + + func updateUserSubscription(completion: @escaping (Subscription?) -> Void) { + if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { + DDLogInfo("plan status: have confirmed API credentials, using them") + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .then { (signin: SignIn) -> Promise in + DDLogInfo("plan status: signin result: \(signin)") + return try Client.subscriptionEvent() + } + .then { (result: SubscriptionEvent) -> Promise<[Subscription]> in + DDLogInfo("plan status: subscriptionevent result: \(result)") + return try Client.activeSubscriptions() + }.done { subscriptions in + DDLogInfo("active-subs: \(subscriptions)") + NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) + + self.user.updateSubscription(to: subscriptions.first) + completion(subscriptions.first) + } + .catch { error in + if let apiError = error as? ApiError { + DDLogError("Error loading plan: API error code - \(apiError.code)") + } else { + DDLogError("Error loading plan: Non-API Error - \(error.localizedDescription)") + } + self.user.updateSubscription(to: nil) + completion(nil) + } + } else { + firstly { + try Client.signIn() + }.then { _ in + try Client.activeSubscriptions() + }.done { subscriptions in + DDLogInfo("active-subs: \(subscriptions)") + NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) + + self.user.updateSubscription(to: subscriptions.first) + completion(subscriptions.first) + }.catch { error in + DDLogError("Error reloading subscription: \(error.localizedDescription)") + if let apiError = error as? ApiError { + DDLogError("Error loading plan: API error code - \(apiError.code)") + } else { + DDLogError("Error loading plan: Non-API Error - \(error.localizedDescription)") + } + self.user.updateSubscription(to: nil) + completion(nil) + } + } + } +} diff --git a/LockdowniOS/VPNController.swift b/LockdowniOS/VPNController.swift index 952ed13..de6a14e 100644 --- a/LockdowniOS/VPNController.swift +++ b/LockdowniOS/VPNController.swift @@ -29,15 +29,14 @@ class VPNController: NSObject { func restart() { // Don't let this affect userWantsVPNOn/Off config - VPNController.shared.setEnabled(false, completion: { - error in + VPNController.shared.setEnabled(false) { _ in // TODO: Handle the error VPNController.shared.setEnabled(true) - }) + } } - func isConfigurationExisting(_ completion: @escaping (Bool) -> ()) { - manager.loadFromPreferences { (error) in + func isConfigurationExisting(_ completion: @escaping (Bool) -> Void) { + manager.loadFromPreferences { (_) in completion(self.manager.protocolConfiguration != nil) } } @@ -48,12 +47,11 @@ class VPNController: NSObject { if #available(iOSApplicationExtension 14.0, iOS 14.0, *) { WidgetCenter.shared.reloadAllTimelines() } - if (enabled) { + if enabled { setUpAndEnableVPN { error in completion(error) } - } - else { + } else { manager.loadFromPreferences(completionHandler: {(_ error: Error?) -> Void in self.manager.isEnabled = false self.manager.isOnDemandEnabled = false @@ -88,6 +86,9 @@ class VPNController: NSObject { p.useExtendedAuthentication = false p.disconnectOnSleep = false p.enablePFS = true +// if #available(iOSApplicationExtension 14.0, iOS 14.0, *) { +// p.includeAllNetworks = true +// } p.childSecurityAssociationParameters.diffieHellmanGroup = NEVPNIKEv2DiffieHellmanGroup.group19 p.childSecurityAssociationParameters.encryptionAlgorithm = NEVPNIKEv2EncryptionAlgorithm.algorithmAES128GCM @@ -118,17 +119,15 @@ class VPNController: NSObject { self.manager.saveToPreferences(completionHandler: {(_ error: Error?) -> Void in if let e = error { DDLogError("Saving VPN Error \(e)") - if ((e as NSError).code == 4) { // if config is stale, probably multithreading bug + if (e as NSError).code == 4 { // if config is stale, probably multithreading bug DDLogInfo("Stale config, trying again") self.setUpAndEnableVPN(completion: { error in completion(error) }) - } - else { + } else { completion(e) } - } - else { + } else { do { // manually activate the starting of the tunnel, and also do a dummy connect to a nonexistant, invalid URL to force enabling try self.manager.connection.startVPNTunnel() @@ -137,12 +136,11 @@ class VPNController: NSObject { config.urlCache = nil let session = URLSession.init(configuration: config) let url = URL(string: "https://nonexistant_invalid_url") - let task = session.dataTask(with: url!) { (data, response, error) in + let task = session.dataTask(with: url!) { (_, _, _) in return } task.resume() - } - catch { + } catch { DDLogError("Unable to start the tunnel after saving: " + error.localizedDescription) } completion(nil) diff --git a/LockdowniOS/VPNSubscription.swift b/LockdowniOS/VPNSubscription.swift index 9169ec5..900217b 100644 --- a/LockdowniOS/VPNSubscription.swift +++ b/LockdowniOS/VPNSubscription.swift @@ -20,40 +20,44 @@ class VPNSubscription: NSObject { static let productIdAnnual = "LockdowniOSVpnAnnual" static let productIdMonthlyPro = "LockdowniOSVpnMonthlyPro" static let productIdAnnualPro = "LockdowniOSVpnAnnualPro" - static let productIds: Set = [productIdMonthly, productIdAnnual, productIdMonthlyPro, productIdAnnualPro] + static let productIdAnnualProLTO = "LockdownProAnnualLTO" + + static let productIds: Set = [productIdMonthly, productIdAnnual, productIdMonthlyPro, productIdAnnualPro, productIdAnnualProLTO] static var selectedProductId = productIdMonthly static var defaultPriceStringMonthly = "$7.99 per month after" static var defaultPriceStringMonthlyPro = "$11.99 per month after" static var defaultPriceStringAnnual = "$49.99/year after (~$4.17/month)" static var defaultPriceStringAnnualPro = "$99.99/year after (~$8.33/month)" + static var defaultPriceStringAnnualProLTO = "$59.99/year after (~$8.33/month)" static var defaultUpgradePriceStringMonthly = "$7.99 per month" static var defaultUpgradePriceStringMonthlyPro = "$11.99 per month" static var defaultUpgradePriceStringAnnual = "$49.99/year (~$4.17/month)" static var defaultUpgradePriceStringAnnualPro = "$99.99/year (~$8.33/month)" + static var defaultUpgradePriceStringAnnualProLTO = "$59.99/year (~$4.99/month)" static func purchase(succeeded: @escaping () -> Void, errored: @escaping (Error) -> Void) { DDLogInfo("purchase") SwiftyStoreKit.purchaseProduct(selectedProductId, atomically: true) { result in switch result { - case .success: - firstly { - try Client.signIn() - } - .then { (signin: SignIn) -> Promise in - try Client.getKey() - } - .done { (getKey: GetKey) in - try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) - succeeded() - } - .catch { error in - errored(error) - } - case .error(let error): - DDLogError("purchase error: \(error)") + case .success: + firstly { + try Client.signIn() + } + .then { _ in + try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + succeeded() + } + .catch { error in errored(error) + } + case .error(let error): + DDLogError("purchase error: \(error)") + errored(error) } } } @@ -87,21 +91,22 @@ class VPNSubscription: NSObject { if let price = UserDefaults.standard.string(forKey: productId + "Price") { DDLogInfo("Got product id price for \(productId): \(price)") return price - } - else { + } else { DDLogError("Found no cached price for productId \(productId), returning default") switch productId { - case productIdMonthly: - return defaultPriceStringMonthly - case productIdMonthlyPro: - return defaultPriceStringMonthlyPro - case productIdAnnual: - return defaultPriceStringAnnual - case productIdAnnualPro: - return defaultPriceStringAnnualPro - default: - DDLogError("Invalid product Id: \(productId)") - return "Invalid Price" + case productIdMonthly: + return defaultPriceStringMonthly + case productIdMonthlyPro: + return defaultPriceStringMonthlyPro + case productIdAnnual: + return defaultPriceStringAnnual + case productIdAnnualPro: + return defaultPriceStringAnnualPro + case productIdAnnualProLTO: + return defaultPriceStringAnnualProLTO + default: + DDLogError("Invalid product Id: \(productId)") + return "Invalid Price" } } } @@ -111,26 +116,27 @@ class VPNSubscription: NSObject { if let upgradePrice = UserDefaults.standard.string(forKey: productId + "UpgradePrice") { DDLogInfo("Got product id upgrade price for \(productId): \(upgradePrice)") return upgradePrice - } - else { + } else { DDLogError("Found no cached upgrade price for productId \(productId), returning default") switch productId { - case productIdMonthly: - return defaultUpgradePriceStringMonthly - case productIdMonthlyPro: - return defaultUpgradePriceStringMonthlyPro - case productIdAnnual: - return defaultUpgradePriceStringAnnual - case productIdAnnualPro: - return defaultUpgradePriceStringAnnualPro - default: - DDLogError("Invalid product Id: \(productId)") - return "Invalid Upgrade Price" + case productIdMonthly: + return defaultUpgradePriceStringMonthly + case productIdMonthlyPro: + return defaultUpgradePriceStringMonthlyPro + case productIdAnnual: + return defaultUpgradePriceStringAnnual + case productIdAnnualPro: + return defaultUpgradePriceStringAnnualPro + case productIdAnnualProLTO: + return defaultUpgradePriceStringAnnualProLTO + default: + DDLogError("Invalid product Id: \(productId)") + return "Invalid Upgrade Price" } } } - static func cacheLocalizedPrices() -> Void { + static func cacheLocalizedPrices() { let currencyFormatter = NumberFormatter() currencyFormatter.usesGroupingSeparator = true @@ -145,60 +151,65 @@ class VPNSubscription: NSObject { if product.productIdentifier == productIdMonthly { if product.localizedPrice != nil { DDLogInfo("setting monthly display price = " + product.localizedPrice!) - setProductIdPrice(productId: productIdMonthly, price: "\(product.localizedPrice!) per month after") - setProductIdUpgradePrice(productId: productIdMonthly, upgradePrice: "\(product.localizedPrice!) per month") - } - else { + setProductIdPrice(productId: productIdMonthly, price: "\(product.localizedPrice!)/" + .localized("month_after_slash")) + setProductIdUpgradePrice(productId: productIdMonthly, + upgradePrice: "\(product.localizedPrice!)/" + .localized("month_after_slash")) + } else { DDLogError("monthly nil localizedPrice, setting default") setProductIdPrice(productId: productIdMonthly, price: defaultPriceStringMonthly) setProductIdUpgradePrice(productId: productIdMonthly, upgradePrice: defaultUpgradePriceStringMonthly) } - } - else if product.productIdentifier == productIdMonthlyPro { + } else if product.productIdentifier == productIdMonthlyPro { if product.localizedPrice != nil { DDLogInfo("setting monthlyPro display price = " + product.localizedPrice!) - setProductIdPrice(productId: productIdMonthlyPro, price: "\(product.localizedPrice!) per month after") - setProductIdUpgradePrice(productId: productIdMonthlyPro, upgradePrice: "\(product.localizedPrice!) per month") - } - else { + setProductIdPrice(productId: productIdMonthlyPro, price: "\(product.localizedPrice!)/" + .localized("month_after_slash")) + setProductIdUpgradePrice(productId: productIdMonthlyPro, + upgradePrice: "\(product.localizedPrice!)/" + .localized("month_after_slash")) + } else { DDLogError("monthlyPro nil localizedPrice, setting default") setProductIdPrice(productId: productIdMonthlyPro, price: defaultPriceStringMonthlyPro) setProductIdUpgradePrice(productId: productIdMonthlyPro, upgradePrice: defaultUpgradePriceStringMonthlyPro) } - } - else if product.productIdentifier == productIdAnnual { + } else if product.productIdentifier == productIdAnnual { currencyFormatter.locale = product.priceLocale - let priceMonthly = product.price.dividing(by: 12) DDLogInfo("annual price = \(product.price)") - if let priceString = currencyFormatter.string(from: product.price), let priceStringMonthly = currencyFormatter.string(from: priceMonthly) { + if let priceString = currencyFormatter.string(from: product.price) { DDLogInfo("setting annual display price = annual product price / 12 = " + priceString) - setProductIdPrice(productId: productIdAnnual, price: "\(priceString)/year after (~\(priceStringMonthly)/month)") - setProductIdUpgradePrice(productId: productIdAnnual, upgradePrice: "\(priceString)/year (~\(priceStringMonthly)/month)") - } - else { + setProductIdPrice(productId: productIdAnnual, price: "\(priceString)/" + .localized("year_after_slash")) + setProductIdUpgradePrice(productId: productIdAnnual, upgradePrice: "\(priceString)/" + .localized("year_after_slash")) + } else { DDLogError("unable to format price with currencyformatter: " + product.price.stringValue) setProductIdPrice(productId: productIdAnnual, price: defaultPriceStringAnnual) setProductIdUpgradePrice(productId: productIdAnnual, upgradePrice: defaultUpgradePriceStringAnnual) } - } - else if product.productIdentifier == productIdAnnualPro { + } else if product.productIdentifier == productIdAnnualPro { currencyFormatter.locale = product.priceLocale - let priceMonthly = product.price.dividing(by: 12) DDLogInfo("annualPro price = \(product.price)") - if let priceString = currencyFormatter.string(from: product.price), let priceStringMonthly = currencyFormatter.string(from: priceMonthly) { + if let priceString = currencyFormatter.string(from: product.price) { DDLogInfo("setting annualPro display price = annualPro product price / 12 = " + priceString) - setProductIdPrice(productId: productIdAnnualPro, price: "\(priceString)/year after (~\(priceStringMonthly)/month)") - setProductIdUpgradePrice(productId: productIdAnnualPro, upgradePrice: "\(priceString)/year (~\(priceStringMonthly)/month)") - } - else { + setProductIdPrice(productId: productIdAnnualPro, price: "\(priceString)/" + .localized("year_after_slash")) + setProductIdUpgradePrice(productId: productIdAnnualPro, upgradePrice: "\(priceString)/" + .localized("year_after_slash")) + } else { DDLogError("unable to format price with currencyformatter: " + product.price.stringValue) setProductIdPrice(productId: productIdAnnualPro, price: defaultPriceStringAnnualPro) setProductIdUpgradePrice(productId: productIdAnnualPro, upgradePrice: defaultUpgradePriceStringAnnualPro) } + } else if product.productIdentifier == productIdAnnualProLTO { + currencyFormatter.locale = product.priceLocale + DDLogInfo("annualPro price = \(product.price)") + if let priceString = currencyFormatter.string(from: product.price) { + DDLogInfo("setting annualPro LTO display price = annualPro product price / 12 = " + priceString) + setProductIdPrice(productId: productIdAnnualProLTO, price: "\(priceString)/" + .localized("year_after_slash")) + setProductIdUpgradePrice(productId: productIdAnnualProLTO, upgradePrice: "\(priceString)/" + .localized("year_after_slash")) + } else { + DDLogError("unable to format price with currencyformatter: " + product.price.stringValue) + setProductIdPrice(productId: productIdAnnualProLTO, price: defaultPriceStringAnnualProLTO) + setProductIdUpgradePrice(productId: productIdAnnualProLTO, upgradePrice: defaultUpgradePriceStringAnnualProLTO) + } } } for invalidProductId in result.invalidProductIDs { - DDLogError("invalid product id: \(invalidProductId)"); + DDLogError("invalid product id: \(invalidProductId)") } } } @@ -216,13 +227,15 @@ extension Subscription.PlanType { return VPNSubscription.productIdMonthlyPro case .proAnnual: return VPNSubscription.productIdAnnualPro + case .proAnnualLTO: + return VPNSubscription.productIdAnnualProLTO default: return nil } } static var supported: [Subscription.PlanType] { - return [.monthly, .annual, .proMonthly, .proAnnual] + return [.monthly, .annual, .proMonthly, .proAnnual, .proAnnualLTO] } var availableUpgrades: [Subscription.PlanType]? { diff --git a/LockdowniOS/WebViewViewController.swift b/LockdowniOS/WebViewViewController.swift index 4cbf3ac..235be5d 100644 --- a/LockdowniOS/WebViewViewController.swift +++ b/LockdowniOS/WebViewViewController.swift @@ -8,41 +8,97 @@ import WebKit import UIKit -class WebViewViewController: BaseViewController, WKNavigationDelegate { +protocol WebViewViewControllerDelegate: AnyObject { + func webViewDidDisappear() +} + +final class WebViewViewController: BaseViewController { + + @IBOutlet private var webView: WKWebView! + @IBOutlet private var titleLabel: UILabel! + + weak var delegate: WebViewViewControllerDelegate? - @IBOutlet weak var activity: UIActivityIndicatorView! - @IBOutlet weak var webView: WKWebView! - @IBOutlet weak var titleLabel: UILabel! var url: URL? var titleLabelText: String = "" override func viewDidLoad() { super.viewDidLoad() - if let u = url { - webView.load(URLRequest(url: u)) + + if let url { + webView.load(URLRequest(url: url)) } webView.navigationDelegate = self + webView.uiDelegate = self + webView.allowsBackForwardNavigationGestures = true + titleLabel.text = titleLabelText } - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - if (webView.isLoading == false) { - self.activity.stopAnimating() + @IBAction private func safariTapped(_ sender: Any) { + if let url { + UIApplication.shared.open(url, options: [:], completionHandler: nil) } } - func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { - self.activity.startAnimating() + @IBAction private func dismiss() { + dismiss(animated: true) { + self.delegate?.webViewDidDisappear() + } } +} + +extension WebViewViewController: WKNavigationDelegate, Loadable { - @IBAction func safariTapped(_ sender: Any) { - if let u = url { - UIApplication.shared.open(u, options: [:], completionHandler: nil) - } + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + showLoadingView() } - @IBAction func dismiss() { - self.dismiss(animated: true, completion: nil) + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + hideLoadingView() } + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + guard let url = navigationAction.request.url, + let scheme = url.scheme else { + decisionHandler(.cancel) + return + } + + if scheme.lowercased() == "mailto" { + let email = url.absoluteString.replacingOccurrences(of: "mailto:", with: "") + + if let pageUrl = self.url, let urlComponents = URLComponents(url: pageUrl, resolvingAgainstBaseURL: false) { + if urlComponents.path == "/faq" { + composeEmail(.blockingImprovementIdeas, to: email) + } else if urlComponents.path == "/privacy" { + composeEmail(.termsAndPrivacyPolicy, to: email) + } else { + composeEmail(.custom(subject: "", body: ""), to: email) + } + } + + decisionHandler(.cancel) + return + } + + decisionHandler(.allow) + } +} + +extension WebViewViewController: WKUIDelegate, EmailComposable { + func webView(_ webView: WKWebView, + createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, + windowFeatures: WKWindowFeatures) -> WKWebView? { + + guard navigationAction.targetFrame == nil || navigationAction.targetFrame?.isMainFrame == false else { return nil } + guard let urlToLoad = navigationAction.request.url else { return nil } + + webView.load(URLRequest(url: urlToLoad)) + + return nil + } } diff --git a/LockdowniOS/WhatIsVpnViewController.swift b/LockdowniOS/WhatIsVpnViewController.swift index 0e75133..cac96e6 100644 --- a/LockdowniOS/WhatIsVpnViewController.swift +++ b/LockdowniOS/WhatIsVpnViewController.swift @@ -29,7 +29,7 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate @IBOutlet weak var toggleAnimatedCircle: NVActivityIndicatorView! @IBOutlet weak var button: UIButton! - var parentVC: HomeViewController? = nil + var parentVC: HomeViewController? override func viewDidLoad() { super.viewDidLoad() @@ -51,7 +51,8 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate @IBAction func getStartedTapped(_ sender: Any) { self.dismiss(animated: true, completion: { - self.parentVC?.performSegue(withIdentifier: "showSignup", sender: self) + guard let parentVC = self.parentVC else { return } + BasePaywallService.shared.showPaywall(on: parentVC) }) } @@ -61,14 +62,16 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - let s1 = AwesomeSpotlight(withRect: getRectForView(toggleCircle).insetBy(dx: -20.0, dy: -20.0), shape: .circle, text: NSLocalizedString("Tap to see a demo of how Secure Tunnel protects and anonymizes you.", comment: "")) + let spotlightRect = getRectForView(toggleCircle).insetBy(dx: -20.0, dy: -20.0) + let spotlightText: String = .localized("Tap to see a demo of how Secure Tunnel protects and anonymizes you.") + let s1 = AwesomeSpotlight(withRect: spotlightRect, shape: .circle, text: spotlightText) let spotlightView = AwesomeSpotlightView(frame: view.frame, spotlight: [s1]) spotlightView.cutoutRadius = 8 - spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); + spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75) spotlightView.enableArrowDown = true - spotlightView.textLabelFont = fontMedium16 - spotlightView.labelSpacing = 24; + spotlightView.textLabelFont = .mediumLockdownFont(size: 16) + spotlightView.labelSpacing = 24 spotlightView.delegate = self view.addSubview(spotlightView) spotlightView.start() @@ -76,8 +79,8 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate func setPrivacyState(state: Bool) { privacyEnabled = state - if (state == true) { - vpnActiveLabel.text = NSLocalizedString("Activating", comment: "").uppercased() + if state == true { + vpnActiveLabel.text = .localized("Activating").uppercased() vpnActiveLabel.backgroundColor = .tunnelsBlue toggleCircle.isHidden = true @@ -91,46 +94,45 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate self.toggleCircle.isHidden = false self.toggleCircle.tintColor = .tunnelsBlue - self.vpnActiveLabel.text = NSLocalizedString("Tunnel On", comment: "").uppercased() + self.vpnActiveLabel.text = .localized("Tunnel On").uppercased() self.vpnActiveLabel.backgroundColor = .tunnelsBlue - self.locationLabel.text = NSLocalizedString("Location: 🇯🇵", comment: "") - self.ipLabel.text = NSLocalizedString("IP: [Anonymized]", comment: "") + self.locationLabel.text = .localized("Location: 🇯🇵") + self.ipLabel.text = .localized("IP: [Anonymized]") self.dataLabel.text = "AC90BD4B0A53ED74543425B269\n62179C21D8DAF733EB16F4B41F" self.dataFlow.primaryColor = .blue self.dataFlow.secondaryColor = .tunnelsBlue self.descriptionLabel.attributedText = self.add(stringList: [ - NSLocalizedString("Location changed and hidden", comment: ""), - NSLocalizedString("Anonymize IP against trackers", comment: ""), - NSLocalizedString("Encrypted, private connections", comment: "") + .localized("Location changed and hidden"), + .localized("Anonymize IP against trackers"), + .localized("Encrypted, private connections") ], - font: fontSemiBold15_5, - bulletFont: fontMedium18, + font: .semiboldLockdownFont(size: 15.5), + bulletFont: .mediumLockdownFont(size: 18), bullet: "•", textColor: .tunnelsBlue, bulletColor: .tunnelsBlue) }) - } - else { + } else { toggleCircle.tintColor = .lightGray toggleCircle.isHidden = false toggleAnimatedCircle.stopAnimating() button.tintColor = .lightGray - locationLabel.text = NSLocalizedString("Location: 🇺🇸", comment: "") - ipLabel.text = NSLocalizedString("IP: 18.132.2.87", comment: "") - dataLabel.text = NSLocalizedString("To: joe@email.com\nRe: Q4 2019 Finance Review", comment: "") + locationLabel.text = .localized("Location: 🇺🇸") + ipLabel.text = .localized("IP: 18.132.2.87") + dataLabel.text = .localized("To: joe@email.com\nRe: Q4 2019 Finance Review") dataFlow.primaryColor = .orange dataFlow.secondaryColor = .tunnelsWarning - vpnActiveLabel.text = NSLocalizedString("Tunnel Off", comment: "").uppercased() + vpnActiveLabel.text = .localized("Tunnel Off").uppercased() vpnActiveLabel.backgroundColor = .tunnelsWarning descriptionLabel.attributedText = add(stringList: [ - NSLocalizedString("Precise location exposed", comment: ""), - NSLocalizedString("Unique IP address broadcasted", comment: ""), - NSLocalizedString("Readable browsing and data", comment: "") + .localized("Precise location exposed"), + .localized("Unique IP address broadcasted"), + .localized("Readable browsing and data") ], - font: fontSemiBold15_5, - bulletFont: fontMedium18, + font: .semiboldLockdownFont(size: 15.5), + bulletFont: .mediumLockdownFont(size: 18), bullet: "•", textColor: .tunnelsWarning, bulletColor: .tunnelsWarning) @@ -138,10 +140,9 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate } @IBAction func privTapped(_ sender: Any) { - if(privacyEnabled == true) { + if privacyEnabled == true { setPrivacyState(state: false) - } - else { + } else { setPrivacyState(state: true) } } @@ -160,8 +161,8 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate textColor: UIColor = .darkGray, bulletColor: UIColor = .darkGray) -> NSAttributedString { - let textAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: textColor] - let bulletAttributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: bulletFont, NSAttributedString.Key.foregroundColor: bulletColor] + let textAttributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: textColor] + let bulletAttributes: [NSAttributedString.Key: Any] = [.font: bulletFont, .foregroundColor: bulletColor] let paragraphStyle = NSMutableParagraphStyle() let nonOptions = [NSTextTab.OptionKey: Any]() @@ -178,15 +179,15 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate let attributedString = NSMutableAttributedString(string: formattedString) attributedString.addAttributes( - [NSAttributedString.Key.paragraphStyle : paragraphStyle], - range: NSMakeRange(0, attributedString.length)) + [NSAttributedString.Key.paragraphStyle: paragraphStyle], + range: NSRange(location: 0, length: attributedString.length)) attributedString.addAttributes( textAttributes, - range: NSMakeRange(0, attributedString.length)) + range: NSRange(location: 0, length: attributedString.length)) - let string:NSString = NSString(string: formattedString) - let rangeForBullet:NSRange = string.range(of: bullet) + let string: NSString = NSString(string: formattedString) + let rangeForBullet: NSRange = string.range(of: bullet) attributedString.addAttributes(bulletAttributes, range: rangeForBullet) bulletList.append(attributedString) } @@ -195,4 +196,3 @@ class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate } } - diff --git a/LockdowniOS/WhitelistViewController.swift b/LockdowniOS/WhitelistViewController.swift index e051a07..b1dc2f9 100644 --- a/LockdowniOS/WhitelistViewController.swift +++ b/LockdowniOS/WhitelistViewController.swift @@ -52,17 +52,20 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl tableView.reloadData() case .notValid(let reason): DDLogWarn("Custom whitelist domain not valid - \(userEnteredDomainName), reason - \(reason)") + let notValidEntry: String = .localized(""" + is not a valid entry. Please only enter the host of the domain you want to add to a whitelist. \ +For example, \"google.com\" without \"https://\" +""") showPopupDialog( - title: NSLocalizedString("Invalid domain", comment: ""), - message: "\"\(userEnteredDomainName)\"" + NSLocalizedString(" is not a valid entry. Please only enter the host of the domain you want to add to a whitelist. For example, \"google.com\" without \"https://\"", comment: ""), - acceptButton: NSLocalizedString("Okay", comment: "") - ) { + title: .localized("Invalid domain"), + message: "\"\(userEnteredDomainName)\"" + notValidEntry, + acceptButton: .localizedOkay) { self.addDomainTextField?.becomeFirstResponder() } } } - //MARK: - TABLE VIEW + // MARK: - TABLE VIEW func numberOfSections(in tableView: UITableView) -> Int { return 2 @@ -81,12 +84,10 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl // Add Domain row if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { return 75 - } - else { + } else { return 50 } - } - else { + } else { return 50 } } @@ -94,24 +95,23 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == 0 { return getUserWhitelistedDomains().count + 1 - } - else { + } else { return getLockdownWhitelistedDomains().count } } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let view = UIView(frame: CGRect.init(x: 0, y: 0, width: tableView.frame.size.width, height: 45)) - view.backgroundColor = UIColor.groupTableViewBackground - let label = UILabel(frame: CGRect.init(x: 20, y: 20, width: tableView.frame.size.width, height: 24)) - label.font = fontMedium14 + view.backgroundColor = .systemGroupedBackground + let isLeftToRight = traitCollection.layoutDirection == .leftToRight + let label = UILabel(frame: CGRect.init(x: isLeftToRight ? 20 : -20, y: 20, width: tableView.frame.size.width, height: 24)) + label.font = .mediumLockdownFont(size: 14) label.textColor = UIColor.darkGray if section == 0 { - label.text = NSLocalizedString("Your Settings", comment: "") - } - else { - label.text = NSLocalizedString("Pre-configured Suggestions", comment: "") + label.text = .localized("Your Settings") + } else { + label.text = .localized("Pre-configured Suggestions") } view.addSubview(label) @@ -122,7 +122,7 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl tableView.deselectRow(at: indexPath, animated: true) didMakeChange = true - var domains: Dictionary; + var domains: [String: Any] if indexPath.section == 0 { if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { // Do nothing for add domain row @@ -133,20 +133,17 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl if domainArray.count > indexPath.row { if let status = domainArray[indexPath.row].value as? NSNumber, status.boolValue == true { setUserWhitelistedDomain(domain: domainArray[indexPath.row].key, enabled: false) - } - else { + } else { setUserWhitelistedDomain(domain: domainArray[indexPath.row].key, enabled: true) } } - } - else { + } else { domains = getLockdownWhitelistedDomains() let domainArray = domains.sorted {$0.key < $1.key} if domainArray.count > indexPath.row { if let status = domainArray[indexPath.row].value as? NSNumber, status.boolValue == true { setLockdownWhitelistedDomain(domain: domainArray[indexPath.row].key, enabled: false) - } - else { + } else { setLockdownWhitelistedDomain(domain: domainArray[indexPath.row].key, enabled: true) } } @@ -170,8 +167,8 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl } func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - let cell = tableView.cellForRow(at: indexPath) as! WhitelistCell + if editingStyle == .delete, + let cell = tableView.cellForRow(at: indexPath) as? WhitelistCell { let domainLabel = cell.whitelistDomain didMakeChange = true @@ -185,35 +182,32 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - var domains: Dictionary; + var domains: [String: Any] if indexPath.section == 0 { // Add Domain - if indexPath.row == tableView.numberOfRows(inSection: 0) - 1 { - let cell = tableView.dequeueReusableCell(withIdentifier: "whitelistAddCell", for: indexPath) as! WhitelistAddCell + if indexPath.row == tableView.numberOfRows(inSection: 0) - 1, + let cell = tableView.dequeueReusableCell(withIdentifier: "whitelistAddCell", for: indexPath) as? WhitelistAddCell { let textfield = cell.addWhitelistDomain textfield?.addTarget(self, action: #selector(textFieldDidEndOnExit), for: .editingDidEndOnExit) textfield?.addTarget(self, action: #selector(didSelectTextField), for: .editingDidBegin) addDomainTextField = textfield return cell - } - else { + } else { domains = getUserWhitelistedDomains() } - } - else { + } else { domains = getLockdownWhitelistedDomains() } let domainArray = domains.sorted {$0.key < $1.key} - if domainArray.count > indexPath.row { - let cell = tableView.dequeueReusableCell(withIdentifier: "whitelistCell", for: indexPath) as! WhitelistCell + if domainArray.count > indexPath.row, + let cell = tableView.dequeueReusableCell(withIdentifier: "whitelistCell", for: indexPath) as? WhitelistCell { cell.whitelistDomain?.text = domainArray[indexPath.row].key if let status = domainArray[indexPath.row].value as? NSNumber, status.boolValue == true { - cell.whitelistStatus?.text = NSLocalizedString("Whitelisted", comment: "") - } - else { - cell.whitelistStatus?.text = NSLocalizedString("Not Whitelisted", comment: "") + cell.whitelistStatus?.text = .localized("Whitelisted") + } else { + cell.whitelistStatus?.text = .localized("Not Whitelisted") } return cell } diff --git a/LockdowniOS/WhyTrustViewController.swift b/LockdowniOS/WhyTrustViewController.swift index b3d45d7..21d8a43 100644 --- a/LockdowniOS/WhyTrustViewController.swift +++ b/LockdowniOS/WhyTrustViewController.swift @@ -14,7 +14,7 @@ class WhyTrustViewController: BaseViewController, UIScrollViewDelegate { @IBOutlet weak var pageControl: UIPageControl! @IBOutlet weak var scrollView: UIScrollView! - var pages:[UIViewController] = [] + var pages: [UIViewController] = [] override func viewDidLoad() { super.viewDidLoad() diff --git a/LockdowniOS/ar.lproj/LaunchScreen.strings b/LockdowniOS/ar.lproj/LaunchScreen.strings new file mode 100644 index 0000000..7dfa239 --- /dev/null +++ b/LockdowniOS/ar.lproj/LaunchScreen.strings @@ -0,0 +1,3 @@ + +/* Class = "UILabel"; text = "Lockdown Privacy"; ObjectID = "Cc5-n9-wvQ"; */ +"Cc5-n9-wvQ.text" = "Lockdown Privacy"; diff --git a/LockdowniOS/ar.lproj/Main.strings b/LockdowniOS/ar.lproj/Main.strings new file mode 100644 index 0000000..16b33d1 --- /dev/null +++ b/LockdowniOS/ar.lproj/Main.strings @@ -0,0 +1,446 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "أوافق على"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "كلمة المرور"; + +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "اليوم"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "اللقب"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "ليس نشطًا"; + +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "تعلم"; + +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "تحذير"; + +/* Class = "UIButton"; normalTitle = "􀈂"; ObjectID = "3tZ-rh-ZOa"; */ +"3tZ-rh-ZOa.normalTitle" = "􀈂"; + +/* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ +"5Ce-KH-z1j.text" = "iOS سنويًا (توفير ~45%)"; + +/* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ +"6Qj-yK-k5A.text" = "جدار حماية بسيط وقوي يوقف الاتصال بالمتعقبات والبرامج الضارة والعوامل السيئة الأخرى.

مجاني ومفتوح المصدر."; + +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "اضغط للموافقة على هذا"; + +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "ليس في القائمة البيضاء"; + +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "أبسط سياسة خصوصية عالميًا"; + +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "فبراير 2022"; + +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "أوافق"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "إلغاء"; + +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "تحصل الحسابات على المنافع التالية:"; + +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "سجل الحظر معطل"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "لماذا تثق في Lockdown؟"; + +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "احترافي سنويًا (توفير ~30%)"; + +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "التسجيل"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "Secure Tunnel VPN"; + +/* Class = "UIView"; accessibilityHint = "iOS Annual supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityHint" = "يدعم اشتراك iOS سنويًا أجهزة iPad و iPhone. انقر نقرًا مزدوجًا باستخدام VoiceOver للتحديد."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "ضع علامة في المربع لخطة \"iOS سنويًا\""; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "لماذا تثق في Lockdown؟"; + +/* Class = "UILabel"; accessibilityHint = "Instructs user to tap the blue circle to the left of this label to activate."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityHint" = "وجه المستخدم إلى النقر على الدائرة الزرقاء الموجودة على يسار هذا الوسم للتفعيل."; + +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "وسم يقول اضغط للتنشيط. وليس الزر الفعلي."; + +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "اضغط للتنشيط"; + +/* Class = "UILabel"; text = "0"; ObjectID = "C0f-ye-V9U"; */ +"C0f-ye-V9U.text" = "0"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Cjo-hk-1dr"; */ +"Cjo-hk-1dr.normalTitle" = "إغلاق"; + +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "استعادة عملية الشراء"; + +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "نسيت كلمة المرور؟"; + +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "الولايات المتحدة - الغرب"; + +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "12:22 م"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "إغلاق"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "تسجيل الدخول"; + +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "عرض السجل"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "لديك بالفعل حساب Lockdown؟
سجل الدخول أدناه."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "عنوان البريد الإلكتروني"; + +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "البدء"; + +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "الحظر مفعل"; + +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "تحديد المنطقة"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "جدار الحماية"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "إلغاء"; + +/* Class = "UILabel"; text = "0"; ObjectID = "Msx-Nc-p9M"; */ +"Msx-Nc-p9M.text" = "0"; + +/* Class = "UILabel"; text = "Lockdown"; ObjectID = "NAN-dg-xBj"; */ +"NAN-dg-xBj.text" = "Lockdown"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "NvC-GW-2NJ"; */ +"NvC-GW-2NJ.normalTitle" = "إلغاء"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "زر"; + +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "حماية"; + +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "الموقع: 🇺🇸"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "أجهزة iPad و iPhone"; + +/* Class = "UIButton"; normalTitle = "􀍟"; ObjectID = "OIa-zY-ALo"; */ +"OIa-zY-ALo.normalTitle" = "􀍟"; + +/* Class = "UIButton"; normalTitle = "􀁝"; ObjectID = "OLt-18-OkW"; */ +"OLt-18-OkW.normalTitle" = "􀁝"; + +/* Class = "UILabel"; text = "Block Invasive Tracking"; ObjectID = "PGe-EF-Ixy"; */ +"PGe-EF-Ixy.text" = "حظر التعقب الجائر"; + +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; + +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "التسجيل"; + +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "القائمة البيضاء"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "سياسة الخصوصية"; + +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "كل الوقت"; + +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "أجهزة iPad و iPhone و Mac"; + +/* Class = "UILabel"; text = "3421"; ObjectID = "Sar-GU-wUS"; */ +"Sar-GU-wUS.text" = "3421"; + +/* Class = "UILabel"; text = "Lockdown Firewall is 100% on-device, so it does not collect or transmit any data to any servers - everything stays on your device."; ObjectID = "T2B-yz-40I"; */ +"T2B-yz-40I.text" = "Lockdown Firewall يوجد على الجهاز بنسبة 100٪، لذا فهو لا يجمع أو يرسل أي بيانات إلى أي خوادم - كل شيء يظل على جهازك."; + +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "احمي الاتصالات الجيدة"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "حفظ"; + +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domain-to-whitelist.com"; + +/* Class = "UILabel"; text = "Lockdown VPN is 100% open source, fully audited, and has a strict no-logs policy. Proof of your data protection is in the Privacy Policy."; ObjectID = "UJ5-nt-aYy"; */ +"UJ5-nt-aYy.text" = "Lockdown VPN هو 100٪ مفتوح المصدر ومدقق بالكامل ولديه سياسة صارمة لعدم الاحتفاظ بالسجلات. يوجد الدليل على حماية بياناتك في سياسة الخصوصية."; + +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "ابدأ مجانًا"; + +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "اعرف المزيد"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "كلمة المرور"; + +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "يدعم اشتراك iOS شهريًا أجهزة iPad و iPhone. انقر نقرًا مزدوجًا باستخدام VoiceOver للتحديد."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "ضع علامة في المربع لخطة \"iOS شهريًا\""; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "إغلاق"; + +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "هذا الأسبوع"; + +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "49.99 دولارًا في السنة بعد ذلك (حوالي 4.17 دولارًا في الشهر)"; + +/* Class = "UILabel"; text = "0"; ObjectID = "XSr-oZ-hkd"; */ +"XSr-oZ-hkd.text" = "0"; + +/* Class = "UILabel"; text = "Block Group Title"; ObjectID = "XcQ-Zo-7hE"; */ +"XcQ-Zo-7hE.text" = "اسم مجموعة الحظر"; + +/* Class = "UILabel"; text = "✓ Get new block lists for trackers
✓ Access Lockdown Mac and Desktop
✓ Critical announcements and features"; ObjectID = "XhT-hq-Dd2"; */ +"XhT-hq-Dd2.text" = "✓ احصل على قوائم حظر جديدة للمتعقبات
✓ تمتع بحماية Lockdown على Mac والكمبيوتر
✓ الإعلانات والميزات الهامة"; + +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "قائمة الحظر"; + +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "الحساب"; + +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "الحساب"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "اضغط على هذا الزر لتفعيل Secure Tunnel"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "سياسة الخصوصية"; + +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "تحديد المنطقة"; + +/* Class = "UILabel"; text = "VPN"; ObjectID = "aXL-VV-lJa"; */ +"aXL-VV-lJa.text" = "VPN"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "anO-Te-34C"; */ +"anO-Te-34C.normalTitle" = "إلغاء"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "asx-fS-ufQ"; */ +"asx-fS-ufQ.text" = "البريد الإلكتروني"; + +/* Class = "UILabel"; text = "🇺🇸"; ObjectID = "bDO-jU-rIH"; */ +"bDO-jU-rIH.text" = "الولايات المتحدة"; + +/* Class = "UILabel"; text = "Pro Monthly"; ObjectID = "c6Y-nW-xhv"; */ +"c6Y-nW-xhv.text" = "احترافي شهريًا"; + +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "إلغاء"; + +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "النفق قيد الإيقاف"; + +/* Class = "UILabel"; text = "The connections blocked by Lockdown for the last day are shown below. As per our Privacy Policy, all the blocking is done on-device and never transmitted to any servers for processing."; ObjectID = "ccb-xf-LSM"; */ +"ccb-xf-LSM.text" = "الاتصالات التي حظرها Lockdown في آخر يوم موضحة أدناه. وفقًا لسياسة الخصوصية الخاصة بنا، يتم إجراء جميع عمليات الحظر على الجهاز ولا يتم نقلها أبدًا إلى أي خوادم للمعالجة."; + +/* Class = "UIView"; accessibilityHint = "Pro Annual supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityHint" = "يدعم اشتراك احترافي سنويًا أجهزة iPad و iPhone و Mac. انقر نقرًا مزدوجًا باستخدام VoiceOver للتحديد."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "ضع علامة في المربع لخطة \"احترافي سنويًا\""; + +/* Class = "UILabel"; text = "Enter your email below and we'll send you an email to reset your password."; ObjectID = "dLw-qu-1Oe"; */ +"dLw-qu-1Oe.text" = "أدخل بريدك الإلكتروني أدناه وسنرسل لك بريدًا إلكترونيًا لإعادة تعيين كلمة مرورك."; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "تسجيل الدخول"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "كلمة المرور"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "blocked-domain.com"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "اضغط على هذا الزر لتفعيل جدار الحماية"; + +/* Class = "UILabel"; text = "For fastest speeds, choose a region closest to you. You can also anonymize your IP through other regions."; ObjectID = "fTe-nS-2bH"; */ +"fTe-nS-2bH.text" = "للحصول على سرعات أعلى، اختر المنطقة الأقرب إليك. يمكنك أيضًا إخفاء هوية IP الخاص بك من خلال مناطق أخرى."; + +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "سجل الحظر"; + +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "منع محسن للتعقب"; + +/* Class = "UILabel"; text = "Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol."; ObjectID = "gJW-8m-Vk5"; */ +"gJW-8m-Vk5.text" = "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل، وتحتوي على حرف كبير واحد وحرف صغير واحد ورقم واحد ورمز واحد على الأقل."; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "لماذا تثق في Lockdown؟"; + +/* Class = "UIView"; accessibilityHint = "Pro Monthly supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityHint" = "يدعم اشتراك احترافي شهريًا أجهزة iPad و iPhone و Mac. انقر نقرًا مزدوجًا باستخدام VoiceOver للتحديد."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "ضع علامة في المربع لخطة \"احترافي شهريًا\""; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "إرسال"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "تمكين سجل الحظر"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "jMa-F3-KUG"; */ +"jMa-F3-KUG.text" = "البريد الإلكتروني"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "jXy-3G-JaC"; */ +"jXy-3G-JaC.accessibilityLabel" = "اضغط على هذا الزر لتفعيل Secure Tunnel"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "تصفح خاص + إخفاء الموقع و IP"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "ليس نشطًا"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "اضغط على هذا الزر لتفعيل جدار الحماية"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "عرض تقرير التدقيق المفتوح"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "ستتم تلقائيًا إضافة الرصيد إلى اشتراكك الحالي."; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "شروط الخدمة"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "إلى: joe@email.com
Re: المراجعة المالية للربع الرابع في 2019"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "حفظ"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "أجهزة iPad و iPhone"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "نسيت كلمة المرور"; + +/* Class = "UILabel"; text = "Secure Tunnel"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "تسجيل الدخول"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "حُظر اليوم:"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "إغلاق"; + +/* Class = "UILabel"; text = "Some sites or apps don't work well with VPNs. The whitelist below allows you to whitelist sites so they bypass the VPN for a better experience."; ObjectID = "pVa-cX-EbQ"; */ +"pVa-cX-EbQ.text" = "لا تعمل بعض المواقع أو التطبيقات بشكل جيد مع شبكات VPN. تتيح لك القائمة البيضاء أدناه إضافة المواقع إلى القائمة البيضاء حتى تتجاوز VPN لتحظى بتجربة أفضل."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "عنوان البريد الإلكتروني"; + +/* Class = "UILabel"; text = "whitelisted-domain.com"; ObjectID = "pao-zq-e98"; */ +"pao-zq-e98.text" = "whitelisted-domain.com"; + +/* Class = "UIButton"; normalTitle = "See How It Works"; ObjectID = "qBb-qF-2nJ"; */ +"qBb-qF-2nJ.normalTitle" = "تعرف على طريقة العمل"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "كلمة المرور"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "حُظر أكثر من 1 مليار متعقب"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "rJk-m6-qqA"; */ +"rJk-m6-qqA.text" = "blocked-domain.com"; + +/* Class = "UILabel"; text = "IP: 18.142.2.87"; ObjectID = "rhx-SZ-Kki"; */ +"rhx-SZ-Kki.text" = "IP: 18.142.2.87"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "syJ-Jd-iUo"; */ +"syJ-Jd-iUo.text" = "البريد الإلكتروني"; + +/* Class = "UIButton"; normalTitle = "Continue"; ObjectID = "tzL-mY-IJs"; */ +"tzL-mY-IJs.normalTitle" = "استمرار"; + +/* Class = "UILabel"; text = "Tunnel Whitelist"; ObjectID = "uA3-l4-Qe0"; */ +"uA3-l4-Qe0.text" = "القائمة البيضاء للنفق"; + +/* Class = "UILabel"; text = "📱"; ObjectID = "uSD-At-Csa"; */ +"uSD-At-Csa.text" = "📱"; + +/* Class = "UILabel"; text = "Secure Tunnel encrypts your connections, anonymizes browsing history, and hides your location and unique IP address."; ObjectID = "ujS-r7-ItM"; */ +"ujS-r7-ItM.text" = "يقوم Secure Tunnel بتشفير اتصالاتك وإخفاء سجل التصفح وإخفاء موقعك وعنوان IP الفريد الخاص بك."; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "سياسة الخصوصية"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "ابدأ تجربة مجانية لمدة أسبوع"; + +/* Class = "UILabel"; text = "-"; ObjectID = "vKc-uc-qfV"; */ +"vKc-uc-qfV.text" = "-"; + +/* Class = "UILabel"; text = "Add a domain to whitelist"; ObjectID = "w5F-U8-hnF"; */ +"w5F-U8-hnF.text" = "إضافة موقع إلى القائمة البيضاء"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "عنوان البريد الإلكتروني"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "iOS شهريًا"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "yIB-Tk-mf1"; */ +"yIB-Tk-mf1.text" = "جدار الحماية"; + +/* Class = "UILabel"; text = "Browse Safer - Fully Audited Privacy"; ObjectID = "yXI-U1-vLz"; */ +"yXI-U1-vLz.text" = "تصفح أكثر أمانًا - خصوصية مدققة بالكامل"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "أجهزة iPad و iPhone و Mac"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "إلغاء"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "جدار الحماية سنويًا (وفر ~38%)"; diff --git a/LockdowniOS/de.lproj/LaunchScreen.strings b/LockdowniOS/de.lproj/LaunchScreen.strings new file mode 100644 index 0000000..7dfa239 --- /dev/null +++ b/LockdowniOS/de.lproj/LaunchScreen.strings @@ -0,0 +1,3 @@ + +/* Class = "UILabel"; text = "Lockdown Privacy"; ObjectID = "Cc5-n9-wvQ"; */ +"Cc5-n9-wvQ.text" = "Lockdown Privacy"; diff --git a/LockdowniOS/de.lproj/Main.strings b/LockdowniOS/de.lproj/Main.strings new file mode 100644 index 0000000..60fb87a --- /dev/null +++ b/LockdowniOS/de.lproj/Main.strings @@ -0,0 +1,446 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "Ich akzeptiere"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "Passwort"; + +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "HEUTE"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "Titel"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "INAKTIV"; + +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "Lernen"; + +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "Warnung"; + +/* Class = "UIButton"; normalTitle = "􀈂"; ObjectID = "3tZ-rh-ZOa"; */ +"3tZ-rh-ZOa.normalTitle" = "􀈂"; + +/* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ +"5Ce-KH-z1j.text" = "iOS Jahresabo (speichern Sie ~45 %)"; + +/* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ +"6Qj-yK-k5A.text" = "Eine einfache, effektive Firewall, die Verbindungen zu Trackern, Malware und sonstigen bösartigen Agenten unterbricht.

Gratis und Open Source."; + +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "Tippen, um anzunehmen"; + +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "Nicht auf Positivliste"; + +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "Weltweit einfachste Datenschutzerklärung"; + +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "Februar 2022"; + +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "Zustimmen"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "ABBRECHEN"; + +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "Konten genießen folgende Vorteile:"; + +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "Sperrprotokoll deaktiviert"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "Warum sollten Sie Lockdown vertrauen?"; + +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "Pro Jahresabo (speichern Sie ~30 %)"; + +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "Registrieren"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "Secure Tunnel VPN"; + +/* Class = "UIView"; accessibilityHint = "iOS Annual supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityHint" = "Das iOS Jahresabo unterstützt iPads und iPhones. Mit VoiceOver doppeltippen zur Auswahl."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "Kontrollkästchen für \"iOS Jahresabo\""; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "Warum sollten Sie Lockdown vertrauen?"; + +/* Class = "UILabel"; accessibilityHint = "Instructs user to tap the blue circle to the left of this label to activate."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityHint" = "Weisen Sie Nutzer an, auf den blauen Kreis links von diesem Label zu tippen, um es zu aktivieren."; + +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "Ein Label auf dem \"Tippen zum Aktivieren\" steht. Nicht die tatsächliche Schaltfläche."; + +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "Tippen zum Aktivieren"; + +/* Class = "UILabel"; text = "0"; ObjectID = "C0f-ye-V9U"; */ +"C0f-ye-V9U.text" = "0"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Cjo-hk-1dr"; */ +"Cjo-hk-1dr.normalTitle" = "SCHLIESSEN"; + +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "Kauf wiederherstellen"; + +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "Passwort vergessen?"; + +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "Vereinigte Staaten - West"; + +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "12:22 Uhr"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "SCHLIESSEN"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "Anmelden"; + +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "Protokoll anzeigen"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "Sie haben bereits ein Lockdown-Konto?
Registrieren Sie sich unten."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "E-Mail-Adresse"; + +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "Erste Schritte"; + +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "Sperrung aktiviert"; + +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "Region festlegen"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "Firewall"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "ABBRECHEN"; + +/* Class = "UILabel"; text = "0"; ObjectID = "Msx-Nc-p9M"; */ +"Msx-Nc-p9M.text" = "0"; + +/* Class = "UILabel"; text = "Lockdown"; ObjectID = "NAN-dg-xBj"; */ +"NAN-dg-xBj.text" = "Lockdown"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "NvC-GW-2NJ"; */ +"NvC-GW-2NJ.normalTitle" = "ABBRECHEN"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "Schaltfläche"; + +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "Schützen"; + +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "Standort: 🇺🇸"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "iPads und iPhones"; + +/* Class = "UIButton"; normalTitle = "􀍟"; ObjectID = "OIa-zY-ALo"; */ +"OIa-zY-ALo.normalTitle" = "􀍟"; + +/* Class = "UIButton"; normalTitle = "􀁝"; ObjectID = "OLt-18-OkW"; */ +"OLt-18-OkW.normalTitle" = "􀁝"; + +/* Class = "UILabel"; text = "Block Invasive Tracking"; ObjectID = "PGe-EF-Ixy"; */ +"PGe-EF-Ixy.text" = "Invasives Tracking blockieren"; + +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; + +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "Registrieren"; + +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "Whitelist"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "Datenschutzerklärung"; + +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "JEDERZEIT"; + +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "iPads, iPhones und Macs"; + +/* Class = "UILabel"; text = "3421"; ObjectID = "Sar-GU-wUS"; */ +"Sar-GU-wUS.text" = "3421"; + +/* Class = "UILabel"; text = "Lockdown Firewall is 100% on-device, so it does not collect or transmit any data to any servers - everything stays on your device."; ObjectID = "T2B-yz-40I"; */ +"T2B-yz-40I.text" = "Lockdown Firewall ist 100 % auf dem Gerät, weshalb es keine Daten sammelt oder an Server senden - alles bleibt auf Ihrem Gerät."; + +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "Schützen Sie gute Verbindungen"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "SPEICHERN"; + +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domain-to-whitelist.com"; + +/* Class = "UILabel"; text = "Lockdown VPN is 100% open source, fully audited, and has a strict no-logs policy. Proof of your data protection is in the Privacy Policy."; ObjectID = "UJ5-nt-aYy"; */ +"UJ5-nt-aYy.text" = "Lockdown VPN ist 100 % Open Source, vollständig geprüft und verfolgt eine strenge Kein-Protokoll-Politik. Beweis für den Schutz Ihrer Daten ist die Datenschutzerklärung."; + +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "Erste Schritte gratis"; + +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "Mehr erfahren"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "Passwort"; + +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "Das iOS Monatsabo unterstützt iPads und iPhones. Mit VoiceOver doppeltippen zur Auswahl."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "Kontrollkästchen für \"iOS Monatsabo\""; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "SCHLIESSEN"; + +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "DIESE WOCHE"; + +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "49,99 US$/Jahr nach (~4,17 US$/Monat)"; + +/* Class = "UILabel"; text = "0"; ObjectID = "XSr-oZ-hkd"; */ +"XSr-oZ-hkd.text" = "0"; + +/* Class = "UILabel"; text = "Block Group Title"; ObjectID = "XcQ-Zo-7hE"; */ +"XcQ-Zo-7hE.text" = "Titel Sperrgruppe"; + +/* Class = "UILabel"; text = "✓ Get new block lists for trackers
✓ Access Lockdown Mac and Desktop
✓ Critical announcements and features"; ObjectID = "XhT-hq-Dd2"; */ +"XhT-hq-Dd2.text" = "✓ Erhalten Sie neue Sperrlisten für Tracker
✓ Zugriffssperre für Mac und Desktop
✓ Kritische Ankündigungen und Funktionen"; + +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "Sperrliste"; + +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "Konto"; + +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "Konto"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "Tippen Sie auf diese Schaltfläche, um Secure Tunnel zu aktivieren"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "Datenschutzerklärung"; + +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "Region festlegen"; + +/* Class = "UILabel"; text = "VPN"; ObjectID = "aXL-VV-lJa"; */ +"aXL-VV-lJa.text" = "VPN"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "anO-Te-34C"; */ +"anO-Te-34C.normalTitle" = "ABBRECHEN"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "asx-fS-ufQ"; */ +"asx-fS-ufQ.text" = "E-Mail"; + +/* Class = "UILabel"; text = "🇺🇸"; ObjectID = "bDO-jU-rIH"; */ +"bDO-jU-rIH.text" = "🇺🇸"; + +/* Class = "UILabel"; text = "Pro Monthly"; ObjectID = "c6Y-nW-xhv"; */ +"c6Y-nW-xhv.text" = "Pro Monatsabo"; + +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "ABBRECHEN"; + +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "TUNNEL AUS"; + +/* Class = "UILabel"; text = "The connections blocked by Lockdown for the last day are shown below. As per our Privacy Policy, all the blocking is done on-device and never transmitted to any servers for processing."; ObjectID = "ccb-xf-LSM"; */ +"ccb-xf-LSM.text" = "Die von Lockdown blockierten Verbindungen des letzten Tages werden unten angezeigt. Gemäß unserer Datenschutzerklärung erfolgen alle Sperrungen auf dem Gerät und werden niemals zur Verarbeitung auf einen Server gesendet."; + +/* Class = "UIView"; accessibilityHint = "Pro Annual supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityHint" = "Das Pro Jahresabo unterstützt iPads, iPhones und Macs. Mit VoiceOver doppeltippen zur Auswahl."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "Kontrollkästchen für \"Pro Jahresabo\""; + +/* Class = "UILabel"; text = "Enter your email below and we'll send you an email to reset your password."; ObjectID = "dLw-qu-1Oe"; */ +"dLw-qu-1Oe.text" = "Geben Sie unten Ihre E-Mail-Adresse an und wir senden Ihnen eine E-Mail, um Ihr Passwort zurückzusetzen."; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "Anmelden"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "Passwort"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "blocked-domain.com"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "Tippen Sie auf diese Schaltfläche, um Firewall zu aktivieren"; + +/* Class = "UILabel"; text = "For fastest speeds, choose a region closest to you. You can also anonymize your IP through other regions."; ObjectID = "fTe-nS-2bH"; */ +"fTe-nS-2bH.text" = "Für schnellste Geschwindigkeiten, wählen Sie eine Region in Ihrer Nähe. Sie können Ihre IP auch über andere Regionen anonymisieren."; + +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "Sperrprotokoll"; + +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "Verbesserte Tracking-Verhinderung"; + +/* Class = "UILabel"; text = "Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol."; ObjectID = "gJW-8m-Vk5"; */ +"gJW-8m-Vk5.text" = "Das Passwort muss mindestens 8 Zeichen lang sein, mindestens ein Großbuchstabe, ein Kleinbuchstabe, eine Ziffer und ein Sonderzeichen enthalten."; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "Warum sollten Sie Lockdown vertrauen?"; + +/* Class = "UIView"; accessibilityHint = "Pro Monthly supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityHint" = "Das Pro Monatsabo unterstützt iPads, iPhones und Macs. Mit VoiceOver doppeltippen zur Auswahl."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "Kontrollkästchen für \"Pro Monatsabo\""; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "Senden"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "Sperrprotokoll aktivieren"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "jMa-F3-KUG"; */ +"jMa-F3-KUG.text" = "E-Mail"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "jXy-3G-JaC"; */ +"jXy-3G-JaC.accessibilityLabel" = "Tippen Sie auf diese Schaltfläche, um Secure Tunnel zu aktivieren"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "Privates Browsing + Standort & IP verbergen"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "INAKTIV"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "Tippen Sie auf diese Schaltfläche, um Firewall zu aktivieren"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "OPENAUDIT-BERICHT ANZEIGEN"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "Ihr bestehendes Abonnement wird Ihnen automatisch gutgeschrieben."; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "Nutzungsbedingungen"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "An: joe@email.com
Re: Finanzbericht Q4 2019"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "SPEICHERN"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "iPads und iPhones"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "Passwort vergessen"; + +/* Class = "UILabel"; text = "Secure Tunnel"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "Anmelden"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "Heute gesperrt:"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "SCHLIESSEN"; + +/* Class = "UILabel"; text = "Some sites or apps don't work well with VPNs. The whitelist below allows you to whitelist sites so they bypass the VPN for a better experience."; ObjectID = "pVa-cX-EbQ"; */ +"pVa-cX-EbQ.text" = "Einige Websites oder Apps funktionieren nicht gut mit VPN. Über die nachstehende Whitelist können Sie Websites zulassen, um das VPN für ein besseres Nutzererlebnis zu umgehen."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "E-Mail-Adresse"; + +/* Class = "UILabel"; text = "whitelisted-domain.com"; ObjectID = "pao-zq-e98"; */ +"pao-zq-e98.text" = "whitelisted-domain.com"; + +/* Class = "UIButton"; normalTitle = "See How It Works"; ObjectID = "qBb-qF-2nJ"; */ +"qBb-qF-2nJ.normalTitle" = "So funktioniert es"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "Passwort"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "Über 1 Mrd. Tracker blockiert"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "rJk-m6-qqA"; */ +"rJk-m6-qqA.text" = "blocked-domain.com"; + +/* Class = "UILabel"; text = "IP: 18.142.2.87"; ObjectID = "rhx-SZ-Kki"; */ +"rhx-SZ-Kki.text" = "IP: 18.142.2.87"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "syJ-Jd-iUo"; */ +"syJ-Jd-iUo.text" = "E-Mail"; + +/* Class = "UIButton"; normalTitle = "Continue"; ObjectID = "tzL-mY-IJs"; */ +"tzL-mY-IJs.normalTitle" = "Weiter"; + +/* Class = "UILabel"; text = "Tunnel Whitelist"; ObjectID = "uA3-l4-Qe0"; */ +"uA3-l4-Qe0.text" = "Tunnel-Whitelist"; + +/* Class = "UILabel"; text = "📱"; ObjectID = "uSD-At-Csa"; */ +"uSD-At-Csa.text" = "📱"; + +/* Class = "UILabel"; text = "Secure Tunnel encrypts your connections, anonymizes browsing history, and hides your location and unique IP address."; ObjectID = "ujS-r7-ItM"; */ +"ujS-r7-ItM.text" = "Secure Tunnel verschlüsselt Ihre Verbindungen, anonymisiert den Browser-Verlauf und verbirgt Ihren Standort und Ihre individuelle IP-Adresse."; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "Datenschutzerklärung"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "Einwöchige kostenlose Testphase starten"; + +/* Class = "UILabel"; text = "-"; ObjectID = "vKc-uc-qfV"; */ +"vKc-uc-qfV.text" = "-"; + +/* Class = "UILabel"; text = "Add a domain to whitelist"; ObjectID = "w5F-U8-hnF"; */ +"w5F-U8-hnF.text" = "Domain zur Whitelist hinzufügen"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "E-Mail-Adresse"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "iOS Monatsabo"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "yIB-Tk-mf1"; */ +"yIB-Tk-mf1.text" = "Firewall"; + +/* Class = "UILabel"; text = "Browse Safer - Fully Audited Privacy"; ObjectID = "yXI-U1-vLz"; */ +"yXI-U1-vLz.text" = "Sicherer browsen - Vollständig geprüfter Datenschutz"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "iPads, iPhones und Macs"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "ABBRECHEN"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "Firewall Jahresabo (sparen Sie ~38 %)"; diff --git a/LockdowniOS/en.lproj/Main.strings b/LockdowniOS/en.lproj/Main.strings index 3b9d9dc..dc5b9d6 100644 --- a/LockdowniOS/en.lproj/Main.strings +++ b/LockdowniOS/en.lproj/Main.strings @@ -23,7 +23,7 @@ "3tZ-rh-ZOa.normalTitle" = "􀈂"; /* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ -"5Ce-KH-z1j.text" = "iOS Annual (Save ~50%)"; +"5Ce-KH-z1j.text" = "iOS Annual (Save ~45%)"; /* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ "6Qj-yK-k5A.text" = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; @@ -362,7 +362,7 @@ "nu2-zq-dta.text" = "Forgot Password"; /* Class = "UILabel"; text = "Secure Tunnel"; ObjectID = "nuc-D4-P1M"; */ -"nuc-D4-P1M.text" = "Secure Tunnel"; +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; /* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ "oPf-b7-d1V.text" = "Sign In"; @@ -441,3 +441,6 @@ /* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ "ziI-ly-5qb.normalTitle" = "CANCEL"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "Firewall Annual (save ~38%)"; diff --git a/LockdowniOS/es.lproj/Main.strings b/LockdowniOS/es.lproj/Main.strings index ad5f193..15a4264 100644 --- a/LockdowniOS/es.lproj/Main.strings +++ b/LockdowniOS/es.lproj/Main.strings @@ -23,7 +23,7 @@ "3tZ-rh-ZOa.normalTitle" = "􀈂"; /* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ -"5Ce-KH-z1j.text" = "iOS Anual (Ahorre ~50%)"; +"5Ce-KH-z1j.text" = "iOS Anual (Ahorre ~45%)"; /* Class = "UILabel"; text = "Un firewall simple y potente que detiene las conexiones a rastreadores, malware y otros agentes maliciosos.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ "6Qj-yK-k5A.text" = "Un firewall simple y potente que detiene las conexiones a rastreadores, malware y otros agentes maliciosos.

Libre y de código abierto."; @@ -441,3 +441,6 @@ /* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ "ziI-ly-5qb.normalTitle" = "CANCELAR"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "Firewall Anual (Ahorre ~38%)"; diff --git a/LockdowniOS/fr.lproj/Main.strings b/LockdowniOS/fr.lproj/Main.strings index d85b11e..7f807bf 100644 --- a/LockdowniOS/fr.lproj/Main.strings +++ b/LockdowniOS/fr.lproj/Main.strings @@ -23,7 +23,7 @@ "3tZ-rh-ZOa.normalTitle" = "􀈂"; /* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ -"5Ce-KH-z1j.text" = "iOS Annuel (Economisez ~50%)"; +"5Ce-KH-z1j.text" = "iOS Annuel (Economisez ~45%)"; /* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ "6Qj-yK-k5A.text" = "Un pare-feu simple et puissant qui bloque les connexions aux traqueurs, logiciels malveillants et autres agents.

 Libre et open source."; @@ -443,3 +443,6 @@ /* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ "ziI-ly-5qb.normalTitle" = "ANNULER"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "Pare-Feu Annuel (Economisez ~38%)"; diff --git a/LockdowniOS/ja.lproj/Main.strings b/LockdowniOS/ja.lproj/Main.strings index 29e8764..fd613cc 100644 --- a/LockdowniOS/ja.lproj/Main.strings +++ b/LockdowniOS/ja.lproj/Main.strings @@ -23,7 +23,7 @@ "3tZ-rh-ZOa.normalTitle" = "􀈂"; /* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ -"5Ce-KH-z1j.text" = "iOSの年次 (約50%割引)"; +"5Ce-KH-z1j.text" = "iOSの年次 (約45%割引)"; /* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ "6Qj-yK-k5A.text" = "トラッカー、マルウェア、その他の悪意のあるエージェントへの接続を停止するシンプルで強力なファイアウォール。

無料でオープンソース。"; @@ -441,3 +441,6 @@ /* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ "ziI-ly-5qb.normalTitle" = "キャンセル"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "Pare-Feu Annuel (Economisez ~38%)"; diff --git a/LockdowniOS/ru.lproj/LaunchScreen.strings b/LockdowniOS/ru.lproj/LaunchScreen.strings new file mode 100644 index 0000000..7dfa239 --- /dev/null +++ b/LockdowniOS/ru.lproj/LaunchScreen.strings @@ -0,0 +1,3 @@ + +/* Class = "UILabel"; text = "Lockdown Privacy"; ObjectID = "Cc5-n9-wvQ"; */ +"Cc5-n9-wvQ.text" = "Lockdown Privacy"; diff --git a/LockdowniOS/ru.lproj/Main.strings b/LockdowniOS/ru.lproj/Main.strings new file mode 100644 index 0000000..ab83a34 --- /dev/null +++ b/LockdowniOS/ru.lproj/Main.strings @@ -0,0 +1,446 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "Я принимаю условия"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "Пароль"; + +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "СЕГОДНЯ"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "Название"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "ОТКЛЮЧЕН"; + +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "Обучить"; + +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "Предупреждение"; + +/* Class = "UIButton"; normalTitle = "􀈂"; ObjectID = "3tZ-rh-ZOa"; */ +"3tZ-rh-ZOa.normalTitle" = "􀈂"; + +/* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ +"5Ce-KH-z1j.text" = "iOS, годовая подписка (выгода ~45%)"; + +/* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ +"6Qj-yK-k5A.text" = "Простой и эффективный брандмауэр, который останавливает попытки отслеживания, вредоносные программы и недоброжелателей.

Бесплатная программа с открытым источником."; + +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "Коснитесь, чтобы принять"; + +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "Отсутствует в белом списке"; + +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "Самая простая политика конфиденциальности в мире"; + +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "Февраль 2022"; + +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "Принять"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "ОТМЕНА"; + +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "Учетные записи получают доступ к следующим возможностям:"; + +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "Журнал блокировок отключен"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "Почему Lockdown можно доверять?"; + +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "Pro, годовая подписка (выгода ~30%)"; + +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "Зарегистрироваться"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "Secure Tunnel VPN"; + +/* Class = "UIView"; accessibilityHint = "iOS Annual supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityHint" = "Тарифный план «iOS, годовая подписка» включает поддержку устройств iPad и iPhone. Коснитесь дважды с помощью VoiceOver, чтобы выбрать."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "Галочка для тарифного плана \"iOS, годовая подписка\""; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "Почему Lockdown можно доверять?"; + +/* Class = "UILabel"; accessibilityHint = "Instructs user to tap the blue circle to the left of this label to activate."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityHint" = "Просит пользователя коснуться синего кружка слева от этой метки, чтобы активировать"; + +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "Метка со словами «Коснитесь, чтобы активировать». Не настоящая кнопка."; + +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "Коснитесь, чтобы активировать"; + +/* Class = "UILabel"; text = "0"; ObjectID = "C0f-ye-V9U"; */ +"C0f-ye-V9U.text" = "0"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Cjo-hk-1dr"; */ +"Cjo-hk-1dr.normalTitle" = "ЗАКРЫТЬ"; + +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "Восстановить покупки"; + +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "Забыли пароль?"; + +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "США (Запад)"; + +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "12:22"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "ЗАКРЫТЬ"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "Войти"; + +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "Открыть журнал"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "У вас уже есть учетная запись Lockdown?
Войдите."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "Email-адрес"; + +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "Защитите себя"; + +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "Блокировка включена"; + +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "Выбрать регион"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "Брандмауэр"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "ОТМЕНА"; + +/* Class = "UILabel"; text = "0"; ObjectID = "Msx-Nc-p9M"; */ +"Msx-Nc-p9M.text" = "0"; + +/* Class = "UILabel"; text = "Lockdown"; ObjectID = "NAN-dg-xBj"; */ +"NAN-dg-xBj.text" = "Lockdown"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "NvC-GW-2NJ"; */ +"NvC-GW-2NJ.normalTitle" = "ОТМЕНА"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "Кнопка"; + +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "Защитить"; + +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "Местоположение: США"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "устройства iPad и iPhone"; + +/* Class = "UIButton"; normalTitle = "􀍟"; ObjectID = "OIa-zY-ALo"; */ +"OIa-zY-ALo.normalTitle" = "􀍟"; + +/* Class = "UIButton"; normalTitle = "􀁝"; ObjectID = "OLt-18-OkW"; */ +"OLt-18-OkW.normalTitle" = "􀁝"; + +/* Class = "UILabel"; text = "Block Invasive Tracking"; ObjectID = "PGe-EF-Ixy"; */ +"PGe-EF-Ixy.text" = "Заблокируйте попытки отслеживания"; + +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; + +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "Зарегистрироваться"; + +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "Белый список"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "Политика конфиденциальности"; + +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "ВСЕ ВРЕМЯ"; + +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "Устройства iPad, iPhone и Mac"; + +/* Class = "UILabel"; text = "3421"; ObjectID = "Sar-GU-wUS"; */ +"Sar-GU-wUS.text" = "3421"; + +/* Class = "UILabel"; text = "Lockdown Firewall is 100% on-device, so it does not collect or transmit any data to any servers - everything stays on your device."; ObjectID = "T2B-yz-40I"; */ +"T2B-yz-40I.text" = "Брандмауэр Lockdown полностью устанавливается на устройство. Он не собирает и не передает данные ни на какие серверы - вся информация остается на вашем устройстве."; + +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "Защита хороших подключений"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "СОХРАНИТЬ"; + +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domain-to-whitelist.com"; + +/* Class = "UILabel"; text = "Lockdown VPN is 100% open source, fully audited, and has a strict no-logs policy. Proof of your data protection is in the Privacy Policy."; ObjectID = "UJ5-nt-aYy"; */ +"UJ5-nt-aYy.text" = "Программный код Lockdown VPN на 100% открыт, проверен экспертами и не создает журналов вашей активности. Доказательство того, что мы защищаем ваши данные, можно найти в политике конфиденциальности."; + +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "Защитите себя бесплатно"; + +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "Подробности"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "Пароль"; + +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "Тарифный план «iOS, месячная подписка» включает поддержку устройств iPad и iPhone. Коснитесь дважды с помощью VoiceOver, чтобы выбрать."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "Галочка для тарифного плана \"iOS, месячная подписка\""; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "ЗАКРЫТЬ"; + +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "ЭТА НЕДЕЛЯ"; + +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "Далее $49.99/год (~$4.17/месяц)"; + +/* Class = "UILabel"; text = "0"; ObjectID = "XSr-oZ-hkd"; */ +"XSr-oZ-hkd.text" = "0"; + +/* Class = "UILabel"; text = "Block Group Title"; ObjectID = "XcQ-Zo-7hE"; */ +"XcQ-Zo-7hE.text" = "Название группы черного списка"; + +/* Class = "UILabel"; text = "✓ Get new block lists for trackers
✓ Access Lockdown Mac and Desktop
✓ Critical announcements and features"; ObjectID = "XhT-hq-Dd2"; */ +"XhT-hq-Dd2.text" = "✓ Новые черные списки для отслеживающих программ
✓ Доступ к Lockdown для Mac и настольных компьютеров
✓ Важные объявления и дополнительные возможности"; + +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "Чёрный список"; + +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "Учетная запись"; + +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "Учетная запись"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "Нажмите на эту кнопку, чтобы активировать Secure Tunnel"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "Политика конфиденциальности"; + +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "Выбрать регион"; + +/* Class = "UILabel"; text = "VPN"; ObjectID = "aXL-VV-lJa"; */ +"aXL-VV-lJa.text" = "VPN"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "anO-Te-34C"; */ +"anO-Te-34C.normalTitle" = "ОТМЕНА"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "asx-fS-ufQ"; */ +"asx-fS-ufQ.text" = "Email"; + +/* Class = "UILabel"; text = "🇺🇸"; ObjectID = "bDO-jU-rIH"; */ +"bDO-jU-rIH.text" = "🇺🇸"; + +/* Class = "UILabel"; text = "Pro Monthly"; ObjectID = "c6Y-nW-xhv"; */ +"c6Y-nW-xhv.text" = "Pro, на месяц"; + +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "ОТМЕНА"; + +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "ТУННЕЛЬ ОТКЛЮЧЕН"; + +/* Class = "UILabel"; text = "The connections blocked by Lockdown for the last day are shown below. As per our Privacy Policy, all the blocking is done on-device and never transmitted to any servers for processing."; ObjectID = "ccb-xf-LSM"; */ +"ccb-xf-LSM.text" = "Здесь показан список подключений, заблокированный Lockdown в течение последнего дня. В соответствии с нашей политикой конфиденциальности, все блокировки осуществляются на вашем устройстве и не передаются ни на какие серверы для обработки."; + +/* Class = "UIView"; accessibilityHint = "Pro Annual supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityHint" = "Тарифный план «Pro, годовая подписка» включает поддержку устройств iPad, iPhone и Mac. Коснитесь дважды с помощью VoiceOver, чтобы выбрать."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "Галочка для тарифного плана \"Pro, годовая подписка\""; + +/* Class = "UILabel"; text = "Enter your email below and we'll send you an email to reset your password."; ObjectID = "dLw-qu-1Oe"; */ +"dLw-qu-1Oe.text" = "Введите ваш адрес электронной почты, и мы отправим письмо, которое позволит сбросить пароль."; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "Войти"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "Пароль"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "blocked-domain.com"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "Нажмите на эту кнопку, чтобы активировать брандмауэр"; + +/* Class = "UILabel"; text = "For fastest speeds, choose a region closest to you. You can also anonymize your IP through other regions."; ObjectID = "fTe-nS-2bH"; */ +"fTe-nS-2bH.text" = "Для максимальной скорости выберите регион неподалеку от вас. Вы также можете скрыть ваш IP, выбрав один из других регионов."; + +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "Журнал блокировок"; + +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "Усиленная защита от отслеживания"; + +/* Class = "UILabel"; text = "Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol."; ObjectID = "gJW-8m-Vk5"; */ +"gJW-8m-Vk5.text" = "Пароль должен быть не короче 8 знаков и должен содержать минимум одну букву верхнего регистра, одну букву нижнего регистра, одну цифру и один символ."; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "Почему Lockdown можно доверять?"; + +/* Class = "UIView"; accessibilityHint = "Pro Monthly supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityHint" = "Тарифный план «Pro, месячная подписка» включает поддержку устройств iPad, iPhone и Mac. Коснитесь дважды с помощью VoiceOver, чтобы выбрать."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "Галочка для тарифного плана \"Pro, месячная подписка\""; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "Отправить"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "Включить журнал блокировок"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "jMa-F3-KUG"; */ +"jMa-F3-KUG.text" = "Email"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "jXy-3G-JaC"; */ +"jXy-3G-JaC.accessibilityLabel" = "Нажмите на эту кнопку, чтобы активировать Secure Tunnel"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "Конфиденциальный просмотр веб-страниц + скрытое местонахождение и IP-адрес"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "ОТКЛЮЧЕН"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "Нажмите на эту кнопку, чтобы активировать брандмауэр"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "ОЗНАКОМИТЬСЯ С ОТЧЕТОМ OPENAUDIT"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "Средства автоматически будут списаны в соответствии с текущим тарифным планом."; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "Условия предоставления услуг"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "Кому: joe@email.com
Тема: финансовый отчет за 4-й квартал 2019 г."; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "СОХРАНИТЬ"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "устройства iPad и iPhone"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "Забыли пароль?"; + +/* Class = "UILabel"; text = "Secure Tunnel"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "Войти"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "Заблокировано сегодня"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "ЗАКРЫТЬ"; + +/* Class = "UILabel"; text = "Some sites or apps don't work well with VPNs. The whitelist below allows you to whitelist sites so they bypass the VPN for a better experience."; ObjectID = "pVa-cX-EbQ"; */ +"pVa-cX-EbQ.text" = "Некоторые приложения и сайты плохо работают через VPN. Белый список позволяет вам отключить VPN для таких сайтов и стабилизировать их работу."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "Email-адрес"; + +/* Class = "UILabel"; text = "whitelisted-domain.com"; ObjectID = "pao-zq-e98"; */ +"pao-zq-e98.text" = "whitelisted-domain.com"; + +/* Class = "UIButton"; normalTitle = "See How It Works"; ObjectID = "qBb-qF-2nJ"; */ +"qBb-qF-2nJ.normalTitle" = "Как это работает"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "Пароль"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "Заблокировано более 1 миллиарда попыток отслеживания"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "rJk-m6-qqA"; */ +"rJk-m6-qqA.text" = "blocked-domain.com"; + +/* Class = "UILabel"; text = "IP: 18.142.2.87"; ObjectID = "rhx-SZ-Kki"; */ +"rhx-SZ-Kki.text" = "IP: 18.142.2.87"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "syJ-Jd-iUo"; */ +"syJ-Jd-iUo.text" = "Email"; + +/* Class = "UIButton"; normalTitle = "Continue"; ObjectID = "tzL-mY-IJs"; */ +"tzL-mY-IJs.normalTitle" = "Продолжить"; + +/* Class = "UILabel"; text = "Tunnel Whitelist"; ObjectID = "uA3-l4-Qe0"; */ +"uA3-l4-Qe0.text" = "Белый список Tunnel"; + +/* Class = "UILabel"; text = "📱"; ObjectID = "uSD-At-Csa"; */ +"uSD-At-Csa.text" = "📱"; + +/* Class = "UILabel"; text = "Secure Tunnel encrypts your connections, anonymizes browsing history, and hides your location and unique IP address."; ObjectID = "ujS-r7-ItM"; */ +"ujS-r7-ItM.text" = "Secure Tunnel шифрует ваши подключения, анонимизирует историю посещения веб-страниц и скрывает ваше местонахождение и уникальный IP-адрес."; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "Политика конфиденциальности"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "Начать бесплатный пробный период в 1 неделю"; + +/* Class = "UILabel"; text = "-"; ObjectID = "vKc-uc-qfV"; */ +"vKc-uc-qfV.text" = "-"; + +/* Class = "UILabel"; text = "Add a domain to whitelist"; ObjectID = "w5F-U8-hnF"; */ +"w5F-U8-hnF.text" = "Добавить домен в белый список"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "Email-адрес"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "iOS, на месяц"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "yIB-Tk-mf1"; */ +"yIB-Tk-mf1.text" = "Брандмауэр"; + +/* Class = "UILabel"; text = "Browse Safer - Fully Audited Privacy"; ObjectID = "yXI-U1-vLz"; */ +"yXI-U1-vLz.text" = "Безопасное посещение страниц - конфиденциальность, проверенная экспертами"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "Устройства iPad, iPhone и Mac"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "ОТМЕНА"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "Брандмауэр, на год (выгода ~38%)"; diff --git a/LockdowniOS/vi.lproj/LaunchScreen.strings b/LockdowniOS/vi.lproj/LaunchScreen.strings new file mode 100644 index 0000000..7dfa239 --- /dev/null +++ b/LockdowniOS/vi.lproj/LaunchScreen.strings @@ -0,0 +1,3 @@ + +/* Class = "UILabel"; text = "Lockdown Privacy"; ObjectID = "Cc5-n9-wvQ"; */ +"Cc5-n9-wvQ.text" = "Lockdown Privacy"; diff --git a/LockdowniOS/vi.lproj/Main.strings b/LockdowniOS/vi.lproj/Main.strings new file mode 100644 index 0000000..e765a44 --- /dev/null +++ b/LockdowniOS/vi.lproj/Main.strings @@ -0,0 +1,446 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "Tôi đồng ý với"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "Mật khẩu"; + +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "HÔM NAY"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "Chức danh"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "KHÔNG HOẠT ĐỘNG"; + +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "Học tập"; + +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "Cảnh báo"; + +/* Class = "UIButton"; normalTitle = "􀈂"; ObjectID = "3tZ-rh-ZOa"; */ +"3tZ-rh-ZOa.normalTitle" = ""; + +/* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ +"5Ce-KH-z1j.text" = "iOS hàng năm (Tiết kiệm ~45%)"; + +/* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ +"6Qj-yK-k5A.text" = "Tường lửa đơn giản, mạnh mẽ, ngăn chặn các kết nối với trình theo dõi, phần mềm độc hại và các tác nhân xấu khác.

Mã nguồn mở và miễn phí."; + +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "Nhấn để đồng ý với điều này"; + +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "Không có trong Danh sách trắng"; + +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "Chính sách Quyền riêng tư Đơn giản nhất Thế giới"; + +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "Tháng 2/2022"; + +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "Đồng ý"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "HỦY"; + +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "Tài khoản nhận được những lợi ích sau:"; + +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "Đã tắt Nhật ký chặn"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "Vì sao nên tin dùng Lockdown?"; + +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "Pro hàng năm (Tiết kiệm ~30%)"; + +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "Đăng ký"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "Secure Tunnel VPN"; + +/* Class = "UIView"; accessibilityHint = "iOS Annual supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityHint" = "iOS hàng năm hỗ trợ iPad và iPhone. Nhấn đúp bằng VoiceOver để chọn."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "Hộp kiểm cho Gói \"iOS Hàng năm\""; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "Vì sao nên tin dùng Lockdown?"; + +/* Class = "UILabel"; accessibilityHint = "Instructs user to tap the blue circle to the left of this label to activate."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityHint" = "Hướng dẫn người dùng nhấn vào vòng tròn màu xanh ở bên trái của nhãn này để kích hoạt."; + +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "Nhãn có nội dung Nhấn để kích hoạt. Không phải là nút thực tế."; + +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "Nhấn để kích hoạt"; + +/* Class = "UILabel"; text = "0"; ObjectID = "C0f-ye-V9U"; */ +"C0f-ye-V9U.text" = "0"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Cjo-hk-1dr"; */ +"Cjo-hk-1dr.normalTitle" = "ĐÓNG"; + +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "Khôi phục Mua hàng"; + +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "Quên mật khẩu?"; + +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "Hoa Kỳ - Miền Tây"; + +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "12:22 sáng"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "ĐÓNG"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "Đăng nhập"; + +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "Xem nhật ký"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "Đã có tài khoản Lockdown?
Đăng nhập bên dưới."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "Địa chỉ email"; + +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "Bắt đầu"; + +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "Đã bật Chặn"; + +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "Đặt khu vực"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "Tường lửa"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "HỦY"; + +/* Class = "UILabel"; text = "0"; ObjectID = "Msx-Nc-p9M"; */ +"Msx-Nc-p9M.text" = "0"; + +/* Class = "UILabel"; text = "Lockdown"; ObjectID = "NAN-dg-xBj"; */ +"NAN-dg-xBj.text" = "Lockdown"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "NvC-GW-2NJ"; */ +"NvC-GW-2NJ.normalTitle" = "HỦY"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "Nút"; + +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "Bảo vệ"; + +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "Vị trí: "; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "iPad và iPhone"; + +/* Class = "UIButton"; normalTitle = "􀍟"; ObjectID = "OIa-zY-ALo"; */ +"OIa-zY-ALo.normalTitle" = ""; + +/* Class = "UIButton"; normalTitle = "􀁝"; ObjectID = "OLt-18-OkW"; */ +"OLt-18-OkW.normalTitle" = ""; + +/* Class = "UILabel"; text = "Block Invasive Tracking"; ObjectID = "PGe-EF-Ixy"; */ +"PGe-EF-Ixy.text" = "Chặn theo dõi xâm lấn"; + +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = ""; + +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "Đăng ký"; + +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "Danh sách trắng"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "Chính sách Quyền riêng tư"; + +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "TẤT CẢ THỜI GIAN"; + +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "iPad, iPhone và Mac"; + +/* Class = "UILabel"; text = "3421"; ObjectID = "Sar-GU-wUS"; */ +"Sar-GU-wUS.text" = "3421"; + +/* Class = "UILabel"; text = "Lockdown Firewall is 100% on-device, so it does not collect or transmit any data to any servers - everything stays on your device."; ObjectID = "T2B-yz-40I"; */ +"T2B-yz-40I.text" = "Lockdown Firewall hoạt động 100% trên thiết bị, vì vậy nó không thu thập hoặc truyền bất kỳ dữ liệu nào đến bất kỳ máy chủ nào - mọi thứ vẫn nằm trên thiết bị của bạn."; + +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "Bảo vệ kết nối tốt"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "LƯU"; + +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domain-to-whitelist.com"; + +/* Class = "UILabel"; text = "Lockdown VPN is 100% open source, fully audited, and has a strict no-logs policy. Proof of your data protection is in the Privacy Policy."; ObjectID = "UJ5-nt-aYy"; */ +"UJ5-nt-aYy.text" = "Lockdown VPN là phần mềm nguồn mở 100%, đã được kiểm tra đầy đủ và có chính sách không ghi nhật ký nghiêm ngặt. Bằng chứng về việc bảo vệ dữ liệu của bạn nằm trong Chính sách Quyền riêng tư."; + +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "Bắt đầu Miễn phí"; + +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "Tìm hiểu thêm"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "Mật khẩu"; + +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "iOS Hàng tháng hỗ trợ iPad và iPhone. Nhấn đúp bằng VoiceOver để chọn."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "Hộp kiểm cho Gói \"iOS Hàng tháng\""; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "ĐÓNG"; + +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "TUẦN NÀY"; + +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "$49,99/năm sau (~$4,17/tháng)"; + +/* Class = "UILabel"; text = "0"; ObjectID = "XSr-oZ-hkd"; */ +"XSr-oZ-hkd.text" = "0"; + +/* Class = "UILabel"; text = "Block Group Title"; ObjectID = "XcQ-Zo-7hE"; */ +"XcQ-Zo-7hE.text" = "Tiêu đề Nhóm chặn"; + +/* Class = "UILabel"; text = "✓ Get new block lists for trackers
✓ Access Lockdown Mac and Desktop
✓ Critical announcements and features"; ObjectID = "XhT-hq-Dd2"; */ +"XhT-hq-Dd2.text" = "✓ Nhận danh sách chặn mới cho các trình theo dõi
✓ Truy cập Lockdown Mac và Desktop
✓ Các thông báo và tính năng quan trọng"; + +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "Danh sách chặn"; + +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "Tài khoản"; + +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "Tài khoản"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "Nhấn nút này để kích hoạt Secure Tunnel"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "Chính sách Quyền riêng tư"; + +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "Đặt khu vực"; + +/* Class = "UILabel"; text = "VPN"; ObjectID = "aXL-VV-lJa"; */ +"aXL-VV-lJa.text" = "VPN"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "anO-Te-34C"; */ +"anO-Te-34C.normalTitle" = "HỦY"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "asx-fS-ufQ"; */ +"asx-fS-ufQ.text" = "Email"; + +/* Class = "UILabel"; text = "🇺🇸"; ObjectID = "bDO-jU-rIH"; */ +"bDO-jU-rIH.text" = ""; + +/* Class = "UILabel"; text = "Pro Monthly"; ObjectID = "c6Y-nW-xhv"; */ +"c6Y-nW-xhv.text" = "Pro Hàng tháng"; + +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = ""; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "HỦY"; + +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "ĐƯỜNG HẦM ĐANG TẮT"; + +/* Class = "UILabel"; text = "The connections blocked by Lockdown for the last day are shown below. As per our Privacy Policy, all the blocking is done on-device and never transmitted to any servers for processing."; ObjectID = "ccb-xf-LSM"; */ +"ccb-xf-LSM.text" = "Các kết nối bị Lockdown chặn trong ngày qua được hiển thị bên dưới. Theo Chính sách Quyền riêng tư của chúng tôi, tất cả hoạt động chặn được thực hiện trên thiết bị và không bao giờ được truyền đến bất kỳ máy chủ nào để xử lý."; + +/* Class = "UIView"; accessibilityHint = "Pro Annual supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityHint" = "Pro Hàng năm hỗ trợ iPad và iPhone và Mac. Nhấn đúp bằng VoiceOver để chọn."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "Hộp kiểm cho Gói \"Pro Hàng năm\""; + +/* Class = "UILabel"; text = "Enter your email below and we'll send you an email to reset your password."; ObjectID = "dLw-qu-1Oe"; */ +"dLw-qu-1Oe.text" = "Nhập email của bạn vào bên dưới và chúng tôi sẽ gửi cho bạn một email để đặt lại mật khẩu."; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "Đăng nhập"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "Mật khẩu"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "blocked-domain.com"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "Nhấn nút này để kích hoạt Tường lửa"; + +/* Class = "UILabel"; text = "For fastest speeds, choose a region closest to you. You can also anonymize your IP through other regions."; ObjectID = "fTe-nS-2bH"; */ +"fTe-nS-2bH.text" = "Để có tốc độ nhanh nhất, hãy chọn khu vực gần bạn nhất. Bạn cũng có thể ẩn danh IP của mình thông qua các khu vực khác."; + +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "Nhật ký chặn"; + +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "Tăng cường Ngăn chặn Theo dõi"; + +/* Class = "UILabel"; text = "Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol."; ObjectID = "gJW-8m-Vk5"; */ +"gJW-8m-Vk5.text" = "Mật khẩu phải có tối thiểu 8 ký tự, chứa ít nhất một chữ cái viết hoa, một chữ thường, một số và một ký hiệu."; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "Vì sao nên tin dùng Lockdown?"; + +/* Class = "UIView"; accessibilityHint = "Pro Monthly supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityHint" = "Pro Hàng tháng hỗ trợ iPad và iPhone và Mac. Nhấn đúp bằng VoiceOver để chọn."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "Hộp kiểm cho Gói \"Pro Hàng tháng\""; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "Gửi"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "Bật Nhật ký chặn"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "jMa-F3-KUG"; */ +"jMa-F3-KUG.text" = "Email"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "jXy-3G-JaC"; */ +"jXy-3G-JaC.accessibilityLabel" = "Nhấn nút này để kích hoạt Secure Tunnel"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "Duyệt web riêng tư + Ẩn Vị trí & IP"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "KHÔNG HOẠT ĐỘNG"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "Nhấn nút này để kích hoạt Tường lửa"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "XEM BÁO CÁO KIỂM TOÁN MỞ"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "Bạn sẽ tự động được trừ tiền cho đăng ký hiện có của mình."; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "Điều khoản Dịch vụ"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "Tới: joe@email.com
V/v: Đánh giá Tài chính Quý 4 Năm 2019"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "LƯU"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "iPad và iPhone"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "Quên mật khẩu"; + +/* Class = "UILabel"; text = "Secure Tunnel"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "Đăng nhập"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "Đã chặn hôm nay:"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "ĐÓNG"; + +/* Class = "UILabel"; text = "Some sites or apps don't work well with VPNs. The whitelist below allows you to whitelist sites so they bypass the VPN for a better experience."; ObjectID = "pVa-cX-EbQ"; */ +"pVa-cX-EbQ.text" = "Một số trang web hoặc ứng dụng không hoạt động tốt với VPN. Danh sách trắng bên dưới cho phép bạn đưa các trang web vào danh sách trắng để chúng bỏ qua VPN để có trải nghiệm tốt hơn."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "Địa chỉ email"; + +/* Class = "UILabel"; text = "whitelisted-domain.com"; ObjectID = "pao-zq-e98"; */ +"pao-zq-e98.text" = "whitelisted-domain.com"; + +/* Class = "UIButton"; normalTitle = "See How It Works"; ObjectID = "qBb-qF-2nJ"; */ +"qBb-qF-2nJ.normalTitle" = "Xem cách nó hoạt động"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "Mật khẩu"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "Hơn 1 tỷ Trình theo dõi bị chặn"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "rJk-m6-qqA"; */ +"rJk-m6-qqA.text" = "blocked-domain.com"; + +/* Class = "UILabel"; text = "IP: 18.142.2.87"; ObjectID = "rhx-SZ-Kki"; */ +"rhx-SZ-Kki.text" = "IP: 18.142.2.87"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "syJ-Jd-iUo"; */ +"syJ-Jd-iUo.text" = "Email"; + +/* Class = "UIButton"; normalTitle = "Continue"; ObjectID = "tzL-mY-IJs"; */ +"tzL-mY-IJs.normalTitle" = "Tiếp tục"; + +/* Class = "UILabel"; text = "Tunnel Whitelist"; ObjectID = "uA3-l4-Qe0"; */ +"uA3-l4-Qe0.text" = "Danh sách trắng Đường hầm"; + +/* Class = "UILabel"; text = "📱"; ObjectID = "uSD-At-Csa"; */ +"uSD-At-Csa.text" = ""; + +/* Class = "UILabel"; text = "Secure Tunnel encrypts your connections, anonymizes browsing history, and hides your location and unique IP address."; ObjectID = "ujS-r7-ItM"; */ +"ujS-r7-ItM.text" = "Secure Tunnel mã hóa các kết nối của bạn, ẩn danh lịch sử duyệt web và ẩn vị trí cũng như địa chỉ IP riêng của bạn."; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "Chính sách Quyền riêng tư"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "Bắt Đầu 1 Tuần Dùng Thử Miễn Phí"; + +/* Class = "UILabel"; text = "-"; ObjectID = "vKc-uc-qfV"; */ +"vKc-uc-qfV.text" = "-"; + +/* Class = "UILabel"; text = "Add a domain to whitelist"; ObjectID = "w5F-U8-hnF"; */ +"w5F-U8-hnF.text" = "Thêm tên miền vào danh sách trắng"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "Địa chỉ email"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "iOS Hàng tháng"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "yIB-Tk-mf1"; */ +"yIB-Tk-mf1.text" = "Tường lửa"; + +/* Class = "UILabel"; text = "Browse Safer - Fully Audited Privacy"; ObjectID = "yXI-U1-vLz"; */ +"yXI-U1-vLz.text" = "Duyệt web an toàn hơn - Quyền riêng tư được kiểm tra đầy đủ"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "iPad, iPhone và Mac"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "HỦY"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "Tường lửa Hàng năm (tiết kiệm ~38%)"; diff --git a/LockdowniOS/zh-Hans.lproj/LaunchScreen.strings b/LockdowniOS/zh-Hans.lproj/LaunchScreen.strings new file mode 100644 index 0000000..7dfa239 --- /dev/null +++ b/LockdowniOS/zh-Hans.lproj/LaunchScreen.strings @@ -0,0 +1,3 @@ + +/* Class = "UILabel"; text = "Lockdown Privacy"; ObjectID = "Cc5-n9-wvQ"; */ +"Cc5-n9-wvQ.text" = "Lockdown Privacy"; diff --git a/LockdowniOS/zh-Hans.lproj/Main.strings b/LockdowniOS/zh-Hans.lproj/Main.strings new file mode 100644 index 0000000..3248b41 --- /dev/null +++ b/LockdowniOS/zh-Hans.lproj/Main.strings @@ -0,0 +1,446 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "我同意"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "密码"; + +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "今天"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "标题"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "未激活"; + +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "学习"; + +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "警告"; + +/* Class = "UIButton"; normalTitle = "􀈂"; ObjectID = "3tZ-rh-ZOa"; */ +"3tZ-rh-ZOa.normalTitle" = "􀈂"; + +/* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ +"5Ce-KH-z1j.text" = "年度 iOS(优惠约 45%)"; + +/* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ +"6Qj-yK-k5A.text" = "一款简单、强大的防火墙,帮你阻止与跟踪器、恶意软件和其他不良代理的连接。

免费且开源。"; + +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "点击同意此"; + +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "未列入白名单"; + +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "世界上最简单的隐私政策"; + +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "2022 年 2 月"; + +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "同意"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "帐户可获以下好处:"; + +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "阻止日志已禁用"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "为什么要信任 Lockdown?"; + +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "年度 Pro(优惠约 30%)"; + +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "注册"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "Secure Tunnel VPN"; + +/* Class = "UIView"; accessibilityHint = "iOS Annual supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityHint" = "年度 iOS 支持 iPad 和 iPhone。使用 VoiceOver 双击进行选择。"; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "\"年度 iOS\"套餐复选框"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "为什么要信任 Lockdown?"; + +/* Class = "UILabel"; accessibilityHint = "Instructs user to tap the blue circle to the left of this label to activate."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityHint" = "指示用户点击此标签左侧的蓝色圆圈来激活。"; + +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "一个写着\"点击激活\"字样的标签。不是真正的按钮。"; + +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "点击激活"; + +/* Class = "UILabel"; text = "0"; ObjectID = "C0f-ye-V9U"; */ +"C0f-ye-V9U.text" = "0"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Cjo-hk-1dr"; */ +"Cjo-hk-1dr.normalTitle" = "关闭"; + +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "恢复购买"; + +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "忘记密码?"; + +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "美国 - 西部"; + +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "下午 12:22"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "关闭"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "登录"; + +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "查看日志"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "已有 Lockdown 帐户?
在下方登录。"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "电子邮件地址"; + +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "开始使用"; + +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "阻止已启用"; + +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "设置地区"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "防火墙"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "0"; ObjectID = "Msx-Nc-p9M"; */ +"Msx-Nc-p9M.text" = "0"; + +/* Class = "UILabel"; text = "Lockdown"; ObjectID = "NAN-dg-xBj"; */ +"NAN-dg-xBj.text" = "Lockdown"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "NvC-GW-2NJ"; */ +"NvC-GW-2NJ.normalTitle" = "取消"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "按钮"; + +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "保护"; + +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "位置:美国"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "iPad 和 iPhone"; + +/* Class = "UIButton"; normalTitle = "􀍟"; ObjectID = "OIa-zY-ALo"; */ +"OIa-zY-ALo.normalTitle" = "􀍟"; + +/* Class = "UIButton"; normalTitle = "􀁝"; ObjectID = "OLt-18-OkW"; */ +"OLt-18-OkW.normalTitle" = "􀁝"; + +/* Class = "UILabel"; text = "Block Invasive Tracking"; ObjectID = "PGe-EF-Ixy"; */ +"PGe-EF-Ixy.text" = "阻止侵入性跟踪"; + +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; + +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "注册"; + +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "白名单"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "隐私政策"; + +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "全时"; + +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "iPad、iPhone 和 Mac"; + +/* Class = "UILabel"; text = "3421"; ObjectID = "Sar-GU-wUS"; */ +"Sar-GU-wUS.text" = "3421"; + +/* Class = "UILabel"; text = "Lockdown Firewall is 100% on-device, so it does not collect or transmit any data to any servers - everything stays on your device."; ObjectID = "T2B-yz-40I"; */ +"T2B-yz-40I.text" = "Lockdown 防火墙 100% 在设备上运行,因此它不会收集或传输任何数据到任何服务器--所有数据都保留在您的设备上。"; + +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "保护正常连接"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "保存"; + +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domain-to-whitelist.com"; + +/* Class = "UILabel"; text = "Lockdown VPN is 100% open source, fully audited, and has a strict no-logs policy. Proof of your data protection is in the Privacy Policy."; ObjectID = "UJ5-nt-aYy"; */ +"UJ5-nt-aYy.text" = "Lockdown VPN 100% 开放源码,经过全面审计,并遵守严格的无日志策略。要证明对您数据的保护,请参阅《隐私政策》。"; + +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "免费开始使用"; + +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "了解更多"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "密码"; + +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "月度 iOS 支持 iPad 和 iPhone。使用 VoiceOver 双击进行选择。"; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "\"月度 iOS\"套餐复选框"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "关闭"; + +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "本周"; + +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "之后 49.99 美元/年(约 4.17 美元/月)"; + +/* Class = "UILabel"; text = "0"; ObjectID = "XSr-oZ-hkd"; */ +"XSr-oZ-hkd.text" = "0"; + +/* Class = "UILabel"; text = "Block Group Title"; ObjectID = "XcQ-Zo-7hE"; */ +"XcQ-Zo-7hE.text" = "阻止组标题"; + +/* Class = "UILabel"; text = "✓ Get new block lists for trackers
✓ Access Lockdown Mac and Desktop
✓ Critical announcements and features"; ObjectID = "XhT-hq-Dd2"; */ +"XhT-hq-Dd2.text" = "✓ 获得新的追踪器阻止列表
✓ 访问 Lockdown Mac 和桌面
✓ 重要公告和功能"; + +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "屏蔽列表"; + +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "帐户"; + +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "帐户"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "点击此按钮激活 Secure Tunnel"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "隐私政策"; + +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "设置地区"; + +/* Class = "UILabel"; text = "VPN"; ObjectID = "aXL-VV-lJa"; */ +"aXL-VV-lJa.text" = "VPN"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "anO-Te-34C"; */ +"anO-Te-34C.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "asx-fS-ufQ"; */ +"asx-fS-ufQ.text" = "电邮"; + +/* Class = "UILabel"; text = "🇺🇸"; ObjectID = "bDO-jU-rIH"; */ +"bDO-jU-rIH.text" = "我们"; + +/* Class = "UILabel"; text = "Pro Monthly"; ObjectID = "c6Y-nW-xhv"; */ +"c6Y-nW-xhv.text" = "月度 Pro"; + +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "隧道关闭"; + +/* Class = "UILabel"; text = "The connections blocked by Lockdown for the last day are shown below. As per our Privacy Policy, all the blocking is done on-device and never transmitted to any servers for processing."; ObjectID = "ccb-xf-LSM"; */ +"ccb-xf-LSM.text" = "Lockdown 前一日阻止的连接如下所示。根据我们的隐私政策,所有阻止均在设备上完成,绝不会传输到任何服务器进行处理。"; + +/* Class = "UIView"; accessibilityHint = "Pro Annual supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityHint" = "年度 Pro 支持 iPad、iPhone 和 Mac。使用 VoiceOver 双击进行选择。"; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "\"年度 Pro\"套餐复选框"; + +/* Class = "UILabel"; text = "Enter your email below and we'll send you an email to reset your password."; ObjectID = "dLw-qu-1Oe"; */ +"dLw-qu-1Oe.text" = "请在下方输入您的电子邮箱地址,我们会给您发送一封电子邮件,以便您重新设置自己的密码。"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "登录"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "密码"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "blocked-domain.com"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "点击此按钮激活防火墙"; + +/* Class = "UILabel"; text = "For fastest speeds, choose a region closest to you. You can also anonymize your IP through other regions."; ObjectID = "fTe-nS-2bH"; */ +"fTe-nS-2bH.text" = "为了获得最快速度,请选择离您最近的地区。您也可以通过其他地区来匿名自己的 IP。"; + +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "阻止日志"; + +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "跟踪防范优化"; + +/* Class = "UILabel"; text = "Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol."; ObjectID = "gJW-8m-Vk5"; */ +"gJW-8m-Vk5.text" = "密码必须至少为 8 个字符,至少包含一个大写字母、一个小写字母、一个数字和一个符号。"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "为什么要信任 Lockdown?"; + +/* Class = "UIView"; accessibilityHint = "Pro Monthly supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityHint" = "月度 Pro 支持 iPad、iPhone 和 Mac。使用 VoiceOver 双击进行选择。"; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "\"月度 Pro\"套餐复选框"; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "提交"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "启用阻止日志"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "jMa-F3-KUG"; */ +"jMa-F3-KUG.text" = "电子邮件"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "jXy-3G-JaC"; */ +"jXy-3G-JaC.accessibilityLabel" = "点击此按钮激活 Secure Tunnel"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "隐私浏览 + 隐藏位置和 IP"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "未激活"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "点击此按钮激活防火墙"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "查看 OPENAUDIT 报告"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "您现有的订阅将自动退款。"; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "服务条款"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "收件人:joe@email.com
回复:2019 年第四季度财务回顾"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "保存"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "iPad 和 iPhone"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "忘记密码"; + +/* Class = "UILabel"; text = "Secure Tunnel"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "登录"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "今天已阻止:"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "关闭"; + +/* Class = "UILabel"; text = "Some sites or apps don't work well with VPNs. The whitelist below allows you to whitelist sites so they bypass the VPN for a better experience."; ObjectID = "pVa-cX-EbQ"; */ +"pVa-cX-EbQ.text" = "一些网站或应用在 VPN 下不太好用。您可以使用以下白名单将这些网站列入白名单,让它们绕过 VPN,从而获得更好的体验。"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "电子邮件地址"; + +/* Class = "UILabel"; text = "whitelisted-domain.com"; ObjectID = "pao-zq-e98"; */ +"pao-zq-e98.text" = "whitelisted-domain.com"; + +/* Class = "UIButton"; normalTitle = "See How It Works"; ObjectID = "qBb-qF-2nJ"; */ +"qBb-qF-2nJ.normalTitle" = "了解运作方式"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "密码"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "已阻止逾 10 亿追踪器"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "rJk-m6-qqA"; */ +"rJk-m6-qqA.text" = "blocked-domain.com"; + +/* Class = "UILabel"; text = "IP: 18.142.2.87"; ObjectID = "rhx-SZ-Kki"; */ +"rhx-SZ-Kki.text" = "IP:18.142.2.87"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "syJ-Jd-iUo"; */ +"syJ-Jd-iUo.text" = "电子邮件"; + +/* Class = "UIButton"; normalTitle = "Continue"; ObjectID = "tzL-mY-IJs"; */ +"tzL-mY-IJs.normalTitle" = "继续"; + +/* Class = "UILabel"; text = "Tunnel Whitelist"; ObjectID = "uA3-l4-Qe0"; */ +"uA3-l4-Qe0.text" = "隧道白名单"; + +/* Class = "UILabel"; text = "📱"; ObjectID = "uSD-At-Csa"; */ +"uSD-At-Csa.text" = "📱"; + +/* Class = "UILabel"; text = "Secure Tunnel encrypts your connections, anonymizes browsing history, and hides your location and unique IP address."; ObjectID = "ujS-r7-ItM"; */ +"ujS-r7-ItM.text" = "安全隧道会将您的连接加密,匿名化浏览历史记录,隐藏您的位置和唯一 IP 地址。"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "隐私政策"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "开始一周免费试用"; + +/* Class = "UILabel"; text = "-"; ObjectID = "vKc-uc-qfV"; */ +"vKc-uc-qfV.text" = "-"; + +/* Class = "UILabel"; text = "Add a domain to whitelist"; ObjectID = "w5F-U8-hnF"; */ +"w5F-U8-hnF.text" = "添加一个列入白名单的域"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "电子邮件地址"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "月度 iOS"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "yIB-Tk-mf1"; */ +"yIB-Tk-mf1.text" = "防火墙"; + +/* Class = "UILabel"; text = "Browse Safer - Fully Audited Privacy"; ObjectID = "yXI-U1-vLz"; */ +"yXI-U1-vLz.text" = "浏览更安全 - 隐私经全面审核"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "iPad、iPhone 和 Mac"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "防火墙(节省约 38%)"; diff --git a/LockdowniOS/zh-Hant.lproj/LaunchScreen.strings b/LockdowniOS/zh-Hant.lproj/LaunchScreen.strings new file mode 100644 index 0000000..7dfa239 --- /dev/null +++ b/LockdowniOS/zh-Hant.lproj/LaunchScreen.strings @@ -0,0 +1,3 @@ + +/* Class = "UILabel"; text = "Lockdown Privacy"; ObjectID = "Cc5-n9-wvQ"; */ +"Cc5-n9-wvQ.text" = "Lockdown Privacy"; diff --git a/LockdowniOS/zh-Hant.lproj/Main.strings b/LockdowniOS/zh-Hant.lproj/Main.strings new file mode 100644 index 0000000..4090ecb --- /dev/null +++ b/LockdowniOS/zh-Hant.lproj/Main.strings @@ -0,0 +1,446 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "我同意"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "密碼"; + +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "今日"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "標題"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "未啟動"; + +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "了解"; + +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "警告"; + +/* Class = "UIButton"; normalTitle = "􀈂"; ObjectID = "3tZ-rh-ZOa"; */ +"3tZ-rh-ZOa.normalTitle" = "􀈂"; + +/* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ +"5Ce-KH-z1j.text" = "iOS 年度方案(可省下大約 45% 的價格)"; + +/* Class = "UILabel"; text = "A simple, powerful firewall that stops connections to trackers, malware and other bad agents.

Free and open source."; ObjectID = "6Qj-yK-k5A"; */ +"6Qj-yK-k5A.text" = "簡易、強大的防火牆,可阻止追蹤器、惡意軟體及其他惡意代理程式的連結。

免費且為開放來源。"; + +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "點選以同意"; + +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "未放入白名單"; + +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "世界上最簡易的隱私權政策"; + +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "2022 年 2 月"; + +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "同意"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "帳號可獲得以下好處:"; + +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "封鎖紀錄已關閉"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "為什麼要信任 Lockdown?"; + +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "Pro 年度方案(可省下大約 30% 的價格)"; + +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "註冊"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "Secure Tunnel VPN"; + +/* Class = "UIView"; accessibilityHint = "iOS Annual supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityHint" = "iOS 年度方案可支援 iPads 及 iPhones。使用 VoiceOver 點選兩次以選取。"; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "\"iOS 年度\"方案的核取方塊"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "為什麼要信任 Lockdown?"; + +/* Class = "UILabel"; accessibilityHint = "Instructs user to tap the blue circle to the left of this label to activate."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityHint" = "指示使用者點選此標籤左方的藍色圓圈以啟動。"; + +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "寫著「點選以啟動」的標籤,而非真正的按鈕。"; + +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "點選以啟動"; + +/* Class = "UILabel"; text = "0"; ObjectID = "C0f-ye-V9U"; */ +"C0f-ye-V9U.text" = "0"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Cjo-hk-1dr"; */ +"Cjo-hk-1dr.normalTitle" = "關閉"; + +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "恢復購買"; + +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "忘記密碼?"; + +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "美國--西部"; + +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "12:22 PM"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "關閉"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "登入"; + +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "檢視紀錄"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "已有 Lockdown 帳號?
在下方登入。"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "電子郵件地址"; + +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "開始使用"; + +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "已啟用封鎖"; + +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "設定區域"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "Firewall"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "0"; ObjectID = "Msx-Nc-p9M"; */ +"Msx-Nc-p9M.text" = "0"; + +/* Class = "UILabel"; text = "Lockdown"; ObjectID = "NAN-dg-xBj"; */ +"NAN-dg-xBj.text" = "Lockdown"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "NvC-GW-2NJ"; */ +"NvC-GW-2NJ.normalTitle" = "取消"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "按鈕"; + +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "保護"; + +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "地點:🇺🇸"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "iPads 及 iPhones"; + +/* Class = "UIButton"; normalTitle = "􀍟"; ObjectID = "OIa-zY-ALo"; */ +"OIa-zY-ALo.normalTitle" = "􀁝"; + +/* Class = "UIButton"; normalTitle = "􀁝"; ObjectID = "OLt-18-OkW"; */ +"OLt-18-OkW.normalTitle" = "􀁝"; + +/* Class = "UILabel"; text = "Block Invasive Tracking"; ObjectID = "PGe-EF-Ixy"; */ +"PGe-EF-Ixy.text" = "封鎖侵入式追蹤"; + +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; + +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "註冊"; + +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "白名單"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "隱私權政策"; + +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "無時無刻"; + +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "iPads、iPhones 及 Macs"; + +/* Class = "UILabel"; text = "3421"; ObjectID = "Sar-GU-wUS"; */ +"Sar-GU-wUS.text" = "3421"; + +/* Class = "UILabel"; text = "Lockdown Firewall is 100% on-device, so it does not collect or transmit any data to any servers - everything stays on your device."; ObjectID = "T2B-yz-40I"; */ +"T2B-yz-40I.text" = "Lockdown Firewall 100% 在裝置上,所以不需要收集或傳送任何資料至任何伺服器--所有內容皆在你的裝置上。"; + +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "保護良好的連線"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "儲存"; + +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domain-to-whitelist.com"; + +/* Class = "UILabel"; text = "Lockdown VPN is 100% open source, fully audited, and has a strict no-logs policy. Proof of your data protection is in the Privacy Policy."; ObjectID = "UJ5-nt-aYy"; */ +"UJ5-nt-aYy.text" = "Lockdown VPN 為 100% 開放來源、完全受到稽核,且有嚴格的無紀錄政策。為隱私權政策中資料保護的證明。"; + +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "開始免費使用"; + +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "了解更多"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "密碼"; + +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "iOS 月度方案可支援 iPads 及 iPhones。使用 VoiceOver 點選兩次以選取。"; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "\"iOS 月度\"方案的核取方塊"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "關閉"; + +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "本週"; + +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "以下時間後每年 $49.99(約為每月 $4.17):"; + +/* Class = "UILabel"; text = "0"; ObjectID = "XSr-oZ-hkd"; */ +"XSr-oZ-hkd.text" = "0"; + +/* Class = "UILabel"; text = "Block Group Title"; ObjectID = "XcQ-Zo-7hE"; */ +"XcQ-Zo-7hE.text" = "封鎖群組標題"; + +/* Class = "UILabel"; text = "✓ Get new block lists for trackers
✓ Access Lockdown Mac and Desktop
✓ Critical announcements and features"; ObjectID = "XhT-hq-Dd2"; */ +"XhT-hq-Dd2.text" = "✓ 取得全新的追蹤器封鎖清單
✓ 存取 Mac 及 Desktop 版 Lockdown
✓ 重要公告及功能"; + +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "封鎖清單"; + +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "帳號"; + +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "帳號"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "點選此按鈕以啟動 Secure Tunnel"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "隱私權政策"; + +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "設定區域"; + +/* Class = "UILabel"; text = "VPN"; ObjectID = "aXL-VV-lJa"; */ +"aXL-VV-lJa.text" = "VPN"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "anO-Te-34C"; */ +"anO-Te-34C.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "asx-fS-ufQ"; */ +"asx-fS-ufQ.text" = "電子郵件"; + +/* Class = "UILabel"; text = "🇺🇸"; ObjectID = "bDO-jU-rIH"; */ +"bDO-jU-rIH.text" = "🇺🇸"; + +/* Class = "UILabel"; text = "Pro Monthly"; ObjectID = "c6Y-nW-xhv"; */ +"c6Y-nW-xhv.text" = "Pro 月度"; + +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "TUNNEL 關閉"; + +/* Class = "UILabel"; text = "The connections blocked by Lockdown for the last day are shown below. As per our Privacy Policy, all the blocking is done on-device and never transmitted to any servers for processing."; ObjectID = "ccb-xf-LSM"; */ +"ccb-xf-LSM.text" = "前一天遭 Lockdown 封鎖的連線已顯示於下方。根據我們的隱私權政策,所有的封鎖皆在裝置上完成,且永遠不會傳送至任何伺服器進行處理。"; + +/* Class = "UIView"; accessibilityHint = "Pro Annual supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityHint" = "Pro 年度方案可支援 iPads、iPhones 及 Macs。使用 VoiceOver 點選兩次以選取。"; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "\"Pro 年度\"方案的核取方塊"; + +/* Class = "UILabel"; text = "Enter your email below and we'll send you an email to reset your password."; ObjectID = "dLw-qu-1Oe"; */ +"dLw-qu-1Oe.text" = "請在下方輸入您的電子郵件,我們將會寄給您一封電子郵件,以讓您重設密碼。"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "登入"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "密碼"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "blocked-domain.com"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "點選此按鈕以啟動 Firewall"; + +/* Class = "UILabel"; text = "For fastest speeds, choose a region closest to you. You can also anonymize your IP through other regions."; ObjectID = "fTe-nS-2bH"; */ +"fTe-nS-2bH.text" = "若想體驗最快的速度,請選擇最接近您的區域。您也能透過其他區域來匿名您的 IP。"; + +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "封鎖紀錄"; + +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "強化的追蹤防護"; + +/* Class = "UILabel"; text = "Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol."; ObjectID = "gJW-8m-Vk5"; */ +"gJW-8m-Vk5.text" = "密碼必須至少為 8 個字元,須包含至少一個大寫字母、一個小寫字母、一個數字及一個符號。"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "為什麼要信任 Lockdown?"; + +/* Class = "UIView"; accessibilityHint = "Pro Monthly supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityHint" = "Pro 月度方案可支援 iPads、iPhones 及 Macs。使用 VoiceOver 點選兩次以選取。"; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "\"Pro 月度\"方案的核取方塊"; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "提交"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "啟用封鎖清單"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "jMa-F3-KUG"; */ +"jMa-F3-KUG.text" = "電子郵件"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "jXy-3G-JaC"; */ +"jXy-3G-JaC.accessibilityLabel" = "點選此按鈕以啟動 Secure Tunnel"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "隱私瀏覽 + 隱藏地點及 IP"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "未啟動"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "點選此按鈕以啟動 Firewall"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "檢視開啟稽核報告"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "將自動存入至您既有的訂閱內容。"; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "服務條款"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "收件者:joe@email.com
回覆:Q4 2019 財務報告"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "儲存"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "iPads 及 iPhones"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "忘記密碼"; + +/* Class = "UILabel"; text = "Secure Tunnel"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "登入"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "今日已封鎖:"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "關閉"; + +/* Class = "UILabel"; text = "Some sites or apps don't work well with VPNs. The whitelist below allows you to whitelist sites so they bypass the VPN for a better experience."; ObjectID = "pVa-cX-EbQ"; */ +"pVa-cX-EbQ.text" = "有些網站或應用程式與 VPN 無法正常互動。下方的白名單將能讓您前往白名單網站,並能略過 VPN,以獲得更佳的遊戲體驗。"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "電子郵件地址"; + +/* Class = "UILabel"; text = "whitelisted-domain.com"; ObjectID = "pao-zq-e98"; */ +"pao-zq-e98.text" = "whitelisted-domain.com"; + +/* Class = "UIButton"; normalTitle = "See How It Works"; ObjectID = "qBb-qF-2nJ"; */ +"qBb-qF-2nJ.normalTitle" = "來瞧瞧如何運作"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "密碼"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "已封鎖超過 10 億個追蹤器"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "rJk-m6-qqA"; */ +"rJk-m6-qqA.text" = "blocked-domain.com"; + +/* Class = "UILabel"; text = "IP: 18.142.2.87"; ObjectID = "rhx-SZ-Kki"; */ +"rhx-SZ-Kki.text" = "IP:18.132.2.87"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "syJ-Jd-iUo"; */ +"syJ-Jd-iUo.text" = "電子郵件"; + +/* Class = "UIButton"; normalTitle = "Continue"; ObjectID = "tzL-mY-IJs"; */ +"tzL-mY-IJs.normalTitle" = "繼續"; + +/* Class = "UILabel"; text = "Tunnel Whitelist"; ObjectID = "uA3-l4-Qe0"; */ +"uA3-l4-Qe0.text" = "Tunnel 白名單"; + +/* Class = "UILabel"; text = "📱"; ObjectID = "uSD-At-Csa"; */ +"uSD-At-Csa.text" = "📱"; + +/* Class = "UILabel"; text = "Secure Tunnel encrypts your connections, anonymizes browsing history, and hides your location and unique IP address."; ObjectID = "ujS-r7-ItM"; */ +"ujS-r7-ItM.text" = "Secure Tunnel 會加密您的連線、匿名瀏覽紀錄,並隱藏您的地點及獨特的 IP 位址。"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "隱私權政策"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "開始 1 週免費試用"; + +/* Class = "UILabel"; text = "-"; ObjectID = "vKc-uc-qfV"; */ +"vKc-uc-qfV.text" = "-"; + +/* Class = "UILabel"; text = "Add a domain to whitelist"; ObjectID = "w5F-U8-hnF"; */ +"w5F-U8-hnF.text" = "將一個網域加入白名單"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "電子郵件地址"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "iOS 月度"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "yIB-Tk-mf1"; */ +"yIB-Tk-mf1.text" = "Firewall"; + +/* Class = "UILabel"; text = "Browse Safer - Fully Audited Privacy"; ObjectID = "yXI-U1-vLz"; */ +"yXI-U1-vLz.text" = "瀏覽更為安全--完全受到稽核的隱私"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "iPads、iPhones 及 Macs"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "取消"; + +/* Class = "UILabel"; text = "Firewall Annual (save ~38%)"; ObjectID = "HoY-Z7-gjw"; */ +"HoY-Z7-gjw.text" = "Firewall 年度方案(可省下大約 38% 的價格)"; diff --git a/Podfile b/Podfile index edcb4c8..d6f69d0 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,15 @@ inhibit_all_warnings! use_frameworks! -platform :ios, '11.0' +platform :ios, '13.0' + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET' + end + end +end target :'Lockdown' do plugin 'cocoapods-acknowledgements', :settings_bundle => true @@ -13,8 +21,11 @@ target :'Lockdown' do pod 'PromiseKit' pod 'SwiftyStoreKit', '0.13.1' pod 'KeychainAccess' - pod 'CocoaLumberjack', '3.5.2' + pod 'CocoaLumberjack' pod 'PopupDialog', '~> 0.9' + + #Linter + pod 'SwiftLint' end target :'LockdownTunnel' do @@ -28,7 +39,7 @@ target :'Lockdown VPN Widget' do pod 'PromiseKit' pod 'SwiftyStoreKit', '0.13.1' pod 'KeychainAccess' - pod 'CocoaLumberjack', '3.5.2' + pod 'CocoaLumberjack' pod 'ReachabilitySwift', '4.3.1' end @@ -36,10 +47,18 @@ target :'Lockdown Firewall Widget' do pod 'PromiseKit' pod 'SwiftyStoreKit', '0.13.1' pod 'KeychainAccess' - pod 'CocoaLumberjack', '3.5.2' + pod 'CocoaLumberjack' pod 'ReachabilitySwift', '4.3.1' end +target :'LockdownIntents' do + pod 'PromiseKit' + pod 'SwiftyStoreKit', '0.13.1' + pod 'KeychainAccess' + pod 'CocoaLumberjack' + pod 'ReachabilitySwift', '4.3.1' + end + target :'LockdownTests' do # see https://github.com/pointfreeco/swift-snapshot-testing/pull/308 pod 'SnapshotTesting', :git => 'https://github.com/pointfreeco/swift-snapshot-testing.git', :commit => '8e9f685' diff --git a/PushNotificationsAuthorizationUI.swift b/PushNotificationsAuthorizationUI.swift index c736956..56c3731 100644 --- a/PushNotificationsAuthorizationUI.swift +++ b/PushNotificationsAuthorizationUI.swift @@ -84,7 +84,7 @@ extension PushNotifications.Authorization { } } - static private func showGoToSettingsPopup(on vc: UIViewController, completion: @escaping () -> ()) { + static func showGoToSettingsPopup(on vc: UIViewController, completion: @escaping () -> ()) { let popup = PopupDialog( title: NSLocalizedString("Please first go to your iOS Settings > Notifications > Lockdown to enable notifications", comment: ""), message: nil, @@ -96,6 +96,17 @@ extension PushNotifications.Authorization { panGestureDismissal: false, hideStatusBar: false ) { + let settingsURL: URL? + if #available(iOS 16.0, *) { + settingsURL = URL(string: UIApplication.openNotificationSettingsURLString) + } else { + settingsURL = URL(string: UIApplication.openSettingsURLString) + } + + if let settingsURL, UIApplication.shared.canOpenURL(settingsURL) { + UIApplication.shared.open(settingsURL) + } + completion() } diff --git a/Settings.bundle/Pods-Lockdown-settings-metadata.plist b/Settings.bundle/Pods-Lockdown-settings-metadata.plist index cb8ee21..9fac593 100644 --- a/Settings.bundle/Pods-Lockdown-settings-metadata.plist +++ b/Settings.bundle/Pods-Lockdown-settings-metadata.plist @@ -259,6 +259,35 @@ THE SOFTWARE. Type PSGroupSpecifier + + FooterText + The MIT License (MIT) + +Copyright (c) 2020 Realm Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + Title + SwiftLint + Type + PSGroupSpecifier + FooterText Copyright (c) 2016 SwiftKick Mobile LLC diff --git a/Settings.bundle/Pods-LockdownIntents-settings-metadata.plist b/Settings.bundle/Pods-LockdownIntents-settings-metadata.plist new file mode 100644 index 0000000..a9422f7 --- /dev/null +++ b/Settings.bundle/Pods-LockdownIntents-settings-metadata.plist @@ -0,0 +1,151 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + BSD 3-Clause License + +Copyright (c) 2010-2019, Deusty, LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of Deusty nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of Deusty, LLC. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Title + CocoaLumberjack + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2014 kishikawa katsumi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + Title + KeychainAccess + Type + PSGroupSpecifier + + + FooterText + Copyright 2016-present, Max Howell; mxcl@me.com + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Title + PromiseKit + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2016 Ashley Mills + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + Title + ReachabilitySwift + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2015-2016 Andrea Bizzotto bizz84@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Title + SwiftyStoreKit + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Shared.swift b/Shared.swift index a4cec46..b710b0b 100644 --- a/Shared.swift +++ b/Shared.swift @@ -248,46 +248,3 @@ extension UIDevice { return identifier } } - -enum AppConfiguration { - case Debug - case TestFlight - case AppStore -} - -struct Config { - private static let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" - - static var isDebug: Bool { - #if DEBUG - return true - #else - return false - #endif - } - - static var appConfiguration: AppConfiguration { - if isDebug { - return .Debug - } else if isTestFlight { - return .TestFlight - } else { - return .AppStore - } - } -} - -// MARK: - Fonts -let fontRegular14 = UIFont(name: "Montserrat-Regular", size: 14)! -let fontRegular17 = UIFont(name: "Montserrat-Regular", size: 17)! -let fontMedium14 = UIFont(name: "Montserrat-Medium", size: 14)! -let fontMedium15 = UIFont(name: "Montserrat-Medium", size: 15)! -let fontMedium16 = UIFont(name: "Montserrat-Medium", size: 16)! -let fontMedium17 = UIFont(name: "Montserrat-Medium", size: 17)! -let fontMedium18 = UIFont(name: "Montserrat-Medium", size: 18)! -let fontSemiBold15_5 = UIFont(name: "Montserrat-SemiBold", size: 15.5)! -let fontSemiBold17 = UIFont(name: "Montserrat-SemiBold", size: 17)! -let fontBold11 = UIFont(name: "Montserrat-Bold", size: 11)! -let fontBold13 = UIFont(name: "Montserrat-Bold", size: 13)! -let fontBold15 = UIFont(name: "Montserrat-Bold", size: 15)! -let fontBold18 = UIFont(name: "Montserrat-Bold", size: 18)! diff --git a/SpeedTest.swift b/SpeedTest.swift index 419c785..0fd40df 100644 --- a/SpeedTest.swift +++ b/SpeedTest.swift @@ -21,10 +21,10 @@ public class SpeedTest: NSObject, URLSessionDelegate, URLSessionDataDelegate { var bytesReceived: Int! var fileName = "10MB.bin" - var fileSizeBytes:Double = 10485760 + var fileSizeBytes: Double = 10485760 public func testDownloadSpeedWithTimeout(timeout: TimeInterval) -> Promise { - if let r = reachability, r.connection == .wifi { + if reachability?.connection == .wifi { fileName = "50MB.bin" fileSizeBytes = 52428800 } else { diff --git a/Tests/LockdownTests/Core/ConnectivityServiceTests.swift b/Tests/LockdownTests/Core/ConnectivityServiceTests.swift new file mode 100644 index 0000000..dbd7c7b --- /dev/null +++ b/Tests/LockdownTests/Core/ConnectivityServiceTests.swift @@ -0,0 +1,50 @@ +// +// ConnectivityServiceTests.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/7/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import XCTest +@testable import Lockdown + +final class ConnectivityServiceTests: XCTestCase { + + func testUnknownState() { + testConnectivity(state: .unknown, hasMessage: .defaultSwiftMessagesLabelText) + } + + func testSatisfiedState() { + testConnectivity(state: .satisfied, hasMessage: .defaultSwiftMessagesLabelText) + } + + func testRestrictedCellularState() { + testConnectivity(state: .restrictedCellular, hasMessage: "Enable Cellular Data for Lockdown") + } + + func testNoConnectionState() { + testConnectivity(state: .noConnection, hasMessage: .localized("No Internet Connection")) + } + + private func testConnectivity(state: ConnectionState, hasMessage message: String) { + // Given + let connectivityService = ConnectivityService(connectionState: state) + + // When + connectivityService.showConnectionErrorIfNeeded() + + let expectation = expectation(description: "ConnectivityService uses main.async, so we test it there") + DispatchQueue.main.async { + expectation.fulfill() + } + waitForExpectations(timeout: 2, handler: nil) + + // Then + XCTAssertEqual(connectivityService.noInternetMessageView.bodyLabel?.text, message) + } +} + +private extension String { + static let defaultSwiftMessagesLabelText = "[Message Body]" +} diff --git a/Tests/LockdownTests/Core/EmailValidatableTests.swift b/Tests/LockdownTests/Core/EmailValidatableTests.swift new file mode 100644 index 0000000..27a5eec --- /dev/null +++ b/Tests/LockdownTests/Core/EmailValidatableTests.swift @@ -0,0 +1,93 @@ +// +// EmailValidatableTests.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/7/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import XCTest +@testable import Lockdown + +final class EmailValidatableTests: XCTestCase, EmailValidatable { + + func testKnownValidEmail() throws { + // Base line test with known working email address + let validEmailError = errorValidatingEmail("example-user@example.com") + XCTAssertNil(validEmailError) + } + + func testEmptyTextField() throws { + let emptyEmailError = try XCTUnwrap(errorValidatingEmail("")) + XCTAssertEqual(emptyEmailError.localizedDescription, EmailValidationError.notFilledIn.localizedDescription) + } + + func testWhiteSpaceTextField() throws { + let whiteSpaceEmailError = try XCTUnwrap(errorValidatingEmail(" ")) + XCTAssertEqual(whiteSpaceEmailError.localizedDescription, EmailValidationError.noValidEmailAddressDetected.localizedDescription) + } + + func testCharactersNoEmail() throws { + let noEmailError = try XCTUnwrap(errorValidatingEmail("sfjowejfiojaojfoajfojweofj")) + XCTAssertEqual(noEmailError.localizedDescription, EmailValidationError.noValidEmailAddressDetected.localizedDescription) + } + + func testCheckForCon() throws { + let endingWithConError = try XCTUnwrap(errorValidatingEmail("email@email.con")) + XCTAssertEqual(endingWithConError.localizedDescription, EmailValidationError.enteredConInsteadOfCom.localizedDescription) + } + + func testCheckForCaseAndCon() throws { + let endingWithCaseAndConError = try XCTUnwrap(errorValidatingEmail("email@EMAIL.CON")) + XCTAssertEqual(endingWithCaseAndConError.localizedDescription, EmailValidationError.enteredConInsteadOfCom.localizedDescription) + } + + func testMultipleEmails() throws { + let multipleEmailsError = try XCTUnwrap(errorValidatingEmail("email@email.com emailtest@email.com")) + XCTAssertEqual(multipleEmailsError.localizedDescription, EmailValidationError.tooManyEmailAddressesEntered.localizedDescription) + } + + // The link that is found contains an email address (mailto: link) + func testOtherURLTypes() throws { + let webUrlError = try XCTUnwrap(errorValidatingEmail("https://www.example.com")) + XCTAssertEqual(webUrlError.localizedDescription, EmailValidationError.noValidEmailAddressDetected.localizedDescription) + + let phoneNumberError = try XCTUnwrap(errorValidatingEmail("+1(555)555-5555")) + XCTAssertEqual(phoneNumberError.localizedDescription, EmailValidationError.noValidEmailAddressDetected.localizedDescription) + } + + func testUkrainianEmail() throws { + let emailUkrainian = errorValidatingEmail("квіточка@пошта.укр") + XCTAssertNil(emailUkrainian) + } + + func testGermanEmail() throws { + let emailGerman = errorValidatingEmail("Dörte@example.de") + let emailGermanSubDomain = errorValidatingEmail("Dörte@Sörensen.example.com") + XCTAssertNil(emailGerman) + XCTAssertNil(emailGermanSubDomain) + } + + func testRussianEmail() throws { + let emailRussian = errorValidatingEmail("коля@пример.рф") + let emailRussianSubDomain = errorValidatingEmail("иван.сергеев@пример.рф") + XCTAssertNil(emailRussian) + XCTAssertNil(emailRussianSubDomain) + } + + func testKatanaEmail() throws { + let emailKatana = errorValidatingEmail("support@ツッ.com") + XCTAssertNil(emailKatana) + } + + func testEmojiEmail() throws { + let emailEmojiError = try XCTUnwrap(errorValidatingEmail("🙈@📧.com")) + XCTAssertEqual(emailEmojiError.localizedDescription, EmailValidationError.noValidEmailAddressDetected.localizedDescription) + } + + // This type of error gets caught on the server side + func testLongEmail() throws { + let longEmail = errorValidatingEmail("email@email.comeemail@email.com") + XCTAssertNil(longEmail) + } +} diff --git a/Today/VPNTodayViewController.swift b/Today/VPNTodayViewController.swift index e83bb21..0989875 100644 --- a/Today/VPNTodayViewController.swift +++ b/Today/VPNTodayViewController.swift @@ -90,23 +90,23 @@ class VPNTodayViewController: UIViewController, NCWidgetProviding { } func setVPNButtonConnected() { - vpnStatusLabel.text = NSLocalizedString("VPN Active", comment: "") + vpnStatusLabel.text = .localized("VPN Active") toggleVPN?.tintColor = .tunnelsBlue toggleVPN.layer.borderColor = UIColor.tunnelsBlue.cgColor } func setVPNButtonDisconnected() { - vpnStatusLabel.text = NSLocalizedString("VPN Not Active", comment: "") + vpnStatusLabel.text = .localized("VPN Not Active") toggleVPN?.tintColor = UIColor.darkGray toggleVPN?.layer.borderColor = UIColor.darkGray.cgColor } func setVPNButtonConnecting() { - vpnStatusLabel.text = NSLocalizedString("Activating", comment: "") + vpnStatusLabel.text = .localized("Activating") } func setVPNButtonDisconnecting() { - vpnStatusLabel.text = NSLocalizedString("Deactivating", comment: "") + vpnStatusLabel.text = .localized("Deactivating") } // MARK: - Helpers diff --git a/Today/ar.lproj/MainInterface.strings b/Today/ar.lproj/MainInterface.strings new file mode 100644 index 0000000..1841457 --- /dev/null +++ b/Today/ar.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "VPN Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "VPN Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "United Kingdom"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "United Kingdom"; diff --git a/Today/de.lproj/MainInterface.strings b/Today/de.lproj/MainInterface.strings new file mode 100644 index 0000000..1841457 --- /dev/null +++ b/Today/de.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "VPN Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "VPN Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "United Kingdom"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "United Kingdom"; diff --git a/Today/ru.lproj/MainInterface.strings b/Today/ru.lproj/MainInterface.strings new file mode 100644 index 0000000..1841457 --- /dev/null +++ b/Today/ru.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "VPN Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "VPN Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "United Kingdom"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "United Kingdom"; diff --git a/Today/vi.lproj/MainInterface.strings b/Today/vi.lproj/MainInterface.strings new file mode 100644 index 0000000..1841457 --- /dev/null +++ b/Today/vi.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "VPN Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "VPN Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "United Kingdom"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "United Kingdom"; diff --git a/Today/zh-Hans.lproj/MainInterface.strings b/Today/zh-Hans.lproj/MainInterface.strings new file mode 100644 index 0000000..1841457 --- /dev/null +++ b/Today/zh-Hans.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "VPN Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "VPN Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "United Kingdom"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "United Kingdom"; diff --git a/Today/zh-Hant.lproj/MainInterface.strings b/Today/zh-Hant.lproj/MainInterface.strings new file mode 100644 index 0000000..1841457 --- /dev/null +++ b/Today/zh-Hant.lproj/MainInterface.strings @@ -0,0 +1,9 @@ + +/* Class = "UILabel"; text = "VPN Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "VPN Active"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Open Lockdown"; + +/* Class = "UILabel"; text = "United Kingdom"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "United Kingdom"; diff --git a/ar.lproj/Localizable.strings b/ar.lproj/Localizable.strings new file mode 100644 index 0000000..79f4eee --- /dev/null +++ b/ar.lproj/Localizable.strings @@ -0,0 +1,809 @@ +/* Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com */ +" and " = " و "; + +/* refers to number of connections blocked today, as in '34 Blocked Today' */ +" Blocked Today" = " حُظر اليوم"; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to add to a whitelist. For example, \"google.com\" without \"https://\"" = " ليس إدخالاً صالحًا. الرجاء إدخال مضيف النطاق الذي تريد إضافته إلى القائمة البيضاء فقط. على سبيل المثال، \"google.com\" بدون \"https://\""; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to block. For example, \"google.com\" without \"https://\"" = " ليس إدخالاً صالحًا. الرجاء إدخال مضيف النطاق الذي تريد حظره فقط. على سبيل المثال، \"google.com\" بدون \"https://\""; + +/* No comment provided by engineer. */ +" Off" = " إيقاف"; + +/* No comment provided by engineer. */ +" On" = " تشغيل"; + +/* Used in the sentence: I blocked 500 trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com. */ +" trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com." = " أدوات التتبع والإعلانات والبرامج الضارة باستخدام Lockdown، وهو جدار الحماية الذي يمنع الاتصالات غير المرغوب فيها في جميع تطبيقاتك. احصل عليه مجانًا من lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"1 Week Free" = "1 أسبوع مجانًا"; + +/* No comment provided by engineer. */ +"\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). You can also set custom domains to block." = "\"قائمة الحظر\" تتيح لك اختيار ما تريد حظره (على سبيل المثال، Facebook، محتوى جذب النقرات، إلخ). يمكنك أيضًا تعيين نطاقات مخصصة لحظرها."; + +/* No comment provided by engineer. */ +"\"iOS Annual\" is your current plan" = "\"iOS Annual\" هي خطتك الحالية"; + +/* No comment provided by engineer. */ +"\"iOS Monthly\" is your current plan" = "\"iOS Monthly\" هي خطتك الحالية"; + +/* No comment provided by engineer. */ +"\"Pro Annual\" is your current plan" = "\"Pro Annual\" هي خطتك الحالية"; + +/* No comment provided by engineer. */ +"\"Pro Monthly\" is your current plan" = "\"Pro Monthly\" هي خطتك الحالية"; + +/* No comment provided by engineer. */ +"\"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." = "\"عرض السجل\" يظهر بالضبط ما الاتصالات التي تم حظرها في اليوم الماضي. يُمسح هذا السجل في منتصف الليل ويظل على الجهاز، لذا فهو مرئي لك فقط."; + +/* No comment provided by engineer. */ +"\n\n If this error persists, please contact team@lockdownprivacy.com." = "\n\n إذا استمرت المشكلة، فيرجى التواصل مع team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"\n\n If this persists, please contact team@lockdownprivacy.com." = "\n\n إذا استمر هذا، فيرجى التواصل مع team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"About Lockdown VPN" = "حول Lockdown VPN"; + +/* No comment provided by engineer. */ +"Activate Firewall with this button." = "قم بتنشيط جدار الحماية باستخدام هذا الزر."; + +/* No comment provided by engineer. */ +"Activating" = "جارٍ التنشيط"; + +/* No comment provided by engineer. */ +"Anonymize IP against trackers" = "إخفاء هوية IP أمام أدوات التتبع"; + +/* No comment provided by engineer. */ +"Apple Outage" = "انقطاع Apple"; + +/* No comment provided by engineer. */ +"at least 8 characters" = "8 أحرف على الأقل"; + +/* No comment provided by engineer. */ +"Audit Reports" = "تقارير المراجعة"; + +/* No comment provided by engineer. */ +"Australia" = "استراليا"; + +/* No comment provided by engineer. */ +"Be sure to check your spam folder in case it got stuck there.\n\nYou can also request a re-send of the confirmation." = "تأكد من فحص مجلد البريد العشوائي الخاص بك في حالة وجوده هناك.\n\nيمكنك أيضًا طلب إعادة إرسال التأكيد."; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"block list" = "قائمة الحظر"; + +/* No comment provided by engineer. */ +"Blocked" = "حُظر"; + +/* No comment provided by engineer. */ +"Blocked Domains" = "النطاقات المحظورة"; + +/* No comment provided by engineer. */ +"Blocked Today" = "حُظر اليوم"; + +/* No comment provided by engineer. */ +"Blocked Trackers Summary" = "موجز أدوات التتبع المحظورة"; + +/* No comment provided by engineer. */ +"Brazil" = "البرازيل"; + +/* No comment provided by engineer. */ +"Canada" = "كندا"; + +/* No comment provided by engineer. */ +"Cancel" = "إلغاء"; + +/* No comment provided by engineer. */ +"Check Your Inbox" = "تحقق من صندوق بريدك"; + +/* No comment provided by engineer. */ +"Clear Block Log" = "مسح سجل الحظر"; + +/* The title of a list of trackers */ +"Clickbait" = "محتوى جذب النقرات"; + +/* No comment provided by engineer. */ +"Confirm Email" = "تأكيد عنوان البريد الإلكتروني"; + +/* No comment provided by engineer. */ +"Confirmed Site" = "موقع تم تأكيده"; + +/* No comment provided by engineer. */ +"Couldn't Find Your Email Client" = "تعذر العثور على برنامج بريدك الإلكتروني"; + +/* The title of a list of trackers */ +"Crypto Mining" = "تعدين العملات الرقمية"; + +/* No comment provided by engineer. */ +"Custom Settings (Advanced)" = "إعدادات مخصصة (متقدمة)"; + +/* The title of a list of trackers */ +"Data Trackers" = "أدوات تتبع البيانات"; + +/* No comment provided by engineer. */ +"Deactivating" = "جارٍ إلغاء التنشيط"; + +/* No comment provided by engineer. */ +"Disable" = "تعطيل"; + +/* No comment provided by engineer. */ +"Disable Block Log" = "تعطيل سجل الحظر"; + +/* No comment provided by engineer. */ +"Disable Block Log?" = "تعطيل سجل الحظر؟"; + +/* No comment provided by engineer. */ +"Done" = "تم"; + +/* No comment provided by engineer. */ +"Email Support" = "دعم البريد الإلكتروني"; + +/* The title of a list of trackers */ +"Email Trackers" = "أدوات تتبع البريد الإلكتروني"; + +/* No comment provided by engineer. */ +"Email Us" = "راسلنا عبر البريد الإلكتروني"; + +/* No comment provided by engineer. */ +"Enable" = "تمكين"; + +/* No comment provided by engineer. */ +"Enable Block Log" = "تمكين سجل الحظر"; + +/* No comment provided by engineer. */ +"Enable notifications to get a once-a-week summary and the latest block list updates. You can disable this anytime." = "قم بتمكين الإشعارات للحصول على ملخص مرة في الأسبوع وآخر تحديثات قائمة الحظر. يمكنك تعطيل هذا في أي وقت."; + +/* No comment provided by engineer. */ +"Encrypted, private connections" = "اتصالات خاصة مشفرة"; + +/* No comment provided by engineer. */ +"Error Code " = "رمز الخطأ "; + +/* No comment provided by engineer. */ +"Error Creating Email Account" = "خطأ في إنشاء حساب بريد إلكتروني"; + +/* No comment provided by engineer. */ +"Error Loading Plan: Retry" = "خطأ في تحميل الخطة: أعد المحاولة"; + +/* No comment provided by engineer. */ +"Error Re-sending Email Confirmation" = "خطأ في إعادة إرسال رسالة تأكيد البريد الإلكتروني"; + +/* No comment provided by engineer. */ +"Error Restoring Subscription" = "خطأ في استعادة الاشتراك"; + +/* No comment provided by engineer. */ +"Error Signing In To Verify Subscription" = "خطأ في تسجيل الدخول للتحقق من الاشتراك"; + +/* No comment provided by engineer. */ +"Error Starting Trial" = "خطأ في بدء الفترة التجريبية"; + +/* The title of a list of trackers */ +"Facebook & WhatsApp" = "Facebook و WhatsApp"; + +/* The title of a list of trackers */ +"Facebook Trackers" = "أدوات Facebook التتبعية"; + +/* No comment provided by engineer. */ +"Failed to re-send email confirmation." = "فشل في إعادة إرسال رسالة تأكيد البريد الإلكتروني."; + +/* No comment provided by engineer. */ +"FAQs" = "الأسئلة الشائعة"; + +/* No comment provided by engineer. */ +"Firewall" = "جدار الحماية"; + +/* No comment provided by engineer. */ +"Firewall Active" = "جدار الحماية نشط"; + +/* No comment provided by engineer. */ +"Firewall Not Active" = "جدار الحماية ليس نشطًا"; + +/* No comment provided by engineer. */ +"FIREWALL OFF" = "جدار الحماية مقفل"; + +/* No comment provided by engineer. */ +"FIREWALL ON" = "جدار الحماية قيد التشغيل"; + +/* No comment provided by engineer. */ +"For maximum privacy, activate Secure Tunnel, which uses bank-level encryption to protect connections, anonymize your browsing, and hide your location and IP." = "للحصول على أقصى قدر من الخصوصية، نشط Secure Tunnel، والذي يستخدم تشفير من مستوى بنكي لحماية الاتصالات وإخفاء هوية تصفحك وإخفاء موقعك وعنوان IP الخاص بك."; + +/* No comment provided by engineer. */ +"France" = "فرنسا"; + +/* The title of a list of trackers */ +"Game Marketing" = "تسويق الألعاب"; + +/* The title of a list of trackers */ +"General Marketing" = "تسويق عام"; + +/* No comment provided by engineer. */ +"Germany" = "ألمانيا"; + +/* The title of a list of trackers */ +"Google Shopping" = "Google Shopping"; + +/* No comment provided by engineer. */ +"Hey Lockdown Team, \nI have a question, issue, or suggestion - " = "مرحبًا يا فريق Lockdown، \nلدي سؤال أو مشكلة أو اقتراح - "; + +/* No comment provided by engineer. */ +"Hi, my question or feedback for Lockdown is: " = "مرحبًا، سؤالي أو ملاحظاتي على Lockdown هي: "; + +/* Used in the sentence: I blocked 500 trackers with Lockdown. */ +"I blocked" = "لقد حظرت"; + +/* Used in: You blocked tracker.com in your custom blocked domains. */ +"in your custom blocked domains." = "في نطاقات حظرك المخصصة."; + +/* No comment provided by engineer. */ +"India" = "الهند"; + +/* No comment provided by engineer. */ +"Invalid domain" = "نطاق خطأ"; + +/* No comment provided by engineer. */ +"IP: 18.132.2.87" = "IP: 18.132.2.87"; + +/* No comment provided by engineer. */ +"IP: [Anonymized]" = "IP: [تم تجهيله]"; + +/* No comment provided by engineer. */ +"Ireland" = "أيرلندا"; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"is a part of" = "كجزء من"; + +/* No comment provided by engineer. */ +"Japan" = "اليابان"; + +/* Shows when loading the details of a subscription plan */ +"Loading Plan" = "جارِ تحميل الخطة"; + +/* No comment provided by engineer. */ +"Location changed and hidden" = "تم تغيير الموقع وإخفائه"; + +/* No comment provided by engineer. */ +"Location: 🇯🇵" = "الموقع: 🇯🇵"; + +/* No comment provided by engineer. */ +"Location: 🇺🇸" = "الموقع: 🇺🇸"; + +/* No comment provided by engineer. */ +"Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari." = "يحظر Lockdown Firewall الاتصالات السيئة وغير الموثوق بها في جميع تطبيقاتك - وليس Safari فقط."; + +/* No comment provided by engineer. */ +"Lockdown is open source and fully transparent, which means anyone can see exactly what it's doing. Also, Lockdown Firewall has a simple, strict Privacy Policy, while Lockdown VPN is fully audited by security experts." = "يُعد Lockdown مفتوح المصدر وشفافًا تمامًا، مما يعني أن أي شخص يمكنه رؤية ما يفعله بالضبط. أيضًا، يحتوي Lockdown Firewall على سياسة خصوصية بسيطة وصارمة، بينما يخضع Lockdown VPN للتدقيق الكامل من قبل خبراء الأمن."; + +/* No comment provided by engineer. */ +"Lockdown VPN is powered by Confirmed VPN, the open source, no-logs, and fully audited VPN." = "يعمل Lockdown VPN بواسطة Confirmed VPN، الشبكة الافتراضية الخاصة (VPN) المفتوحة المصدر وبدون سجلات والمدققة بالكامل."; + +/* The title of a list of trackers */ +"Marketing Trackers" = "أدوات المسوقين التتبعية"; + +/* The title of a list of trackers */ +"Marketing Trackers II" = "أدوات المسوقين التتبعية 2"; + +/* No comment provided by engineer. */ +"Network Error" = "خطأ في الشبكة"; + +/* No comment provided by engineer. */ +"Next" = "التالي"; + +/* No comment provided by engineer. */ +"Nice" = "جيد"; + +/* No comment provided by engineer. */ +"No" = "لا"; + +/* No comment provided by engineer. */ +"No Active Subscription" = "لا يوجد اشتراك نشط"; + +/* No comment provided by engineer. */ +"No additional information on this blocked domain found." = "لم يتم العثور على معلومات إضافية حول هذا النطاق المحظور."; + +/* No comment provided by engineer. */ +"No Block Lists Enabled" = "لا توجد قوائم حظر مفعلة"; + +/* No comment provided by engineer. */ +"No Info Found" = "لم يتم العثور على معلومات"; + +/* No comment provided by engineer. */ +"No Internet Connection" = "لا يوجد اتصال بالإنترنت"; + +/* No comment provided by engineer. */ +"No stored API credentials found. Please contact team@lockdownprivacy.com about this error." = "لم يتم العثور على بيانات اعتماد API مخزنة. يرجى التواصل مع team@lockdownprivacy.com بخصوص هذا الخطأ."; + +/* No comment provided by engineer. */ +"Not Blocked" = "غير محظور"; + +/* No comment provided by engineer. */ +"Not Now" = "ليس الآن"; + +/* No comment provided by engineer. */ +"Not Whitelisted" = "ليس في القائمة البيضاء"; + +/* No comment provided by engineer. */ +"Notifications: Off" = "الإشعارات: قيد الإيقاف"; + +/* No comment provided by engineer. */ +"Notifications: On" = "الإشعارات: قيد التشغيل"; + +/* No comment provided by engineer. */ +"OK" = "موافق"; + +/* No comment provided by engineer. */ +"Okay" = "حسنًا"; + +/* No comment provided by engineer. */ +"one lowercase letter" = "حرف صغير"; + +/* No comment provided by engineer. */ +"one number" = "عدد"; + +/* No comment provided by engineer. */ +"one symbol" = "رمز"; + +/* No comment provided by engineer. */ +"one uppercase letter" = "حرف كبير"; + +/* No comment provided by engineer. */ +"Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol." = "يجب أن تتكون كلمة المرور من 8 أحرف على الأقل، وتحتوي على حرف كبير واحد وحرف صغير واحد ورقم واحد ورمز واحد على الأقل."; + +/* No comment provided by engineer. */ +"Plan: Annual Pro" = "الخطة: Annual Pro"; + +/* No comment provided by engineer. */ +"Please check your internet connection. If this persists, please contact team@lockdownprivacy.com.\n\nError Description\n" = "الرجاء التحقق من اتصال الإنترنت الخاص بك. إذا استمر ذلك، فيرجى التواصل مع team@lockdownprivacy.com.\n\nوصف الخطأ\n"; + +/* No comment provided by engineer. */ +"Please contact team@lockdownprivacy.com.\n\nError details:\n" = "إذا استمر هذا، فيرجى التواصل مع team@lockdownprivacy.com.\n\nتفاصيل الخطأ\n"; + +/* No comment provided by engineer. */ +"Please email team@lockdownprivacy.com with the following Error Code " = "يرجى إرسال رسالة إلى team@lockdownprivacy.com مع رمز الخطأ التالي "; + +/* No comment provided by engineer. */ +"Please first go to your iOS Settings > Notifications > Lockdown to enable notifications" = "يُرجى أولاً الانتقال إلى iOS Settings > Notifications > Lockdown لتمكين الإشعارات"; + +/* No comment provided by engineer. */ +"Please make sure you have added an e-mail account to your iOS device and try again." = "يرجى التأكد من إضافة حساب بريد إلكتروني إلى جهاز iOS الخاص بك والمحاولة مرة أخرى."; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active and that you have an active subscription. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "يرجى التأكد من أن اتصالك بالإنترنت نشط وأن لديك اشتراكًا نشطًا. بخلاف ذلك، يرجى بدء فترتك التجريبية المجانية أو إرسال بريد إلكتروني إلى team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with the following error message: " = "يرجى التأكد من أن اتصالك بالإنترنت نشط. إذا استمر هذا الخطأ، فأرسل بريدًا إلكترونيًا إلى team@lockdownprivacy.com برسالة الخطأ التالية: "; + +/* No comment provided by engineer. */ +"Please renew your subscription to activate the Secure Tunnel." = "يرجى تجديد اشتراكك لتفعيل Secure Tunnel."; + +/* No comment provided by engineer. */ +"Please renew your subscription to re-activate the VPN." = "يرجى تجديد اشتراكك لإعادة تنشيط VPN."; + +/* No comment provided by engineer. */ +"Please tap Block List and enable at least one block list to activate Firewall." = "يرجى النقر فوق قائمة الحظر وتمكين قائمة حظر واحدة على الأقل لتنشيط جدار الحماية."; + +/* No comment provided by engineer. */ +"Pre-configured Block Lists" = "قوائم حظر مُعدّة مسبقًا"; + +/* No comment provided by engineer. */ +"Pre-configured Suggestions" = "اقتراحات مُعدّة مسبقًا"; + +/* No comment provided by engineer. */ +"Precise location exposed" = "الموقع الدقيق مكشوف"; + +/* No comment provided by engineer. */ +"Press & Media" = "الصحافة والإعلام"; + +/* No comment provided by engineer. */ +"Privacy Policy" = "سياسة الخصوصية"; + +/* The title of a list of trackers */ +"Ransomware" = "برامج الفدية"; + +/* No comment provided by engineer. */ +"Re-send" = "إعادة إرسال"; + +/* No comment provided by engineer. */ +"Readable browsing and data" = "تصفح وبيانات قابلة للقراءة"; + +/* No comment provided by engineer. */ +"Remember to check our FAQs first, for answers to the most frequently asked questions.\n\nIf it's not answered there, we're happy to provide support and take feedback by email." = "تذكر أن تتحقق من الأسئلة الشائعة أولاً، للحصول على إجابات عن الأسئلة الأكثر شيوعًا.\n\nإذا لم تجد الإجابة هناك، فيسعدنا تقديم الدعم وتلقي الملاحظات عبر البريد الإلكتروني."; + +/* The title of a list of trackers */ +"Reporting" = "الإبلاغ"; + +/* No comment provided by engineer. */ +"Secure Tunnel VPN" = "Secure Tunnel VPN"; + +/* No comment provided by engineer. */ +"See live metrics for how many bad connections Firewall has blocked." = "اطلع على المقاييس اللحظية لمعرفة عدد الاتصالات السيئة التي حظرها جدار الحماية."; + +/* No comment provided by engineer. */ +"Settings" = "الإعدادات"; + +/* No comment provided by engineer. */ +"Share your anonymized metrics and show other people how to block invasive tracking." = "شارك مقاييسك المجهلة ووضح للآخرين كيفية منع التتبع التطفلي."; + +/* No comment provided by engineer. */ +"Share Your Stats" = "شارك احصائياتك"; + +/* No comment provided by engineer. */ +"SHOW" = "عرض"; + +/* No comment provided by engineer. */ +"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." = "أظهر مدى تطفل تطبيقات اليوم، وساعد الآخرين على حظر أدوات التتبع والبرامج الضارة أيضًا.\n\nلم يتم تضمين سجل الحظر الخاص بك - فقط الصورة أعلاه. اختر مكان المشاركة في الخطوة التالية."; + +/* No comment provided by engineer. */ +"Sign Out" = "تسجيل الخروج"; + +/* No comment provided by engineer. */ +"Sign Out?" = "تسجيل الخروج؟"; + +/* No comment provided by engineer. */ +"Sign Up | Sign In" = "التسجيل | تسجيل الدخول"; + +/* No comment provided by engineer. */ +"Sign up below to unlock benefits of a Lockdown account." = "قم بالتسجيل أدناه لفتح مزايا حساب Lockdown."; + +/* No comment provided by engineer. */ +"Signed In" = "تم تسجيل الدخول"; + +/* No comment provided by engineer. */ +"Signed out successfully." = "تم تسجيل الخروج بنجاح."; + +/* No comment provided by engineer. */ +"Singapore" = "سنغافورة"; + +/* No comment provided by engineer. */ +"Skip Trial" = "تخطي الفترة التجريبية"; + +/* The title of a list of trackers */ +"Snapchat Trackers" = "أدوات Snapchat التتبعية"; + +/* No comment provided by engineer. */ +"South Korea" = "كوريا الجنوبية"; + +/* No comment provided by engineer. */ +"Stay Protected" = "ابق محميًا"; + +/* No comment provided by engineer. */ +"Subscription Expired" = "انتهت صلاحية الاشتراك"; + +/* No comment provided by engineer. */ +"Success" = "تم بنجاح"; + +/* No comment provided by engineer. */ +"Success!" = "تم بنجاح!"; + +/* No comment provided by engineer. */ +"Successfully re-sent your email confirmation to " = "تم بنجاح إعادة إرسال تأكيد البريد الإلكتروني الخاص بك إلى "; + +/* No comment provided by engineer. */ +"Support | Feedback" = "الدعم| الملاحظات"; + +/* No comment provided by engineer. */ +"Tap to activate Lockdown Firewall and update to the newest block lists." = "اضغط لتنشيط Lockdown Firewall والتحديث إلى أحدث قوائم الحظر."; + +/* No comment provided by engineer. */ +"Tap to see a demo of how Secure Tunnel protects and anonymizes you." = "اضغط لمشاهدة عرض توضيحي عن كيفية حماية Secure Tunnel لك وإخفائه لهويتك."; + +/* No comment provided by engineer. */ +"Tap to see the blocked tracking attempts." = "اضغط لرؤية محاولات التتبع التي حظرت."; + +/* Used in the paragraph: You've just blocked 100 tracking attempts! Tap to see what they are. */ +"Tap to see what they are." = "اضغط لمعرفة تفاصيلها."; + +/* No comment provided by engineer. */ +"Terms" = "الشروط"; + +/* No comment provided by engineer. */ +"Thanks for helping to increase privacy and tracking awareness." = "شكرًا للمساعدة في زيادة الوعي بالخصوصية والتتبع."; + +/* No comment provided by engineer. */ +"The block log can be manually cleared or disabled. Disabling the Block Log only disables the log of connections - the number of tracking attempts will still be displayed." = "يمكن مسح سجل الحظر يدويًا أو تعطيله. يؤدي تعطيل سجل الحظر إلى تعطيل سجل الاتصالات فقط - وسيظل عدد محاولات التعقب معروضًا."; + +/* Used in To complete your signup, click the confirmation link we sent to you@gmail.com */ +"To complete your signup, click the confirmation link we sent to" = "لإكمال تسجيلك، انقر فوق ارتباط التأكيد الذي أرسلناه إلى"; + +/* No comment provided by engineer. */ +"To finish signup, click the confirmation link in the email we just sent. If you don't see it, check if it's stuck in your spam folder." = "لإنهاء التسجيل، انقر فوق ارتباط التأكيد في البريد الإلكتروني الذي أرسلناه للتو. إذا لم تره، فتحقق مما إذا كان عالقًا في مجلد البريد العشوائي."; + +/* No comment provided by engineer. */ +"To see this tutorial again, open the Account tab." = "لمشاهدة هذا البرنامج التعليمي مرة أخرى، افتح علامة التبويب \"الحساب\"."; + +/* No comment provided by engineer. */ +"To: joe@email.com\nRe: Q4 2019 Finance Review" = "إلى: joe@email.com\nRe: المراجعة المالية للربع الرابع في 2019"; + +/* No comment provided by engineer. */ +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "تم الآن حظر أدوات التتبع والإعلانات والبرامج النصية الضارة الأخرى في جميع تطبيقاتك، حتى خارج Safari.\n\nاحصل على أقصى قدر من الخصوصية مع Secure Tunnel الذي يحمي الاتصالات ويجعل تصفحك مجهلاً ويخفي موقعك."; + +/* Used in the sentence: You blocked 500 tracking attempts this week. Tap to update to the newsst block lists. */ +"tracking attempts this week. Tap to update to the newest block lists." = "محاولات التتبع لهذا الأسبوع. انقر للتحديث إلى أحدث قوائم الحظر."; + +/* No comment provided by engineer. */ +"Tunnel" = "النفق"; + +/* No comment provided by engineer. */ +"Tunnel Off" = "النفق قيد الإيقاف"; + +/* No comment provided by engineer. */ +"Tunnel On" = "النفق قيد التشغيل"; + +/* No comment provided by engineer. */ +"Tutorial" = "البرنامج التعليمي"; + +/* No comment provided by engineer. */ +"Unique IP address broadcasted" = "تم بث عنوان IP فريد"; + +/* No comment provided by engineer. */ +"United Kingdom" = "المملكة المتحدة"; + +/* No comment provided by engineer. */ +"United States - East" = "الولايات المتحدة - الشرق"; + +/* No comment provided by engineer. */ +"United States - West" = "الولايات المتحدة - الغرب"; + +/* No comment provided by engineer. */ +"Update Success" = "نجح التحديث"; + +/* No comment provided by engineer. */ +"Updating Block Lists" = "قوائم الحظر قيد التحديث"; + +/* No comment provided by engineer. */ +"USA East" = "الولايات المتحدة الشرق"; + +/* No comment provided by engineer. */ +"USA West" = "الولايات المتحدة الغرب"; + +/* No comment provided by engineer. */ +"View FAQs" = "عرض الأسئلة الشائعة"; + +/* No comment provided by engineer. */ +"View or Upgrade Plan" = "عرض الخطة أو ترقيتها"; + +/* No comment provided by engineer. */ +"View Upgrade Options" = "عرض خيارات التحديث"; + +/* No comment provided by engineer. */ +"VPN Active" = "VPN نشط"; + +/* No comment provided by engineer. */ +"VPN Not Active" = "VPN ليس نشطًا"; + +/* No comment provided by engineer. */ +"VPN Subscription Expired" = "انتهت صلاحية اشتراك VPN"; + +/* No comment provided by engineer. */ +"Website" = "موقع الويب"; + +/* No comment provided by engineer. */ +"Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue." = "مرحبًا بك في البرنامج التعليمي لـ Lockdown.\n\nاضغط في أي مكان للمتابعة."; + +/* No comment provided by engineer. */ +"What is VPN?" = "ما هو الـ VPN؟"; + +/* No comment provided by engineer. */ +"Whitelisted" = "في القائمة البيضاء"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown" = "لماذا تثق في Lockdown"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown?" = "لماذا تثق في Lockdown؟"; + +/* No comment provided by engineer. */ +"Why Use VPN?" = "لماذا استخدم VPN؟"; + +/* Used in the sentence: You blocked 500 tracking attempts this week. + Used in: You blocked tracker.com in your custom blocked domains. */ +"You blocked" = "لقد حظرت"; + +/* No comment provided by engineer. */ +"You blocked this domain in your custom blocked domains." = "لقد حظرت هذا النطاق في نطاقات حظرك المخصصة."; + +/* No comment provided by engineer. */ +"You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)" = "يمكنك الضغط فوق هذا لاحقًا إذا كنت ترغب في المشاركة.\n(اضغط في أي مكان للتجاهل)"; + +/* No comment provided by engineer. */ +"You'll be signed out from this account." = "سيتم تسجيل خروجك من هذا الحساب."; + +/* No comment provided by engineer. */ +"You'll have to reenable it later here to start seeing blocked entries again." = "سيتعين عليك إعادة تمكينه لاحقًا هنا لبدء مشاهدة الإدخالات المحظورة مرة أخرى."; + +/* No comment provided by engineer. */ +"You've just blocked 100 tracking attempts!" = "لقد منعت للتو 100 محاولة تتبع!"; + +/* No comment provided by engineer. */ +"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." = "تم تأكيد حسابك وقمت بتسجيل الدخول الآن. ستحصل على أحدث قوائم الحظر، والوصول إلى Lockdown Mac، والحصول على إعلانات مهمة."; + +/* No comment provided by engineer. */ +"Your Settings" = "إعداداتك"; + +/* No comment provided by engineer. */ +"⚠️ Not Signed In" = "⚠️ ليس مسجلاً الدخول"; + +/* No comment provided by engineer. */ +"🔥🧱 Firewall Activated 🎊🎉" = "🔥🧱 تم تنشيط جدار الحماية 🎊🎉"; + +/* No comment provided by engineer. */ +"Error while trying to confirm your account:" = "خطأ أثناء محاولة تأكيد حسابك:"; + +/* No comment provided by engineer. */ +"If this persists, please contact team@lockdownprivacy.com." = "إذا استمر هذا، فيرجى التواصل مع team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"Add a domain to block" = "إضافة نطاق لحظره"; + +/* No comment provided by engineer. */ +"Block all your apps from connecting to the domains and sites below. For your convenience, Lockdown also has pre-configured suggestions." = "امنع جميع تطبيقاتك من الاتصال بالنطاقات والمواقع أدناه. لراحتك، يحتوي Lockdown أيضًا على اقتراحات معدة مسبقًا."; + +/* No comment provided by engineer. */ +"Block Lists" = "قوائم الحظر"; + +/* No comment provided by engineer. */ +"Configure Blocking" = "تهيئة الحظر"; + +/* No comment provided by engineer. */ +"Custom" = "مخصص"; + +/* No comment provided by engineer. */ +"SAVE" = "حفظ"; + +/* No comment provided by engineer. */ +"Access to cloud service information is not allowed" = "الوصول إلى معلومات الخدمة السحابية غير مسموح به"; + +/* No comment provided by engineer. */ +"Could not connect to the network" = "تعذر الاتصال بالشبكة"; + +/* No comment provided by engineer. */ +"Not allowed to make the payment" = "غير مسموح بإجراء عملية الدفع"; + +/* No comment provided by engineer. */ +"Payment was cancelled" = "تم إلغاء عملية الدفع"; + +/* No comment provided by engineer. */ +"The device is not allowed to make the payment" = "هذا الجهاز غير مسموح له بإجراء عملية الدفع"; + +/* No comment provided by engineer. */ +"The product is not available in the current storefront" = "المنتج غير متوفر في واجهة المتجر الحالية"; + +/* No comment provided by engineer. */ +"The purchase identifier was invalid" = "معرّف الشراء غير صالح"; + +/* No comment provided by engineer. */ +"Unknown error. Please contact support at team@lockdownprivacy.com." = "خطأ غير معروف، يرجى التواصل مع الدعم على team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"User has revoked permission to use this cloud service" = "ألغى المستخدم الإذن لاستخدام هذه الخدمة السحابية"; + +/* No comment provided by engineer. */ +"Please that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "يرجى أن يكون لديك اشتراك نشط. إذا كنت تحاول مشاركة اشتراك من نفس الحساب، فستحتاج إلى تسجيل الدخول باستخدام نفس عنوان البريد الإلكتروني. بخلاف ذلك، يرجى بدء فترتك التجريبية المجانية أو إرسال بريد إلكتروني إلى team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "تم تعطيل عمليات الشراء داخل التطبيق. يرجى السماح لها في Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. ثم أعد المحاولة."; + +/* No comment provided by engineer. */ +"Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "الدفع غير مسموح به.\nإما أن هذا الجهاز غير مسموح له بإجراء عمليات شراء، أو تم تعطيل عمليات الشراء داخل التطبيق. يرجى السماح لها في Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. ثم أعد المحاولة."; + +/* No comment provided by engineer. */ +"Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com" = "يرجى التأكد من أن لديك اشتراك نشط. إذا كنت تحاول مشاركة اشتراك من نفس الحساب، فستحتاج إلى تسجيل الدخول باستخدام نفس عنوان البريد الإلكتروني. بخلاف ذلك، ابدأ فترتك التجريبية المجانية أو إرسال بريد إلكتروني إلى team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Upgrade Plan" = "ترقية الخطة"; + +/* The title of a list of trackers */ +"Amazon Trackers" = "أدوات Amazon التتبعية"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "بسيط وقوي"; +"onboarding_lockdown_stops_trackers" = "يمنع Lockdown المتعقبات والبرامج الضارة والعوامل السيئة الأخرى من الوصول إلى بياناتك."; +"onboarding_worlds_first_on_device" = "الأول في العالم على الأجهزة"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "جدار حماية مفتوح المصدر و Secure Tunnel VPN يعمل عبر جميع تطبيقاتك وعند تصفح الويب."; +"onboarding_staying_protected_is_easy_with_lockdown" = "البقاء محميًا هو أمر سهل للغاية مع Lockdown"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "ابق مجهولاً مع Secure Tunnel VPN"; +"onboarding_no_logs_fast_and_fully_audited" = "VPN لا يحتفظ بسجلات، وتوفر سرعة خارقة، وموجه للخصوصية أولاً. VPN الوحيد الذي يثبت سياسة الخصوصية الخاصة به ويخضع للتدقيق الكامل من قبل خبراء الأمن."; +"onboarding_take_control_of_who_you_block" = "تحكم في من الذي\nتحظره"; +"onboarding_when_using_our_intuitive_blocklists" = "عند استخدام أداة إنشاء قائمة الحظر سهلة الاستخدام."; +"onboarding_stay_protected" = "ابق محميًا!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "احصل على ملخصات أسبوعية وتحديثات لقائمة الحظر."; +"onboarding_over_1_billion_trackers_blocked" = "حُظر أكثر من 1 مليار متعقب"; +"onboarding_used_by_more_than_1_million" = "يستخدمه أكثر من مليون شخص مهتم بالخصوصية."; +"onboarding_sign_up" = "التسجيل"; +"onboarding_login" = "تسجيل الدخول"; +"continue" = "استمرار"; +"plan_annual" = "سنوي"; +"plan_monthly" = "شهري"; + +// MARK: - SignUpViewController +"welcome" = "مرحبًا"; +"already_have_account_login" = "لديك حساب بالفعل؟ سجل الدخول"; +"sign_up_to_access_new_block_lists_from_trackers" = "اشترك للوصول إلى قوائم حظر جديدة من أدوات التعقب"; +"already_have_account_login_below" = "لديك بالفعل حساب Lockdown؟\nسجل الدخول أدناه."; +"forgot_password" = "نسيت كلمة المرور"; +"email" = "البريد الإلكتروني"; +"password" = "كلمة المرور"; +"access_our_curated_block_lists_and_build_your_own" = "احظى بالوصول إلى قوائم الحظر المنسقة الخاصة بنا وابنِ قوائمك الخاصة"; +"access_lockdown_across_all_your_devices" = "تمتع بالوصول إلى Lockdown عبر جميع أجهزتك"; +"firewall_and_vpn_support" = "دعم جدار الحماية و VPN"; +"by_continuing_you_agree_with_our" = "من خلال الاستمرار فإنك توافق على"; +"and" = "و"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "ابق مطلعًا"; +"once_a_week_helpful_reminders" = "مرة في الأسبوع احصل على تذكيرات مفيدة حول أمانك وكيفية تحقيق أقصى استفادة منه."; +"enable_notifications" = "تمكين الإشعارات"; +"maybe_later" = "ربما لاحقًا"; + +// MARK: - Previously Unlocalized +"check_fields" = "تحقق من الحقول"; +"email_and_password_must_not_be_empty" = "يجب ألا يكون حقلي البريد الإلكتروني وكلمة المرور فارغين"; +"Success! 🎉" = "تم بنجاح! 🎉"; +"error_signing_in" = "خطأ في تسجيل الدخول"; +"confirm_your_email" = "راجع بريدك الإلكتروني"; +"you_have_successfully_sign_in" = "لقد سجلت الدخول بنجاح."; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "لقد منعت %@ من المتعقبات والإعلانات والبرامج الضارة باستخدام Lockdown، وهو جدار الحماية الذي يمنع الاتصالات غير المرغوب فيها في جميع تطبيقاتك.\nاحصل عليه مجانًا من lockdownprivacy.com."; +"check_email" = "تحقق من البريد الإلكتروني"; +"we_have_sent_a_reset_password_email" = "لقد أرسلنا إليك بريدًا إلكترونيًا لإعادة تعيين كلمة المرور. تأكد من فحص أي مجلدات للبريد غير المرغوب، في حال وجوده به."; +"since_date" = "منذ %@"; +"password_reset_successfully" = "تم تغيير كلمة المرور"; +"please_sign_in_with_new_password" = "سجل دخول بكلمة المرور الجديدة"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "ابدأ فترة التجربة الآن للحصول على وصول كامل"; +"restore_purchase" = "استعادة عملية الشراء"; +"terms_of_service" = "شروط الخدمة"; +"CHOOSE_YOUR_PLAN" = "اختر خطة اشتراكك"; +"skip" = "تخطي"; +"save_annual_vpn_subscription" = "وفر ~50%"; +"save_annual_pro_subscription" = "وفر ~30%"; +"current_plan" = "الخطة الحالية"; +"feature_table_column" = "الميزات"; +"feature_level_table_column" = "مجاني"; +"stops_ads_and_hidden_trackers" = "يوقف الإعلانات والمتعقبات الخفية"; +"blocks_badware_in_all_your_apps" = "يحجب التعليمات البرمجية الضارة في جميع تطبيقاتك"; +"access_our_curated_block_lists" = "احظى بالوصول إلى قوائم الحظر المنسقة الخاصة بنا"; +"build_your_custom_block_lists" = "قم ببناء قوائم الحظر المخصصة"; +"stops_browsing_history_tracking" = "يوقف تتبع سجل التصفح"; +"anonymizing_your_browsing" = "يُخفي هويتك أثناء التصفح"; +"hides_your_location_and_ip" = "يُخفي موقعك وعنوان IP الخاص بك"; +"lets_you_change_your_ip_to_other_region" = "يُمكنك من تغيير عنوان IP\nإلى منطقة أخرى"; +"app_for_iphone_plus_ipad" = "تطبيق لأجهزة iPhone + iPad"; +"app_for_mac" = "تطبيق لأجهزة Mac"; +"year_after_slash" = "السنة"; +"month_after_slash" = "الشهر"; + +// MARK: - Delete My Account +"delete_account" = "حذف حساب"; +"delete_my_account" = "حذف حسابي"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "عبر الاستمرار، سترسل طلبًا لحذف سجل حسابك بالكامل، إلى جانب البيانات الشخصية المرتبطة به. بمجرد حذف الحساب، سيتم إخطارك بهذه التغييرات عبر البريد الإلكتروني لحسابك.\n\nيرجى العلم أنك ستفقد جميع إعداداتك بما في ذلك خيارات الترقية ولن تتمكن من إنشاء حساب بنفس البريد الإلكتروني."; +"proceed" = "استمرار"; + +// MARK: - EmailValidatable +"invalid_email_address" = "عنوان البريد الإلكتروني غير صالح"; +"please_fill_in_your_email" = "يرجى كتابة بريدك الإلكتروني."; +"you_entered_con_instead_of_com" = "لقد كتبت .con بدلاً من .com."; +"no_valid_email_address_detected" = "لم يتم اكتشاف عنوان بريد إلكتروني صالح."; +"too_many_email_addresses_entered" = "تم إدخال عدد كبير جدًا من عناوين البريد الإلكتروني."; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "قائمة الحظر هي قيد الصيانة حاليًا وسيتم تحديثها قريبًا."; +"this_block_is_intended_to_block_facebook_owned_apps" = "تهدف هذه القائمة إلى حظر التطبيقات المملوكة لـ Facebook تمامًا. لا تقم بتفعيلها إذا كنت تستخدم تطبيقات مملوكة لـ Facebook مثل WhatsApp و Facebook Messenger و Instagram."; +"this_blocks_background_google_tracking" = "هذا يحظر تعقب Google في الخلفية ، كما و يحظر أيضًا نتائج التسوق في بحث Google. يتم تفعيل هذه الحظر تلقائياً للحصول على أقصى قدر من الخصوصية ، ولكن إذا كنت تحب نتائج Google Shopping ، فيمكنك إيقاف الحظر."; +"this_blocks_amazon_referral_link_tracking" = "هذا يحظر تعقب رابط إحالة Amazon أيضًا ، لذلك تفعيل هذه القائمة قد يتسبب بأخطاء عند متابعة روابط Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "دقيقة"; +"seconds_genitive" = "ثانية"; +"lockdowns_first_christmas_sale" = "عرض خاص\n لمناسبة عيد الميلاد"; +"subscribe_now" = "اشترك الآن"; + +// MARK: - Code Redemption +"redeem_code" = "استخدام الرمز"; +"cannot_redeem_code" = "لا يمكن استخدام الرمز"; +"the_code_could_not_be_redeemed" = "لم يمكن استخدام الرمز."; +"congratulations_excl" = "تهانينا!"; +"the_code_was_successfully_redeemed" = "تم استخدام الرمز بنجاح. قد تحتاج إلى إعادة تشغيل التطبيق حتى تصبح عملية الشراء سارية المفعول."; diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings new file mode 100644 index 0000000..d65c261 --- /dev/null +++ b/de.lproj/Localizable.strings @@ -0,0 +1,809 @@ +/* Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com */ +" and " = " und "; + +/* refers to number of connections blocked today, as in '34 Blocked Today' */ +" Blocked Today" = " Heute gesperrt"; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to add to a whitelist. For example, \"google.com\" without \"https://\"" = " ist keine zulässige Eingabe. Bitte geben Sie nur den Host der Domain ein, die Sie einer Positivliste hinzufügen möchten, z. B. \"google.com\" ohne \"https://\""; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to block. For example, \"google.com\" without \"https://\"" = " ist keine zulässige Eingabe. Bitte geben Sie nur den Host der Domain ein, die Sie sperren möchten, z. B. \"google.com\" ohne \"https://\""; + +/* No comment provided by engineer. */ +" Off" = " Aus"; + +/* No comment provided by engineer. */ +" On" = " Ein"; + +/* Used in the sentence: I blocked 500 trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com. */ +" trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com." = " Tracker, Werbung und Badware mit Lockdown, der Firewall, die unerwünschte Verbindungen in all Ihren Apps sperrt. Kostenlos erhältlich über lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"1 Week Free" = "1 Woche gratis"; + +/* No comment provided by engineer. */ +"\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). You can also set custom domains to block." = "Über \"Block List\" können Sie angeben, was Sie sperren möchten (z. B. Facebook, Clickbait usw.). Sie können auch benutzerdefinierte Domains einrichten, die gesperrt werden sollen."; + +/* No comment provided by engineer. */ +"\"iOS Annual\" is your current plan" = "\"iOS Annual\" ist Ihr aktuelles Abo"; + +/* No comment provided by engineer. */ +"\"iOS Monthly\" is your current plan" = "\"iOS Monthly\" ist Ihr aktuelles Abo"; + +/* No comment provided by engineer. */ +"\"Pro Annual\" is your current plan" = "\"Pro Annual\" ist Ihr aktuelles Abo"; + +/* No comment provided by engineer. */ +"\"Pro Monthly\" is your current plan" = "\"Pro Monthly\" ist Ihr aktuelles Abo"; + +/* No comment provided by engineer. */ +"\"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." = "\"View Log\" zeigt genau an, welche Verbindungen am Vortag gesperrt wurden. Dieses Protokoll wird um Mitternacht gelöscht und verbleibt auf dem Gerät, wodurch es nur für Sie sichtbar ist."; + +/* No comment provided by engineer. */ +"\n\n If this error persists, please contact team@lockdownprivacy.com." = "\n\n Falls dieser Fehler weiterhin besteht, kontaktieren Sie bitte team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"\n\n If this persists, please contact team@lockdownprivacy.com." = "\n\n Falls dies weiterhin besteht, kontaktieren Sie bitte team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"About Lockdown VPN" = "Über Lockdown VPN"; + +/* No comment provided by engineer. */ +"Activate Firewall with this button." = "Aktivieren Sie die Firewall über diese Schaltfläche."; + +/* No comment provided by engineer. */ +"Activating" = "Wird aktiviert"; + +/* No comment provided by engineer. */ +"Anonymize IP against trackers" = "IP gegen Tracker anonymisieren"; + +/* No comment provided by engineer. */ +"Apple Outage" = "Apple-Ausfall"; + +/* No comment provided by engineer. */ +"at least 8 characters" = "mindestens 8 Zeichen"; + +/* No comment provided by engineer. */ +"Audit Reports" = "Prüfberichte"; + +/* No comment provided by engineer. */ +"Australia" = "Australien"; + +/* No comment provided by engineer. */ +"Be sure to check your spam folder in case it got stuck there.\n\nYou can also request a re-send of the confirmation." = "Prüfen Sie Ihren Spam-Ordner, für den Fall, dass sie dort festhängt.\n\nSie können auch eine erneute Bestätigung anfordern."; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"block list" = "Sperrliste"; + +/* No comment provided by engineer. */ +"Blocked" = "Gesperrt"; + +/* No comment provided by engineer. */ +"Blocked Domains" = "Gesperrte Domains"; + +/* No comment provided by engineer. */ +"Blocked Today" = "Heute gesperrt"; + +/* No comment provided by engineer. */ +"Blocked Trackers Summary" = "Übersicht gesperrter Tracker"; + +/* No comment provided by engineer. */ +"Brazil" = "Brasilien"; + +/* No comment provided by engineer. */ +"Canada" = "Kanada"; + +/* No comment provided by engineer. */ +"Cancel" = "Abbrechen"; + +/* No comment provided by engineer. */ +"Check Your Inbox" = "Prüfen Sie Ihren Posteingang"; + +/* No comment provided by engineer. */ +"Clear Block Log" = "Sperrprotokoll löschen"; + +/* The title of a list of trackers */ +"Clickbait" = "Clickbait"; + +/* No comment provided by engineer. */ +"Confirm Email" = "E-Mail-Adresse bestätigen"; + +/* No comment provided by engineer. */ +"Confirmed Site" = "Website bestätigen"; + +/* No comment provided by engineer. */ +"Couldn't Find Your Email Client" = "Ihr E-Mail-Client konnte nicht gefunden werden"; + +/* The title of a list of trackers */ +"Crypto Mining" = "Krypto-Mining"; + +/* No comment provided by engineer. */ +"Custom Settings (Advanced)" = "Benutzerdefinierte Einstellungen (erweitert)"; + +/* The title of a list of trackers */ +"Data Trackers" = "Daten-Tracker"; + +/* No comment provided by engineer. */ +"Deactivating" = "Wird deaktiviert"; + +/* No comment provided by engineer. */ +"Disable" = "Deaktivieren"; + +/* No comment provided by engineer. */ +"Disable Block Log" = "Sperrprotokoll deaktivieren"; + +/* No comment provided by engineer. */ +"Disable Block Log?" = "Sperrprotokoll deaktivieren?"; + +/* No comment provided by engineer. */ +"Done" = "Fertig"; + +/* No comment provided by engineer. */ +"Email Support" = "E-Mail-Support"; + +/* The title of a list of trackers */ +"Email Trackers" = "E-Mail-Tracker"; + +/* No comment provided by engineer. */ +"Email Us" = "E-Mail senden"; + +/* No comment provided by engineer. */ +"Enable" = "Aktivieren"; + +/* No comment provided by engineer. */ +"Enable Block Log" = "Sperrprotokoll aktivieren"; + +/* No comment provided by engineer. */ +"Enable notifications to get a once-a-week summary and the latest block list updates. You can disable this anytime." = "Aktivieren Sie Benachrichtigungen, um eine wöchentliche Zusammenfassung und die neuesten Updates zur Sperrliste zu erhalten. Sie können diese jederzeit deaktivieren."; + +/* No comment provided by engineer. */ +"Encrypted, private connections" = "Verschlüsselte, private Verbindungen"; + +/* No comment provided by engineer. */ +"Error Code " = "Fehlercode "; + +/* No comment provided by engineer. */ +"Error Creating Email Account" = "Fehler beim Erstellen eines E-Mail-Kontos"; + +/* No comment provided by engineer. */ +"Error Loading Plan: Retry" = "Fehler beim Laden des Abos: Erneut versuchen"; + +/* No comment provided by engineer. */ +"Error Re-sending Email Confirmation" = "Fehler beim erneuten Senden der E-Mail-Bestätigung"; + +/* No comment provided by engineer. */ +"Error Restoring Subscription" = "Fehler beim Wiederherstellen des Abos"; + +/* No comment provided by engineer. */ +"Error Signing In To Verify Subscription" = "Fehler beim Anmelden zur Prüfung Ihres Abos"; + +/* No comment provided by engineer. */ +"Error Starting Trial" = "Fehler beim Start der Testphase"; + +/* The title of a list of trackers */ +"Facebook & WhatsApp" = "Facebook & WhatsApp"; + +/* The title of a list of trackers */ +"Facebook Trackers" = "Facebook-Tracker"; + +/* No comment provided by engineer. */ +"Failed to re-send email confirmation." = "E-Mail-Bestätigung konnte nicht erneut gesendet werden."; + +/* No comment provided by engineer. */ +"FAQs" = "FAQs"; + +/* No comment provided by engineer. */ +"Firewall" = "Firewall"; + +/* No comment provided by engineer. */ +"Firewall Active" = "Firewall aktiv"; + +/* No comment provided by engineer. */ +"Firewall Not Active" = "Firewall nicht aktiv"; + +/* No comment provided by engineer. */ +"FIREWALL OFF" = "FIREWALL AUS"; + +/* No comment provided by engineer. */ +"FIREWALL ON" = "FIREWALL EIN"; + +/* No comment provided by engineer. */ +"For maximum privacy, activate Secure Tunnel, which uses bank-level encryption to protect connections, anonymize your browsing, and hide your location and IP." = "Für maximale Privatsphäre aktivieren Sie Secure Tunnel. Dieser nutzt eine Verschlüsselung auf Datenbankebene, um Verbindungen zu schützen, Ihr Browsing-Verhalten zu anonymisieren und Ihren Standort und Ihre IP zu verbergen."; + +/* No comment provided by engineer. */ +"France" = "Frankreich"; + +/* The title of a list of trackers */ +"Game Marketing" = "Spiel-Marketing"; + +/* The title of a list of trackers */ +"General Marketing" = "Allgemeines Marketing"; + +/* No comment provided by engineer. */ +"Germany" = "Deutschland"; + +/* The title of a list of trackers */ +"Google Shopping" = "Google Shopping"; + +/* No comment provided by engineer. */ +"Hey Lockdown Team, \nI have a question, issue, or suggestion - " = "Hey, Lockdown-Team, \nIch habe eine Frage, ein Problem oder einen Vorschlag - "; + +/* No comment provided by engineer. */ +"Hi, my question or feedback for Lockdown is: " = "Hallo, meine Frage oder mein Feedback für Lockdown lautet: "; + +/* Used in the sentence: I blocked 500 trackers with Lockdown. */ +"I blocked" = "Ich habe gesperrt:"; + +/* Used in: You blocked tracker.com in your custom blocked domains. */ +"in your custom blocked domains." = "in Ihren benutzerdefinierten, gesperrten Domains."; + +/* No comment provided by engineer. */ +"India" = "Indien"; + +/* No comment provided by engineer. */ +"Invalid domain" = "Ungültige Domain"; + +/* No comment provided by engineer. */ +"IP: 18.132.2.87" = "IP: 18.132.2.87"; + +/* No comment provided by engineer. */ +"IP: [Anonymized]" = "IP: [anonymisiert]"; + +/* No comment provided by engineer. */ +"Ireland" = "Irland"; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"is a part of" = "ist Teil von"; + +/* No comment provided by engineer. */ +"Japan" = "Japan"; + +/* Shows when loading the details of a subscription plan */ +"Loading Plan" = "Abo wird geladen"; + +/* No comment provided by engineer. */ +"Location changed and hidden" = "Standort geändert und verborgen"; + +/* No comment provided by engineer. */ +"Location: 🇯🇵" = "Standort: 🇯🇵"; + +/* No comment provided by engineer. */ +"Location: 🇺🇸" = "Standort: 🇺🇸"; + +/* No comment provided by engineer. */ +"Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari." = "Lockdown Firewall blockiert schädliche und nicht vertrauenswürdige Verbindungen in allen Ihren Apps - nicht nur Safari."; + +/* No comment provided by engineer. */ +"Lockdown is open source and fully transparent, which means anyone can see exactly what it's doing. Also, Lockdown Firewall has a simple, strict Privacy Policy, while Lockdown VPN is fully audited by security experts." = "Lockdown ist Open Source und vollständig transparent. Dies bedeutet, dass jeder genau sehen kann, was geschieht. Lockdown Firewall ist besitzt auch eine einfache, strenge Datenschutzerklärung, während Lockdown VPN vollumfänglich von Sicherheitsexperten geprüft wird."; + +/* No comment provided by engineer. */ +"Lockdown VPN is powered by Confirmed VPN, the open source, no-logs, and fully audited VPN." = "Lockdown VPN wird durch Confirmed VPN, dem protokollfreien, vollständig geprüften Open-Source-VPN, unterstützt."; + +/* The title of a list of trackers */ +"Marketing Trackers" = "Marketing-Tracker"; + +/* The title of a list of trackers */ +"Marketing Trackers II" = "Marketing-Tracker II"; + +/* No comment provided by engineer. */ +"Network Error" = "Netzwerkfehler"; + +/* No comment provided by engineer. */ +"Next" = "Weiter"; + +/* No comment provided by engineer. */ +"Nice" = "Schön"; + +/* No comment provided by engineer. */ +"No" = "Nein"; + +/* No comment provided by engineer. */ +"No Active Subscription" = "Kein aktives Abo"; + +/* No comment provided by engineer. */ +"No additional information on this blocked domain found." = "Keine weiteren Informationen zu dieser gesperrten Domain gefunden."; + +/* No comment provided by engineer. */ +"No Block Lists Enabled" = "Keine Sperrlisten aktiviert"; + +/* No comment provided by engineer. */ +"No Info Found" = "Keine Informationen gefunden"; + +/* No comment provided by engineer. */ +"No Internet Connection" = "Keine Internetverbindung"; + +/* No comment provided by engineer. */ +"No stored API credentials found. Please contact team@lockdownprivacy.com about this error." = "Keine API-Benutzerdaten gefunden. Bitte kontaktieren Sie team@lockdownprivacy.com in Bezug auf diesen Fehler."; + +/* No comment provided by engineer. */ +"Not Blocked" = "Nicht gesperrt"; + +/* No comment provided by engineer. */ +"Not Now" = "Nicht jetzt"; + +/* No comment provided by engineer. */ +"Not Whitelisted" = "Nicht auf Positivliste"; + +/* No comment provided by engineer. */ +"Notifications: Off" = "Benachrichtigungen: Aus"; + +/* No comment provided by engineer. */ +"Notifications: On" = "Benachrichtigungen: Ein"; + +/* No comment provided by engineer. */ +"OK" = "OK"; + +/* No comment provided by engineer. */ +"Okay" = "Okay"; + +/* No comment provided by engineer. */ +"one lowercase letter" = "ein Kleinbuchstabe"; + +/* No comment provided by engineer. */ +"one number" = "eine Ziffer"; + +/* No comment provided by engineer. */ +"one symbol" = "ein Sonderzeichen"; + +/* No comment provided by engineer. */ +"one uppercase letter" = "ein Großbuchstabe"; + +/* No comment provided by engineer. */ +"Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol." = "Das Passwort muss mindestens 8 Zeichen lang sein, mindestens ein Großbuchstabe, ein Kleinbuchstabe, eine Ziffer und ein Sonderzeichen enthalten."; + +/* No comment provided by engineer. */ +"Plan: Annual Pro" = "Abo: Jährlich Pro"; + +/* No comment provided by engineer. */ +"Please check your internet connection. If this persists, please contact team@lockdownprivacy.com.\n\nError Description\n" = "Bitte prüfen Sie Ihre Internetverbindung. Falls dies weiterhin besteht, kontaktieren Sie bitte team@lockdownprivacy.com.\n\nFehlerbeschreibung\n"; + +/* No comment provided by engineer. */ +"Please contact team@lockdownprivacy.com.\n\nError details:\n" = "Bitte kontaktieren Sie team@lockdownprivacy.com.\n\nFehlerdetails:\n"; + +/* No comment provided by engineer. */ +"Please email team@lockdownprivacy.com with the following Error Code " = "Bitte senden Sie eine E-Mail an team@lockdownprivacy.com mit folgendem Fehlercode: "; + +/* No comment provided by engineer. */ +"Please first go to your iOS Settings > Notifications > Lockdown to enable notifications" = "Bitte gehen Sie zuerst zu iOS-Einstellungen > Benachrichtigungen > Lockdown, um Benachrichtigungen zu aktivieren"; + +/* No comment provided by engineer. */ +"Please make sure you have added an e-mail account to your iOS device and try again." = "Bitte vergewissern Sie sich, dass ein E-Mail-Konto Ihrem iOS-Gerät hinzugefügt wurde, und versuchen Sie es erneut."; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active and that you have an active subscription. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "Bitte achten Sie darauf, dass Ihre Internetverbindung aktiv ist und dass Sie ein aktives Abonnement haben. Andernfalls starten Sie bitte Ihre kostenlose Testphase oder senden Sie eine E-Mail an team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with the following error message: " = "Bitte achten Sie darauf, dass Ihre Internetverbindung aktiv ist. Falls dieser Fehler weiterhin besteht, senden Sie eine E-Mail mit folgender Fehlermeldung an team@lockdownprivacy.com: "; + +/* No comment provided by engineer. */ +"Please renew your subscription to activate the Secure Tunnel." = "Bitte verlängern Sie Ihr Abonnement, um den Secure Tunnel zu aktivieren."; + +/* No comment provided by engineer. */ +"Please renew your subscription to re-activate the VPN." = "Bitte verlängern Sie Ihr Abonnement, um das VPN erneut zu aktivieren."; + +/* No comment provided by engineer. */ +"Please tap Block List and enable at least one block list to activate Firewall." = "Bitte tippen Sie auf \"Sperrliste\" und aktivieren Sie mindestens eine Sperrliste, um die Firewall zu aktivieren."; + +/* No comment provided by engineer. */ +"Pre-configured Block Lists" = "Vorkonfigurierte Sperrlisten"; + +/* No comment provided by engineer. */ +"Pre-configured Suggestions" = "Vorkonfigurierte Empfehlungen"; + +/* No comment provided by engineer. */ +"Precise location exposed" = "Präziser Standort offengelegt"; + +/* No comment provided by engineer. */ +"Press & Media" = "Presse & Medien"; + +/* No comment provided by engineer. */ +"Privacy Policy" = "Datenschutzerklärung"; + +/* The title of a list of trackers */ +"Ransomware" = "Ransomware"; + +/* No comment provided by engineer. */ +"Re-send" = "Erneut senden"; + +/* No comment provided by engineer. */ +"Readable browsing and data" = "Lesbares Browsing-Verhalten und Daten"; + +/* No comment provided by engineer. */ +"Remember to check our FAQs first, for answers to the most frequently asked questions.\n\nIf it's not answered there, we're happy to provide support and take feedback by email." = "Denken Sie daran, zuerst einen Blick in unsere FAQs zu werfen, um Antworten auf häufig gestellte Fragen zu erhalten.\n\nFalls Sie dort keine Antwort finden, helfen wir Ihnen gerne weiter und nehmen Ihr Feedback per E-Mail an."; + +/* The title of a list of trackers */ +"Reporting" = "Meldung"; + +/* No comment provided by engineer. */ +"Secure Tunnel VPN" = "Secure Tunnel VPN"; + +/* No comment provided by engineer. */ +"See live metrics for how many bad connections Firewall has blocked." = "Siehe die Live-Kennzahlen, um zu erfahren, wie viele schlechte Verbindungen von der Firewall geblockt wurden."; + +/* No comment provided by engineer. */ +"Settings" = "Einstellungen"; + +/* No comment provided by engineer. */ +"Share your anonymized metrics and show other people how to block invasive tracking." = "Teilen Sie Ihre anonymisierten Kennzahlen und zeigen Sie anderen Nutzern, wie Sie invasives Tracking blockieren."; + +/* No comment provided by engineer. */ +"Share Your Stats" = "Werte teilen"; + +/* No comment provided by engineer. */ +"SHOW" = "ANZEIGEN"; + +/* No comment provided by engineer. */ +"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." = "Zeigen Sie, wie invasiv heutige Apps sind, und helfen Sie anderen Nutzern, Tracker und Badware zu blockieren.\n\nIhr Sperrprotokoll ist nicht inbegriffen - nur das obige Bild. Wählen Sie im nächsten Schritt, wo dieses geteilt werden soll."; + +/* No comment provided by engineer. */ +"Sign Out" = "Abmelden"; + +/* No comment provided by engineer. */ +"Sign Out?" = "Abmelden?"; + +/* No comment provided by engineer. */ +"Sign Up | Sign In" = "Registrieren | Anmelden"; + +/* No comment provided by engineer. */ +"Sign up below to unlock benefits of a Lockdown account." = "Registrieren Sie sich unten, um die Vorteile eines Lockdown-Kontos zu genießen."; + +/* No comment provided by engineer. */ +"Signed In" = "Angemeldet"; + +/* No comment provided by engineer. */ +"Signed out successfully." = "Erfolgreich abgemeldet."; + +/* No comment provided by engineer. */ +"Singapore" = "Singapur"; + +/* No comment provided by engineer. */ +"Skip Trial" = "Testphase überspringen"; + +/* The title of a list of trackers */ +"Snapchat Trackers" = "Snapchat-Tracker"; + +/* No comment provided by engineer. */ +"South Korea" = "Südkorea"; + +/* No comment provided by engineer. */ +"Stay Protected" = "Geschützt bleiben"; + +/* No comment provided by engineer. */ +"Subscription Expired" = "Abonnement abgelaufen"; + +/* No comment provided by engineer. */ +"Success" = "Erfolgreich"; + +/* No comment provided by engineer. */ +"Success!" = "Erfolgreich!"; + +/* No comment provided by engineer. */ +"Successfully re-sent your email confirmation to " = "Ihre E-Mail-Bestätigung wurde erfolgreich gesendet an "; + +/* No comment provided by engineer. */ +"Support | Feedback" = "Support | Feedback"; + +/* No comment provided by engineer. */ +"Tap to activate Lockdown Firewall and update to the newest block lists." = "Tippen, um Lockdown Firewall zu aktivieren und auf die neuesten Sperrlisten zu aktualisieren."; + +/* No comment provided by engineer. */ +"Tap to see a demo of how Secure Tunnel protects and anonymizes you." = "Tippen, um eine Demo zu sehen, wie Secure Tunnel Sie schützt und anonymisiert."; + +/* No comment provided by engineer. */ +"Tap to see the blocked tracking attempts." = "Tippen, um die blockierten Tracking-Versuche anzuzeigen."; + +/* Used in the paragraph: You've just blocked 100 tracking attempts! Tap to see what they are. */ +"Tap to see what they are." = "Tippen, um zu sehen, was sie sind."; + +/* No comment provided by engineer. */ +"Terms" = "Laufzeiten"; + +/* No comment provided by engineer. */ +"Thanks for helping to increase privacy and tracking awareness." = "Vielen Dank, dass Sie uns helfen, den Datenschutz und die Aufmerksamkeit auf Tracking zu steigern."; + +/* No comment provided by engineer. */ +"The block log can be manually cleared or disabled. Disabling the Block Log only disables the log of connections - the number of tracking attempts will still be displayed." = "Das Sperrprotokoll kann manuell gelöscht oder deaktiviert werden. Durch Deaktivieren des Sperrprotokolls wird nur das Verbindungsprotokoll deaktiviert - die Anzahl von Tracking-Versuchen wird dennoch angezeigt."; + +/* Used in To complete your signup, click the confirmation link we sent to you@gmail.com */ +"To complete your signup, click the confirmation link we sent to" = "Um Ihre Registrierung abzuschließen, klicken Sie auf den Bestätigungslink, den wir an folgende Adresse gesendet haben:"; + +/* No comment provided by engineer. */ +"To finish signup, click the confirmation link in the email we just sent. If you don't see it, check if it's stuck in your spam folder." = "Um die Registrierung abzuschließen, klicken Sie auf den Bestätigungslink in der E-Mail, die wir Ihnen gerade gesendet haben. Falls Sie diese nicht erhalten haben, prüfen Sie, ob sie sich möglicherweise in Ihrem Spam-Ordner befindet."; + +/* No comment provided by engineer. */ +"To see this tutorial again, open the Account tab." = "Um dieses Tutorial erneut anzuzeigen, öffnen Sie den Reiter \"Konto\"."; + +/* No comment provided by engineer. */ +"To: joe@email.com\nRe: Q4 2019 Finance Review" = "An: joe@email.com\nRe: Finanzbericht Q4 2019"; + +/* No comment provided by engineer. */ +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "Tracker, Werbung und andere bösartige Skripts werden nun in all Ihren Apps blockiert, auch außerhalb von Safari.\n\nErreichen Sie den maximalen Datenschutz mit einem Secure Tunnel, der Verbindungen schützt, Ihr Browsing-Verhalten anonymisiert und Ihren Standort verbirgt."; + +/* Used in the sentence: You blocked 500 tracking attempts this week. Tap to update to the newsst block lists. */ +"tracking attempts this week. Tap to update to the newest block lists." = "Tracking-Versuche in dieser Woche. Tippen, um auf die neuesten Sperrlisten zu aktualisieren."; + +/* No comment provided by engineer. */ +"Tunnel" = "Tunnel"; + +/* No comment provided by engineer. */ +"Tunnel Off" = "Tunnel aus"; + +/* No comment provided by engineer. */ +"Tunnel On" = "Tunnel ein"; + +/* No comment provided by engineer. */ +"Tutorial" = "Tutorial"; + +/* No comment provided by engineer. */ +"Unique IP address broadcasted" = "Einzigartige IP-Adresse übertragen"; + +/* No comment provided by engineer. */ +"United Kingdom" = "Vereinigtes Königreich"; + +/* No comment provided by engineer. */ +"United States - East" = "Vereinigte Staaten - Ost"; + +/* No comment provided by engineer. */ +"United States - West" = "Vereinigte Staaten - West"; + +/* No comment provided by engineer. */ +"Update Success" = "Update erfolgreich"; + +/* No comment provided by engineer. */ +"Updating Block Lists" = "Sperrlisten werden aktualisiert"; + +/* No comment provided by engineer. */ +"USA East" = "USA Ost"; + +/* No comment provided by engineer. */ +"USA West" = "USA West"; + +/* No comment provided by engineer. */ +"View FAQs" = "FAQs anzeigen"; + +/* No comment provided by engineer. */ +"View or Upgrade Plan" = "Abo anzeigen oder aufwerten"; + +/* No comment provided by engineer. */ +"View Upgrade Options" = "Upgrade-Optionen anzeigen"; + +/* No comment provided by engineer. */ +"VPN Active" = "VPN aktiv"; + +/* No comment provided by engineer. */ +"VPN Not Active" = "VPN inaktiv"; + +/* No comment provided by engineer. */ +"VPN Subscription Expired" = "VPN-Abonnement abgelaufen"; + +/* No comment provided by engineer. */ +"Website" = "Website"; + +/* No comment provided by engineer. */ +"Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue." = "Willkommen im Lockdown-Tutorial.\n\nAn beliebiger Stelle tippen, um fortzufahren."; + +/* No comment provided by engineer. */ +"What is VPN?" = "Was ist VPN?"; + +/* No comment provided by engineer. */ +"Whitelisted" = "Auf Positivliste"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown" = "Warum Sie Lockdown vertrauen sollten"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown?" = "Warum sollten Sie Lockdown vertrauen?"; + +/* No comment provided by engineer. */ +"Why Use VPN?" = "Warum VPN nutzen?"; + +/* Used in the sentence: You blocked 500 tracking attempts this week. + Used in: You blocked tracker.com in your custom blocked domains. */ +"You blocked" = "Sie haben gesperrt:"; + +/* No comment provided by engineer. */ +"You blocked this domain in your custom blocked domains." = "Sie haben diese Domain in Ihren benutzerdefinierten, gesperrten Domains gesperrt."; + +/* No comment provided by engineer. */ +"You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)" = "Sie können später hier tippen, wenn Sie dies teilen möchten.\n(An beliebiger Stelle tippen, um abzulehnen)"; + +/* No comment provided by engineer. */ +"You'll be signed out from this account." = "Sie werden aus diesem Konto abgemeldet."; + +/* No comment provided by engineer. */ +"You'll have to reenable it later here to start seeing blocked entries again." = "Sie werden es später erneut aktivieren müssen, um gesperrte Eingaben erneut anzuzeigen."; + +/* No comment provided by engineer. */ +"You've just blocked 100 tracking attempts!" = "Sie haben gerade 100 Tracking-Versuche blockiert!"; + +/* No comment provided by engineer. */ +"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." = "Ihr Konto wurde bestätigt und Sie sind nun angemeldet. Sie erhalten die neuesten Sperrlisten, Zugang zu Lockdown Mac und kritische Ankündigungen."; + +/* No comment provided by engineer. */ +"Your Settings" = "Ihre Einstellungen"; + +/* No comment provided by engineer. */ +"⚠️ Not Signed In" = "⚠️ Nicht angemeldet"; + +/* No comment provided by engineer. */ +"🔥🧱 Firewall Activated 🎊🎉" = "🔥🧱 Firewall aktiviert 🎊🎉"; + +/* No comment provided by engineer. */ +"Error while trying to confirm your account:" = "Fehler beim Versuch, Ihr Konto zu aktivieren:"; + +/* No comment provided by engineer. */ +"If this persists, please contact team@lockdownprivacy.com." = "Falls dies weiterhin besteht, kontaktieren Sie bitte team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"Add a domain to block" = "Domain der Sperrliste hinzufügen"; + +/* No comment provided by engineer. */ +"Block all your apps from connecting to the domains and sites below. For your convenience, Lockdown also has pre-configured suggestions." = "Blockieren Sie die Verbindung aller Ihrer Apps mit den nachstehenden Domains und Websites. Als Annehmlichkeit bietet Lockdown auch vorkonfigurierte Empfehlungen."; + +/* No comment provided by engineer. */ +"Block Lists" = "Sperrlisten"; + +/* No comment provided by engineer. */ +"Configure Blocking" = "Sperrung konfigurieren"; + +/* No comment provided by engineer. */ +"Custom" = "Benutzerdefiniert"; + +/* No comment provided by engineer. */ +"SAVE" = "SPEICHERN"; + +/* No comment provided by engineer. */ +"Access to cloud service information is not allowed" = "Zugang zu Cloud-Service-Informationen ist nicht erlaubt"; + +/* No comment provided by engineer. */ +"Could not connect to the network" = "Verbindung zum Netzwerk nicht möglich"; + +/* No comment provided by engineer. */ +"Not allowed to make the payment" = "Zahlung nicht zulässig"; + +/* No comment provided by engineer. */ +"Payment was cancelled" = "Zahlung wurde storniert"; + +/* No comment provided by engineer. */ +"The device is not allowed to make the payment" = "Es kann keine Zahlung über das Gerät getätigt werden"; + +/* No comment provided by engineer. */ +"The product is not available in the current storefront" = "Das Produkt ist im aktuellen Storefront nicht verfügbar"; + +/* No comment provided by engineer. */ +"The purchase identifier was invalid" = "Der Kaufkennwert war unzulässig"; + +/* No comment provided by engineer. */ +"Unknown error. Please contact support at team@lockdownprivacy.com." = "Unbekannter Fehler. Bitte kontaktieren Sie den Support unter team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"User has revoked permission to use this cloud service" = "Der Nutzer hat die Berechtigung zur Nutzung dieses Cloud-Services widerrufen."; + +/* No comment provided by engineer. */ +"Please that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "Bitte achten Sie darauf, dass Sie ein aktives Abonnement haben. Falls Sie versuchen, ein Abonnement über das gleiche Konto zu teilen, müssen Sie sich mit der gleichen E-Mail-Adresse anmelden. Andernfalls starten Sie bitte Ihre kostenlose Testphase oder senden Sie eine E-Mail an team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "In-App-Käufe wurden deaktiviert. Bitte aktivieren Sie diese über Einstellungen -> Bildschirmzeit -> Einschränkungen -> App Store -> In-App-Käufe. Versuchen Sie es anschließend erneut."; + +/* No comment provided by engineer. */ +"Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "Zahlung nicht erlaubt.\nEntweder sind Käufe über dieses Gerät nicht möglich oder die In-App-Käufe wurden deaktiviert. Bitte aktivieren Sie diese über Einstellungen -> Bildschirmzeit-> Einschränkungen -> App Store -> In-App-Käufe. Versuchen Sie es anschließend erneut."; + +/* No comment provided by engineer. */ +"Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com" = "Bitte stellen Sie sicher, dass Sie ein aktives Abonnement haben. Falls Sie versuchen, ein Abonnement über das gleiche Konto zu teilen, müssen Sie sich mit der gleichen E-Mail-Adresse anmelden. Andernfalls starten Sie Ihre kostenlose Testphase oder senden Sie eine E-Mail an team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"Upgrade Plan" = "Abo aufwerten"; + +/* The title of a list of trackers */ +"Amazon Trackers" = "Amazon-Tracker"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "Einfach und effektiv"; +"onboarding_lockdown_stops_trackers" = "Lockdown verhindert den Zugriff auf Ihre Daten durch Tracker, Malware und andere bösartige Agenten"; +"onboarding_worlds_first_on_device" = "Weltweit Nr. 1 auf Gerät"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "Open-Source-Firewall und gesichertes Tunnel-VPN, das auf ALLEN Ihren Apps und Browsern funktioniert."; +"onboarding_staying_protected_is_easy_with_lockdown" = "Der Eigenschutz ist mit Lockdown ganz einfach"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "Bleiben Sie anonym mit Secure Tunnel VPN"; +"onboarding_no_logs_fast_and_fully_audited" = "Protokollloses, blitzschnelles, Datenschutz-orientiertes VPN. Das einzige VPN, das seine Datenschutzerklärung erfüllt und vollständig von Sicherheitsexperten geprüft wird."; +"onboarding_take_control_of_who_you_block" = "Übernehmen Sie die Kontrolle darüber,\nwen Sie blockieren"; +"onboarding_when_using_our_intuitive_blocklists" = "Wenn Sie unser intuitives Sperrlisten-Aufbautool nutzen."; +"onboarding_stay_protected" = "Bleiben Sie geschützt!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "Erhalten Sie wöchentliche Zusammenfassungen und Updates zu Sperrlisten."; +"onboarding_over_1_billion_trackers_blocked" = "Über 1 Mrd. Tracker blockiert"; +"onboarding_used_by_more_than_1_million" = "Von über einer Million datenschutzbewussten Menschen genutzt."; +"onboarding_sign_up" = "Registrieren"; +"onboarding_login" = "Anmelden"; +"continue" = "Weiter"; +"plan_annual" = "Jährlich"; +"plan_monthly" = "Monatlich"; + +// MARK: - SignUpViewController +"welcome" = "Willkommen"; +"already_have_account_login" = "Konto bereits vorhanden? Anmelden"; +"sign_up_to_access_new_block_lists_from_trackers" = "Registrieren Sie sich, um Zugang zu neuen Sperrlisten von Trackern zu erhalten"; +"already_have_account_login_below" = "Sie haben bereits ein Lockdown-Konto?\nMelden Sie sich unten an."; +"forgot_password" = "Passwort vergessen"; +"email" = "E-Mail"; +"password" = "Passwort"; +"access_our_curated_block_lists_and_build_your_own" = "Erhalten Sie Zugriff auf unsere kuratierte Sperrlisten und erstellen Sie Ihre eigenen"; +"access_lockdown_across_all_your_devices" = "Greifen Sie von allen Geräten auf Lockdown zu"; +"firewall_and_vpn_support" = "Firewall und VPN-Support"; +"by_continuing_you_agree_with_our" = "Wenn Sie fortfahren, akzeptieren Sie unsere"; +"and" = "und"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "Bleiben Sie in der Schleife"; +"once_a_week_helpful_reminders" = "Einmal wöchentlich hilfreiche Erinnerungen an Ihre Sicherheit, und wie Sie das Beste daraus machen."; +"enable_notifications" = "Benachrichtigungen aktivieren"; +"maybe_later" = "Vielleicht später"; + +// MARK: - Previously Unlocalized +"check_fields" = "Felder markieren"; +"email_and_password_must_not_be_empty" = "E-Mail und Passwort dürfen nicht leer sein"; +"Success! 🎉" = "Erfolgreich! 🎉"; +"error_signing_in" = "Fehler bei Registrierung"; +"confirm_your_email" = "Bestätigen Sie Ihre E-Mail"; +"you_have_successfully_sign_in" = "Sie haben sich erfolgreich registriert."; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "Ich habe %@ Tracker, Werbung und Badware mit Lockdown blockiert, der Firewall, die unerwünschte Verbindungen in all Ihren Apps sperrt.\nKostenlos erhältlich über lockdownprivacy.com."; +"check_email" = "E-Mail-Adresse prüfen"; +"we_have_sent_a_reset_password_email" = "Wir haben eine E-Mail zur Wiederherstellung Ihres Passworts gesendet. Prüfen Sie Ihre Spam-Ordner, falls sie dorthin geleitet wurde."; +"since_date" = "SEIT %@"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "Testphase starten, um vollständigen Zugang zu erhalten"; +"restore_purchase" = "Kauf wiederherstellen"; +"terms_of_service" = "Nutzungsbedingungen"; +"CHOOSE_YOUR_PLAN" = "ABO WÄHLEN"; +"skip" = "Überspringen"; +"save_annual_vpn_subscription" = "Sparen Sie ~50 %"; +"save_annual_pro_subscription" = "Sparen Sie ~30 %"; +"current_plan" = "Aktuelles Abo"; +"feature_table_column" = "Feature"; +"feature_level_table_column" = "Gratis"; +"stops_ads_and_hidden_trackers" = "Stoppt Werbung und verborgene Tracker"; +"blocks_badware_in_all_your_apps" = "Blockiert Badware in all Ihren Apps"; +"access_our_curated_block_lists" = "Zugang auf unsere kuratierte Sperrlisten"; +"build_your_custom_block_lists" = "Stellen Sie Ihre benutzerdefinierten Sperrlisten zusammen"; +"stops_browsing_history_tracking" = "Stoppt Tracking des Browser-Verlaufs"; +"anonymizing_your_browsing" = "Anonymisiert Ihr Browsing-Verhalten"; +"hides_your_location_and_ip" = "Verbirgt Ihren Standort und Ihre IP-Adresse"; +"lets_you_change_your_ip_to_other_region" = "Sie können Ihre IP\nauf eine andere Region umstellen"; +"app_for_iphone_plus_ipad" = "App für iPhone + iPad"; +"app_for_mac" = "App für Mac"; +"year_after_slash" = "Jahr"; +"month_after_slash" = "Monat"; + +// MARK: - Delete My Account +"delete_account" = "Konto löschen"; +"delete_my_account" = "Mein Konto löschen"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "Wenn Sie fortfahren, senden Sie eine Anfrage zur Löschung des gesamten Kontos, einschließlich der damit verbundenen personenbezogenen Daten. Sobald das Konto gelöscht wurde, werden Sie über diese Änderungen über Ihre Konto-E-Mail benachrichtigt.\n\nBitte beachten Sie, dass dabei alle Ihre Einstellungen verloren gehen, einschließlich Upgrade-Optionen. Sie werden auch kein weiteres Konto mit der gleichen E-Mail-Adresse erstellen können."; +"proceed" = "Fortfahren"; + +// MARK: - EmailValidatable +"invalid_email_address" = "Ungültige E-Mail-Adresse"; +"please_fill_in_your_email" = "Bitte geben Sie Ihre E-Mail ein."; +"you_entered_con_instead_of_com" = "Sie haben .con anstatt .com geschrieben."; +"no_valid_email_address_detected" = "Keine gültige E-Mail-Adresse erkannt."; +"too_many_email_addresses_entered" = "Zu viele E-Mail-Adressen eingegeben."; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "minuten"; +"seconds_genitive" = "sekunden"; +"lockdowns_first_christmas_sale" = "Der erste Weihnachts-Sale von Lockdown"; +"subscribe_now" = "Jetzt abonnieren"; + +// MARK: - Code Redemption +"redeem_code" = "Code einlösen"; +"cannot_redeem_code" = "Code nicht einlösbar"; +"the_code_could_not_be_redeemed" = "Der Code konnte nicht eingelöst werden."; +"congratulations_excl" = "Herzlichen Glückwunsch!"; +"the_code_was_successfully_redeemed" = "Der Code wurde erfolgreich eingelöst. Sie müssen die App neustarten, damit Ihr Kauf wirksam wird."; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index b0d9f2a..4aa9443 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -518,7 +518,7 @@ "To: joe@email.com\nRe: Q4 2019 Finance Review" = "To: joe@email.com\nRe: Q4 2019 Finance Review"; /* No comment provided by engineer. */ -"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." = "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."; +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "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."; /* Used in the sentence: You blocked 500 tracking attempts this week. Tap to update to the newsst block lists. */ "tracking attempts this week. Tap to update to the newest block lists." = "tracking attempts this week. Tap to update to the newest block lists."; @@ -697,3 +697,113 @@ /* The title of a list of trackers */ "Amazon Trackers" = "Amazon Trackers"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "Simple and Powerful"; +"onboarding_lockdown_stops_trackers" = "Lockdown stops trackers, malware and other bad agents from accessing your data."; +"onboarding_worlds_first_on_device" = "World's First on Device"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "Open Source Firewall and secure tunnel VPN that works across ALL your apps and web browsing."; +"onboarding_staying_protected_is_easy_with_lockdown" = "Staying Protected is so easy with Lockdown"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "Remain Anonymous with Secure Tunnel VPN"; +"onboarding_no_logs_fast_and_fully_audited" = "No-logs, blazing-fast, privacy-first VPN. The only VPN to prove its Privacy Policy and be fully audited by security experts."; +"onboarding_take_control_of_who_you_block" = "Take control of who\nyou Block"; +"onboarding_when_using_our_intuitive_blocklists" = "When using our intuitive block list building tool."; +"onboarding_stay_protected" = "Stay Protected!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "Get weekly summaries and block list updates."; +"onboarding_over_1_billion_trackers_blocked" = "Over 1 Billion Trackers Blocked"; +"onboarding_used_by_more_than_1_million" = "Used by more than one million privacy-conscious people."; +"onboarding_sign_up" = "Sign Up"; +"onboarding_login" = "Login"; +"continue" = "Continue"; +"plan_annual" = "Annual"; +"plan_monthly" = "Monthly"; + +// MARK: - SignUpViewController +"welcome" = "Welcome"; +"already_have_account_login" = "Already have an account? Login"; +"sign_up_to_access_new_block_lists_from_trackers" = "Sign up to access new blocked lists from trackers"; +"already_have_account_login_below" = "Already have Lockdown account?\nLogin below."; +"forgot_password" = "Forgot Password"; +"email" = "Email"; +"password" = "Password"; +"access_our_curated_block_lists_and_build_your_own" = "Access our curated block lists and build your own"; +"access_lockdown_across_all_your_devices" = "Access Lockdown across all your devices"; +"firewall_and_vpn_support" = "Firewall and VPN support"; +"by_continuing_you_agree_with_our" = "By continuing you agree with our"; +"and" = "and"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "Stay in the Loop"; +"once_a_week_helpful_reminders" = "Once a week helpful reminders about your security and how to make most of it."; +"enable_notifications" = "Enable Notifications"; +"maybe_later" = "Maybe Later"; + +// MARK: - Previously Unlocalized +"check_fields" = "Check Fields"; +"email_and_password_must_not_be_empty" = "Email and password must not be empty"; +"Success! 🎉" = "Success! 🎉"; +"error_signing_in" = "Error Signing In"; +"confirm_your_email" = "Confirm Your Email"; +"you_have_successfully_sign_in" = "You've successfully signed in."; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "I blocked %@ trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps.\nGet it free at lockdownprivacy.com."; +"check_email" = "Check Email"; +"we_have_sent_a_reset_password_email" = "We've sent a reset password email to you. Be sure to check any spam/junk folders, in case it got stuck there."; +"since_date" = "SINCE %@"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "Start Trial Now to Get Full Access"; +"restore_purchase" = "Restore Purchase"; +"terms_of_service" = "Terms of Service"; +"CHOOSE_YOUR_PLAN" = "CHOOSE YOUR PLAN"; +"skip" = "Skip"; +"save_annual_vpn_subscription" = "Save ~50%"; +"save_annual_pro_subscription" = "Save ~30%"; +"current_plan" = "Current Plan"; +"feature_table_column" = "Feature"; +"feature_level_table_column" = "Free"; +"stops_ads_and_hidden_trackers" = "Stops Ads and Hidden Trackers"; +"blocks_badware_in_all_your_apps" = "Blocks Badware in All Your Apps"; +"access_our_curated_block_lists" = "Access Our Curated Block Lists"; +"build_your_custom_block_lists" = "Build Your Custom Block Lists"; +"stops_browsing_history_tracking" = "Stops Browsing History Tracking"; +"anonymizing_your_browsing" = "Anonymizing Your Browsing"; +"hides_your_location_and_ip" = "Hides Your Location and IP"; +"lets_you_change_your_ip_to_other_region" = "Lets You Change Your IP\nto Other Region"; +"app_for_iphone_plus_ipad" = "App for iPhone + iPad"; +"app_for_mac" = "App for Mac"; +"year_after_slash" = "year"; +"month_after_slash" = "month"; + +// MARK: - Delete My Account +"delete_account" = "Delete Account"; +"delete_my_account" = "Delete My Account"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "By proceeding you will submit a request to delete your entire account record, along with associated personal data. Once the account has been deleted, you will be notified of these changes via your account email.\n\nPlease, be aware that you will lose all of your settings including upgrade options and will not be able to create an account with the same email."; +"proceed" = "Proceed"; + +// MARK: - EmailValidatable +"invalid_email_address" = "Invalid email address"; +"please_fill_in_your_email" = "Please fill in your email."; +"you_entered_con_instead_of_com" = "You entered .con instead of .com."; +"no_valid_email_address_detected" = "No valid email address detected."; +"too_many_email_addresses_entered" = "Too many email addresses entered."; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "minutes"; +"seconds_genitive" = "seconds"; +"lockdowns_first_christmas_sale" = "Lockdown’s First Christmas Sale"; +"subscribe_now" = "Subscribe Now"; + +// MARK: - Code Redemption +"redeem_code" = "Redeem Code"; +"cannot_redeem_code" = "Cannot Redeem Code"; +"the_code_could_not_be_redeemed" = "The code could not be redeemed."; +"congratulations_excl" = "Congratulations!"; +"the_code_was_successfully_redeemed" = "The code was successfully redeemed. You may need to restart the app for the purchase to take effect."; diff --git a/es.lproj/Localizable.strings b/es.lproj/Localizable.strings index 70eb250..7632d9f 100644 --- a/es.lproj/Localizable.strings +++ b/es.lproj/Localizable.strings @@ -341,7 +341,7 @@ "To: joe@email.com\nRe: Q4 2019 Finance Review" = "Para: joe@email.com\nRe: Q4 2019 Revisión Financiera"; /* No comment provided by engineer. */ -"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." = "Los rastreadores, anuncios y otros scripts maliciosos ahora están bloqueados en todas sus aplicaciones, incluso fuera de Safari.\n\nObtenga la máxima privacidad con un túnel seguro que protege las conexiones, anonimiza su navegación y oculta su ubicación."; +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "Los rastreadores, anuncios y otros scripts maliciosos ahora están bloqueados en todas sus aplicaciones, incluso fuera de Safari.\n\nObtenga la máxima privacidad con un túnel seguro que protege las conexiones, anonimiza su navegación y oculta su ubicación."; /* No comment provided by engineer. */ "Tunnel" = "Túnel"; @@ -703,3 +703,113 @@ /* The title of a list of trackers */ "Amazon Trackers" = "Amazon Trackers"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "Sencillo y potente"; +"onboarding_lockdown_stops_trackers" = "Lockdown bloquea a los rastreadores, programas maliciosos y otras amenazas para que no puedan acceder a tus datos."; +"onboarding_worlds_first_on_device" = "Primeros en el mundo en dispositivos"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "Cuenta con firewall de código abierto y Secure Tunnel VPN que funciona en TODAS tus aplicaciones y navegadores web."; +"onboarding_staying_protected_is_easy_with_lockdown" = "Con Lockdown, estar protegido es muy sencillo"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "Mantén el anonimato con Secure Tunnel VPN"; +"onboarding_no_logs_fast_and_fully_audited" = "VPN sin registros, ultrarrápida y que prioriza tu privacidad. La única VPN que ha demostrado su política de privacidad y que se ha sometido a una auditoría completa realizada por parte de expertos en seguridad."; +"onboarding_take_control_of_who_you_block" = "Controla a quién\nbloqueas"; +"onboarding_when_using_our_intuitive_blocklists" = "mediante nuestra herramienta intuitiva de creación de listas de bloqueo."; +"onboarding_stay_protected" = "¡Protégete!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "Recibe informes semanales y actualizaciones de la lista de bloqueos."; +"onboarding_over_1_billion_trackers_blocked" = "Más de mil millones de rastreadores bloqueados"; +"onboarding_used_by_more_than_1_million" = "Usado por más de un millón de usuarios que se preocupan por su privacidad."; +"onboarding_sign_up" = "Registrarse"; +"onboarding_login" = "Iniciar sesión"; +"continue" = "Continuar"; +"plan_annual" = "Anual"; +"plan_monthly" = "Mensual"; + +// MARK: - SignUpViewController +"welcome" = "Te damos la bienvenida"; +"already_have_account_login" = "¿Ya tienes una cuenta? Inicia sesión"; +"sign_up_to_access_new_block_lists_from_trackers" = "Regístrate para tener acceso a las nuevas listas de bloqueo de rastreadores"; +"already_have_account_login_below" = "¿Ya tienes cuenta con Lockdown?\nInicia sesión a continuación."; +"forgot_password" = "¿Has olvidado tu contraseña?"; +"email" = "Correo electrónico"; +"password" = "Contraseña"; +"access_our_curated_block_lists_and_build_your_own" = "Obtén acceso a nuestras elaboradas listas de bloqueo y crea la tuya propia"; +"access_lockdown_across_all_your_devices" = "Accede a Lockdown desde todos tus dispositivos"; +"firewall_and_vpn_support" = "Servicio de asistencia para el firewall y la VPN"; +"by_continuing_you_agree_with_our" = "Al seguir, estarás aceptando nuestras"; +"and" = "y"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "Mantente al tanto"; +"once_a_week_helpful_reminders" = "Recibe recordatorios semanales sobre tu seguridad y cómo sacarle el máximo partido."; +"enable_notifications" = "Activar notificaciones"; +"maybe_later" = "Tal vez más tarde"; + +// MARK: - Previously Unlocalized +"check_fields" = "Comprobar campos de texto"; +"email_and_password_must_not_be_empty" = "Los campos de correo electrónico y contraseña no pueden estar vacíos"; +"Success! 🎉" = "¡Hecho! 🎉"; +"error_signing_in" = "Se ha producido un error al registrarte"; +"confirm_your_email" = "Confirma tu correo electrónico"; +"you_have_successfully_sign_in" = "Te has registrado correctamente."; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "He bloqueado %@ rastreadores, publicidad web y badware gracias a Lockdown, el firewall que bloquea conexiones no deseadas en todas las aplicaciones.\nConsíguelo gratis en lockdownprivacy.com."; +"check_email" = "Comprueba tu correo electrónico"; +"we_have_sent_a_reset_password_email" = "Te hemos enviado un correo electrónico para restablecer la contraseña. No te olvides de comprobar también tu bandeja de spam/papelera."; +"since_date" = "DESDE %@"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "Empieza ahora la versión de prueba para disfrutar de acceso completo"; +"restore_purchase" = "Restaurar compra"; +"terms_of_service" = "Condiciones de uso"; +"CHOOSE_YOUR_PLAN" = "ELIGE TU PLAN"; +"skip" = "Saltar"; +"save_annual_vpn_subscription" = "Ahorra sobre un 50 %"; +"save_annual_pro_subscription" = "Ahorra sobre un 30 %"; +"current_plan" = "Plan actual"; +"feature_table_column" = "Función"; +"feature_level_table_column" = "Gratis"; +"stops_ads_and_hidden_trackers" = "Detiene la publicidad y los rastreadores ocultos"; +"blocks_badware_in_all_your_apps" = "Bloquea el badware en todas tus aplicaciones"; +"access_our_curated_block_lists" = "Obtén acceso a nuestras elaboradas listas de bloqueo"; +"build_your_custom_block_lists" = "Crea tus propias listas de bloqueo personalizadas"; +"stops_browsing_history_tracking" = "Detiene el registro en el historial de navegación"; +"anonymizing_your_browsing" = "Permite la navegación web de forma anónima"; +"hides_your_location_and_ip" = "Oculta tu ubicación e IP"; +"lets_you_change_your_ip_to_other_region" = "Te permite cambiar tu IP\na otra región"; +"app_for_iphone_plus_ipad" = "Aplicación para iPhone + iPad"; +"app_for_mac" = "Aplicación para Mac"; +"year_after_slash" = "año"; +"month_after_slash" = "mes"; + +// MARK: - Delete My Account +"delete_account" = "Eliminar cuenta"; +"delete_my_account" = "Eliminar mi cuenta"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "Al pulsar en aceptar, enviarás una solicitud para borrar todo tu historial de la cuenta, así como la información personal asociada a ella. Una vez que se haya eliminado tu cuenta, se te notificará mediante el correo electrónico asociado a la cuenta.\n\nTen en cuenta que perderás todos tus ajustes, incluidas las opciones de actualización, y que no podrás volver a crear una cuenta con el mismo correo electrónico."; +"proceed" = "Aceptar"; + +// MARK: - EmailValidatable +"invalid_email_address" = "Dirección de correo electrónico no válida"; +"please_fill_in_your_email" = "Introduce tu correo electrónico."; +"you_entered_con_instead_of_com" = "Has escrito \".con\" en lugar de \".com\"."; +"no_valid_email_address_detected" = "No se ha detectado ninguna dirección de correo electrónico válida."; +"too_many_email_addresses_entered" = "Se han introducido demasiadas direcciones de correo electrónico."; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "minutos"; +"seconds_genitive" = "segundos"; +"lockdowns_first_christmas_sale" = "Primera oferta\nde Navidad de Lockdown"; +"subscribe_now" = "Suscríbete ahora"; + +// MARK: - Code Redemption +"redeem_code" = "Canjear código"; +"cannot_redeem_code" = "Código no canjeado"; +"the_code_could_not_be_redeemed" = "No se ha podido canjear el código."; +"congratulations_excl" = "¡Enhorabuena!"; +"the_code_was_successfully_redeemed" = "El código se ha canjeado con éxito. Puede que tengas que reiniciar la aplicación para que la compra se aplique correctamente."; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index f0ba668..45bc463 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -344,7 +344,7 @@ "To: joe@email.com\nRe: Q4 2019 Finance Review" = "To: joe@email.com\nRe: Revue financière du 4ème trimestre 2019"; /* No comment provided by engineer. */ -"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." = "Les traqueurs, publicités et autres scripts malveillants sont désormais bloqués dans toutes vos applications, même en dehors de Safari.\n\nObtenez une confidentialité maximale grâce à un Tunnel Sécurisé pour protéger vos connexions, anonymiser votre navigation et cacher votre localisation."; +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "Les traqueurs, publicités et autres scripts malveillants sont désormais bloqués dans toutes vos applications, même en dehors de Safari.\n\nObtenez une confidentialité maximale grâce à un Tunnel Sécurisé pour protéger vos connexions, anonymiser votre navigation et cacher votre localisation."; /* No comment provided by engineer. */ "Tunnel" = "Tunnel"; @@ -703,3 +703,114 @@ /* The title of a list of trackers */ "Amazon Trackers" = "Amazon Trackers"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "Simple et puissant"; +"onboarding_lockdown_stops_trackers" = "Lockdown empêche les traqueurs, les malwares et autres agents malintentionnés d'accéder à vos données."; +"onboarding_worlds_first_on_device" = "Numéro 1 mondial sur tous les appareils"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "Un pare-feu en open source et un tunnel VPN sécurisé qui fonctionne sur TOUTES vos applis et navigateurs internet."; +"onboarding_staying_protected_is_easy_with_lockdown" = "Se protéger est facile avec Lockdown"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "Restez anonyme avec un tunnel VPN sécurisé"; +"onboarding_no_logs_fast_and_fully_audited" = "Un VPN incroyablement rapide, sans logs, centré sur la confidentialité. Le seul VPN a prouver sa politique de confidentialité et à être entièrement audité par des experts de la sécurité."; +"onboarding_take_control_of_who_you_block" = "Contrôlez qui\nvous bloquez"; +"onboarding_when_using_our_intuitive_blocklists" = "Avec notre outil de liste de blocage intuitif."; +"onboarding_stay_protected" = "Protégez-vous !"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "Obtenez des rapports et des mises à jour de liste de blocage hebdomadaires."; +"onboarding_over_1_billion_trackers_blocked" = "Plus d'un milliard de traqueurs bloqués"; +"onboarding_used_by_more_than_1_million" = "Utilisé par plus d'un million de personnes soucieuses de confidentialité."; +"onboarding_sign_up" = "S'inscrire"; +"onboarding_login" = "Se connecter"; +"continue" = "Continuer"; +"plan_annual" = "Annuelle"; +"plan_monthly" = "Mensuelle"; + +// MARK: - SignUpViewController +"welcome" = "Bienvenue"; +"already_have_account_login" = "Vous avez déjà un compte ? Se connecter"; +"sign_up_to_access_new_block_lists_from_trackers" = "Inscrivez-vous pour accéder aux nouvelles listes de traqueurs bloqués"; +"already_have_account_login_below" = "Vous avez déjà un compte Lockdown ?\nConnectez-vous ci-dessous."; +"forgot_password" = "Mot de passe oublié"; +"email" = "Adresse e-mail"; +"password" = "Mot de passe"; +"access_our_curated_block_lists_and_build_your_own" = "Accédez à nos listes de blocage élaborées avec soin et créez les vôtres"; +"access_lockdown_across_all_your_devices" = "Accédez à Lockdown sur tous vos appareils"; +"firewall_and_vpn_support" = "Pare-feu et soutien VPN"; +"by_continuing_you_agree_with_our" = "En poursuivant, vous acceptez nos"; +"and" = "et"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "Rester dans la boucle"; +"once_a_week_helpful_reminders" = "Une fois par semaine, des rappels utiles sur votre sécurité et comment en profiter."; +"enable_notifications" = "Activer les notifications"; +"maybe_later" = "Peut-être plus tard"; + +// MARK: - Previously Unlocalized +"check_fields" = "Vérifier les champs"; +"email_and_password_must_not_be_empty" = "Les champs de l'adresse e-mail et du mot de passe ne doivent pas être vides"; +"Success! 🎉" = "Succès ! 🎉"; +"error_signing_in" = "Erreur de connexion"; +"confirm_your_email" = "Confirmez votre adresse e-mail"; +"you_have_successfully_sign_in" = "Vous êtes bien connecté(e)."; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "J'ai bloqué %@ traqueurs, pubs, et badwares avec Lockdown, le pare-feu qui bloque les connexions indésirables sur toutes vos applis.\nObtenez-le gratuitement sur lockdownprivacy.com."; +"check_email" = "Vérifiez vos e-mails"; +"we_have_sent_a_reset_password_email" = "Nous vous avons envoyé un e-mail pour réinitialiser votre mot de passe. Assurez-vous de vérifier qu'il ne se soit pas égaré dans votre dossier de spam."; +"since_date" = "DEPUIS LE %@"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "Démarrer l'essai maintenant pour obtenir l'accès complet"; +"restore_purchase" = "Restaurer l'achat"; +"terms_of_service" = "Conditions d'utilisation"; +"CHOOSE_YOUR_PLAN" = "CHOISISSEZ VOTRE FORMULE"; +"skip" = "Passer"; +"save_annual_vpn_subscription" = "Économisez ~50 %"; +"save_annual_pro_subscription" = "Économisez ~30 %"; +"current_plan" = "Formule actuelle"; +"feature_table_column" = "Fonctionnalité"; +"feature_level_table_column" = "Gratuite"; +"stops_ads_and_hidden_trackers" = "Stoppe les pubs et les traqueurs cachés"; +"blocks_badware_in_all_your_apps" = "Bloque les badwares dans toutes vos applis"; +"access_our_curated_block_lists" = "Accédez à nos listes de blocage élaborées avec soin"; +"build_your_custom_block_lists" = "Créez vos listes de blocages personnalisées"; +"stops_browsing_history_tracking" = "Stoppe le tracking de l'historique des recherches"; +"anonymizing_your_browsing" = "Anonymise vos recherches"; +"hides_your_location_and_ip" = "Dissimule votre position et votre adresse IP"; +"lets_you_change_your_ip_to_other_region" = "Vous laisse choisir une autre région\npour votre adresse IP"; +"app_for_iphone_plus_ipad" = "Application pour iPhone et iPad"; +"app_for_mac" = "Application pour Mac"; +"year_after_slash" = "an"; +"month_after_slash" = "mois"; + +// MARK: - Delete My Account +"delete_account" = "Supprimer le compte"; +"delete_my_account" = "Supprimer mon compte"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "En poursuivant, vous allez envoyer une demande de suppression complète de votre compte, ainsi que de toutes les données personnelles associées. Une fois le compte supprimé, vous recevrez une notification de ces changements sur l'adresse e-mail de votre compte.\n\nVeuillez noter que vous perdrez la totalité de vos paramètres, y compris les options de mise à niveau, et que vous ne pourrez pas créer d'autre compte avec la même adresse e-mail."; +"proceed" = "Poursuivre"; + +// MARK: - EmailValidatable +"invalid_email_address" = "Adresse e-mail non valide"; +"please_fill_in_your_email" = "Veuillez indiquer votre adresse e-mail."; +"you_entered_con_instead_of_com" = "Vous avez entré « .con » au lieu de « .com »."; +"no_valid_email_address_detected" = "Aucune adresse e-mail valide détectée."; +"too_many_email_addresses_entered" = "Trop d'adresses e-mail entrées."; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "minutes"; +"seconds_genitive" = "secondes"; +"lockdowns_first_christmas_sale" = "Premières promotions\nde Noël de Lockdown"; +"subscribe_now" = "S'abonner maintenant"; + +// MARK: - Code Redemption +"redeem_code" = "Utiliser le code"; +"cannot_redeem_code" = "Code inutilisable"; +"the_code_could_not_be_redeemed" = "Impossible d'utiliser ce code."; +"congratulations_excl" = "Félicitations !"; +"the_code_was_successfully_redeemed" = "Votre code a bien été pris en compte. Vous devrez peut-être redémarrer l'application pour que l'achat apparaisse."; + diff --git a/ja.lproj/Localizable.strings b/ja.lproj/Localizable.strings index 405a939..2537e73 100644 --- a/ja.lproj/Localizable.strings +++ b/ja.lproj/Localizable.strings @@ -344,7 +344,7 @@ "To: joe@email.com\nRe: Q4 2019 Finance Review" = "宛先: joe@email.com\nRe: Q4 2019 財務レビュー"; /* No comment provided by engineer. */ -"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." = "トラッカー、 アド、 その他の悪意のあるスクリプトがすべてのアプリでブロックされるようになりました、 Safariの外でも。\n\n接続を保護し、ブラウジングを匿名化し、位置情報を非表示にするセキュアなトンネルで、最大限のプライバシーを確保します。"; +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "トラッカー、 アド、 その他の悪意のあるスクリプトがすべてのアプリでブロックされるようになりました、 Safariの外でも。\n\n接続を保護し、ブラウジングを匿名化し、位置情報を非表示にするセキュアなトンネルで、最大限のプライバシーを確保します。"; /* No comment provided by engineer. */ "Tunnel" = "トンネル"; @@ -703,3 +703,113 @@ /* The title of a list of trackers */ "Amazon Trackers" = "Amazon Trackers"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "シンプルでパワフル"; +"onboarding_lockdown_stops_trackers" = "Lockdownはトラッカー、マルウェア、その他あらゆる不適切なエージェントがあなたのデータにアクセスするのをブロックします。"; +"onboarding_worlds_first_on_device" = "世界初のオンデバイスソリューション"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "オープンソースファイヤーウォールとセキュアトンネルVPNで、すべてのアプリとWebブラウジングを保護。"; +"onboarding_staying_protected_is_easy_with_lockdown" = "Lockdownなら常時保護も簡単"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "セキュアトンネルVPNでプライバシーを保護"; +"onboarding_no_logs_fast_and_fully_audited" = "ログを残さ図、プライバシーを最優先する超高速VPN。セキュリティ専門家の厳重な監査でプライバシーポリシーを証明する唯一のVPN。"; +"onboarding_take_control_of_who_you_block" = "何をブロックするのかを\n完全にコントロール"; +"onboarding_when_using_our_intuitive_blocklists" = "直感的なブロックリスト作成ツールを提供。"; +"onboarding_stay_protected" = "常にプロテクト!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "週次のサマリーとブロックリスト更新。"; +"onboarding_over_1_billion_trackers_blocked" = "10億以上のトラッカーをブロック済み"; +"onboarding_used_by_more_than_1_million" = "100万人以上のセキュリティを重視するユーザーが利用中。"; +"onboarding_sign_up" = "サインアップ"; +"onboarding_login" = "ログイン"; +"continue" = "継続"; +"plan_annual" = "年次"; +"plan_monthly" = "月次"; + +// MARK: - SignUpViewController +"welcome" = "ようこそ"; +"already_have_account_login" = "既にアカウントをお持ちですか? ログイン"; +"sign_up_to_access_new_block_lists_from_trackers" = "サインアップしてトラッカーのブロックリストを新たに取得"; +"already_have_account_login_below" = "Lockdownのアカウントを既にお持ちですか?\n以下からログインしてください。"; +"forgot_password" = "パスワードを忘れた場合"; +"email" = "メール"; +"password" = "パスワード"; +"access_our_curated_block_lists_and_build_your_own" = "当社独自のキュレーションによるブロックリストを取得してカスタマイズ"; +"access_lockdown_across_all_your_devices" = "お使いのすべてのデバイスからLockdownを利用可能"; +"firewall_and_vpn_support" = "ファイヤーウォールとVPNサポート"; +"by_continuing_you_agree_with_our" = "継続すると以下に同意することになります:"; +"and" = "および"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "常に最新情報を取得"; +"once_a_week_helpful_reminders" = "週に一度の便利なセキュリティリマインダーで防護を最大限に。"; +"enable_notifications" = "通知を有効化"; +"maybe_later" = "後にする"; + +// MARK: - Previously Unlocalized +"check_fields" = "フィールドをチェック"; +"email_and_password_must_not_be_empty" = "メールとパスワードは空欄にできません"; +"Success! 🎉" = "成功! 🎉"; +"error_signing_in" = "サインインエラー"; +"confirm_your_email" = "メールを確認"; +"you_have_successfully_sign_in" = "サインインが正常に完了しました。"; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "ファイヤーウォールですべてのアプリについて不必要な接続をブロックしてくれるLockdownによって、%@トラッカー、広告、マルウェアブロックしました。\nlockdownprivacy.com で無料でダウンロードできます。"; +"check_email" = "メールをチェック"; +"we_have_sent_a_reset_password_email" = "パスワードリセット用のメールを送りました。スパムやジャンク用のフォルダに振り分けられていることもありますので、確認してみてください。"; +"since_date" = "%@以来"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "今すぐトライアルですべての機能を使ってみる"; +"restore_purchase" = "購入対象の復元"; +"terms_of_service" = "利用規約"; +"CHOOSE_YOUR_PLAN" = "プランを選択してください"; +"skip" = "スキップ"; +"save_annual_vpn_subscription" = "50%お得"; +"save_annual_pro_subscription" = "30%お得"; +"current_plan" = "現在のプラン"; +"feature_table_column" = "特徴"; +"feature_level_table_column" = "無料"; +"stops_ads_and_hidden_trackers" = "広告と隠されたトラッカーをブロック"; +"blocks_badware_in_all_your_apps" = "すべてのアプリをマルウェアから保護"; +"access_our_curated_block_lists" = "当社独自管理のブロックリストへのアクセス"; +"build_your_custom_block_lists" = "自分だけのブロックリストをカスタマイズ"; +"stops_browsing_history_tracking" = "ブラウズ履歴追跡をブロック"; +"anonymizing_your_browsing" = "ブラウジングを匿名化"; +"hides_your_location_and_ip" = "位置情報とIPを秘匿"; +"lets_you_change_your_ip_to_other_region" = "IPを変更して\n別の地域をカモフラージュ"; +"app_for_iphone_plus_ipad" = "iPhoneおよびiPad向けアプリ"; +"app_for_mac" = "Mac用アプリ"; +"year_after_slash" = "年"; +"month_after_slash" = "月"; + +// MARK: - Delete My Account +"delete_account" = "アカウントを削除"; +"delete_my_account" = "自分のアカウントを削除"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "続行すると、あなたのアカウントに関するすべての記録と関連する個人情報を消去する要求を送信することになります。アカウントが削除された時点で、その処理について、アカウントのメールアドレスに通知が送られます。\n\nアップグレードオプションを含むすべての設定も合わせて消去されることにご注意ください。また同一のメールアドレスでアカウントを作成することはできなくなります。"; +"proceed" = "続行"; + +// MARK: - EmailValidatable +"invalid_email_address" = "無効なメールアドレス"; +"please_fill_in_your_email" = "メールアドレスを入力してください。"; +"you_entered_con_instead_of_com" = ".comではなく.conと入力されています。"; +"no_valid_email_address_detected" = "有効なメールアドレスが検出されませんでした。"; +"too_many_email_addresses_entered" = "入力されたメールアドレスが多すぎます。"; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "分"; +"seconds_genitive" = "秒"; +"lockdowns_first_christmas_sale" = "Lockdown初のクリスマスセール"; +"subscribe_now" = "今すぐ登録"; + +// MARK: - Code Redemption +"redeem_code" = "コードを引き換える"; +"cannot_redeem_code" = "コードを引き換えられません"; +"the_code_could_not_be_redeemed" = "このコードは使用できませんでした。"; +"congratulations_excl" = "おめでとうございます!"; +"the_code_was_successfully_redeemed" = "コードの引き換えに成功しました。なお、購入品を反映するために、アプリを再起動する必要がある場合があります。"; diff --git a/ru.lproj/Localizable.strings b/ru.lproj/Localizable.strings new file mode 100644 index 0000000..d340f7d --- /dev/null +++ b/ru.lproj/Localizable.strings @@ -0,0 +1,809 @@ +/* Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com */ +" and " = " и"; + +/* refers to number of connections blocked today, as in '34 Blocked Today' */ +" Blocked Today" = " Заблокировано сегодня"; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to add to a whitelist. For example, \"google.com\" without \"https://\"" = " не является допустимым объектом. Указывайте только домен, который хотите добавить в белый список. Например, \«google.com\» без \«https://\»"; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to block. For example, \"google.com\" without \"https://\"" = " не является допустимым объектом. Указывайте только домен, который хотите заблокировать. Например, \«google.com\» без \«https://\»"; + +/* No comment provided by engineer. */ +" Off" = " Откл."; + +/* No comment provided by engineer. */ +" On" = " Вкл."; + +/* Used in the sentence: I blocked 500 trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com. */ +" trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com." = " трекеры, рекламу и вредоносное ПО с Lockdown, брандмауэром, который блокирует нежелательные соединения во всех ваших приложениях. Скачайте его бесплатно на сайте lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"1 Week Free" = "1 неделя бесплатно"; + +/* No comment provided by engineer. */ +"\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). You can also set custom domains to block." = "Функция \«Чёрный список\» позволяет выбрать, какие объекты вы хотите заблокировать (напр., Facebook, провокационные ссылки и т. д.). Вы также можете самостоятельно указать заблокированные домены."; + +/* No comment provided by engineer. */ +"\"iOS Annual\" is your current plan" = "Ваш текущий тарифный план - \«iOS (ежегодный)\»"; + +/* No comment provided by engineer. */ +"\"iOS Monthly\" is your current plan" = "Ваш текущий тарифный план - \«iOS (ежемесячный)\»"; + +/* No comment provided by engineer. */ +"\"Pro Annual\" is your current plan" = "Ваш текущий тарифный план - \«PRO (ежегодный)\»"; + +/* No comment provided by engineer. */ +"\"Pro Monthly\" is your current plan" = "Ваш текущий тарифный план - \«PRO (ежемесячный)\»"; + +/* No comment provided by engineer. */ +"\"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." = "Функция \«Открыть журнал\» показывает, какие именно соединения были заблокированы в предыдущий день. В полночь журнал очищается. Он хранится на вашем устройстве, поэтому просмотреть его можете только вы."; + +/* No comment provided by engineer. */ +"\n\n If this error persists, please contact team@lockdownprivacy.com." = "\n\n Если ошибка сохраняется, напишите нам на team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"\n\n If this persists, please contact team@lockdownprivacy.com." = "\n\n Если проблема сохраняется, напишите нам на team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"About Lockdown VPN" = "О Lockdown VPN"; + +/* No comment provided by engineer. */ +"Activate Firewall with this button." = "Активируйте брандмауэр, нажав на эту кнопку."; + +/* No comment provided by engineer. */ +"Activating" = "Активация"; + +/* No comment provided by engineer. */ +"Anonymize IP against trackers" = "Скрывайте IP-адрес для борьбы с трекерами"; + +/* No comment provided by engineer. */ +"Apple Outage" = "Сбой в работе Apple"; + +/* No comment provided by engineer. */ +"at least 8 characters" = "не меньше 8 знаков"; + +/* No comment provided by engineer. */ +"Audit Reports" = "Отчёты о проверке"; + +/* No comment provided by engineer. */ +"Australia" = "Австралия"; + +/* No comment provided by engineer. */ +"Be sure to check your spam folder in case it got stuck there.\n\nYou can also request a re-send of the confirmation." = "Проверьте, не попало ли письмо в папку «Спам».\n\nВы также можете повторно запросить письмо с подтверждением."; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"block list" = "чёрный список"; + +/* No comment provided by engineer. */ +"Blocked" = "Заблокировано"; + +/* No comment provided by engineer. */ +"Blocked Domains" = "Заблокированные домены"; + +/* No comment provided by engineer. */ +"Blocked Today" = "Заблокировано сегодня"; + +/* No comment provided by engineer. */ +"Blocked Trackers Summary" = "Отчёт о заблокированных трекерах"; + +/* No comment provided by engineer. */ +"Brazil" = "Бразилия"; + +/* No comment provided by engineer. */ +"Canada" = "Канада"; + +/* No comment provided by engineer. */ +"Cancel" = "Отменить"; + +/* No comment provided by engineer. */ +"Check Your Inbox" = "Проверьте свой почтовый ящик"; + +/* No comment provided by engineer. */ +"Clear Block Log" = "Очистить журнал блокировок"; + +/* The title of a list of trackers */ +"Clickbait" = "Провокационные ссылки"; + +/* No comment provided by engineer. */ +"Confirm Email" = "Подтвердить адрес электронной почты"; + +/* No comment provided by engineer. */ +"Confirmed Site" = "Подтверждённый сайт"; + +/* No comment provided by engineer. */ +"Couldn't Find Your Email Client" = "Не удается найти ваш почтовый клиент"; + +/* The title of a list of trackers */ +"Crypto Mining" = "Майнинг криптовалюты"; + +/* No comment provided by engineer. */ +"Custom Settings (Advanced)" = "Пользовательские настройки (для опытных пользователей)"; + +/* The title of a list of trackers */ +"Data Trackers" = "Трекеры данных"; + +/* No comment provided by engineer. */ +"Deactivating" = "Деактивация"; + +/* No comment provided by engineer. */ +"Disable" = "Отключить"; + +/* No comment provided by engineer. */ +"Disable Block Log" = "Отключить журнал блокировок"; + +/* No comment provided by engineer. */ +"Disable Block Log?" = "Отключить журнал блокировок?"; + +/* No comment provided by engineer. */ +"Done" = "Готово"; + +/* No comment provided by engineer. */ +"Email Support" = "Написать в службу поддержки"; + +/* The title of a list of trackers */ +"Email Trackers" = "Трекеры электронной почты"; + +/* No comment provided by engineer. */ +"Email Us" = "Написать нам"; + +/* No comment provided by engineer. */ +"Enable" = "Включить"; + +/* No comment provided by engineer. */ +"Enable Block Log" = "Включить журнал блокировок"; + +/* No comment provided by engineer. */ +"Enable notifications to get a once-a-week summary and the latest block list updates. You can disable this anytime." = "Включите уведомления, чтобы получать еженедельные отчёты и последние обновления чёрного списка. Вы можете в любое время отключить данную функцию."; + +/* No comment provided by engineer. */ +"Encrypted, private connections" = "Зашифрованные, частные соединения"; + +/* No comment provided by engineer. */ +"Error Code " = "Код ошибки"; + +/* No comment provided by engineer. */ +"Error Creating Email Account" = "Ошибка при создании электронной почты"; + +/* No comment provided by engineer. */ +"Error Loading Plan: Retry" = "Ошибка при загрузке тарифного плана: попробуйте ещё раз"; + +/* No comment provided by engineer. */ +"Error Re-sending Email Confirmation" = "Ошибка при повторной отправке письма с подтверждением"; + +/* No comment provided by engineer. */ +"Error Restoring Subscription" = "Ошибка при восстановлении подписки"; + +/* No comment provided by engineer. */ +"Error Signing In To Verify Subscription" = "Ошибка при входе для подтверждения подписки"; + +/* No comment provided by engineer. */ +"Error Starting Trial" = "Ошибка активации пробной версии"; + +/* The title of a list of trackers */ +"Facebook & WhatsApp" = "Facebook и WhatsApp"; + +/* The title of a list of trackers */ +"Facebook Trackers" = "Трекеры Facebook"; + +/* No comment provided by engineer. */ +"Failed to re-send email confirmation." = "Не удалось повторно отправить письмо с подтверждением"; + +/* No comment provided by engineer. */ +"FAQs" = "Вопросы и ответы"; + +/* No comment provided by engineer. */ +"Firewall" = "Брандмауэр"; + +/* No comment provided by engineer. */ +"Firewall Active" = "Брандмауэр активен"; + +/* No comment provided by engineer. */ +"Firewall Not Active" = "Брандмауэр неактивен"; + +/* No comment provided by engineer. */ +"FIREWALL OFF" = "БРАНДМАУЭР ОТКЛЮЧЕН"; + +/* No comment provided by engineer. */ +"FIREWALL ON" = "БРАНДМАУЭР ВКЛЮЧЕН"; + +/* No comment provided by engineer. */ +"For maximum privacy, activate Secure Tunnel, which uses bank-level encryption to protect connections, anonymize your browsing, and hide your location and IP." = "Для максимальной конфиденциальности активируйте функцию Secure Tunnel, использующую шифрование на уровне банковских систем для защиты соединений, анонимизации ваших действий в сети и сокрытия вашего местоположения и IP-адреса."; + +/* No comment provided by engineer. */ +"France" = "Франция"; + +/* The title of a list of trackers */ +"Game Marketing" = "Маркетинг видеоигр"; + +/* The title of a list of trackers */ +"General Marketing" = "Общий маркетинг"; + +/* No comment provided by engineer. */ +"Germany" = "Германия"; + +/* The title of a list of trackers */ +"Google Shopping" = "Google Покупки"; + +/* No comment provided by engineer. */ +"Hey Lockdown Team, \nI have a question, issue, or suggestion - " = " Привет, разработчики Lockdown,\nу меня есть вопрос, проблема или предложение -"; + +/* No comment provided by engineer. */ +"Hi, my question or feedback for Lockdown is: " = " Привет, у меня есть вопрос или предложение для Lockdown:"; + +/* Used in the sentence: I blocked 500 trackers with Lockdown. */ +"I blocked" = "Я заблокировал"; + +/* Used in: You blocked tracker.com in your custom blocked domains. */ +"in your custom blocked domains." = "в настроенных вами заблокированных доменах."; + +/* No comment provided by engineer. */ +"India" = "Индия"; + +/* No comment provided by engineer. */ +"Invalid domain" = "Недопустимый домен"; + +/* No comment provided by engineer. */ +"IP: 18.132.2.87" = "IP: 18.132.2.87"; + +/* No comment provided by engineer. */ +"IP: [Anonymized]" = "IP: [скрыт]"; + +/* No comment provided by engineer. */ +"Ireland" = "Ирландия"; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"is a part of" = "является частью"; + +/* No comment provided by engineer. */ +"Japan" = "Япония"; + +/* Shows when loading the details of a subscription plan */ +"Loading Plan" = "Загрузка тарифного плана"; + +/* No comment provided by engineer. */ +"Location changed and hidden" = "Местоположение изменено и скрыто"; + +/* No comment provided by engineer. */ +"Location: 🇯🇵" = "Местоположение: 🇯🇵"; + +/* No comment provided by engineer. */ +"Location: 🇺🇸" = "Местоположение: 🇺🇸"; + +/* No comment provided by engineer. */ +"Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari." = "Lockdown Firewall блокирует вредоносные и сомнительные соединения во всех ваших приложениях, а не только в Safari."; + +/* No comment provided by engineer. */ +"Lockdown is open source and fully transparent, which means anyone can see exactly what it's doing. Also, Lockdown Firewall has a simple, strict Privacy Policy, while Lockdown VPN is fully audited by security experts." = "Lockdown - это полностью прозрачный проект с открытым исходным кодом, а это значит, что каждый может посмотреть, что именно делает наше приложение. Кроме того, у Lockdown Firewall простая и строгая политика конфиденциальности, а Lockdown VPN проверяют эксперты по безопасности."; + +/* No comment provided by engineer. */ +"Lockdown VPN is powered by Confirmed VPN, the open source, no-logs, and fully audited VPN." = "Lockdown VPN создан на базе Confirmed VPN - VPN с открытым исходным кодом, который не ведёт журналы и проверяется экспертами по безопасности."; + +/* The title of a list of trackers */ +"Marketing Trackers" = "Маркетинговые трекеры"; + +/* The title of a list of trackers */ +"Marketing Trackers II" = "Маркетинговые трекеры II"; + +/* No comment provided by engineer. */ +"Network Error" = "Ошибка сети"; + +/* No comment provided by engineer. */ +"Next" = "Далее"; + +/* No comment provided by engineer. */ +"Nice" = "Отлично"; + +/* No comment provided by engineer. */ +"No" = "Нет"; + +/* No comment provided by engineer. */ +"No Active Subscription" = "Нет активной подписки"; + +/* No comment provided by engineer. */ +"No additional information on this blocked domain found." = "Дополнительная информация о заблокированном домене не найдена."; + +/* No comment provided by engineer. */ +"No Block Lists Enabled" = "Нет включённых чёрных списков"; + +/* No comment provided by engineer. */ +"No Info Found" = "Информация не найдена"; + +/* No comment provided by engineer. */ +"No Internet Connection" = "Интернет-соединение отсутствует"; + +/* No comment provided by engineer. */ +"No stored API credentials found. Please contact team@lockdownprivacy.com about this error." = "Не найдено сохранённых учетных данных API. Сообщите об этой ошибке по адресу team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Not Blocked" = "Не заблокировано"; + +/* No comment provided by engineer. */ +"Not Now" = "Не сейчас"; + +/* No comment provided by engineer. */ +"Not Whitelisted" = "Отсутствует в белом списке"; + +/* No comment provided by engineer. */ +"Notifications: Off" = "Уведомления: откл."; + +/* No comment provided by engineer. */ +"Notifications: On" = "Уведомления: вкл."; + +/* No comment provided by engineer. */ +"OK" = "Хорошо"; + +/* No comment provided by engineer. */ +"Okay" = "Ладно"; + +/* No comment provided by engineer. */ +"one lowercase letter" = "одну букву нижнего регистра"; + +/* No comment provided by engineer. */ +"one number" = "одну цифра"; + +/* No comment provided by engineer. */ +"one symbol" = "один символ"; + +/* No comment provided by engineer. */ +"one uppercase letter" = "одну буква верхнего регистра"; + +/* No comment provided by engineer. */ +"Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol." = "Пароль должен быть не короче 8 знаков и должен содержать минимум одну букву верхнего регистра, одну букву нижнего регистра, одну цифру и один символ."; + +/* No comment provided by engineer. */ +"Plan: Annual Pro" = "Тарифный план: Pro (ежегодный)"; + +/* No comment provided by engineer. */ +"Please check your internet connection. If this persists, please contact team@lockdownprivacy.com.\n\nError Description\n" = "Проверьте интернет-соединение. Если проблема сохраняется, напишите нам на team@lockdownprivacy.com.\n\nОписание ошибки\n"; + +/* No comment provided by engineer. */ +"Please contact team@lockdownprivacy.com.\n\nError details:\n" = "Напишите нам на team@lockdownprivacy.com.\n\nПодробности об ошибке:\n"; + +/* No comment provided by engineer. */ +"Please email team@lockdownprivacy.com with the following Error Code " = " Отправьте электронное письмо на team@lockdownprivacy.com с указанием кода ошибки"; + +/* No comment provided by engineer. */ +"Please first go to your iOS Settings > Notifications > Lockdown to enable notifications" = "Для начала зайдите в настройки iOS > Уведомления > Lockdown, чтобы включить уведомления"; + +/* No comment provided by engineer. */ +"Please make sure you have added an e-mail account to your iOS device and try again." = "Убедитесь, что вы добавили учетную запись с адресом электронной почты к устройству на базе iOS, и повторите попытку."; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active and that you have an active subscription. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "Убедитесь в наличии активного интернет-соединения и активной подписки. Также вы можете воспользоваться бесплатной пробной версией либо отправить электронное письмо на team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with the following error message: " = " Убедитесь в наличии активного интернет-соединения. Если проблема сохраняется, отправьте на team@lockdownprivacy.com электронное письмо со следующим сообщением об ошибке:"; + +/* No comment provided by engineer. */ +"Please renew your subscription to activate the Secure Tunnel." = "Продлите подписку, чтобы активировать Secure Tunnel."; + +/* No comment provided by engineer. */ +"Please renew your subscription to re-activate the VPN." = "Продлите подписку, чтобы активировать VPN."; + +/* No comment provided by engineer. */ +"Please tap Block List and enable at least one block list to activate Firewall." = "Чтобы активировать брандмауэр, нажмите на «Чёрный список» и активируйте хотя бы один чёрный список."; + +/* No comment provided by engineer. */ +"Pre-configured Block Lists" = "Заранее настроенные чёрные списки"; + +/* No comment provided by engineer. */ +"Pre-configured Suggestions" = "Шаблоны"; + +/* No comment provided by engineer. */ +"Precise location exposed" = "Точное местоположение не защищено"; + +/* No comment provided by engineer. */ +"Press & Media" = "Пресса и СМИ"; + +/* No comment provided by engineer. */ +"Privacy Policy" = "Политика конфиденциальности"; + +/* The title of a list of trackers */ +"Ransomware" = "Программы-вымогатели"; + +/* No comment provided by engineer. */ +"Re-send" = "Отправить повторно"; + +/* No comment provided by engineer. */ +"Readable browsing and data" = "Считываемые данные и история действий в сети"; + +/* No comment provided by engineer. */ +"Remember to check our FAQs first, for answers to the most frequently asked questions.\n\nIf it's not answered there, we're happy to provide support and take feedback by email." = "Не забудьте сначала проверить раздел с ответами на самые часто задаваемые вопросы.\n\nЕсли вы не нашли там ответа, мы с радостью поможем вам и примем обратную связь по электронной почте."; + +/* The title of a list of trackers */ +"Reporting" = "Отправка отчёта"; + +/* No comment provided by engineer. */ +"Secure Tunnel VPN" = "Secure Tunnel VPN"; + +/* No comment provided by engineer. */ +"See live metrics for how many bad connections Firewall has blocked." = "Посмотрите обновляемую в реальном времени статистику о количестве заблокированных брандмауэром вредоносных соединений."; + +/* No comment provided by engineer. */ +"Settings" = "Настройки"; + +/* No comment provided by engineer. */ +"Share your anonymized metrics and show other people how to block invasive tracking." = "Отправьте анонимную статистику и покажите другим людям, как блокировать назойливые трекеры."; + +/* No comment provided by engineer. */ +"Share Your Stats" = "Отправить статистику"; + +/* No comment provided by engineer. */ +"SHOW" = "ПОКАЗАТЬ"; + +/* No comment provided by engineer. */ +"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." = "Покажите, насколько назойливы современные приложения, и помогите другим людям блокировать трекеры и вредоносное ПО.\n\nВаш журнал блокировок показан не будет - только приведённое выше изображение. Далее вы можете выбрать, где вы хотите поделиться статистикой."; + +/* No comment provided by engineer. */ +"Sign Out" = "Выйти"; + +/* No comment provided by engineer. */ +"Sign Out?" = "Выйти?"; + +/* No comment provided by engineer. */ +"Sign Up | Sign In" = "Зарегистрироваться | Войти"; + +/* No comment provided by engineer. */ +"Sign up below to unlock benefits of a Lockdown account." = "Зарегистрируйтесь ниже, чтобы открыть для себя все преимущества учётной записи Lockdown."; + +/* No comment provided by engineer. */ +"Signed In" = "Осуществлён вход"; + +/* No comment provided by engineer. */ +"Signed out successfully." = "Выход завершен успешно"; + +/* No comment provided by engineer. */ +"Singapore" = "Сингапур"; + +/* No comment provided by engineer. */ +"Skip Trial" = "Пропустить пробную версию"; + +/* The title of a list of trackers */ +"Snapchat Trackers" = "Трекеры Snapchat"; + +/* No comment provided by engineer. */ +"South Korea" = "Южная Корея"; + +/* No comment provided by engineer. */ +"Stay Protected" = "Оставайтесь под защитой"; + +/* No comment provided by engineer. */ +"Subscription Expired" = "Подписка истекла"; + +/* No comment provided by engineer. */ +"Success" = "Успешно"; + +/* No comment provided by engineer. */ +"Success!" = "Успешно!"; + +/* No comment provided by engineer. */ +"Successfully re-sent your email confirmation to " = "Электронное письмо с подтверждением успешно отправлено по адресу "; + +/* No comment provided by engineer. */ +"Support | Feedback" = "Поддержка | Обратная связь"; + +/* No comment provided by engineer. */ +"Tap to activate Lockdown Firewall and update to the newest block lists." = "Нажмите, чтобы активировать Lockdown Firewall и загрузить обновленные чёрные списки."; + +/* No comment provided by engineer. */ +"Tap to see a demo of how Secure Tunnel protects and anonymizes you." = "Нажмите, чтобы посмотреть, как Secure Tunnel защищает вас и сохраняет вашу анонимность."; + +/* No comment provided by engineer. */ +"Tap to see the blocked tracking attempts." = "Нажмите, чтобы посмотреть заблокированные попытки отслеживания."; + +/* Used in the paragraph: You've just blocked 100 tracking attempts! Tap to see what they are. */ +"Tap to see what they are." = "Нажмите, чтобы узнать подробности."; + +/* No comment provided by engineer. */ +"Terms" = "Условия"; + +/* No comment provided by engineer. */ +"Thanks for helping to increase privacy and tracking awareness." = "Спасибо, что сообщаете нам о конфиденциальности и устройствах отслеживания."; + +/* No comment provided by engineer. */ +"The block log can be manually cleared or disabled. Disabling the Block Log only disables the log of connections - the number of tracking attempts will still be displayed." = "Вы можете вручную очистить или отключить журнал блокировок. Отключение журнала блокировок отключает только журнал соединений. Количество попыток отслеживания по-прежнему будет отображаться."; + +/* Used in To complete your signup, click the confirmation link we sent to you@gmail.com */ +"To complete your signup, click the confirmation link we sent to" = "Чтобы завершить регистрацию, нажмите на ссылку-подтверждение, отправленную по адресу"; + +/* No comment provided by engineer. */ +"To finish signup, click the confirmation link in the email we just sent. If you don't see it, check if it's stuck in your spam folder." = "Чтобы завершить регистрацию, нажмите на ссылку-подтверждение в отправленном вам электронном письме. Если вы не можете его найти, проверьте папку «Спам»."; + +/* No comment provided by engineer. */ +"To see this tutorial again, open the Account tab." = "Чтобы ещё раз ознакомиться с этим руководством, откройте вкладку «Учётная запись»."; + +/* No comment provided by engineer. */ +"To: joe@email.com\nRe: Q4 2019 Finance Review" = "Кому: joe@email.com\nТема: Финансовый отчёт за 4-й квартал 2019 г."; + +/* No comment provided by engineer. */ +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "Теперь трекеры, реклама и прочие вредоносные скрипты заблокированы во всех ваших приложениях, а не только в Safari.\n\nДля максимальной конфиденциальности воспользуйтесь Secure Tunnel. Это защитит ваши соединения, сохранит вашу анонимность в сети и скроет ваше местоположение."; + +/* Used in the sentence: You blocked 500 tracking attempts this week. Tap to update to the newsst block lists. */ +"tracking attempts this week. Tap to update to the newest block lists." = "попыток отслеживания на этой неделе. Нажмите, чтобы получить обновленные чёрные списки."; + +/* No comment provided by engineer. */ +"Tunnel" = "Туннель"; + +/* No comment provided by engineer. */ +"Tunnel Off" = "Туннель отключен"; + +/* No comment provided by engineer. */ +"Tunnel On" = "Туннель включен"; + +/* No comment provided by engineer. */ +"Tutorial" = "Руководство"; + +/* No comment provided by engineer. */ +"Unique IP address broadcasted" = "Уникальный IP-адрес общедоступен"; + +/* No comment provided by engineer. */ +"United Kingdom" = "Великобритания"; + +/* No comment provided by engineer. */ +"United States - East" = "США (Восток)"; + +/* No comment provided by engineer. */ +"United States - West" = "США (Запад)"; + +/* No comment provided by engineer. */ +"Update Success" = "Обновление успешно"; + +/* No comment provided by engineer. */ +"Updating Block Lists" = "Обновление чёрных списков"; + +/* No comment provided by engineer. */ +"USA East" = "США (Восток)"; + +/* No comment provided by engineer. */ +"USA West" = "США (Запад)"; + +/* No comment provided by engineer. */ +"View FAQs" = "Посмотреть раздел «Вопросы и ответы»"; + +/* No comment provided by engineer. */ +"View or Upgrade Plan" = "Просмотреть или улучшить тарифный план"; + +/* No comment provided by engineer. */ +"View Upgrade Options" = "Посмотреть возможные улучшения"; + +/* No comment provided by engineer. */ +"VPN Active" = "VPN включен"; + +/* No comment provided by engineer. */ +"VPN Not Active" = "VPN отключен"; + +/* No comment provided by engineer. */ +"VPN Subscription Expired" = "Подписка на VPN истекла"; + +/* No comment provided by engineer. */ +"Website" = "Сайт"; + +/* No comment provided by engineer. */ +"Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue." = "Добро пожаловать в руководство по Lockdown.\n\nЧтобы продолжить, нажмите в любом месте."; + +/* No comment provided by engineer. */ +"What is VPN?" = "Что такое VPN?"; + +/* No comment provided by engineer. */ +"Whitelisted" = "В белом списке"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown" = "Почему Lockdown можно доверять"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown?" = "Почему Lockdown можно доверять?"; + +/* No comment provided by engineer. */ +"Why Use VPN?" = "Зачем пользоваться VPN?"; + +/* Used in the sentence: You blocked 500 tracking attempts this week. + Used in: You blocked tracker.com in your custom blocked domains. */ +"You blocked" = "Вы заблокировали"; + +/* No comment provided by engineer. */ +"You blocked this domain in your custom blocked domains." = "Вы заблокировали этот домен в заданном вами списке заблокированных доменов."; + +/* No comment provided by engineer. */ +"You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)" = "Если позже вы захотите поделиться с другими, нажмите здесь.\n(Чтобы пропустить, нажмите в любом месте)"; + +/* No comment provided by engineer. */ +"You'll be signed out from this account." = "Вы выйдете из этой учётной записи."; + +/* No comment provided by engineer. */ +"You'll have to reenable it later here to start seeing blocked entries again." = "Чтобы снова видеть заблокированные объекты, вам необходимо будет заново активировать эту функцию."; + +/* No comment provided by engineer. */ +"You've just blocked 100 tracking attempts!" = "Вы только что заблокировали сотую попытку отслеживания!"; + +/* No comment provided by engineer. */ +"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." = "Ваша учётная запись подтверждена, и вход в неё осуществлен. Вы получите доступ к новым чёрным спискам и Lockdown Mac, а также сможете получать важные оповещения."; + +/* No comment provided by engineer. */ +"Your Settings" = "Ваши настройки"; + +/* No comment provided by engineer. */ +"⚠️ Not Signed In" = "⚠️ Вы не вошли в систему"; + +/* No comment provided by engineer. */ +"🔥🧱 Firewall Activated 🎊🎉" = "🔥🧱 Брандмауэр активирован 🎊🎉"; + +/* No comment provided by engineer. */ +"Error while trying to confirm your account:" = "Ошибка при попытке подтвердить учётную запись:"; + +/* No comment provided by engineer. */ +"If this persists, please contact team@lockdownprivacy.com." = "Если проблема сохраняется, напишите нам на team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"Add a domain to block" = "Заблокировать домен"; + +/* No comment provided by engineer. */ +"Block all your apps from connecting to the domains and sites below. For your convenience, Lockdown also has pre-configured suggestions." = "Запретите вашим приложениям устанавливать соединения с указанными ниже доменами и сайтами. Для вашего удобства Lockdown также предлагает заранее подготовленные шаблоны."; + +/* No comment provided by engineer. */ +"Block Lists" = "Чёрные списки"; + +/* No comment provided by engineer. */ +"Configure Blocking" = "Настроить блокировку"; + +/* No comment provided by engineer. */ +"Custom" = "Выборочно"; + +/* No comment provided by engineer. */ +"SAVE" = "СОХРАНИТЬ"; + +/* No comment provided by engineer. */ +"Access to cloud service information is not allowed" = "Доступ к информации облачного сервиса запрещён"; + +/* No comment provided by engineer. */ +"Could not connect to the network" = "Не удаётся подключиться к сети"; + +/* No comment provided by engineer. */ +"Not allowed to make the payment" = "Платеж запрещён"; + +/* No comment provided by engineer. */ +"Payment was cancelled" = "Платеж отменён"; + +/* No comment provided by engineer. */ +"The device is not allowed to make the payment" = "Невозможно совершить платёж с этого устройства"; + +/* No comment provided by engineer. */ +"The product is not available in the current storefront" = "Продукт не доступен в этом магазине"; + +/* No comment provided by engineer. */ +"The purchase identifier was invalid" = "Недопустимый идентификатор покупки"; + +/* No comment provided by engineer. */ +"Unknown error. Please contact support at team@lockdownprivacy.com." = "Неизвестная ошибка. Свяжитесь с нами по адресу team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"User has revoked permission to use this cloud service" = "Пользователь отозвал разрешение на использование данного облачного сервиса"; + +/* No comment provided by engineer. */ +"Please that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "Убедитесь в наличии у вас активной подписки. Если вы хотите пользоваться подпиской вместе с кем-то ещё, используя одну и ту же учётную запись, вам необходимо войти с того же самого адреса электронной почты. Либо воспользуйтесь бесплатной пробной версией или напишите нам на team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "Покупки в приложении отключены. Вам необходимо разрешить их. Настройки -> Экранное время -> Ограничения -> App Store -> Покупки в приложении. После этого повторите попытку."; + +/* No comment provided by engineer. */ +"Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "Платеж запрещён.\nЛибо с этого устройства нельзя сделать платеж, либо у вас отключены покупки в приложении. Вам необходимо разрешить их. Настройки -> Экранное время -> Ограничения -> App Store -> Покупки в приложении. После этого повторите попытку."; + +/* No comment provided by engineer. */ +"Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com" = "Убедитесь в наличии у вас активной подписки. Если вы хотите пользоваться подпиской вместе с кем-то ещё, используя одну и ту же учётную запись, вам необходимо войти с того же самого адреса электронной почты. Либо воспользуйтесь бесплатной пробной версией или напишите нам на team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Upgrade Plan" = "Улучшить тарифный план"; + +/* The title of a list of trackers */ +"Amazon Trackers" = "Трекеры Amazon"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "Простота и эффективность"; +"onboarding_lockdown_stops_trackers" = "Lockdown останавливает попытки отслеживания, вредоносные программы и недоброжелателей, заинтересованных в ваших данных."; +"onboarding_worlds_first_on_device" = "Первый в мире брандмауэр для мобильных устройств"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "Брандмауэр с открытым исходным кодом и VPN, который работает на ВСЕХ ваших устройствах и веб-страницах."; +"onboarding_staying_protected_is_easy_with_lockdown" = "Обезопасить себя с помощью Lockdown очень легко"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "Сохраняйте анонимность благодаря Secure Tunnel VPN"; +"onboarding_no_logs_fast_and_fully_audited" = "VPN, который не ведет журнал вашей активности, действует быстро и ставит конфиденциальность на первое место. Единственный VPN, который соблюдает политику конфиденциальности и прошел проверку экспертами по сетевой безопасности."; +"onboarding_take_control_of_who_you_block" = "Вы сами решаете, кого\nвнести в черный список"; +"onboarding_when_using_our_intuitive_blocklists" = "При использовании наших интуитивно понятных инструментов"; +"onboarding_stay_protected" = "Оставайтесь под защитой!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "Получайте еженедельные отчеты и обновления для черного списка"; +"onboarding_over_1_billion_trackers_blocked" = "Заблокировано более 1 миллиарда попыток отслеживания"; +"onboarding_used_by_more_than_1_million" = "Нашей программой пользуется более 1 миллиона людей, ценящих свою конфиденциальность"; +"onboarding_sign_up" = "Зарегистрироваться"; +"onboarding_login" = "Войти"; +"continue" = "Продолжить"; +"plan_annual" = "Годовой"; +"plan_monthly" = "Месячный"; + +// MARK: - SignUpViewController +"welcome" = "Добро пожаловать"; +"already_have_account_login" = "Уже есть учетная запись? Войдите"; +"sign_up_to_access_new_block_lists_from_trackers" = "Зарегистрируйтесь, чтобы получить доступ к новым спискам"; +"already_have_account_login_below" = "У вас уже есть учетная запись Lockdown?\nВойдите."; +"forgot_password" = "Забыли пароль?"; +"email" = "Email"; +"password" = "Пароль"; +"access_our_curated_block_lists_and_build_your_own" = "Получите доступ к нашим черным спискам и составьте собственный"; +"access_lockdown_across_all_your_devices" = "Используйте Lockdown на всех ваших устройствах"; +"firewall_and_vpn_support" = "Брандмауэр и поддержка VPN"; +"by_continuing_you_agree_with_our" = "Продолжив, вы выражаете согласие со следующими документами:"; +"and" = "и"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "Всегда будьте в курсе событий"; +"once_a_week_helpful_reminders" = "Еженедельные напоминания о том, как можно улучшить свою безопасность."; +"enable_notifications" = "Включить уведомления"; +"maybe_later" = "В другой раз"; + +// MARK: - Previously Unlocalized +"check_fields" = "Проверьте поля"; +"email_and_password_must_not_be_empty" = "Поля «E-mail» и «Пароль» не могут быть пустыми"; +"Success! 🎉" = "Успех! 🎉"; +"error_signing_in" = "Ошибка при входе"; +"confirm_your_email" = "Подтвердите E-mail"; +"you_have_successfully_sign_in" = "Вы благополучно вошли в систему."; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "Мне удалось заблокировать %@ трекеров, рекламных объявлений и вредоносного ПО благодаря Lockdown, брандмауэру, который блокирует нежелательные соединения во всех ваших приложениях.\nСкачайте его бесплатно на сайте lockdownprivacy.com."; +"check_email" = "Проверьте почту"; +"we_have_sent_a_reset_password_email" = "Мы отправили вам письмо, которое позволит сбросить пароль. На всякий случай не забудьте заглянуть в папку для спама."; +"since_date" = "С %@"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "Начните пробный период\nи получите полный доступ"; +"restore_purchase" = "Восстановить покупки"; +"terms_of_service" = "Пользовательское соглашение"; +"CHOOSE_YOUR_PLAN" = "ВЫБЕРИТЕ ТАРИФНЫЙ ПЛАН"; +"skip" = "Пропустить"; +"save_annual_vpn_subscription" = "Экономия ~50%"; +"save_annual_pro_subscription" = "Экономия ~30%"; +"current_plan" = "Текущий тариф"; +"feature_table_column" = "Функция"; +"feature_level_table_column" = "Бесплатно"; +"stops_ads_and_hidden_trackers" = "Останавливает рекламу и скрытые попытки отслеживания"; +"blocks_badware_in_all_your_apps" = "Блокирует вредоносные скрипты во всех приложениях"; +"access_our_curated_block_lists" = "Включает настроенные нами черные списки"; +"build_your_custom_block_lists" = "Позволяет создать собственный черный список"; +"stops_browsing_history_tracking" = "Скрывает историю посещенных страниц"; +"anonymizing_your_browsing" = "Позволяет анонимный просмотр веб-страниц"; +"hides_your_location_and_ip" = "Скрывает ваше местонахождение и IP-адрес"; +"lets_you_change_your_ip_to_other_region" = "Позволяет выдать ваш IP\nза адрес другого региона"; +"app_for_iphone_plus_ipad" = "Приложение для iPhone + iPad"; +"app_for_mac" = "Приложение для Mac"; +"year_after_slash" = "год"; +"month_after_slash" = "месяц"; + +// MARK: - Delete My Account +"delete_account" = "Удалить учетную запись"; +"delete_my_account" = "Удалить мою учетную запись"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "Выполнив это действие, вы подадите запрос на удаление своей учетной записи, а также всех связанных с ней личных данных. После удаления учетной записи вы будете уведомлены об этих изменениях по электронной почте.\n\nПожалуйста, имейте в виду, что вы потеряете все настройки, включая приобретенные ранее услуги, и не сможете повторно создать учетную запись с тем же адресом электронной почты."; +"proceed" = "Продолжить"; + +// MARK: - EmailValidatable +"invalid_email_address" = "Недействительный email-адрес"; +"please_fill_in_your_email" = "Укажите ваш email-адрес"; +"you_entered_con_instead_of_com" = "Вы ввели .con вместо .com."; +"no_valid_email_address_detected" = "Email-адрес не обнаружен."; +"too_many_email_addresses_entered" = "Слишком много email-адресов."; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "мин."; +"seconds_genitive" = "сек."; +"lockdowns_first_christmas_sale" = "Первая рождественская\nраспродажа Lockdown"; +"subscribe_now" = "Оформить подписку"; + +// MARK: - Code Redemption +"redeem_code" = "Использовать код"; +"cannot_redeem_code" = "Не удалось использовать код"; +"the_code_could_not_be_redeemed" = "The code could not be redeemed."; +"congratulations_excl" = "Поздравляем!"; +"the_code_was_successfully_redeemed" = "Код использован. Возможно, придется перезапустить приложение, чтобы покупка активировалась."; diff --git a/vi.lproj/Localizable.strings b/vi.lproj/Localizable.strings new file mode 100644 index 0000000..fd1b448 --- /dev/null +++ b/vi.lproj/Localizable.strings @@ -0,0 +1,809 @@ +/* Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com */ +" and " = " và "; + +/* refers to number of connections blocked today, as in '34 Blocked Today' */ +" Blocked Today" = " Đã chặn hôm nay"; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to add to a whitelist. For example, \"google.com\" without \"https://\"" = " không phải là mục nhập hợp lệ. Vui lòng chỉ nhập máy chủ của miền bạn muốn thêm vào danh sách trắng. Ví dụ: \"google.com\" mà không có \"https://\""; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to block. For example, \"google.com\" without \"https://\"" = " không phải là mục nhập hợp lệ. Vui lòng chỉ nhập máy chủ của miền bạn muốn chặn. Ví dụ: \"google.com\" mà không có \"https://\""; + +/* No comment provided by engineer. */ +" Off" = " Tắt"; + +/* No comment provided by engineer. */ +" On" = " Bật"; + +/* Used in the sentence: I blocked 500 trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com. */ +" trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com." = " công cụ theo dõi, quảng cáo và phần mềm độc hại với Lockdown, tường lửa chặn các kết nối không mong muốn trong tất cả các ứng dụng của bạn. Tải về miễn phí tại lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"1 Week Free" = "1 Tuần Miễn Phí"; + +/* No comment provided by engineer. */ +"\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). You can also set custom domains to block." = "\"Danh sách chặn\" cho phép bạn chọn những gì bạn muốn chặn (ví dụ: Facebook, clickbait, v.v.). Bạn cũng có thể đặt các miền tùy chỉnh để chặn."; + +/* No comment provided by engineer. */ +"\"iOS Annual\" is your current plan" = "\"iOS Hàng năm\" là gói hiện tại của bạn"; + +/* No comment provided by engineer. */ +"\"iOS Monthly\" is your current plan" = "\"iOS Hàng tháng\" là gói hiện tại của bạn"; + +/* No comment provided by engineer. */ +"\"Pro Annual\" is your current plan" = "\"Pro Hàng năm\" là gói hiện tại của bạn"; + +/* No comment provided by engineer. */ +"\"Pro Monthly\" is your current plan" = "\"Pro Hàng tháng\" là gói hiện tại của bạn"; + +/* No comment provided by engineer. */ +"\"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." = "\"Xem Nhật ký\" hiển thị chính xác những kết nối đã bị chặn trong ngày hôm qua. Nhật ký này được xóa vào lúc nửa đêm và lưu trong thiết bị, vì vậy nhật ký chỉ hiển thị với bạn."; + +/* No comment provided by engineer. */ +"\n\n If this error persists, please contact team@lockdownprivacy.com." = "\n\n Nếu lỗi này vẫn tiếp diễn, vui lòng liên hệ với team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"\n\n If this persists, please contact team@lockdownprivacy.com." = "\n\n Nếu điều này vẫn tiếp diễn, vui lòng liên hệ team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"About Lockdown VPN" = "Giới thiệu về Lockdown VPN"; + +/* No comment provided by engineer. */ +"Activate Firewall with this button." = "Kích hoạt Tường lửa bằng nút này."; + +/* No comment provided by engineer. */ +"Activating" = "Đang kích hoạt"; + +/* No comment provided by engineer. */ +"Anonymize IP against trackers" = "Ẩn danh IP đối với công cụ theo dõi"; + +/* No comment provided by engineer. */ +"Apple Outage" = "Apple ngừng hoạt động"; + +/* No comment provided by engineer. */ +"at least 8 characters" = "tối thiểu 8 ký tự"; + +/* No comment provided by engineer. */ +"Audit Reports" = "Báo cáo Kiểm toán"; + +/* No comment provided by engineer. */ +"Australia" = "Úc"; + +/* No comment provided by engineer. */ +"Be sure to check your spam folder in case it got stuck there.\n\nYou can also request a re-send of the confirmation." = "Hãy nhớ kiểm tra thư mục thư rác của bạn để đề phòng trường hợp nó bị kẹt ở đó.\n\nBạn cũng có thể yêu cầu gửi lại xác nhận."; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"block list" = "danh sách chặn"; + +/* No comment provided by engineer. */ +"Blocked" = "Bị chặn"; + +/* No comment provided by engineer. */ +"Blocked Domains" = "Miền bị chặn"; + +/* No comment provided by engineer. */ +"Blocked Today" = "Đã chặn hôm nay"; + +/* No comment provided by engineer. */ +"Blocked Trackers Summary" = "Tóm tắt Công cụ theo dõi bị chặn"; + +/* No comment provided by engineer. */ +"Brazil" = "Brazil"; + +/* No comment provided by engineer. */ +"Canada" = "Canada"; + +/* No comment provided by engineer. */ +"Cancel" = "Hủy"; + +/* No comment provided by engineer. */ +"Check Your Inbox" = "Kiểm tra Hộp thư"; + +/* No comment provided by engineer. */ +"Clear Block Log" = "Xóa Nhật ký chặn"; + +/* The title of a list of trackers */ +"Clickbait" = "Clickbait"; + +/* No comment provided by engineer. */ +"Confirm Email" = "Xác nhận Email"; + +/* No comment provided by engineer. */ +"Confirmed Site" = "Trang đã xác nhận"; + +/* No comment provided by engineer. */ +"Couldn't Find Your Email Client" = "Không tìm thấy ứng dụng email của bạn"; + +/* The title of a list of trackers */ +"Crypto Mining" = "Khai thác tiền điện tử"; + +/* No comment provided by engineer. */ +"Custom Settings (Advanced)" = "Cài đặt tùy chỉnh (Nâng cao)"; + +/* The title of a list of trackers */ +"Data Trackers" = "Công cụ theo dõi dữ liệu"; + +/* No comment provided by engineer. */ +"Deactivating" = "Hủy kích hoạt"; + +/* No comment provided by engineer. */ +"Disable" = "Tắt"; + +/* No comment provided by engineer. */ +"Disable Block Log" = "Tắt Nhật ký chặn"; + +/* No comment provided by engineer. */ +"Disable Block Log?" = "Tắt Nhật ký chặn?"; + +/* No comment provided by engineer. */ +"Done" = "Xong"; + +/* No comment provided by engineer. */ +"Email Support" = "Hỗ trợ Email"; + +/* The title of a list of trackers */ +"Email Trackers" = "Công cụ theo dõi Email"; + +/* No comment provided by engineer. */ +"Email Us" = "Email cho chúng tôi"; + +/* No comment provided by engineer. */ +"Enable" = "Bật"; + +/* No comment provided by engineer. */ +"Enable Block Log" = "Bật Nhật ký chặn"; + +/* No comment provided by engineer. */ +"Enable notifications to get a once-a-week summary and the latest block list updates. You can disable this anytime." = "Bật thông báo để nhận tóm tắt mỗi tuần một lần và cập nhật danh sách chặn mới nhất. Bạn có thể tắt tính năng này bất cứ lúc nào."; + +/* No comment provided by engineer. */ +"Encrypted, private connections" = "Kết nối riêng tư được mã hóa"; + +/* No comment provided by engineer. */ +"Error Code " = "Mã lỗi "; + +/* No comment provided by engineer. */ +"Error Creating Email Account" = "Lỗi khi tạo tài khoản email"; + +/* No comment provided by engineer. */ +"Error Loading Plan: Retry" = "Lỗi khi tải gói: Thử lại"; + +/* No comment provided by engineer. */ +"Error Re-sending Email Confirmation" = "Lỗi khi gửi lại xác nhận email"; + +/* No comment provided by engineer. */ +"Error Restoring Subscription" = "Lỗi khi khôi phục đăng ký"; + +/* No comment provided by engineer. */ +"Error Signing In To Verify Subscription" = "Lỗi khi đăng nhập để xác minh đăng ký"; + +/* No comment provided by engineer. */ +"Error Starting Trial" = "Lỗi khi bắt đầu dùng thử"; + +/* The title of a list of trackers */ +"Facebook & WhatsApp" = "Facebook & WhatsApp"; + +/* The title of a list of trackers */ +"Facebook Trackers" = "Công cụ theo dõi Facebook"; + +/* No comment provided by engineer. */ +"Failed to re-send email confirmation." = "Không gửi lại được email xác nhận."; + +/* No comment provided by engineer. */ +"FAQs" = "Câu hỏi thường gặp"; + +/* No comment provided by engineer. */ +"Firewall" = "Tường lửa"; + +/* No comment provided by engineer. */ +"Firewall Active" = "Tường lửa đang hoạt động"; + +/* No comment provided by engineer. */ +"Firewall Not Active" = "Tường lửa không hoạt động"; + +/* No comment provided by engineer. */ +"FIREWALL OFF" = "TƯỜNG LỬA ĐANG TẮT"; + +/* No comment provided by engineer. */ +"FIREWALL ON" = "TƯỜNG LỬA ĐANG BẬT"; + +/* No comment provided by engineer. */ +"For maximum privacy, activate Secure Tunnel, which uses bank-level encryption to protect connections, anonymize your browsing, and hide your location and IP." = "Để bảo đảm tối đa quyền riêng tư, hãy kích hoạt Secure Tunnel, sử dụng mã hóa cấp độ ngân hàng để bảo vệ các kết nối, ẩn danh hoạt động duyệt web cũng như ẩn vị trí và IP của bạn."; + +/* No comment provided by engineer. */ +"France" = "Pháp"; + +/* The title of a list of trackers */ +"Game Marketing" = "Tiếp thị trò chơi"; + +/* The title of a list of trackers */ +"General Marketing" = "Tiếp thị chung"; + +/* No comment provided by engineer. */ +"Germany" = "Đức"; + +/* The title of a list of trackers */ +"Google Shopping" = "Google Shopping"; + +/* No comment provided by engineer. */ +"Hey Lockdown Team, \nI have a question, issue, or suggestion - " = "Xin chào Đội ngũ Lockdown, \nTôi có một câu hỏi, vấn đề hoặc đề xuất - "; + +/* No comment provided by engineer. */ +"Hi, my question or feedback for Lockdown is: " = "Xin chào, câu hỏi hoặc phản hồi của tôi cho Lockdown là: "; + +/* Used in the sentence: I blocked 500 trackers with Lockdown. */ +"I blocked" = "Tôi đã chặn"; + +/* Used in: You blocked tracker.com in your custom blocked domains. */ +"in your custom blocked domains." = "trong các miền bị chặn tùy chỉnh của bạn."; + +/* No comment provided by engineer. */ +"India" = "Ấn Độ"; + +/* No comment provided by engineer. */ +"Invalid domain" = "Miền không hợp lệ"; + +/* No comment provided by engineer. */ +"IP: 18.132.2.87" = "IP: 18.132.2.87"; + +/* No comment provided by engineer. */ +"IP: [Anonymized]" = "IP: [Ẩn danh]"; + +/* No comment provided by engineer. */ +"Ireland" = "Ireland"; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"is a part of" = "là một phần của"; + +/* No comment provided by engineer. */ +"Japan" = "Nhật Bản"; + +/* Shows when loading the details of a subscription plan */ +"Loading Plan" = "Đang tải gói"; + +/* No comment provided by engineer. */ +"Location changed and hidden" = "Đã thay đổi và ẩn vị trí"; + +/* No comment provided by engineer. */ +"Location: 🇯🇵" = "Vị trí: 🇯🇵"; + +/* No comment provided by engineer. */ +"Location: 🇺🇸" = "Vị trí: 🇺🇸"; + +/* No comment provided by engineer. */ +"Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari." = "Lockdown Firewall chặn các kết nối xấu và không đáng tin cậy trong tất cả các ứng dụng của bạn - không chỉ Safari."; + +/* No comment provided by engineer. */ +"Lockdown is open source and fully transparent, which means anyone can see exactly what it's doing. Also, Lockdown Firewall has a simple, strict Privacy Policy, while Lockdown VPN is fully audited by security experts." = "Lockdown sử dụng mã nguồn mở và hoàn toàn minh bạch, có nghĩa là mọi người đều có thể thấy chính xác những gì nó đang làm. Ngoài ra, Lockdown Firewall có Chính sách Quyền riêng tư đơn giản, nghiêm ngặt, và Lockdown VPN đã được các chuyên gia bảo mật kiểm tra đầy đủ."; + +/* No comment provided by engineer. */ +"Lockdown VPN is powered by Confirmed VPN, the open source, no-logs, and fully audited VPN." = "Lockdown VPN được cung cấp bởi Confirmed VPN, một VPN mã nguồn mở, không lưu nhật ký và đã được kiểm tra đầy đủ."; + +/* The title of a list of trackers */ +"Marketing Trackers" = "Công cụ theo dõi tiếp thị"; + +/* The title of a list of trackers */ +"Marketing Trackers II" = "Công cụ theo dõi tiếp thị II"; + +/* No comment provided by engineer. */ +"Network Error" = "Lỗi mạng"; + +/* No comment provided by engineer. */ +"Next" = "Tiếp theo"; + +/* No comment provided by engineer. */ +"Nice" = "Tốt"; + +/* No comment provided by engineer. */ +"No" = "Không"; + +/* No comment provided by engineer. */ +"No Active Subscription" = "Không có Đăng ký đang hoạt động"; + +/* No comment provided by engineer. */ +"No additional information on this blocked domain found." = "Không tìm thấy thông tin bổ sung nào về miền bị chặn này."; + +/* No comment provided by engineer. */ +"No Block Lists Enabled" = "Không có Danh sách chặn nào được kích hoạt"; + +/* No comment provided by engineer. */ +"No Info Found" = "Không tìm thấy thông tin"; + +/* No comment provided by engineer. */ +"No Internet Connection" = "Không có kết nối Internet"; + +/* No comment provided by engineer. */ +"No stored API credentials found. Please contact team@lockdownprivacy.com about this error." = "Không tìm thấy thông tin đăng nhập API nào được lưu trữ. Vui lòng liên hệ với team@lockdownprivacy.com về lỗi này."; + +/* No comment provided by engineer. */ +"Not Blocked" = "Không bị chặn"; + +/* No comment provided by engineer. */ +"Not Now" = "Lúc khác"; + +/* No comment provided by engineer. */ +"Not Whitelisted" = "Không có trong Danh sách trắng"; + +/* No comment provided by engineer. */ +"Notifications: Off" = "Thông báo: Tắt"; + +/* No comment provided by engineer. */ +"Notifications: On" = "Thông báo: Bật"; + +/* No comment provided by engineer. */ +"OK" = "OK"; + +/* No comment provided by engineer. */ +"Okay" = "OK"; + +/* No comment provided by engineer. */ +"one lowercase letter" = "một chữ cái thường"; + +/* No comment provided by engineer. */ +"one number" = "một số"; + +/* No comment provided by engineer. */ +"one symbol" = "một ký hiệu"; + +/* No comment provided by engineer. */ +"one uppercase letter" = "một chữ cái viết hoa"; + +/* No comment provided by engineer. */ +"Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol." = "Mật khẩu phải có tối thiểu 8 ký tự, chứa ít nhất một chữ cái viết hoa, một chữ thường, một số và một ký hiệu."; + +/* No comment provided by engineer. */ +"Plan: Annual Pro" = "Gói: Pro Hàng năm"; + +/* No comment provided by engineer. */ +"Please check your internet connection. If this persists, please contact team@lockdownprivacy.com.\n\nError Description\n" = "Vui lòng kiểm tra kết nối Internet của bạn. Nếu điều này vẫn tiếp diễn, vui lòng liên hệ với team@lockdownprivacy.com.\n\nMô tả lỗi\n"; + +/* No comment provided by engineer. */ +"Please contact team@lockdownprivacy.com.\n\nError details:\n" = "Vui lòng liên hệ với team@lockdownprivacy.com.\n\nChi tiết lỗi:\n"; + +/* No comment provided by engineer. */ +"Please email team@lockdownprivacy.com with the following Error Code " = "Vui lòng gửi email đến team@lockdownprivacy.com với Mã lỗi sau "; + +/* No comment provided by engineer. */ +"Please first go to your iOS Settings > Notifications > Lockdown to enable notifications" = "Trước tiên, vui lòng đi đến Cài đặt iOS > Thông báo > Lockdown để bật thông báo"; + +/* No comment provided by engineer. */ +"Please make sure you have added an e-mail account to your iOS device and try again." = "Hãy bảo đảm rằng bạn đã thêm tài khoản e-mail vào thiết bị iOS của mình và thử lại."; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active and that you have an active subscription. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "Hãy bảo đảm rằng kết nối Internet của bạn đang hoạt động và bạn có đăng ký đang hoạt động. Nếu không, vui lòng bắt đầu bản dùng thử miễn phí hoặc gửi e-mail đến team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with the following error message: " = "Hãy bảo đảm rằng kết nối Internet của bạn đang hoạt động. Nếu lỗi này vẫn tiếp diễn, hãy gửi email tới team@lockdownprivacy.com với thông báo lỗi sau: "; + +/* No comment provided by engineer. */ +"Please renew your subscription to activate the Secure Tunnel." = "Vui lòng gia hạn đăng ký để kích hoạt Secure Tunnel."; + +/* No comment provided by engineer. */ +"Please renew your subscription to re-activate the VPN." = "Vui lòng gia hạn đăng ký để kích hoạt lại VPN."; + +/* No comment provided by engineer. */ +"Please tap Block List and enable at least one block list to activate Firewall." = "Vui lòng nhấn Danh sách chặn và bật ít nhất một danh sách chặn để kích hoạt Tường lửa."; + +/* No comment provided by engineer. */ +"Pre-configured Block Lists" = "Danh sách chặn được định cấu hình trước"; + +/* No comment provided by engineer. */ +"Pre-configured Suggestions" = "Đề xuất được định cấu hình trước"; + +/* No comment provided by engineer. */ +"Precise location exposed" = "Đã để lộ vị trí chính xác"; + +/* No comment provided by engineer. */ +"Press & Media" = "Báo chí & Truyền thông"; + +/* No comment provided by engineer. */ +"Privacy Policy" = "Chính sách Quyền riêng tư"; + +/* The title of a list of trackers */ +"Ransomware" = "Phần mềm tống tiền"; + +/* No comment provided by engineer. */ +"Re-send" = "Gửi lại"; + +/* No comment provided by engineer. */ +"Readable browsing and data" = "Duyệt web và dữ liệu có thể đọc được"; + +/* No comment provided by engineer. */ +"Remember to check our FAQs first, for answers to the most frequently asked questions.\n\nIf it's not answered there, we're happy to provide support and take feedback by email." = "Hãy nhớ kiểm tra Câu hỏi thường gặp của chúng tôi trước để biết câu trả lời cho những câu hỏi thường gặp nhất.\n\nNếu không có câu trả lời ở đó, chúng tôi sẵn lòng hỗ trợ và gửi phản hồi qua email."; + +/* The title of a list of trackers */ +"Reporting" = "Báo cáo"; + +/* No comment provided by engineer. */ +"Secure Tunnel VPN" = "Secure Tunnel VPN"; + +/* No comment provided by engineer. */ +"See live metrics for how many bad connections Firewall has blocked." = "Xem số liệu trực tiếp để biết Tường lửa đã chặn bao nhiêu kết nối xấu."; + +/* No comment provided by engineer. */ +"Settings" = "Cài đặt"; + +/* No comment provided by engineer. */ +"Share your anonymized metrics and show other people how to block invasive tracking." = "Chia sẻ các số liệu ẩn danh của bạn và chỉ cho những người khác cách chặn theo dõi xâm phạm."; + +/* No comment provided by engineer. */ +"Share Your Stats" = "Chia sẻ Số liệu của Bạn"; + +/* No comment provided by engineer. */ +"SHOW" = "HIỆN"; + +/* No comment provided by engineer. */ +"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." = "Cho biết các ứng dụng ngày nay xâm phạm như thế nào, đồng thời giúp người khác chặn các công cụ theo dõi và phần mềm độc hại.\n\nKhông bao gồm nhật ký chặn - chỉ có hình ảnh ở trên. Chọn nơi để chia sẻ trong bước tiếp theo."; + +/* No comment provided by engineer. */ +"Sign Out" = "Đăng xuất"; + +/* No comment provided by engineer. */ +"Sign Out?" = "Đăng xuất?"; + +/* No comment provided by engineer. */ +"Sign Up | Sign In" = "Đăng ký | Đăng nhập"; + +/* No comment provided by engineer. */ +"Sign up below to unlock benefits of a Lockdown account." = "Đăng ký bên dưới để mở khóa các quyền lợi của tài khoản Lockdown."; + +/* No comment provided by engineer. */ +"Signed In" = "Đã đăng nhập"; + +/* No comment provided by engineer. */ +"Signed out successfully." = "Đã đăng xuất thành công."; + +/* No comment provided by engineer. */ +"Singapore" = "Singapore"; + +/* No comment provided by engineer. */ +"Skip Trial" = "Bỏ qua Dùng thử"; + +/* The title of a list of trackers */ +"Snapchat Trackers" = "Công cụ theo dõi Snapchat"; + +/* No comment provided by engineer. */ +"South Korea" = "Hàn Quốc"; + +/* No comment provided by engineer. */ +"Stay Protected" = "Luôn được bảo vệ"; + +/* No comment provided by engineer. */ +"Subscription Expired" = "Đăng ký hết hạn"; + +/* No comment provided by engineer. */ +"Success" = "Thành công"; + +/* No comment provided by engineer. */ +"Success!" = "Thành công!"; + +/* No comment provided by engineer. */ +"Successfully re-sent your email confirmation to " = "Đã gửi lại thành công xác nhận email của bạn tới "; + +/* No comment provided by engineer. */ +"Support | Feedback" = "Hỗ trợ | Phản hồi"; + +/* No comment provided by engineer. */ +"Tap to activate Lockdown Firewall and update to the newest block lists." = "Nhấn để kích hoạt Lockdown Firewall và cập nhật lên danh sách chặn mới nhất."; + +/* No comment provided by engineer. */ +"Tap to see a demo of how Secure Tunnel protects and anonymizes you." = "Nhấn để xem giới thiệu về cách Secure Tunnel bảo vệ và ẩn danh bạn."; + +/* No comment provided by engineer. */ +"Tap to see the blocked tracking attempts." = "Nhấn để xem những lần cố gắng theo dõi bị chặn."; + +/* Used in the paragraph: You've just blocked 100 tracking attempts! Tap to see what they are. */ +"Tap to see what they are." = "Nhấn để xem chúng là gì."; + +/* No comment provided by engineer. */ +"Terms" = "Điều khoản"; + +/* No comment provided by engineer. */ +"Thanks for helping to increase privacy and tracking awareness." = "Cảm ơn bạn đã giúp nâng cao nhận thức về quyền riêng tư và theo dõi."; + +/* No comment provided by engineer. */ +"The block log can be manually cleared or disabled. Disabling the Block Log only disables the log of connections - the number of tracking attempts will still be displayed." = "Nhật ký chặn có thể được xóa hoặc vô hiệu hóa theo cách thủ công. Vô hiệu hóa Nhật ký chặn sẽ chỉ tắt nhật ký kết nối - số lần cố gắng theo dõi sẽ vẫn được hiển thị."; + +/* Used in To complete your signup, click the confirmation link we sent to you@gmail.com */ +"To complete your signup, click the confirmation link we sent to" = "Để hoàn tất quá trình đăng ký, hãy bấm vào liên kết xác nhận mà chúng tôi đã gửi đến"; + +/* No comment provided by engineer. */ +"To finish signup, click the confirmation link in the email we just sent. If you don't see it, check if it's stuck in your spam folder." = "Để hoàn tất quá trình đăng ký, hãy bấm vào liên kết xác nhận trong email chúng tôi vừa gửi. Nếu bạn không thấy, hãy kiểm tra xem nó có bị kẹt trong mục thư rác hay không."; + +/* No comment provided by engineer. */ +"To see this tutorial again, open the Account tab." = "Để xem lại hướng dẫn này, hãy mở tab Tài khoản."; + +/* No comment provided by engineer. */ +"To: joe@email.com\nRe: Q4 2019 Finance Review" = "Tới: joe@email.com\nV/v: Đánh giá Tài chính Quý 4 Năm 2019"; + +/* No comment provided by engineer. */ +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "Công cụ theo dõi, quảng cáo và các tập lệnh độc hại khác hiện đã bị chặn trong tất cả các ứng dụng của bạn, ngay cả bên ngoài Safari.\n\nBảo đảm quyền riêng tư tối đa với Secure Tunnel bảo vệ các kết nối, ẩn danh hoạt động duyệt web và ẩn vị trí của bạn."; + +/* Used in the sentence: You blocked 500 tracking attempts this week. Tap to update to the newsst block lists. */ +"tracking attempts this week. Tap to update to the newest block lists." = "lần cố gắng theo dõi trong tuần này. Nhấn để cập nhật danh sách chặn mới nhất."; + +/* No comment provided by engineer. */ +"Tunnel" = "Đường hầm"; + +/* No comment provided by engineer. */ +"Tunnel Off" = "Đường hầm đang Tắt"; + +/* No comment provided by engineer. */ +"Tunnel On" = "Đường hầm đang Bật"; + +/* No comment provided by engineer. */ +"Tutorial" = "Hướng dẫn"; + +/* No comment provided by engineer. */ +"Unique IP address broadcasted" = "Đã phát địa chỉ IP riêng"; + +/* No comment provided by engineer. */ +"United Kingdom" = "Vương quốc Anh"; + +/* No comment provided by engineer. */ +"United States - East" = "Hoa Kỳ - Miền Đông"; + +/* No comment provided by engineer. */ +"United States - West" = "Hoa Kỳ - Miền Tây"; + +/* No comment provided by engineer. */ +"Update Success" = "Cập nhật thành công"; + +/* No comment provided by engineer. */ +"Updating Block Lists" = "Cập nhật Danh sách chặn"; + +/* No comment provided by engineer. */ +"USA East" = "Miền Đông Hoa Kỳ"; + +/* No comment provided by engineer. */ +"USA West" = "Miền Tây Hoa Kỳ"; + +/* No comment provided by engineer. */ +"View FAQs" = "Xem Câu hỏi thường gặp"; + +/* No comment provided by engineer. */ +"View or Upgrade Plan" = "Xem hoặc nâng cấp Gói"; + +/* No comment provided by engineer. */ +"View Upgrade Options" = "Xem Tùy chọn Nâng cấp"; + +/* No comment provided by engineer. */ +"VPN Active" = "VPN đang hoạt động"; + +/* No comment provided by engineer. */ +"VPN Not Active" = "VPN không hoạt động"; + +/* No comment provided by engineer. */ +"VPN Subscription Expired" = "Đăng ký VPN hết hạn"; + +/* No comment provided by engineer. */ +"Website" = "Trang web"; + +/* No comment provided by engineer. */ +"Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue." = "Chào mừng bạn đến phần Hướng dẫn của Lockdown.\n\nNhấn vào bất kỳ đâu để tiếp tục."; + +/* No comment provided by engineer. */ +"What is VPN?" = "VPN là gì?"; + +/* No comment provided by engineer. */ +"Whitelisted" = "Đã vào danh sách trắng"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown" = "Vì sao nên tin dùng Lockdown"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown?" = "Vì sao nên tin dùng Lockdown?"; + +/* No comment provided by engineer. */ +"Why Use VPN?" = "Vì sao nên sử dụng VPN?"; + +/* Used in the sentence: You blocked 500 tracking attempts this week. + Used in: You blocked tracker.com in your custom blocked domains. */ +"You blocked" = "Bạn đã chặn"; + +/* No comment provided by engineer. */ +"You blocked this domain in your custom blocked domains." = "Bạn đã chặn miền này trong các miền bị chặn tùy chỉnh của mình."; + +/* No comment provided by engineer. */ +"You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)" = "Bạn có thể nhấn vào phần này sau nếu muốn chia sẻ.\n(Nhấn vào bất kỳ đâu để loại bỏ)"; + +/* No comment provided by engineer. */ +"You'll be signed out from this account." = "Bạn sẽ đăng xuất khỏi tài khoản này."; + +/* No comment provided by engineer. */ +"You'll have to reenable it later here to start seeing blocked entries again." = "Sau này bạn sẽ phải kích hoạt lại nó tại đây để bắt đầu xem lại được các mục bị chặn."; + +/* No comment provided by engineer. */ +"You've just blocked 100 tracking attempts!" = "Bạn vừa chặn 100 lần cố gắng theo dõi!"; + +/* No comment provided by engineer. */ +"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." = "Tài khoản của bạn đã được xác nhận và bạn hiện đã đăng nhập. Bạn sẽ nhận được danh sách chặn mới nhất, quyền truy cập Lockdown Mac và nhận các thông báo quan trọng."; + +/* No comment provided by engineer. */ +"Your Settings" = "Cài đặt của bạn"; + +/* No comment provided by engineer. */ +"⚠️ Not Signed In" = "⚠️ Chưa đăng nhập"; + +/* No comment provided by engineer. */ +"🔥🧱 Firewall Activated 🎊🎉" = "🔥🧱 Đã kích hoạt Tường lửa 🎊🎉"; + +/* No comment provided by engineer. */ +"Error while trying to confirm your account:" = "Lỗi khi cố gắng xác nhận tài khoản của bạn:"; + +/* No comment provided by engineer. */ +"If this persists, please contact team@lockdownprivacy.com." = "Nếu điều này vẫn tiếp diễn, vui lòng liên hệ team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"Add a domain to block" = "Thêm miền để chặn"; + +/* No comment provided by engineer. */ +"Block all your apps from connecting to the domains and sites below. For your convenience, Lockdown also has pre-configured suggestions." = "Chặn tất cả các ứng dụng của bạn kết nối với các miền và trang web bên dưới. Để thuận tiện cho bạn, Lockdown cũng có các đề xuất được định cấu hình trước."; + +/* No comment provided by engineer. */ +"Block Lists" = "Danh sách chặn"; + +/* No comment provided by engineer. */ +"Configure Blocking" = "Cấu hình chặn"; + +/* No comment provided by engineer. */ +"Custom" = "Tùy chỉnh"; + +/* No comment provided by engineer. */ +"SAVE" = "LƯU"; + +/* No comment provided by engineer. */ +"Access to cloud service information is not allowed" = "Không được phép truy cập thông tin dịch vụ đám mây"; + +/* No comment provided by engineer. */ +"Could not connect to the network" = "Không thể kết nối với mạng"; + +/* No comment provided by engineer. */ +"Not allowed to make the payment" = "Không được phép thanh toán"; + +/* No comment provided by engineer. */ +"Payment was cancelled" = "Thanh toán đã bị hủy"; + +/* No comment provided by engineer. */ +"The device is not allowed to make the payment" = "Thiết bị không được phép thực hiện thanh toán"; + +/* No comment provided by engineer. */ +"The product is not available in the current storefront" = "Sản phẩm không có sẵn trong cửa hàng hiện tại"; + +/* No comment provided by engineer. */ +"The purchase identifier was invalid" = "Giá trị nhận dạng mua hàng không hợp lệ"; + +/* No comment provided by engineer. */ +"Unknown error. Please contact support at team@lockdownprivacy.com." = "Lỗi không xác định. Vui lòng liên hệ với bộ phận hỗ trợ tại team@lockdownprivacy.com."; + +/* No comment provided by engineer. */ +"User has revoked permission to use this cloud service" = "Người dùng đã thu hồi quyền sử dụng dịch vụ đám mây này"; + +/* No comment provided by engineer. */ +"Please that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "Hãy bảo đảm rằng bạn có một gói đăng ký đang hoạt động. Nếu bạn đang cố gắng chia sẻ đăng ký từ cùng một tài khoản, bạn cần đăng nhập bằng cùng một địa chỉ email. Nếu không, vui lòng bắt đầu bản dùng thử miễn phí hoặc gửi e-mail đến team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "Mua hàng trong ứng dụng đã bị tắt. Vui lòng cho phép chúng trong Ứng dụng Cài đặt -> Thời gian sử dụng -> Hạn chế -> Cửa hàng ứng dụng -> Mua hàng trong ứng dụng. Sau đó thử lại."; + +/* No comment provided by engineer. */ +"Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "Thanh toán không được phép.\nThiết bị này không được phép mua hàng hoặc Mua hàng trong ứng dụng đã bị tắt. Vui lòng cho phép chúng trong Ứng dụng Cài đặt -> Thời gian sử dụng -> Hạn chế -> Cửa hàng ứng dụng -> Mua hàng trong ứng dụng. Sau đó thử lại."; + +/* No comment provided by engineer. */ +"Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com" = "Hãy bảo đảm rằng bạn có một gói đăng ký đang hoạt động. Nếu bạn đang cố gắng chia sẻ đăng ký từ cùng một tài khoản, bạn cần đăng nhập bằng cùng một địa chỉ email. Nếu không, vui bắt đầu bản dùng thử miễn phí hoặc gửi e-mail đến team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Upgrade Plan" = "Nâng cấp Gói"; + +/* The title of a list of trackers */ +"Amazon Trackers" = "Công cụ theo dõi Amazon"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "Đơn giản và Mạnh mẽ"; +"onboarding_lockdown_stops_trackers" = "Lockdown chặn các trình theo dõi, phần mềm độc hại và các tác nhân xấu khác truy cập vào dữ liệu của bạn."; +"onboarding_worlds_first_on_device" = "Đầu tiên trên thế giới trên thiết bị"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "Tường lửa nguồn mở và secure tunnel VPN hoạt động trên TẤT CẢ các ứng dụng và duyệt web của bạn."; +"onboarding_staying_protected_is_easy_with_lockdown" = "Luôn được Bảo vệ thật dễ dàng với Lockdown"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "Duy trì ẩn danh với Secure Tunnel VPN"; +"onboarding_no_logs_fast_and_fully_audited" = "VPN không cần nhật ký, cực nhanh, ưu tiên quyền riêng tư. VPN duy nhất chứng minh được Chính sách Quyền riêng tư của mình và được kiểm tra đầy đủ bởi các chuyên gia bảo mật."; +"onboarding_take_control_of_who_you_block" = "Kiểm soát người\nmà bạn Chặn"; +"onboarding_when_using_our_intuitive_blocklists" = "Khi sử dụng công cụ xây dựng danh sách chặn trực quan của chúng tôi."; +"onboarding_stay_protected" = "Luôn được bảo vệ!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "Nhận tóm tắt hàng tuần và cập nhật danh sách chặn."; +"onboarding_over_1_billion_trackers_blocked" = "Hơn 1 tỷ Trình theo dõi bị chặn"; +"onboarding_used_by_more_than_1_million" = "Được sử dụng bởi hơn một triệu người quan tâm về quyền riêng tư."; +"onboarding_sign_up" = "Đăng ký"; +"onboarding_login" = "Đăng nhập"; +"continue" = "Tiếp tục"; +"plan_annual" = "Hàng năm"; +"plan_monthly" = "Hàng tháng"; + +// MARK: - SignUpViewController +"welcome" = "Chào mừng"; +"already_have_account_login" = "Bạn đã có tài khoản? Đăng nhập"; +"sign_up_to_access_new_block_lists_from_trackers" = "Đăng ký để truy cập danh sách chặn mới từ các trình theo dõi"; +"already_have_account_login_below" = "Đã có tài khoản Lockdown?\nĐăng nhập bên dưới."; +"forgot_password" = "Quên mật khẩu"; +"email" = "Email"; +"password" = "Mật khẩu"; +"access_our_curated_block_lists_and_build_your_own" = "Truy cập danh sách chặn được tuyển chọn của chúng tôi và tạo danh sách chặn của riêng bạn"; +"access_lockdown_across_all_your_devices" = "Truy cập Lockdown từ mọi thiết bị của bạn"; +"firewall_and_vpn_support" = "Hỗ trợ tường lửa và VPN"; +"by_continuing_you_agree_with_our" = "Bằng cách tiếp tục, bạn đồng ý với"; +"and" = "và"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "Duy trì cập nhật"; +"once_a_week_helpful_reminders" = "Mỗi tuần một lời nhắc hữu ích về bảo mật và cách tận dụng tối đa."; +"enable_notifications" = "Bật thông báo"; +"maybe_later" = "Có lẽ để sau"; + +// MARK: - Previously Unlocalized +"check_fields" = "Kiểm tra các trường"; +"email_and_password_must_not_be_empty" = "Email và mật khẩu không được để trống"; +"Success! 🎉" = "Thành công! 🎉"; +"error_signing_in" = "Lỗi khi đăng nhập"; +"confirm_your_email" = "Xác nhận email của bạn"; +"you_have_successfully_sign_in" = "Bạn đã đăng nhập thành công."; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "Tôi đã chặn %@ công cụ theo dõi, quảng cáo và phần mềm độc hại bằng Lockdown, tường lửa chặn các kết nối không mong muốn trong tất cả các ứng dụng của bạn.\nTải về miễn phí tại lockdownprivacy.com."; +"check_email" = "Kiểm tra Email"; +"we_have_sent_a_reset_password_email" = "Chúng tôi đã gửi email đặt lại mật khẩu cho bạn. Hãy nhớ kiểm tra mọi thư mục thư rác, phòng trường hợp thư bị chuyển vào đó."; +"since_date" = "KỂ TỪ %@"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "Bắt đầu dùng thử ngay để sử dụng đầy đủ tính năng"; +"restore_purchase" = "Khôi phục Mua hàng"; +"terms_of_service" = "Điều khoản Dịch vụ"; +"CHOOSE_YOUR_PLAN" = "CHỌN GÓI"; +"skip" = "Bỏ qua"; +"save_annual_vpn_subscription" = "Tiết kiệm ~50%"; +"save_annual_pro_subscription" = "Tiết kiệm ~30%"; +"current_plan" = "Gói hiện tại"; +"feature_table_column" = "Tính năng"; +"feature_level_table_column" = "Miễn phí"; +"stops_ads_and_hidden_trackers" = "Dừng quảng cáo và công cụ theo dõi ẩn"; +"blocks_badware_in_all_your_apps" = "Chặn phần mềm xấu trong tất cả ứng dụng của bạn"; +"access_our_curated_block_lists" = "Sử dụng danh sách chặn được tuyển chọn của chúng tôi"; +"build_your_custom_block_lists" = "Tạo danh sách chặn tùy chỉnh của riêng bạn"; +"stops_browsing_history_tracking" = "Dừng theo dõi lịch sử duyệt web"; +"anonymizing_your_browsing" = "Ẩn danh trình duyệt"; +"hides_your_location_and_ip" = "Ẩn vị trí và địa chỉ IP"; +"lets_you_change_your_ip_to_other_region" = "Cho phép bạn thay đổi IP\nsang khu vực khác"; +"app_for_iphone_plus_ipad" = "Ứng dụng cho iPhone + iPad"; +"app_for_mac" = "Ứng dụng cho Mac"; +"year_after_slash" = "năm"; +"month_after_slash" = "tháng"; + +// MARK: - Delete My Account +"delete_account" = "Xóa tài khoản"; +"delete_my_account" = "Xóa tài khoản của tôi"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "Nếu tiếp tục, bạn sẽ gửi yêu cầu xóa toàn bộ hồ sơ tài khoản của mình, cùng với dữ liệu cá nhân có liên quan. Khi tài khoản được xóa, bạn sẽ được thông báo về những thay đổi này qua email tài khoản của mình.\n\nXin lưu ý rằng bạn sẽ mất tất cả cài đặt, bao gồm cả các tùy chọn nâng cấp và sẽ không thể tạo tài khoản với email đó nữa."; +"proceed" = "Tiếp tục"; + +// MARK: - EmailValidatable +"invalid_email_address" = "Địa chỉ email không hợp lệ"; +"please_fill_in_your_email" = "Vui lòng điền email của bạn."; +"you_entered_con_instead_of_com" = "Bạn đã nhập .con thay vì .com."; +"no_valid_email_address_detected" = "Không tìm thấy địa chỉ email hợp lệ."; +"too_many_email_addresses_entered" = "Đã nhập quá nhiều địa chỉ email."; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "phút"; +"seconds_genitive" = "giây"; +"lockdowns_first_christmas_sale" = "Giảm giá Giáng sinh đầu tiên của Lockdown"; +"subscribe_now" = "Đăng ký ngay"; + +// MARK: - Code Redemption +"redeem_code" = "Đổi mã"; +"cannot_redeem_code" = "Không thể đổi mã"; +"the_code_could_not_be_redeemed" = "Không thể đổi mã này."; +"congratulations_excl" = "Xin chúc mừng!"; +"the_code_was_successfully_redeemed" = "Mã đã được đổi thành công. Bạn có thể sẽ cần khởi động lại ứng dụng để giao dịch mua có hiệu lực."; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000..6ac2878 --- /dev/null +++ b/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,811 @@ +/* Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com */ +" and " = "和 "; + +/* refers to number of connections blocked today, as in '34 Blocked Today' */ +" Blocked Today" = " 今天已阻止"; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to add to a whitelist. For example, \"google.com\" without \"https://\"" = " 不是有效的输入项。请仅输入您想添加到白名单的域主机。例如:\"google.com\",不带\"https://\""; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to block. For example, \"google.com\" without \"https://\"" = " 不是有效的输入项。请仅输入您想阻止的域主机。例如:\"google.com\",不带\"https://\""; + +/* No comment provided by engineer. */ +" Off" = " 关闭"; + +/* No comment provided by engineer. */ +" On" = " 打开"; + +/* Used in the sentence: I blocked 500 trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com. */ +" trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com." = " 追踪器、广告和恶意软件,该防火墙可阻止您的所有应用程序中的多余连接。请在 lockdownprivacy.com 免费获取。"; + +/* No comment provided by engineer. */ +"1 Week Free" = "一周免费"; + +/* No comment provided by engineer. */ +"\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). You can also set custom domains to block." = "\"阻止列表\" 让您选择想要阻止的内容(例如 Facebook、点击诱饵等)。您也可以自行设置要阻止的域。"; + +/* No comment provided by engineer. */ +"\"iOS Annual\" is your current plan" = "\"年度 iOS\"是您的当前套餐"; + +/* No comment provided by engineer. */ +"\"iOS Monthly\" is your current plan" = "\"月度 iOS\"是您的当前套餐"; + +/* No comment provided by engineer. */ +"\"Pro Annual\" is your current plan" = "\"年度 Pro\"是您的当前套餐"; + +/* No comment provided by engineer. */ +"\"Pro Monthly\" is your current plan" = "\"月度 Pro\"是您的当前套餐"; + +/* No comment provided by engineer. */ +"\"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." = "\"查看日志\"显示过去一天阻止了哪些连接。此日志在午夜清除并保留在设备上,因此只有您才能看到。"; + +/* No comment provided by engineer. */ +"\n\n If this error persists, please contact team@lockdownprivacy.com." = "\n\n 如果此错误仍然存在,请与 team@lockdownprivacy.com 联系。"; + +/* No comment provided by engineer. */ +"\n\n If this persists, please contact team@lockdownprivacy.com." = "\n\n 如果这种情况仍然存在,请与 team@lockdownprivacy.com 联系。"; + +/* No comment provided by engineer. */ +"About Lockdown VPN" = "关于 Lockdown VPN"; + +/* No comment provided by engineer. */ +"Activate Firewall with this button." = "使用此按钮激活防火墙。"; + +/* No comment provided by engineer. */ +"Activating" = "正在激活"; + +/* No comment provided by engineer. */ +"Anonymize IP against trackers" = "使 IP 对追踪器匿名"; + +/* No comment provided by engineer. */ +"Apple Outage" = "Apple 中断"; + +/* No comment provided by engineer. */ +"at least 8 characters" = "至少 8 个字符"; + +/* No comment provided by engineer. */ +"Audit Reports" = "审计报告"; + +/* No comment provided by engineer. */ +"Australia" = "澳大利亚"; + +/* No comment provided by engineer. */ +"Be sure to check your spam folder in case it got stuck there.\n\nYou can also request a re-send of the confirmation." = "请务必检查您的垃圾邮件文件夹,看看它是否卡在那里。\n\n您也可以请求重新发送确认信息。"; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"block list" = "阻止列表"; + +/* No comment provided by engineer. */ +"Blocked" = "已阻止"; + +/* No comment provided by engineer. */ +"Blocked Domains" = "已阻止的域"; + +/* No comment provided by engineer. */ +"Blocked Today" = "今天已阻止"; + +/* No comment provided by engineer. */ +"Blocked Trackers Summary" = "所阻止的追踪器摘要"; + +/* No comment provided by engineer. */ +"Brazil" = "巴西"; + +/* No comment provided by engineer. */ +"Canada" = "加拿大"; + +/* No comment provided by engineer. */ +"Cancel" = "取消"; + +/* No comment provided by engineer. */ +"Check Your Inbox" = "检查您的收件箱"; + +/* No comment provided by engineer. */ +"Clear Block Log" = "清除阻止日志"; + +/* The title of a list of trackers */ +"Clickbait" = "点击诱饵"; + +/* No comment provided by engineer. */ +"Confirm Email" = "确认电子邮件"; + +/* No comment provided by engineer. */ +"Confirmed Site" = "已确认的网站"; + +/* No comment provided by engineer. */ +"Couldn't Find Your Email Client" = "无法找到您的电子邮件客户端"; + +/* The title of a list of trackers */ +"Crypto Mining" = "加密货币挖矿"; + +/* No comment provided by engineer. */ +"Custom Settings (Advanced)" = "自定义设置(高级)"; + +/* The title of a list of trackers */ +"Data Trackers" = "数据追踪器"; + +/* No comment provided by engineer. */ +"Deactivating" = "正在停用"; + +/* No comment provided by engineer. */ +"Disable" = "禁用"; + +/* No comment provided by engineer. */ +"Disable Block Log" = "禁用阻止日志"; + +/* No comment provided by engineer. */ +"Disable Block Log?" = "禁用阻止日志?"; + +/* No comment provided by engineer. */ +"Done" = "完成"; + +/* No comment provided by engineer. */ +"Email Support" = "电子邮件支持"; + +/* The title of a list of trackers */ +"Email Trackers" = "电子邮件追踪器"; + +/* No comment provided by engineer. */ +"Email Us" = "电邮我们"; + +/* No comment provided by engineer. */ +"Enable" = "启用"; + +/* No comment provided by engineer. */ +"Enable Block Log" = "启用阻止日志"; + +/* No comment provided by engineer. */ +"Enable notifications to get a once-a-week summary and the latest block list updates. You can disable this anytime." = "启用通知以获取每周一次的摘要报告和最新阻止列表。您可以随时禁用此功能。"; + +/* No comment provided by engineer. */ +"Encrypted, private connections" = "加密的专用连接"; + +/* No comment provided by engineer. */ +"Error Code " = "错误代码 "; + +/* No comment provided by engineer. */ +"Error Creating Email Account" = "创建电子邮件帐户时出错"; + +/* No comment provided by engineer. */ +"Error Loading Plan: Retry" = "加载套餐时出错:重试"; + +/* No comment provided by engineer. */ +"Error Re-sending Email Confirmation" = "重新发送电子邮件确认时出错"; + +/* No comment provided by engineer. */ +"Error Restoring Subscription" = "恢复订阅时出错"; + +/* No comment provided by engineer. */ +"Error Signing In To Verify Subscription" = "登录以验证订阅时出错"; + +/* No comment provided by engineer. */ +"Error Starting Trial" = "开始试用时出错"; + +/* The title of a list of trackers */ +"Facebook & WhatsApp" = "Facebook 和 WhatsApp"; + +/* The title of a list of trackers */ +"Facebook Trackers" = "Facebook 追踪器"; + +/* No comment provided by engineer. */ +"Failed to re-send email confirmation." = "无法重新发送电子邮件确认。"; + +/* No comment provided by engineer. */ +"FAQs" = "常见问题"; + +/* No comment provided by engineer. */ +"Firewall" = "防火墙"; + +/* No comment provided by engineer. */ +"Firewall Active" = "防火墙已激活"; + +/* No comment provided by engineer. */ +"Firewall Not Active" = "防火墙未激活"; + +/* No comment provided by engineer. */ +"FIREWALL OFF" = "防火墙关闭"; + +/* No comment provided by engineer. */ +"FIREWALL ON" = "防火墙打开"; + +/* No comment provided by engineer. */ +"For maximum privacy, activate Secure Tunnel, which uses bank-level encryption to protect connections, anonymize your browsing, and hide your location and IP." = "为了最大限度地保护隐私,请激活 Secure Tunnel,它使用银行级加密来保护连接,让您匿名浏览,并隐藏您的位置和 IP。"; + +/* No comment provided by engineer. */ +"France" = "法国"; + +/* The title of a list of trackers */ +"Game Marketing" = "游戏营销"; + +/* The title of a list of trackers */ +"General Marketing" = "一般营销"; + +/* No comment provided by engineer. */ +"Germany" = "德国"; + +/* The title of a list of trackers */ +"Google Shopping" = "谷歌购物"; + +/* No comment provided by engineer. */ +"Hey Lockdown Team, \nI have a question, issue, or suggestion - " = "嗨,Lockdown 团队,\n我有疑问、问题或建议 - "; + +/* No comment provided by engineer. */ +"Hi, my question or feedback for Lockdown is: " = "嗨,我对 Lockdown 的疑问或反馈是: "; + +/* Used in the sentence: I blocked 500 trackers with Lockdown. */ +"I blocked" = "我用 Lockdown 阻止了"; + +/* Used in: You blocked tracker.com in your custom blocked domains. */ +"in your custom blocked domains." = "在您的自定义阻止域中。"; + +/* No comment provided by engineer. */ +"India" = "印度"; + +/* No comment provided by engineer. */ +"Invalid domain" = "无效的域"; + +/* No comment provided by engineer. */ +"IP: 18.132.2.87" = "IP:18.132.2.87"; + +/* No comment provided by engineer. */ +"IP: [Anonymized]" = "IP:[匿名]"; + +/* No comment provided by engineer. */ +"Ireland" = "爱尔兰"; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"is a part of" = "属于"; + +/* No comment provided by engineer. */ +"Japan" = "日本"; + +/* Shows when loading the details of a subscription plan */ +"Loading Plan" = "正在加载套餐"; + +/* No comment provided by engineer. */ +"Location changed and hidden" = "位置已更改和隐藏"; + +/* No comment provided by engineer. */ +"Location: 🇯🇵" = "位置:日本"; + +/* No comment provided by engineer. */ +"Location: 🇺🇸" = "位置:美国"; + +/* No comment provided by engineer. */ +"Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari." = "Lockdown Firewall 可阻止您的所有应用中的不良和不可信连接 - 而不仅限于 Safari。"; + +/* No comment provided by engineer. */ +"Lockdown is open source and fully transparent, which means anyone can see exactly what it's doing. Also, Lockdown Firewall has a simple, strict Privacy Policy, while Lockdown VPN is fully audited by security experts." = "Lockdown 是开源且完全透明的,这意味着任何人都能确切地看到它在做什么。此外,Lockdown Firewall 具有简单、严格的隐私政策,且 Lockdown VPN 得到安全专家的全面审核。 +"; + +/* No comment provided by engineer. */ +"Lockdown VPN is powered by Confirmed VPN, the open source, no-logs, and fully audited VPN." = "Lockdown VPN 由 Confirmed VPN 提供支持,为开源、无日志和经全面审核的 VPN。"; + +/* The title of a list of trackers */ +"Marketing Trackers" = "营销追踪器"; + +/* The title of a list of trackers */ +"Marketing Trackers II" = "营销追踪器 II"; + +/* No comment provided by engineer. */ +"Network Error" = "网络错误"; + +/* No comment provided by engineer. */ +"Next" = "下一步"; + +/* No comment provided by engineer. */ +"Nice" = "良好"; + +/* No comment provided by engineer. */ +"No" = "否"; + +/* No comment provided by engineer. */ +"No Active Subscription" = "无有效订阅"; + +/* No comment provided by engineer. */ +"No additional information on this blocked domain found." = "未找到关于此被阻止域的其他信息。"; + +/* No comment provided by engineer. */ +"No Block Lists Enabled" = "未启用阻止列表"; + +/* No comment provided by engineer. */ +"No Info Found" = "未找到信息"; + +/* No comment provided by engineer. */ +"No Internet Connection" = "无互联网连接"; + +/* No comment provided by engineer. */ +"No stored API credentials found. Please contact team@lockdownprivacy.com about this error." = "未找到存储的 API 凭证。关于此错误,请与 team@lockdownprivacy.com 联系。"; + +/* No comment provided by engineer. */ +"Not Blocked" = "未阻止"; + +/* No comment provided by engineer. */ +"Not Now" = "以后再说"; + +/* No comment provided by engineer. */ +"Not Whitelisted" = "未列入白名单"; + +/* No comment provided by engineer. */ +"Notifications: Off" = "通知:关闭"; + +/* No comment provided by engineer. */ +"Notifications: On" = "通知:打开"; + +/* No comment provided by engineer. */ +"OK" = "确定"; + +/* No comment provided by engineer. */ +"Okay" = "确定"; + +/* No comment provided by engineer. */ +"one lowercase letter" = "一个小写字母"; + +/* No comment provided by engineer. */ +"one number" = "一个数字"; + +/* No comment provided by engineer. */ +"one symbol" = "一个符号"; + +/* No comment provided by engineer. */ +"one uppercase letter" = "一个大写字母"; + +/* No comment provided by engineer. */ +"Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol." = "密码必须至少为 8 个字符,至少包含一个大写字母、一个小写字母、一个数字和一个符号。"; + +/* No comment provided by engineer. */ +"Plan: Annual Pro" = "套餐:年度专业版"; + +/* No comment provided by engineer. */ +"Please check your internet connection. If this persists, please contact team@lockdownprivacy.com.\n\nError Description\n" = "请检查您的互联网连接。如果这种情况仍然存在,请联系 team@lockdownprivacy.com。\n\n错误描述\n"; + +/* No comment provided by engineer. */ +"Please contact team@lockdownprivacy.com.\n\nError details:\n" = "请联系 team@lockdownprivacy.com。\n\n错误详情:\n"; + +/* No comment provided by engineer. */ +"Please email team@lockdownprivacy.com with the following Error Code " = "请发送电子邮件至 team@lockdownprivacy.com 并附上以下错误代码"; + +/* No comment provided by engineer. */ +"Please first go to your iOS Settings > Notifications > Lockdown to enable notifications" = "请先进入您的 iOS 设置 > 通知 > Lockdown 以启用通知"; + +/* No comment provided by engineer. */ +"Please make sure you have added an e-mail account to your iOS device and try again." = "请确保您已将电子邮件帐户添加到您的 iOS 设备,然后重试。"; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active and that you have an active subscription. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "请确保您的互联网连接处于活动状态,并且您有有效的订阅。否则,请开始您的免费试用,或发送电子邮件至 team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with the following error message: " = "请确保您的互联网连接处于活动状态。如果此错误仍然存在,请发送电子邮件至 team@lockdownprivacy.com,并附上以下错误消息:"; + +/* No comment provided by engineer. */ +"Please renew your subscription to activate the Secure Tunnel." = "请续订您的订阅以激活 Secure Tunnel。"; + +/* No comment provided by engineer. */ +"Please renew your subscription to re-activate the VPN." = "请续订您的订阅以重新激活 VPN。"; + +/* No comment provided by engineer. */ +"Please tap Block List and enable at least one block list to activate Firewall." = "请点击\"阻止列表\",并启用至少一个阻止列表以激活防火墙。"; + +/* No comment provided by engineer. */ +"Pre-configured Block Lists" = "预配置的阻止列表"; + +/* No comment provided by engineer. */ +"Pre-configured Suggestions" = "预配置建议"; + +/* No comment provided by engineer. */ +"Precise location exposed" = "已暴露的精确位置"; + +/* No comment provided by engineer. */ +"Press & Media" = "新闻与媒体"; + +/* No comment provided by engineer. */ +"Privacy Policy" = "隐私政策"; + +/* The title of a list of trackers */ +"Ransomware" = "勒索软件"; + +/* No comment provided by engineer. */ +"Re-send" = "重新发送"; + +/* No comment provided by engineer. */ +"Readable browsing and data" = "可读的浏览和数据"; + +/* No comment provided by engineer. */ +"Remember to check our FAQs first, for answers to the most frequently asked questions.\n\nIf it's not answered there, we're happy to provide support and take feedback by email." = "请务必先查看我们的常见问题解答,以获取最常见问题的答案。\n\n如果那里没有答案,我们很乐意通过电子邮件提供支持并获取反馈。"; + +/* The title of a list of trackers */ +"Reporting" = "报告"; + +/* No comment provided by engineer. */ +"Secure Tunnel VPN" = "Secure Tunnel VPN"; + +/* No comment provided by engineer. */ +"See live metrics for how many bad connections Firewall has blocked." = "查看实时指标,了解防火墙已阻止的不良连接数量。"; + +/* No comment provided by engineer. */ +"Settings" = "设置"; + +/* No comment provided by engineer. */ +"Share your anonymized metrics and show other people how to block invasive tracking." = "分享您的匿名指标,并向他人展示如何阻止侵入性跟踪。"; + +/* No comment provided by engineer. */ +"Share Your Stats" = "分享您的统计数据"; + +/* No comment provided by engineer. */ +"SHOW" = "显示"; + +/* No comment provided by engineer. */ +"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." = "显示当前应用的侵入性有多强,并帮助他人阻止追踪器和恶意软件。\n\n不会显示您的阻止日志 - 仅显示上图。在下一步中选择分享位置。"; + +/* No comment provided by engineer. */ +"Sign Out" = "注销"; + +/* No comment provided by engineer. */ +"Sign Out?" = "注销?"; + +/* No comment provided by engineer. */ +"Sign Up | Sign In" = "注册 | 登录"; + +/* No comment provided by engineer. */ +"Sign up below to unlock benefits of a Lockdown account." = "在下方注册以解锁 Lockdown 帐户的福利。"; + +/* No comment provided by engineer. */ +"Signed In" = "已登录"; + +/* No comment provided by engineer. */ +"Signed out successfully." = "已成功注销。"; + +/* No comment provided by engineer. */ +"Singapore" = "新加坡"; + +/* No comment provided by engineer. */ +"Skip Trial" = "跳过试用"; + +/* The title of a list of trackers */ +"Snapchat Trackers" = "Snapchat 追踪器"; + +/* No comment provided by engineer. */ +"South Korea" = "韩国"; + +/* No comment provided by engineer. */ +"Stay Protected" = "继续受到保护"; + +/* No comment provided by engineer. */ +"Subscription Expired" = "订阅已过期"; + +/* No comment provided by engineer. */ +"Success" = "成功"; + +/* No comment provided by engineer. */ +"Success!" = "成功!"; + +/* No comment provided by engineer. */ +"Successfully re-sent your email confirmation to " = "已成功将您的电子邮件确认重新发送至 "; + +/* No comment provided by engineer. */ +"Support | Feedback" = "支持 | 反馈"; + +/* No comment provided by engineer. */ +"Tap to activate Lockdown Firewall and update to the newest block lists." = "点击以激活 Lockdown Firewall 并更新至最新的阻止列表。"; + +/* No comment provided by engineer. */ +"Tap to see a demo of how Secure Tunnel protects and anonymizes you." = "点击查看演示,了解 Secure Tunnel 如何保护您并让您匿名。"; + +/* No comment provided by engineer. */ +"Tap to see the blocked tracking attempts." = "点击查看被阻止的跟踪尝试。"; + +/* Used in the paragraph: You've just blocked 100 tracking attempts! Tap to see what they are. */ +"Tap to see what they are." = "点击查看它们是何方神圣。"; + +/* No comment provided by engineer. */ +"Terms" = "术语"; + +/* No comment provided by engineer. */ +"Thanks for helping to increase privacy and tracking awareness." = "感谢您帮助人们提高隐私和反跟踪意识。"; + +/* No comment provided by engineer. */ +"The block log can be manually cleared or disabled. Disabling the Block Log only disables the log of connections - the number of tracking attempts will still be displayed." = "阻止日志可以手动清除或禁用。禁用阻止日志只会禁用连接日志 - 仍会显示跟踪尝试的次数。"; + +/* Used in To complete your signup, click the confirmation link we sent to you@gmail.com */ +"To complete your signup, click the confirmation link we sent to" = "要完成注册,请点击我们发送的确认链接"; + +/* No comment provided by engineer. */ +"To finish signup, click the confirmation link in the email we just sent. If you don't see it, check if it's stuck in your spam folder." = "要完成注册,请点击我们刚发送的电子邮件中的确认链接。如果您没有看到该电子邮件,请检查它是否卡在您的垃圾邮件文件夹中。"; + +/* No comment provided by engineer. */ +"To see this tutorial again, open the Account tab." = "若要再次查看本教程,请打开\"帐户\"选项卡。"; + +/* No comment provided by engineer. */ +"To: joe@email.com\nRe: Q4 2019 Finance Review" = "收件人:joe@email.com\n回复:2019 年第四季度财务回顾"; + +/* No comment provided by engineer. */ +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "现已在您的所有应用(甚至 Safari 之外的应用)中阻止追踪器、广告和其他恶意脚本。\n\n通过 Secure Tunnel 最大限度地保护您的隐私:它可以保护连接,让您匿名浏览,并隐藏您的位置。"; + +/* Used in the sentence: You blocked 500 tracking attempts this week. Tap to update to the newsst block lists. */ +"tracking attempts this week. Tap to update to the newest block lists." = "本周的跟踪尝试。点击以更新至最新的阻止列表。"; + +/* No comment provided by engineer. */ +"Tunnel" = "隧道"; + +/* No comment provided by engineer. */ +"Tunnel Off" = "隧道关闭"; + +/* No comment provided by engineer. */ +"Tunnel On" = "隧道打开"; + +/* No comment provided by engineer. */ +"Tutorial" = "教程"; + +/* No comment provided by engineer. */ +"Unique IP address broadcasted" = "广播的唯一 IP 地址"; + +/* No comment provided by engineer. */ +"United Kingdom" = "英国"; + +/* No comment provided by engineer. */ +"United States - East" = "美国 - 东部"; + +/* No comment provided by engineer. */ +"United States - West" = "美国 - 西部"; + +/* No comment provided by engineer. */ +"Update Success" = "更新成功"; + +/* No comment provided by engineer. */ +"Updating Block Lists" = "正在更新阻止列表"; + +/* No comment provided by engineer. */ +"USA East" = "美国东部"; + +/* No comment provided by engineer. */ +"USA West" = "美国西部"; + +/* No comment provided by engineer. */ +"View FAQs" = "查看常见问题"; + +/* No comment provided by engineer. */ +"View or Upgrade Plan" = "查看或升级套餐"; + +/* No comment provided by engineer. */ +"View Upgrade Options" = "查看升级选项"; + +/* No comment provided by engineer. */ +"VPN Active" = "VPN 已激活"; + +/* No comment provided by engineer. */ +"VPN Not Active" = "VPN 未激活"; + +/* No comment provided by engineer. */ +"VPN Subscription Expired" = "VPN 订阅已过期"; + +/* No comment provided by engineer. */ +"Website" = "网站"; + +/* No comment provided by engineer. */ +"Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue." = "欢迎观看 Lockdown 教程。\n\n点击任意位置以继续。"; + +/* No comment provided by engineer. */ +"What is VPN?" = "什么是 VPN?"; + +/* No comment provided by engineer. */ +"Whitelisted" = "已列入白名单"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown" = "为什么要信任 Lockdown"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown?" = "为什么要信任 Lockdown?"; + +/* No comment provided by engineer. */ +"Why Use VPN?" = "为什么使用 VPN?"; + +/* Used in the sentence: You blocked 500 tracking attempts this week. + Used in: You blocked tracker.com in your custom blocked domains. */ +"You blocked" = "您阻止了"; + +/* No comment provided by engineer. */ +"You blocked this domain in your custom blocked domains." = "您阻止了此域,它在您的自定义受阻止域中。"; + +/* No comment provided by engineer. */ +"You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)" = "如果您想分享,可在稍后点击此按钮。\n(点击任意位置即可取消)"; + +/* No comment provided by engineer. */ +"You'll be signed out from this account." = "您将退出此帐户。"; + +/* No comment provided by engineer. */ +"You'll have to reenable it later here to start seeing blocked entries again." = "您必须稍后在此处重新启用它,才能重新开始看到阻止记录。"; + +/* No comment provided by engineer. */ +"You've just blocked 100 tracking attempts!" = "您刚刚阻止了 100 次跟踪尝试!"; + +/* No comment provided by engineer. */ +"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." = "已确认您的帐户,您现已登录。您将获得最新阻止列表,可以访问 Lockdown Mac,并获悉重要公告。"; + +/* No comment provided by engineer. */ +"Your Settings" = "您的设置"; + +/* No comment provided by engineer. */ +"⚠️ Not Signed In" = "⚠️ 未登录"; + +/* No comment provided by engineer. */ +"🔥🧱 Firewall Activated 🎊🎉" = "🔥🧱 防火墙已激活🎊🎉"; + +/* No comment provided by engineer. */ +"Error while trying to confirm your account:" = "尝试确认您的帐户时出错:"; + +/* No comment provided by engineer. */ +"If this persists, please contact team@lockdownprivacy.com." = "如果这种情况仍然存在,请联系 team@lockdownprivacy.com。"; + +/* No comment provided by engineer. */ +"Add a domain to block" = "添加一个要阻止的域"; + +/* No comment provided by engineer. */ +"Block all your apps from connecting to the domains and sites below. For your convenience, Lockdown also has pre-configured suggestions." = "阻止您的所有应用连接到以下域和网站。为您的方便起见,Lockdown 还提供了预配置建议。"; + +/* No comment provided by engineer. */ +"Block Lists" = "阻止列表"; + +/* No comment provided by engineer. */ +"Configure Blocking" = "配置阻止"; + +/* No comment provided by engineer. */ +"Custom" = "自定义"; + +/* No comment provided by engineer. */ +"SAVE" = "保存"; + +/* No comment provided by engineer. */ +"Access to cloud service information is not allowed" = "不允许访问云服务信息"; + +/* No comment provided by engineer. */ +"Could not connect to the network" = "无法连接到网络"; + +/* No comment provided by engineer. */ +"Not allowed to make the payment" = "不允许付款"; + +/* No comment provided by engineer. */ +"Payment was cancelled" = "付款已取消"; + +/* No comment provided by engineer. */ +"The device is not allowed to make the payment" = "设备不允许付款"; + +/* No comment provided by engineer. */ +"The product is not available in the current storefront" = "产品在当前店面中不可用"; + +/* No comment provided by engineer. */ +"The purchase identifier was invalid" = "购买标识符无效"; + +/* No comment provided by engineer. */ +"Unknown error. Please contact support at team@lockdownprivacy.com." = "未知错误。请通过 team@lockdownprivacy.com 联系支持人员。"; + +/* No comment provided by engineer. */ +"User has revoked permission to use this cloud service" = "用户已撤销使用此云服务的权限"; + +/* No comment provided by engineer. */ +"Please that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "请确认您有有效订阅。如果您尝试从同一帐户共享订阅,则需要使用相同的电子邮件地址登录。否则,请开始您的免费试用,或发送电子邮件至 team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "应用内购买已被禁用。请进入设置应用 -> 屏幕时间 -> 限制 -> 应用商店 -> 应用内购买,允许应用内购买。然后重试。"; + +/* No comment provided by engineer. */ +"Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "不允许付款。\n不允许此设备进行购买,或者已禁用应用内购买。 +请进入设置应用 -> 屏幕时间 -> 限制 -> 应用商店 - > 应用内购买,允许应用内购买。然后重试。"; + +/* No comment provided by engineer. */ +"Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com" = "请确保您有有效订阅。如果你尝试从同一帐户共享订阅,则需要使用相同的电子邮件地址登录。否则,请开始您的免费试用,或发送电子邮件至 team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Upgrade Plan" = "升级套餐"; + +/* The title of a list of trackers */ +"Amazon Trackers" = "亚马逊追踪器"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "简单而强大"; +"onboarding_lockdown_stops_trackers" = "Lockdown 可以阻止追踪器、恶意软件和其他恶意代理访问您的数据。"; +"onboarding_worlds_first_on_device" = "世界首创的设备端应用"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "开源防火墙和安全隧道 VPN,为您的所有应用和网页浏览保驾护航。"; +"onboarding_staying_protected_is_easy_with_lockdown" = "使用 Lockdown 保护数据轻松无比"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "使用 Secure Tunnel VPN 保持匿名"; +"onboarding_no_logs_fast_and_fully_audited" = "无日志、速度超快、隐私至上的 VPN。唯一一个证明其隐私政策,并经安全专家全面审计的 VPN。"; +"onboarding_take_control_of_who_you_block" = "使用我们直观好用的\n阻止列表创建工具"; +"onboarding_when_using_our_intuitive_blocklists" = "轻松调整您要阻止的人。"; +"onboarding_stay_protected" = "继续受到保护!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "获取每周摘要和阻止列表更新。"; +"onboarding_over_1_billion_trackers_blocked" = "已阻止逾 10 亿追踪器"; +"onboarding_used_by_more_than_1_million" = "拥有逾百万注重隐私的用户。"; +"onboarding_sign_up" = "注册"; +"onboarding_login" = "登录"; +"continue" = "继续"; +"plan_annual" = "年度套餐"; +"plan_monthly" = "月度套餐"; + +// MARK: - SignUpViewController +"welcome" = "欢迎"; +"already_have_account_login" = "已有一个帐户?登录"; +"sign_up_to_access_new_block_lists_from_trackers" = "注册以访问新的追踪器阻止列表"; +"already_have_account_login_below" = "已有 Lockdown 帐户?\n在下方登录。"; +"forgot_password" = "忘记密码"; +"email" = "电子邮件"; +"password" = "密码"; +"access_our_curated_block_lists_and_build_your_own" = "访问我们预设的阻止列表以及建立您自己的阻止列表"; +"access_lockdown_across_all_your_devices" = "在您的所有设备上访问 Lockdown"; +"firewall_and_vpn_support" = "防火墙和 VPN 支持"; +"by_continuing_you_agree_with_our" = "继续即表明您同意我们的"; +"and" = "和"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "随时了解最新动态"; +"once_a_week_helpful_reminders" = "一周一次的有用安全提醒以及如何充分利用它。"; +"enable_notifications" = "启用通知"; +"maybe_later" = "以后再说"; + +// MARK: - Previously Unlocalized +"check_fields" = "检查字段"; +"email_and_password_must_not_be_empty" = "电子邮件和密码不能为空"; +"Success! 🎉" = "成功!🎉"; +"error_signing_in" = "登录错误"; +"confirm_your_email" = "确认您的电子邮件"; +"you_have_successfully_sign_in" = "您已成功登录。"; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "我用 Lockdown 阻止了 %@ 个追踪器、广告和恶意软件,该防火墙可阻止您的所有应用程序中的多余连接。\n请在 lockdownprivacy.com 免费获取。"; +"check_email" = "请检查电子邮件"; +"we_have_sent_a_reset_password_email" = "我们给您发送了一封重置密码电子邮件。请务必检查所有垃圾邮件文件夹,因为它可能会卡在那里。"; +"since_date" = "自从 %@"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "立即开始试用即可获得完整功能"; +"restore_purchase" = "恢复购买"; +"terms_of_service" = "服务条款"; +"CHOOSE_YOUR_PLAN" = "选择您的套餐"; +"skip" = "跳过"; +"save_annual_vpn_subscription" = "优惠约 50%"; +"save_annual_pro_subscription" = "优惠约 30%"; +"current_plan" = "当前套餐"; +"feature_table_column" = "特点"; +"feature_level_table_column" = "免费"; +"stops_ads_and_hidden_trackers" = "阻止广告和隐藏追踪器"; +"blocks_badware_in_all_your_apps" = "阻止您的所有应用中的恶意软件"; +"access_our_curated_block_lists" = "访问我们预设的阻止列表"; +"build_your_custom_block_lists" = "使用您的自定义阻止列表"; +"stops_browsing_history_tracking" = "阻止浏览历史记录跟踪"; +"anonymizing_your_browsing" = "匿名您的浏览"; +"hides_your_location_and_ip" = "隐藏您的位置和 IP"; +"lets_you_change_your_ip_to_other_region" = "让您将自己的 IP 更改为\n其他地区"; +"app_for_iphone_plus_ipad" = "适用于 iPhone + iPad 的应用"; +"app_for_mac" = "适用于 Mac 的应用"; +"year_after_slash" = "年"; +"month_after_slash" = "月"; + +// MARK: - Delete My Account +"delete_account" = "删除帐户"; +"delete_my_account" = "删除我的帐户"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "如果继续,您就会提交一项请求,从而删除您的整个帐户记录,以及相关个人数据。一旦帐户被删除,您将通过您的帐户电子邮件收到有关这些变更的通知。请注意,您将丢失您的所有设置,包括升级选项,且将无法使用同一电子邮件地址创建帐户。"; +"proceed" = "继续"; + +// MARK: - EmailValidatable +"invalid_email_address" = "无效电子邮件地址"; +"please_fill_in_your_email" = "请填写您的电子邮件。"; +"you_entered_con_instead_of_com" = "您输入了 .con 而不是 .com。"; +"no_valid_email_address_detected" = "检测到无效电子邮件地址。"; +"too_many_email_addresses_entered" = "输入的电子邮件地址过多。"; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "分钟"; +"seconds_genitive" = "秒钟"; +"lockdowns_first_christmas_sale" = "Lockdown首次圣诞节销售"; +"subscribe_now" = "立即订阅"; + +// MARK: - Code Redemption +"redeem_code" = "兑换代码"; +"cannot_redeem_code" = "无法兑换代码"; +"the_code_could_not_be_redeemed" = "代码不能被兑换。"; +"congratulations_excl" = "恭喜!"; +"the_code_was_successfully_redeemed" = "已成功兑换代码。你可能需要重新启动应用才能让购买生效。"; diff --git a/zh-Hant.lproj/Localizable.strings b/zh-Hant.lproj/Localizable.strings new file mode 100644 index 0000000..e39121b --- /dev/null +++ b/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,809 @@ +/* Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com */ +" and " = "及"; + +/* refers to number of connections blocked today, as in '34 Blocked Today' */ +" Blocked Today" = "今日已封鎖"; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to add to a whitelist. For example, \"google.com\" without \"https://\"" = "並非有效的條目。請只輸入想要加至白名單的網域主機。例如:\"google.com\" 而不加上 \"https://\""; + +/* No comment provided by engineer. */ +" is not a valid entry. Please only enter the host of the domain you want to block. For example, \"google.com\" without \"https://\"" = "並非有效的條目。請只輸入想要封鎖的網域主機。例如:\"google.com\" 而不加上 \"https://\""; + +/* No comment provided by engineer. */ +" Off" = "關閉"; + +/* No comment provided by engineer. */ +" On" = "開啟"; + +/* Used in the sentence: I blocked 500 trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com. */ +" trackers, ads, and badware with Lockdown, the firewall that blocks unwanted connections in all your apps. Get it free at lockdownprivacy.com." = "Lockdown 可阻擋追蹤器、廣告及具惡意軟體,是道能在您所有應用程式中封鎖垃圾連結的防火牆。立即於 lockdownprivacy.com 免費取得。"; + +/* No comment provided by engineer. */ +"1 Week Free" = "1 週免費"; + +/* No comment provided by engineer. */ +"\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). You can also set custom domains to block." = "\"封鎖清單\"讓您能選擇想要封鎖的內容(例如 Facebook、點擊誘餌等)。您也可以自訂想要封鎖的網域。"; + +/* No comment provided by engineer. */ +"\"iOS Annual\" is your current plan" = "\"iOS 年度方案\"為您目前的方案"; + +/* No comment provided by engineer. */ +"\"iOS Monthly\" is your current plan" = "\"iOS 月度方案\"為您目前的方案"; + +/* No comment provided by engineer. */ +"\"Pro Annual\" is your current plan" = "\"Pro 年度方案\"為您目前的方案"; + +/* No comment provided by engineer. */ +"\"Pro Monthly\" is your current plan" = "\"Pro 月度方案\"為您目前的方案"; + +/* No comment provided by engineer. */ +"\"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." = "\"檢視紀錄\"顯示出過去數日確實已封鎖的連結。此紀錄會於午夜時分清除,並保留於裝置上,所以僅有您能查閱。"; + +/* No comment provided by engineer. */ +"\n\n If this error persists, please contact team@lockdownprivacy.com." = "\n\n 若此問題持續發生,請聯繫 team@lockdownprivacy.com。"; + +/* No comment provided by engineer. */ +"\n\n If this persists, please contact team@lockdownprivacy.com." = "\n\n 若持續發生,請聯繫 team@lockdownprivacy.com。"; + +/* No comment provided by engineer. */ +"About Lockdown VPN" = "關於 Lockdown VPN"; + +/* No comment provided by engineer. */ +"Activate Firewall with this button." = "使用此按鈕啟動 Firewall。"; + +/* No comment provided by engineer. */ +"Activating" = "正在啟動中"; + +/* No comment provided by engineer. */ +"Anonymize IP against trackers" = "匿名 IP 以阻擋追蹤器"; + +/* No comment provided by engineer. */ +"Apple Outage" = "Apple 大當機"; + +/* No comment provided by engineer. */ +"at least 8 characters" = "至少 8 個字元"; + +/* No comment provided by engineer. */ +"Audit Reports" = "稽核報告"; + +/* No comment provided by engineer. */ +"Australia" = "澳大利亞"; + +/* No comment provided by engineer. */ +"Be sure to check your spam folder in case it got stuck there.\n\nYou can also request a re-send of the confirmation." = "請務必確認您的垃圾郵件資料夾,以免郵件卡在該處。\n\n您也能請求重新發送確認郵件。"; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"block list" = "封鎖清單"; + +/* No comment provided by engineer. */ +"Blocked" = "已封鎖"; + +/* No comment provided by engineer. */ +"Blocked Domains" = "已封鎖的網域"; + +/* No comment provided by engineer. */ +"Blocked Today" = "今日已封鎖"; + +/* No comment provided by engineer. */ +"Blocked Trackers Summary" = "已封鎖的追蹤器摘要"; + +/* No comment provided by engineer. */ +"Brazil" = "巴西"; + +/* No comment provided by engineer. */ +"Canada" = "加拿大"; + +/* No comment provided by engineer. */ +"Cancel" = "取消"; + +/* No comment provided by engineer. */ +"Check Your Inbox" = "確認您的收件夾"; + +/* No comment provided by engineer. */ +"Clear Block Log" = "清除封鎖紀錄"; + +/* The title of a list of trackers */ +"Clickbait" = "點擊誘餌"; + +/* No comment provided by engineer. */ +"Confirm Email" = "確認郵件"; + +/* No comment provided by engineer. */ +"Confirmed Site" = "已確認的網站"; + +/* No comment provided by engineer. */ +"Couldn't Find Your Email Client" = "找不到您的電子郵件客戶"; + +/* The title of a list of trackers */ +"Crypto Mining" = "加密貨幣挖礦"; + +/* No comment provided by engineer. */ +"Custom Settings (Advanced)" = "自訂設定(進階)"; + +/* The title of a list of trackers */ +"Data Trackers" = "資料追蹤器"; + +/* No comment provided by engineer. */ +"Deactivating" = "正在關閉"; + +/* No comment provided by engineer. */ +"Disable" = "停用"; + +/* No comment provided by engineer. */ +"Disable Block Log" = "停用封鎖清單"; + +/* No comment provided by engineer. */ +"Disable Block Log?" = "停用封鎖清單?"; + +/* No comment provided by engineer. */ +"Done" = "已完成"; + +/* No comment provided by engineer. */ +"Email Support" = "電子郵件支援"; + +/* The title of a list of trackers */ +"Email Trackers" = "電子郵件追蹤器"; + +/* No comment provided by engineer. */ +"Email Us" = "寄信給我們"; + +/* No comment provided by engineer. */ +"Enable" = "啟用"; + +/* No comment provided by engineer. */ +"Enable Block Log" = "啟用封鎖清單"; + +/* No comment provided by engineer. */ +"Enable notifications to get a once-a-week summary and the latest block list updates. You can disable this anytime." = "啟用提醒以取得一週一次的摘要,以及最新的封鎖清單更新。您可隨以隨時停用此功能。"; + +/* No comment provided by engineer. */ +"Encrypted, private connections" = "已加密、私密的連結"; + +/* No comment provided by engineer. */ +"Error Code " = "錯誤代碼"; + +/* No comment provided by engineer. */ +"Error Creating Email Account" = "建立電子郵件帳號發生錯誤"; + +/* No comment provided by engineer. */ +"Error Loading Plan: Retry" = "載入方案發生錯誤:重試"; + +/* No comment provided by engineer. */ +"Error Re-sending Email Confirmation" = "重新發送確認郵件發生錯誤"; + +/* No comment provided by engineer. */ +"Error Restoring Subscription" = "恢復訂閱發生錯誤"; + +/* No comment provided by engineer. */ +"Error Signing In To Verify Subscription" = "登入確認訂閱發生錯誤"; + +/* No comment provided by engineer. */ +"Error Starting Trial" = "開始試用發生錯誤"; + +/* The title of a list of trackers */ +"Facebook & WhatsApp" = "Facebook 及 WhatsApp"; + +/* The title of a list of trackers */ +"Facebook Trackers" = "Facebook 追蹤器"; + +/* No comment provided by engineer. */ +"Failed to re-send email confirmation." = "無法重新發送確認郵件。"; + +/* No comment provided by engineer. */ +"FAQs" = "常見問題集"; + +/* No comment provided by engineer. */ +"Firewall" = "Firewall"; + +/* No comment provided by engineer. */ +"Firewall Active" = "Firewall 已啟動"; + +/* No comment provided by engineer. */ +"Firewall Not Active" = "Firewall 未啟動"; + +/* No comment provided by engineer. */ +"FIREWALL OFF" = "Firewall 已關閉"; + +/* No comment provided by engineer. */ +"FIREWALL ON" = "Firewall 已開啟"; + +/* No comment provided by engineer. */ +"For maximum privacy, activate Secure Tunnel, which uses bank-level encryption to protect connections, anonymize your browsing, and hide your location and IP." = "為達到最大的隱私,請啟動 Secure Tunnel,此程式會使用銀行等級的加密行為以保護連結、匿名您的瀏覽行為,並隱藏您的位置及 IP。"; + +/* No comment provided by engineer. */ +"France" = "法國"; + +/* The title of a list of trackers */ +"Game Marketing" = "遊戲行銷"; + +/* The title of a list of trackers */ +"General Marketing" = "一般行銷"; + +/* No comment provided by engineer. */ +"Germany" = "德國"; + +/* The title of a list of trackers */ +"Google Shopping" = "Google 購物"; + +/* No comment provided by engineer. */ +"Hey Lockdown Team, \nI have a question, issue, or suggestion - " = "Lockdown 團隊您好:\n我有一個疑問、問題或建議--"; + +/* No comment provided by engineer. */ +"Hi, my question or feedback for Lockdown is: " = "您好,我對 Lockdown 的問題或意見回饋為:"; + +/* Used in the sentence: I blocked 500 trackers with Lockdown. */ +"I blocked" = "我已封鎖"; + +/* Used in: You blocked tracker.com in your custom blocked domains. */ +"in your custom blocked domains." = "在您自訂已封鎖的網域中。"; + +/* No comment provided by engineer. */ +"India" = "印度"; + +/* No comment provided by engineer. */ +"Invalid domain" = "無效的網域"; + +/* No comment provided by engineer. */ +"IP: 18.132.2.87" = "IP:18.132.2.87"; + +/* No comment provided by engineer. */ +"IP: [Anonymized]" = "IP:[已匿名]"; + +/* No comment provided by engineer. */ +"Ireland" = "愛爾蘭"; + +/* Used in the sentence: 'tracker.com' is a part of 'Marketing Trackers' block list */ +"is a part of" = "為一部分"; + +/* No comment provided by engineer. */ +"Japan" = "日本"; + +/* Shows when loading the details of a subscription plan */ +"Loading Plan" = "正在載入方案"; + +/* No comment provided by engineer. */ +"Location changed and hidden" = "已改變及隱藏地點"; + +/* No comment provided by engineer. */ +"Location: 🇯🇵" = "地點:🇯🇵"; + +/* No comment provided by engineer. */ +"Location: 🇺🇸" = "地點:🇺🇸"; + +/* No comment provided by engineer. */ +"Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari." = "Lockdown Firewall 會封鎖您所有應用程式中未受信任的連結--不僅僅 Safari 可以做到這點。"; + +/* No comment provided by engineer. */ +"Lockdown is open source and fully transparent, which means anyone can see exactly what it's doing. Also, Lockdown Firewall has a simple, strict Privacy Policy, while Lockdown VPN is fully audited by security experts." = "Lockdown 為開放來源且完全透明,代表任何人都能看到此程式正在執行的動作。另外,Lockdown Firewall 有個簡單又嚴格的隱私權政策,且 Lockdown VPN 完全由安全專家稽查。"; + +/* No comment provided by engineer. */ +"Lockdown VPN is powered by Confirmed VPN, the open source, no-logs, and fully audited VPN." = "Lockdown VPN 的提供者為 Confirmed VPN,為開放來源、無紀錄且完全受稽查的 VPN。"; + +/* The title of a list of trackers */ +"Marketing Trackers" = "行銷追蹤器"; + +/* The title of a list of trackers */ +"Marketing Trackers II" = "行銷追蹤器 II"; + +/* No comment provided by engineer. */ +"Network Error" = "網路錯誤"; + +/* No comment provided by engineer. */ +"Next" = "下一個"; + +/* No comment provided by engineer. */ +"Nice" = "很好"; + +/* No comment provided by engineer. */ +"No" = "沒有"; + +/* No comment provided by engineer. */ +"No Active Subscription" = "沒有啟動的訂閱"; + +/* No comment provided by engineer. */ +"No additional information on this blocked domain found." = "在這個已封鎖的網域上找不到額外的資訊。"; + +/* No comment provided by engineer. */ +"No Block Lists Enabled" = "未啟用封鎖清單"; + +/* No comment provided by engineer. */ +"No Info Found" = "找不到資訊"; + +/* No comment provided by engineer. */ +"No Internet Connection" = "沒有網路連線"; + +/* No comment provided by engineer. */ +"No stored API credentials found. Please contact team@lockdownprivacy.com about this error." = "找不到已儲存的 API 認證。請聯繫 team@lockdownprivacy.com 以告知這項錯誤。"; + +/* No comment provided by engineer. */ +"Not Blocked" = "未封鎖"; + +/* No comment provided by engineer. */ +"Not Now" = "現在先不要"; + +/* No comment provided by engineer. */ +"Not Whitelisted" = "未放入白名單"; + +/* No comment provided by engineer. */ +"Notifications: Off" = "提醒:關閉"; + +/* No comment provided by engineer. */ +"Notifications: On" = "提醒:開啟"; + +/* No comment provided by engineer. */ +"OK" = "好的"; + +/* No comment provided by engineer. */ +"Okay" = "好的"; + +/* No comment provided by engineer. */ +"one lowercase letter" = "一個小寫字母"; + +/* No comment provided by engineer. */ +"one number" = "一個數字"; + +/* No comment provided by engineer. */ +"one symbol" = "一個符號"; + +/* No comment provided by engineer. */ +"one uppercase letter" = "一個大寫字母"; + +/* No comment provided by engineer. */ +"Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol." = "密碼必須至少為 8 個字元,須包含至少一個大寫字母、一個小寫字母、一個數字及一個符號。"; + +/* No comment provided by engineer. */ +"Plan: Annual Pro" = "方案:年度 Pro"; + +/* No comment provided by engineer. */ +"Please check your internet connection. If this persists, please contact team@lockdownprivacy.com.\n\nError Description\n" = "請確認您的網路連線。若持續發生,請聯繫 team@lockdownprivacy.com。\n\n錯誤說明\n"; + +/* No comment provided by engineer. */ +"Please contact team@lockdownprivacy.com.\n\nError details:\n" = "請聯繫 team@lockdownprivacy.com。\n\n錯誤詳情:\n"; + +/* No comment provided by engineer. */ +"Please email team@lockdownprivacy.com with the following Error Code " = "請連同下列的錯誤代碼發送電子郵件至 team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Please first go to your iOS Settings > Notifications > Lockdown to enable notifications" = "首先請前往您的 iOS 設定 > 提醒 > Lockdown 以啟用提醒"; + +/* No comment provided by engineer. */ +"Please make sure you have added an e-mail account to your iOS device and try again." = "請確保您已在您的 iOS 裝置上加入一個電子郵件帳號並再試一次"; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active and that you have an active subscription. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "請確保您的網路連線正常啟動,且您有已啟用的訂閱。否則,請開始您的免費試用或發送電子郵件至 team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Please make sure your Internet connection is active. If this error persists, email team@lockdownprivacy.com with the following error message: " = "請確保您的網路連線正常啟動。若此錯誤持續發生,請連同以下的錯誤訊息發送電子郵件至 team@lockdownprivacy.com:"; + +/* No comment provided by engineer. */ +"Please renew your subscription to activate the Secure Tunnel." = "請續訂您的訂閱,以啟動 Secure Tunnel。"; + +/* No comment provided by engineer. */ +"Please renew your subscription to re-activate the VPN." = "請續訂您的訂閱,以重啟 VPN。"; + +/* No comment provided by engineer. */ +"Please tap Block List and enable at least one block list to activate Firewall." = "請點選封鎖清單,並啟用至少一個封鎖清單以啟動 Firewall。"; + +/* No comment provided by engineer. */ +"Pre-configured Block Lists" = "預先設定的封鎖清單"; + +/* No comment provided by engineer. */ +"Pre-configured Suggestions" = "預先設定的建議"; + +/* No comment provided by engineer. */ +"Precise location exposed" = "已暴露精確的地點"; + +/* No comment provided by engineer. */ +"Press & Media" = "新聞媒體"; + +/* No comment provided by engineer. */ +"Privacy Policy" = "隱私權政策"; + +/* The title of a list of trackers */ +"Ransomware" = "勒索軟體"; + +/* No comment provided by engineer. */ +"Re-send" = "重新發送"; + +/* No comment provided by engineer. */ +"Readable browsing and data" = "可讀的瀏覽及資料"; + +/* No comment provided by engineer. */ +"Remember to check our FAQs first, for answers to the most frequently asked questions.\n\nIf it's not answered there, we're happy to provide support and take feedback by email." = "請記得先確認我們的常見問題集,以在其中找到最常詢問之問題解答。\n\n若仍無法找到解答,我們很樂意提供支援並透過電子郵件收到意見回饋。"; + +/* The title of a list of trackers */ +"Reporting" = "正在回報問題"; + +/* No comment provided by engineer. */ +"Secure Tunnel VPN" = "Secure Tunnel VPN"; + +/* No comment provided by engineer. */ +"See live metrics for how many bad connections Firewall has blocked." = "查看即時計量,以了解 Firewall 已封鎖多少不良連結。"; + +/* No comment provided by engineer. */ +"Settings" = "設定"; + +/* No comment provided by engineer. */ +"Share your anonymized metrics and show other people how to block invasive tracking." = "分享您的匿名計量,並向其他人展示如何封鎖侵入式的追蹤。"; + +/* No comment provided by engineer. */ +"Share Your Stats" = "分享您的統計資料"; + +/* No comment provided by engineer. */ +"SHOW" = "顯示"; + +/* No comment provided by engineer. */ +"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." = "顯示出現今的應用程式入侵性有多強烈,並也能協助其他人封鎖追蹤器及惡意軟體。\n\n未包含您的封鎖紀錄--僅有上方的圖案。在下一步選擇要分享在哪個地方。"; + +/* No comment provided by engineer. */ +"Sign Out" = "登出"; + +/* No comment provided by engineer. */ +"Sign Out?" = "登出?"; + +/* No comment provided by engineer. */ +"Sign Up | Sign In" = "註冊 | 登入"; + +/* No comment provided by engineer. */ +"Sign up below to unlock benefits of a Lockdown account." = "在下方註冊,以解鎖 Lockdown 帳號的所有好處。"; + +/* No comment provided by engineer. */ +"Signed In" = "已登入"; + +/* No comment provided by engineer. */ +"Signed out successfully." = "已成功登出"; + +/* No comment provided by engineer. */ +"Singapore" = "新加坡"; + +/* No comment provided by engineer. */ +"Skip Trial" = "跳過試用"; + +/* The title of a list of trackers */ +"Snapchat Trackers" = "Snapchat 追蹤器"; + +/* No comment provided by engineer. */ +"South Korea" = "南韓"; + +/* No comment provided by engineer. */ +"Stay Protected" = "維持受到保護"; + +/* No comment provided by engineer. */ +"Subscription Expired" = "訂閱已過期"; + +/* No comment provided by engineer. */ +"Success" = "成功"; + +/* No comment provided by engineer. */ +"Success!" = "成功!"; + +/* No comment provided by engineer. */ +"Successfully re-sent your email confirmation to " = "已成功重新發送您的確認郵件至 "; + +/* No comment provided by engineer. */ +"Support | Feedback" = "服務支援 | 意見回饋"; + +/* No comment provided by engineer. */ +"Tap to activate Lockdown Firewall and update to the newest block lists." = "點選以啟動 Lockdown Firewall,並更新至最新的封鎖名單。"; + +/* No comment provided by engineer. */ +"Tap to see a demo of how Secure Tunnel protects and anonymizes you." = "點選以查看 Secure Tunnel 如何保護及將您匿名的演示。"; + +/* No comment provided by engineer. */ +"Tap to see the blocked tracking attempts." = "點選以查看遭封鎖的追蹤嘗試。"; + +/* Used in the paragraph: You've just blocked 100 tracking attempts! Tap to see what they are. */ +"Tap to see what they are." = "點選以查看內容。"; + +/* No comment provided by engineer. */ +"Terms" = "條款"; + +/* No comment provided by engineer. */ +"Thanks for helping to increase privacy and tracking awareness." = "感謝您協助提升隱私及追蹤意識。"; + +/* No comment provided by engineer. */ +"The block log can be manually cleared or disabled. Disabling the Block Log only disables the log of connections - the number of tracking attempts will still be displayed." = "可手動清除或停用封鎖紀錄。停用封鎖紀錄僅會停用連結的紀錄--仍會顯示追蹤器嘗試的次數。"; + +/* Used in To complete your signup, click the confirmation link we sent to you@gmail.com */ +"To complete your signup, click the confirmation link we sent to" = "若想完成您的註冊,請點擊我們傳送至此處的確認連結:"; + +/* No comment provided by engineer. */ +"To finish signup, click the confirmation link in the email we just sent. If you don't see it, check if it's stuck in your spam folder." = "若想完成註冊,請點擊我們稍早發送至電子郵件的確認連結。若您尚未看到,請確認是否卡在您的垃圾郵件夾中。"; + +/* No comment provided by engineer. */ +"To see this tutorial again, open the Account tab." = "若想再次觀看此教學,請開啟帳號分頁。"; + +/* No comment provided by engineer. */ +"To: joe@email.com\nRe: Q4 2019 Finance Review" = "收件者:joe@email.com\n回覆:Q4 2019 財務報告"; + +/* No comment provided by engineer. */ +"trackers_ads_and_malicious_scripts_are_now_blocked_get_max_privacy" = "您所有應用程式中的追蹤器、廣告及其他惡意內容已遭封鎖,就連 Safari 外的亦是如此。\n\n利用保護連結的 Secure Tunnel 來取得最大的隱私效益、匿名您的瀏覽並隱藏您的地點。"; + +/* Used in the sentence: You blocked 500 tracking attempts this week. Tap to update to the newsst block lists. */ +"tracking attempts this week. Tap to update to the newest block lists." = "本週的追蹤嘗試。點選以更新至最新的封鎖清單。"; + +/* No comment provided by engineer. */ +"Tunnel" = "Tunnel"; + +/* No comment provided by engineer. */ +"Tunnel Off" = "Tunnel 關閉"; + +/* No comment provided by engineer. */ +"Tunnel On" = "Tunnel 開啟"; + +/* No comment provided by engineer. */ +"Tutorial" = "教學"; + +/* No comment provided by engineer. */ +"Unique IP address broadcasted" = "已廣播獨特的 IP 位址"; + +/* No comment provided by engineer. */ +"United Kingdom" = "英國"; + +/* No comment provided by engineer. */ +"United States - East" = "美國--東部"; + +/* No comment provided by engineer. */ +"United States - West" = "美國--西部"; + +/* No comment provided by engineer. */ +"Update Success" = "更新成功"; + +/* No comment provided by engineer. */ +"Updating Block Lists" = "正在更新封鎖清單"; + +/* No comment provided by engineer. */ +"USA East" = "美東地區"; + +/* No comment provided by engineer. */ +"USA West" = "美西地區"; + +/* No comment provided by engineer. */ +"View FAQs" = "檢視常見問題集"; + +/* No comment provided by engineer. */ +"View or Upgrade Plan" = "檢視或更新方案"; + +/* No comment provided by engineer. */ +"View Upgrade Options" = "檢視更新選項"; + +/* No comment provided by engineer. */ +"VPN Active" = "VPN 已啟動"; + +/* No comment provided by engineer. */ +"VPN Not Active" = "VPN 未啟動"; + +/* No comment provided by engineer. */ +"VPN Subscription Expired" = "VPN 訂閱已過期"; + +/* No comment provided by engineer. */ +"Website" = "網站"; + +/* No comment provided by engineer. */ +"Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue." = "歡迎來到 Lockdown 教學。\n\n點選任一處以繼續。"; + +/* No comment provided by engineer. */ +"What is VPN?" = "VPN 是什麼?"; + +/* No comment provided by engineer. */ +"Whitelisted" = "已加入白名單"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown" = "為什麼要信任 Lockdown"; + +/* No comment provided by engineer. */ +"Why Trust Lockdown?" = "為什麼要信任 Lockdown?"; + +/* No comment provided by engineer. */ +"Why Use VPN?" = "為什麼要使用 VPN?"; + +/* Used in the sentence: You blocked 500 tracking attempts this week. + Used in: You blocked tracker.com in your custom blocked domains. */ +"You blocked" = "您已封鎖"; + +/* No comment provided by engineer. */ +"You blocked this domain in your custom blocked domains." = "您已在自訂已封鎖的網域中封鎖了這個網域。"; + +/* No comment provided by engineer. */ +"You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)" = "若您想要分享,可於稍後點選此處。\n(點選任意處以關閉)"; + +/* No comment provided by engineer. */ +"You'll be signed out from this account." = "您將從此帳號登出。"; + +/* No comment provided by engineer. */ +"You'll have to reenable it later here to start seeing blocked entries again." = "您稍後會需要於此重新啟用,才能再次開始看見已封鎖的條目。"; + +/* No comment provided by engineer. */ +"You've just blocked 100 tracking attempts!" = "您剛才已封鎖了 100 次追蹤嘗試!"; + +/* No comment provided by engineer. */ +"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." = "已確認您的帳號,且您已登入。您會取得最新的封鎖清單、可存取 Lockdown Mac,並獲得重要的公告。"; + +/* No comment provided by engineer. */ +"Your Settings" = "您的設定"; + +/* No comment provided by engineer. */ +"⚠️ Not Signed In" = "⚠️ 未登入"; + +/* No comment provided by engineer. */ +"🔥🧱 Firewall Activated 🎊🎉" = "🔥🧱 已啟動 Firewall 🎊🎉"; + +/* No comment provided by engineer. */ +"Error while trying to confirm your account:" = "嘗試確認您的帳號時發生了錯誤:"; + +/* No comment provided by engineer. */ +"If this persists, please contact team@lockdownprivacy.com." = "若持續發生,請聯繫 team@lockdownprivacy.com。"; + +/* No comment provided by engineer. */ +"Add a domain to block" = "新增一個想要封鎖的網域"; + +/* No comment provided by engineer. */ +"Block all your apps from connecting to the domains and sites below. For your convenience, Lockdown also has pre-configured suggestions." = "阻擋您所有的應用程式連結至以下的網域及網站。為了您的方便起見,Lockdown 亦預先設定了一些建議。"; + +/* No comment provided by engineer. */ +"Block Lists" = "封鎖清單"; + +/* No comment provided by engineer. */ +"Configure Blocking" = "設定封鎖"; + +/* No comment provided by engineer. */ +"Custom" = "自訂"; + +/* No comment provided by engineer. */ +"SAVE" = "儲存"; + +/* No comment provided by engineer. */ +"Access to cloud service information is not allowed" = "不允許存取雲端服務資訊"; + +/* No comment provided by engineer. */ +"Could not connect to the network" = "無法連線至網路"; + +/* No comment provided by engineer. */ +"Not allowed to make the payment" = "不允許進行支付"; + +/* No comment provided by engineer. */ +"Payment was cancelled" = "支付已遭取消"; + +/* No comment provided by engineer. */ +"The device is not allowed to make the payment" = "不允許此裝置進行支付"; + +/* No comment provided by engineer. */ +"The product is not available in the current storefront" = "目前的店面買不到此產品"; + +/* No comment provided by engineer. */ +"The purchase identifier was invalid" = "產品辨識碼無效"; + +/* No comment provided by engineer. */ +"Unknown error. Please contact support at team@lockdownprivacy.com." = "發生未知的錯誤。請發送電子郵件至 team@lockdownprivacy.com 以連繫服務支援。"; + +/* No comment provided by engineer. */ +"User has revoked permission to use this cloud service" = "使用者已撤銷使用此雲端服務的權限"; + +/* No comment provided by engineer. */ +"Please that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, please start your free trial or e-mail team@lockdownprivacy.com" = "請確保您已有啟用的訂閱。若您正嘗試使用相同的帳號分享訂閱,您會需要使用相同的電子郵件地址登入。否則,請開始您的免費試用,或發送電子郵件至 team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "應用程式內購功能已停用。請至設定應用程式 -> 螢幕時間 -> 限制 -> App Store -> 應用程式內購來允許此功能。之後再試一次。"; + +/* No comment provided by engineer. */ +"Payment not allowed.\nEither this device is not allowed to make purchases, or In-App Purchases have been disabled. Please allow them in Settings App -> Screen Time -> Restrictions -> App Store -> In-app Purchases. Then try again." = "未受允許支付。\n原因可能為此裝置未受允許進行購買,或應用程式內購功能已停用。請至設定應用程式 -> 螢幕時間 -> 限制 -> App Store -> 應用程式內購來允許此功能。之後再試一次。"; + +/* No comment provided by engineer. */ +"Please ensure that you have an active subscription. If you're attempting to share a subscription from the same account, you'll need to sign in with the same email address. Otherwise, start your free trial or e-mail team@lockdownprivacy.com" = "請確保您已有啟用的訂閱。若您正嘗試使用相同的帳號分享訂閱,您會需要使用相同的電子郵件地址登入。否則,請開始您的免費試用,或發送電子郵件至 team@lockdownprivacy.com"; + +/* No comment provided by engineer. */ +"Upgrade Plan" = "更新方案"; + +/* The title of a list of trackers */ +"Amazon Trackers" = "Amazon 追蹤器"; + +// MARK: - Onboarding +"onboarding_simple_and_powerful" = "簡易又強大"; +"onboarding_lockdown_stops_trackers" = "Lockdown 可阻止追蹤器、惡意軟體及其他代理程式存取您的資料。"; +"onboarding_worlds_first_on_device" = "世界首創的裝置端應用程式"; +"onboarding_firewall_and_vpn_work_across_all_apps" = "能在所有應用程式及網站瀏覽運作的開放來源 Firewall 及 Secure Tunnel VPN。"; +"onboarding_staying_protected_is_easy_with_lockdown" = "使用 Lockdown 就能輕鬆受到保護"; +"onboarding_remain_anonymous_with_secure_tunnel_vpn" = "使用 Secure Tunnel VPN 就能維持匿名"; +"onboarding_no_logs_fast_and_fully_audited" = "無紀錄、速度超快、隱私至上的 VPN。是唯一能證明自己的隱私權政策,且完全受到安全專家稽核的 VPN。"; +"onboarding_take_control_of_who_you_block" = "使用我們直覺式的封鎖清單建立工具時,"; +"onboarding_when_using_our_intuitive_blocklists" = "可自由掌控\n想要封鎖的對象。"; +"onboarding_stay_protected" = "維持受到保護!"; +"onboarding_get_weekly_summary_and_blocklist_updates" = "獲得每週摘要,及封鎖清單更新。"; +"onboarding_over_1_billion_trackers_blocked" = "已封鎖超過 10 億個追蹤器"; +"onboarding_used_by_more_than_1_million" = "已有超過 100 萬名對隱私有警覺的使用者使用"; +"onboarding_sign_up" = "註冊"; +"onboarding_login" = "登入"; +"continue" = "繼續"; +"plan_annual" = "年度"; +"plan_monthly" = "月度"; + +// MARK: - SignUpViewController +"welcome" = "歡迎"; +"already_have_account_login" = "已有帳號?請登入"; +"sign_up_to_access_new_block_lists_from_trackers" = "註冊就能存取追蹤器中新的已封鎖清單"; +"already_have_account_login_below" = "已有 Lockdown 帳號?\n在下方登入。"; +"forgot_password" = "忘記密碼"; +"email" = "電子郵件"; +"password" = "密碼"; +"access_our_curated_block_lists_and_build_your_own" = "存取我們所建立的封鎖清單,並建立自己的封鎖清單"; +"access_lockdown_across_all_your_devices" = "在所有的裝置上存取 Lockdown"; +"firewall_and_vpn_support" = "Firewall 及 VPN 支援"; +"by_continuing_you_agree_with_our" = "只要繼續,表示您同意我們的"; +"and" = "及"; + +// MARK: - Enable Notifications +"stay_in_the_loop" = "隨時掌握資訊"; +"once_a_week_helpful_reminders" = "每週一次,有助於提醒您安全性,及如何多加利用。"; +"enable_notifications" = "啟用提醒"; +"maybe_later" = "稍後再說"; + +// MARK: - Previously Unlocalized +"check_fields" = "確認欄位"; +"email_and_password_must_not_be_empty" = "電子郵件及密碼不得為空白"; +"Success! 🎉" = "成功!🎉"; +"error_signing_in" = "登入時發生錯誤"; +"confirm_your_email" = "確認您的電子郵件"; +"you_have_successfully_sign_in" = "您已成功登入。"; +"i_blocked_trackers_ads_and_badware_with_lockdown" = "我已使用了 Lockdown 封鎖 %@ 個追蹤器、廣告及惡意程式,此防火牆可封鎖應用程式中所有多餘的連線。\n立即在 lockdownprivacy.com 免費取得。"; +"check_email" = "確認電子郵件"; +"we_have_sent_a_reset_password_email" = "我們已寄給了您一封重設密碼的電子郵件。請務必確認任何垃圾郵件資料夾,以免卡在該處。"; +"since_date" = "自 %@ 起"; +"password_reset_successfully" = "Password Reset Successfully"; +"please_sign_in_with_new_password" = "Please Sign In With Your New Password"; + +// MARK: - TablePaywallViewController +"start_trial_now_to_get_full_access" = "立即開始試用,以完整存取"; +"restore_purchase" = "恢復購買"; +"terms_of_service" = "服務條款"; +"CHOOSE_YOUR_PLAN" = "選擇您的方案"; +"skip" = "跳過"; +"save_annual_vpn_subscription" = "省下約 50%"; +"save_annual_pro_subscription" = "省下約 30%"; +"current_plan" = "目前的方案"; +"feature_table_column" = "功能"; +"feature_level_table_column" = "免費"; +"stops_ads_and_hidden_trackers" = "阻擋廣告及隱藏的追蹤器"; +"blocks_badware_in_all_your_apps" = "封鎖應用程式中的惡意軟體"; +"access_our_curated_block_lists" = "存取我們已建立的封鎖清單"; +"build_your_custom_block_lists" = "建立自訂的封鎖清單"; +"stops_browsing_history_tracking" = "阻擋瀏覽歷史紀錄追蹤"; +"anonymizing_your_browsing" = "匿名您的瀏覽"; +"hides_your_location_and_ip" = "隱藏您的地點及 IP"; +"lets_you_change_your_ip_to_other_region" = "讓您變更自己的 IP\n至其他地區"; +"app_for_iphone_plus_ipad" = "適合 iPhone + iPad 的應用程式"; +"app_for_mac" = "適合 Mac 的應用程式"; +"year_after_slash" = "年"; +"month_after_slash" = "月"; + +// MARK: - Delete My Account +"delete_account" = "刪除帳號"; +"delete_my_account" = "刪除我的帳號"; +"by_proceeding_you_will_submit_request_for_deleting_account" = "若執行,便會提交刪除整個帳號紀錄以及相關個人資料的請求。刪除帳號後,將透過您的帳號電子郵件通知這些變更。\n\n請留意,您將會遺失包括升級選項等所有設定內容,且無法再使用相同的電子郵件建立帳號。"; +"proceed" = "執行"; + +// MARK: - EmailValidatable +"invalid_email_address" = "無效的電子郵件地址"; +"please_fill_in_your_email" = "請填入您的電子郵件。"; +"you_entered_con_instead_of_com" = "您輸入了 .con 而非 .com。"; +"no_valid_email_address_detected" = "未偵測到有效的電子郵件地址。"; +"too_many_email_addresses_entered" = "已輸入太多電子郵件地址。"; + +// MARK: - Firewall Blocking Warnings +"this_block_is_under_maintenance_and_will_be_updated" = "This block list is currently under maintenance and will be updated soon."; +"this_block_is_intended_to_block_facebook_owned_apps" = "This list is intended to completely block Facebook-owned apps. Do not enable it if you use apps owned by Facebook like WhatsApp, Facebook Messenger, and Instagram."; +"this_blocks_background_google_tracking" = "This blocks background Google tracking, but also blocks the shopping results at the top of Google search results. This is on by default for maximum privacy, but if you like the Google Shopping results, you can turn blocking off."; +"this_blocks_amazon_referral_link_tracking" = "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon."; + +// MARK: - Limited Time Offer +"minutes_genitive" = "分"; +"seconds_genitive" = "秒"; +"lockdowns_first_christmas_sale" = "Lockdown 的首個聖誕促銷"; +"subscribe_now" = "立即訂閱"; + +// MARK: - Code Redemption +"redeem_code" = "兌換代碼"; +"cannot_redeem_code" = "無法兌換代碼"; +"the_code_could_not_be_redeemed" = "此代碼無法用來兌換。"; +"congratulations_excl" = "恭喜!"; +"the_code_was_successfully_redeemed" = "已成功兌換代碼。您可能需要重新開啟應用程式,此次購買才會生效。";