diff --git a/.bartycrouch.toml b/.bartycrouch.toml new file mode 100644 index 0000000..334dc53 --- /dev/null +++ b/.bartycrouch.toml @@ -0,0 +1,34 @@ +[update] +tasks = ["interfaces", "code", "transform", "normalize"] + +[update.interfaces] +paths = ["."] +defaultToBase = false +ignoreEmptyStrings = false +unstripped = false + +[update.code] +codePaths = ["."] +localizablePaths = ["."] +defaultToKeys = true +unstripped = false +plistArguments = true + +# [update.transform] +# codePaths = ["."] +# localizablePaths = ["."] +# transformer = "foundation" +# supportedLanguageEnumPath = "." +# typeName = "BartyCrouch" +# translateMethodName = "translate" + +[update.normalize] +paths = ["."] +sourceLocale = "en" +harmonizeWithSource = false +sortByKeys = false + +[lint] +paths = ["."] +duplicateKeys = true +emptyValues = true diff --git a/AccessLevel.swift b/AccessLevel.swift new file mode 100644 index 0000000..0e013f8 --- /dev/null +++ b/AccessLevel.swift @@ -0,0 +1,20 @@ +// +// AccessLevel.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +enum AccessLevel: Int, CaseIterable { + case basic + case advanced + case anonymous + case universal + + func hasFeature() -> Bool { + return true + } +} diff --git a/Assets.xcassets/AppIcon.appiconset/100.png b/Assets.xcassets/AppIcon.appiconset/100.png deleted file mode 100644 index d92e384..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/100.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/114.png b/Assets.xcassets/AppIcon.appiconset/114.png deleted file mode 100644 index 72bc77f..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/114.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/120-1.png b/Assets.xcassets/AppIcon.appiconset/120-1.png deleted file mode 100644 index 62d7174..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/120-1.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/120.png b/Assets.xcassets/AppIcon.appiconset/120.png deleted file mode 100644 index 62d7174..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/120.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/144.png b/Assets.xcassets/AppIcon.appiconset/144.png deleted file mode 100644 index e2820cb..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/144.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/152.png b/Assets.xcassets/AppIcon.appiconset/152.png deleted file mode 100644 index 5032512..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/152.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/167.png b/Assets.xcassets/AppIcon.appiconset/167.png deleted file mode 100644 index 348eb94..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/167.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/180.png b/Assets.xcassets/AppIcon.appiconset/180.png deleted file mode 100644 index 2088293..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/180.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/20.png b/Assets.xcassets/AppIcon.appiconset/20.png deleted file mode 100644 index b9886a7..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/20.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/29-1.png b/Assets.xcassets/AppIcon.appiconset/29-1.png deleted file mode 100644 index 18f66f6..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/29-1.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/29.png b/Assets.xcassets/AppIcon.appiconset/29.png deleted file mode 100644 index 18f66f6..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/29.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/40-1.png b/Assets.xcassets/AppIcon.appiconset/40-1.png deleted file mode 100644 index 2015aee..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/40-1.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/40-2.png b/Assets.xcassets/AppIcon.appiconset/40-2.png deleted file mode 100644 index 2015aee..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/40-2.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/40.png b/Assets.xcassets/AppIcon.appiconset/40.png deleted file mode 100644 index 2015aee..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/40.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/50.png b/Assets.xcassets/AppIcon.appiconset/50.png deleted file mode 100644 index 8f7c6a1..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/50.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/57.png b/Assets.xcassets/AppIcon.appiconset/57.png deleted file mode 100644 index 6b744e3..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/57.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/58-1.png b/Assets.xcassets/AppIcon.appiconset/58-1.png deleted file mode 100644 index eb6d7eb..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/58-1.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/58.png b/Assets.xcassets/AppIcon.appiconset/58.png deleted file mode 100644 index eb6d7eb..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/58.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/60.png b/Assets.xcassets/AppIcon.appiconset/60.png deleted file mode 100644 index 051ffab..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/60.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/72.png b/Assets.xcassets/AppIcon.appiconset/72.png deleted file mode 100644 index dc66e6c..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/72.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/76.png b/Assets.xcassets/AppIcon.appiconset/76.png deleted file mode 100644 index 5abd222..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/76.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/80-1.png b/Assets.xcassets/AppIcon.appiconset/80-1.png deleted file mode 100644 index b156cc8..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/80-1.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/80.png b/Assets.xcassets/AppIcon.appiconset/80.png deleted file mode 100644 index b156cc8..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/80.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/87.png b/Assets.xcassets/AppIcon.appiconset/87.png deleted file mode 100644 index 43c9d8d..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/87.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/Contents.json b/Assets.xcassets/AppIcon.appiconset/Contents.json index 7a83853..28d9cc7 100644 --- a/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,158 +1,158 @@ { "images" : [ { - "size" : "20x20", + "filename" : "notification-icon@2x.png", "idiom" : "iphone", - "filename" : "40.png", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { - "size" : "20x20", + "filename" : "notification-icon@3x.png", "idiom" : "iphone", - "filename" : "60.png", - "scale" : "3x" + "scale" : "3x", + "size" : "20x20" }, { - "size" : "29x29", + "filename" : "icon-small.png", "idiom" : "iphone", - "filename" : "29.png", - "scale" : "1x" + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", + "filename" : "icon-small@2x.png", "idiom" : "iphone", - "filename" : "58.png", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { - "size" : "29x29", + "filename" : "icon-small@3x.png", "idiom" : "iphone", - "filename" : "87.png", - "scale" : "3x" + "scale" : "3x", + "size" : "29x29" }, { - "size" : "40x40", + "filename" : "icon-40@2x.png", "idiom" : "iphone", - "filename" : "80.png", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { - "size" : "40x40", + "filename" : "icon-40@3x.png", "idiom" : "iphone", - "filename" : "120.png", - "scale" : "3x" + "scale" : "3x", + "size" : "40x40" }, { - "size" : "57x57", + "filename" : "icon.png", "idiom" : "iphone", - "filename" : "57.png", - "scale" : "1x" + "scale" : "1x", + "size" : "57x57" }, { - "size" : "57x57", + "filename" : "icon@2x.png", "idiom" : "iphone", - "filename" : "114.png", - "scale" : "2x" + "scale" : "2x", + "size" : "57x57" }, { - "size" : "60x60", + "filename" : "icon-60@2x.png", "idiom" : "iphone", - "filename" : "120-1.png", - "scale" : "2x" + "scale" : "2x", + "size" : "60x60" }, { - "size" : "60x60", + "filename" : "icon-60@3x.png", "idiom" : "iphone", - "filename" : "180.png", - "scale" : "3x" + "scale" : "3x", + "size" : "60x60" }, { - "size" : "20x20", + "filename" : "notification-icon~ipad.png", "idiom" : "ipad", - "filename" : "20.png", - "scale" : "1x" + "scale" : "1x", + "size" : "20x20" }, { - "size" : "20x20", + "filename" : "notification-icon~ipad@2x.png", "idiom" : "ipad", - "filename" : "40-1.png", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { - "size" : "29x29", + "filename" : "icon-small.png", "idiom" : "ipad", - "filename" : "29-1.png", - "scale" : "1x" + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", + "filename" : "icon-small@2x.png", "idiom" : "ipad", - "filename" : "58-1.png", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { - "size" : "40x40", + "filename" : "icon-40.png", "idiom" : "ipad", - "filename" : "40-2.png", - "scale" : "1x" + "scale" : "1x", + "size" : "40x40" }, { - "size" : "40x40", + "filename" : "icon-40@2x.png", "idiom" : "ipad", - "filename" : "80-1.png", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { - "size" : "50x50", + "filename" : "icon-small-50.png", "idiom" : "ipad", - "filename" : "50.png", - "scale" : "1x" + "scale" : "1x", + "size" : "50x50" }, { - "size" : "50x50", + "filename" : "icon-small-50@2x.png", "idiom" : "ipad", - "filename" : "100.png", - "scale" : "2x" + "scale" : "2x", + "size" : "50x50" }, { - "size" : "72x72", + "filename" : "icon-72.png", "idiom" : "ipad", - "filename" : "72.png", - "scale" : "1x" + "scale" : "1x", + "size" : "72x72" }, { - "size" : "72x72", + "filename" : "icon-72@2x.png", "idiom" : "ipad", - "filename" : "144.png", - "scale" : "2x" + "scale" : "2x", + "size" : "72x72" }, { - "size" : "76x76", + "filename" : "icon-76.png", "idiom" : "ipad", - "filename" : "76.png", - "scale" : "1x" + "scale" : "1x", + "size" : "76x76" }, { - "size" : "76x76", + "filename" : "icon-76@2x.png", "idiom" : "ipad", - "filename" : "152.png", - "scale" : "2x" + "scale" : "2x", + "size" : "76x76" }, { - "size" : "83.5x83.5", + "filename" : "icon-83.5@2x.png", "idiom" : "ipad", - "filename" : "167.png", - "scale" : "2x" + "scale" : "2x", + "size" : "83.5x83.5" }, { - "size" : "1024x1024", + "filename" : "ios-marketing.png", "idiom" : "ios-marketing", - "filename" : "Icon-1024.png", - "scale" : "1x" + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Assets.xcassets/AppIcon.appiconset/Icon-1024.png b/Assets.xcassets/AppIcon.appiconset/Icon-1024.png deleted file mode 100644 index 4247180..0000000 Binary files a/Assets.xcassets/AppIcon.appiconset/Icon-1024.png and /dev/null differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-40.png b/Assets.xcassets/AppIcon.appiconset/icon-40.png new file mode 100644 index 0000000..df37be7 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 0000000..e32b637 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 0000000..cc0ab7e Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 0000000..cc0ab7e Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 0000000..805fcc8 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-72.png b/Assets.xcassets/AppIcon.appiconset/icon-72.png new file mode 100644 index 0000000..b4c5244 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-72.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png b/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png new file mode 100644 index 0000000..ef55962 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-76.png b/Assets.xcassets/AppIcon.appiconset/icon-76.png new file mode 100644 index 0000000..beb0e66 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 0000000..e20ae82 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 0000000..a829af4 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-small-50.png b/Assets.xcassets/AppIcon.appiconset/icon-small-50.png new file mode 100644 index 0000000..766512b Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-small-50.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png b/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png new file mode 100644 index 0000000..9b5f1b4 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-small.png b/Assets.xcassets/AppIcon.appiconset/icon-small.png new file mode 100644 index 0000000..a234b36 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-small.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png b/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png new file mode 100644 index 0000000..c26bed0 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png b/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png new file mode 100644 index 0000000..173686b Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon.png b/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 0000000..91e2ffe Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/icon@2x.png b/Assets.xcassets/AppIcon.appiconset/icon@2x.png new file mode 100644 index 0000000..4eb75dd Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/icon@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/ios-marketing.png b/Assets.xcassets/AppIcon.appiconset/ios-marketing.png new file mode 100644 index 0000000..8a0250e Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/ios-marketing.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png b/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png new file mode 100644 index 0000000..df37be7 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png b/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png new file mode 100644 index 0000000..d77b643 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png b/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png new file mode 100644 index 0000000..be9e6b9 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png differ diff --git a/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png b/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png new file mode 100644 index 0000000..df37be7 Binary files /dev/null and b/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png differ diff --git a/Assets.xcassets/Block Lists/Advanced Lists/Contents.json b/Assets.xcassets/Block Lists/Advanced Lists/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Block Lists/Advanced Lists/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn-junes-journey.imageset/Contents.json b/Assets.xcassets/Block Lists/Advanced Lists/icn-junes-journey.imageset/Contents.json new file mode 100644 index 0000000..b63ef64 --- /dev/null +++ b/Assets.xcassets/Block Lists/Advanced Lists/icn-junes-journey.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn-junes-journey.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn-junes-journey.imageset/icn-junes-journey.pdf b/Assets.xcassets/Block Lists/Advanced Lists/icn-junes-journey.imageset/icn-junes-journey.pdf new file mode 100644 index 0000000..adf0c2a Binary files /dev/null and b/Assets.xcassets/Block Lists/Advanced Lists/icn-junes-journey.imageset/icn-junes-journey.pdf differ diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_analytics.imageset/Contents.json b/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_analytics.imageset/Contents.json new file mode 100644 index 0000000..94a2b43 --- /dev/null +++ b/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_analytics.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_advanced_analytics.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_analytics.imageset/icn_advanced_analytics.pdf b/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_analytics.imageset/icn_advanced_analytics.pdf new file mode 100644 index 0000000..20c3b8f Binary files /dev/null and b/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_analytics.imageset/icn_advanced_analytics.pdf differ diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_gaming.imageset/Contents.json b/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_gaming.imageset/Contents.json new file mode 100644 index 0000000..268276f --- /dev/null +++ b/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_gaming.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_advanced_gaming.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_gaming.imageset/icn_advanced_gaming.pdf b/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_gaming.imageset/icn_advanced_gaming.pdf new file mode 100644 index 0000000..350bc42 Binary files /dev/null and b/Assets.xcassets/Block Lists/Advanced Lists/icn_advanced_gaming.imageset/icn_advanced_gaming.pdf differ diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_ifunny.imageset/Contents.json b/Assets.xcassets/Block Lists/Advanced Lists/icn_ifunny.imageset/Contents.json new file mode 100644 index 0000000..c89b3f9 --- /dev/null +++ b/Assets.xcassets/Block Lists/Advanced Lists/icn_ifunny.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_ifunny.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_ifunny.imageset/icn_ifunny.pdf b/Assets.xcassets/Block Lists/Advanced Lists/icn_ifunny.imageset/icn_ifunny.pdf new file mode 100644 index 0000000..6002b89 Binary files /dev/null and b/Assets.xcassets/Block Lists/Advanced Lists/icn_ifunny.imageset/icn_ifunny.pdf differ diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_scams.imageset/Contents.json b/Assets.xcassets/Block Lists/Advanced Lists/icn_scams.imageset/Contents.json new file mode 100644 index 0000000..41b559a --- /dev/null +++ b/Assets.xcassets/Block Lists/Advanced Lists/icn_scams.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_scams.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_scams.imageset/icn_scams.pdf b/Assets.xcassets/Block Lists/Advanced Lists/icn_scams.imageset/icn_scams.pdf new file mode 100644 index 0000000..7bfe4a6 Binary files /dev/null and b/Assets.xcassets/Block Lists/Advanced Lists/icn_scams.imageset/icn_scams.pdf differ diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_tiktok.imageset/Contents.json b/Assets.xcassets/Block Lists/Advanced Lists/icn_tiktok.imageset/Contents.json new file mode 100644 index 0000000..34e6f76 --- /dev/null +++ b/Assets.xcassets/Block Lists/Advanced Lists/icn_tiktok.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_tiktok.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Block Lists/Advanced Lists/icn_tiktok.imageset/icn_tiktok.pdf b/Assets.xcassets/Block Lists/Advanced Lists/icn_tiktok.imageset/icn_tiktok.pdf new file mode 100644 index 0000000..08f8622 Binary files /dev/null and b/Assets.xcassets/Block Lists/Advanced Lists/icn_tiktok.imageset/icn_tiktok.pdf differ diff --git a/Assets.xcassets/Block Lists/Contents.json b/Assets.xcassets/Block Lists/Contents.json index da4a164..73c0059 100644 --- a/Assets.xcassets/Block Lists/Contents.json +++ b/Assets.xcassets/Block Lists/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Assets.xcassets/Block Lists/ads_icon.imageset/Contents.json b/Assets.xcassets/Block Lists/ads_icon.imageset/Contents.json new file mode 100644 index 0000000..3ae77e6 --- /dev/null +++ b/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/Assets.xcassets/Block Lists/ads_icon.imageset/ads-1.png b/Assets.xcassets/Block Lists/ads_icon.imageset/ads-1.png new file mode 100644 index 0000000..0d9c199 Binary files /dev/null and b/Assets.xcassets/Block Lists/ads_icon.imageset/ads-1.png differ diff --git a/Assets.xcassets/Block Lists/ads_icon.imageset/ads-2.png b/Assets.xcassets/Block Lists/ads_icon.imageset/ads-2.png new file mode 100644 index 0000000..0d9c199 Binary files /dev/null and b/Assets.xcassets/Block Lists/ads_icon.imageset/ads-2.png differ diff --git a/Assets.xcassets/Block Lists/ads_icon.imageset/ads.png b/Assets.xcassets/Block Lists/ads_icon.imageset/ads.png new file mode 100644 index 0000000..0d9c199 Binary files /dev/null and b/Assets.xcassets/Block Lists/ads_icon.imageset/ads.png differ diff --git a/Assets.xcassets/Block Lists/amazon_icon.imageset/Contents.json b/Assets.xcassets/Block Lists/amazon_icon.imageset/Contents.json new file mode 100644 index 0000000..01e1bfa --- /dev/null +++ b/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/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-1.png b/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-1.png new file mode 100644 index 0000000..420ad0e Binary files /dev/null and b/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-1.png differ diff --git a/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-2.png b/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-2.png new file mode 100644 index 0000000..420ad0e Binary files /dev/null and b/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg-2.png differ diff --git a/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg.png b/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg.png new file mode 100644 index 0000000..420ad0e Binary files /dev/null and b/Assets.xcassets/Block Lists/amazon_icon.imageset/pngegg.png differ diff --git a/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Contents.json b/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Contents.json index 6f0edd2..32f8535 100644 --- a/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Contents.json +++ b/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "Logos-Facebook-icon.png", + "filename" : "facebook_white_icon.png", "scale" : "1x" }, { diff --git a/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Logos-Facebook-icon.png b/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Logos-Facebook-icon.png deleted file mode 100644 index d7db7af..0000000 Binary files a/Assets.xcassets/Block Lists/facebook_white_icon.imageset/Logos-Facebook-icon.png and /dev/null differ diff --git a/Assets.xcassets/Block Lists/facebook_white_icon.imageset/facebook_white_icon.png b/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/Assets.xcassets/Block Lists/facebook_white_icon.imageset/facebook_white_icon.png differ diff --git a/Assets.xcassets/power_button.imageset/Contents.json b/Assets.xcassets/Block Lists/game_ads_icon.imageset/Contents.json similarity index 69% rename from Assets.xcassets/power_button.imageset/Contents.json rename to Assets.xcassets/Block Lists/game_ads_icon.imageset/Contents.json index ce9a723..7c88ab0 100644 --- a/Assets.xcassets/power_button.imageset/Contents.json +++ b/Assets.xcassets/Block Lists/game_ads_icon.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "power-button.png", + "filename" : "game_ads.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "power-button-1.png", + "filename" : "game_ads_icon-1.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "power-button-2.png", + "filename" : "game_ads_icon-2.png", "scale" : "3x" } ], diff --git a/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads.png b/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads.png new file mode 100644 index 0000000..b8ae567 Binary files /dev/null and b/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads.png differ diff --git a/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-1.png b/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/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-1.png differ diff --git a/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-2.png b/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/Assets.xcassets/Block Lists/game_ads_icon.imageset/game_ads_icon-2.png differ diff --git a/Assets.xcassets/safari.imageset/Contents.json b/Assets.xcassets/Block Lists/google_icon.imageset/Contents.json similarity index 72% rename from Assets.xcassets/safari.imageset/Contents.json rename to Assets.xcassets/Block Lists/google_icon.imageset/Contents.json index e9db547..afbe6f7 100644 --- a/Assets.xcassets/safari.imageset/Contents.json +++ b/Assets.xcassets/Block Lists/google_icon.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "safari.png", + "filename" : "google-1.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "safari-1.png", + "filename" : "google-2.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "safari-2.png", + "filename" : "google.png", "scale" : "3x" } ], diff --git a/Assets.xcassets/Block Lists/google_icon.imageset/google-1.png b/Assets.xcassets/Block Lists/google_icon.imageset/google-1.png new file mode 100644 index 0000000..35e7062 Binary files /dev/null and b/Assets.xcassets/Block Lists/google_icon.imageset/google-1.png differ diff --git a/Assets.xcassets/Block Lists/google_icon.imageset/google-2.png b/Assets.xcassets/Block Lists/google_icon.imageset/google-2.png new file mode 100644 index 0000000..35e7062 Binary files /dev/null and b/Assets.xcassets/Block Lists/google_icon.imageset/google-2.png differ diff --git a/Assets.xcassets/Block Lists/google_icon.imageset/google.png b/Assets.xcassets/Block Lists/google_icon.imageset/google.png new file mode 100644 index 0000000..35e7062 Binary files /dev/null and b/Assets.xcassets/Block Lists/google_icon.imageset/google.png differ diff --git a/Assets.xcassets/Block Lists/marketing_icon.imageset/Contents.json b/Assets.xcassets/Block Lists/marketing_icon.imageset/Contents.json index a13fb58..30e09a5 100644 --- a/Assets.xcassets/Block Lists/marketing_icon.imageset/Contents.json +++ b/Assets.xcassets/Block Lists/marketing_icon.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "noun_Magnifying Glass_251109.png", + "filename" : "marketing.png", "scale" : "1x" }, { diff --git a/Assets.xcassets/Block Lists/marketing_icon.imageset/marketing.png b/Assets.xcassets/Block Lists/marketing_icon.imageset/marketing.png new file mode 100644 index 0000000..f065691 Binary files /dev/null and b/Assets.xcassets/Block Lists/marketing_icon.imageset/marketing.png differ diff --git a/Assets.xcassets/Block Lists/marketing_icon.imageset/noun_Magnifying Glass_251109.png b/Assets.xcassets/Block Lists/marketing_icon.imageset/noun_Magnifying Glass_251109.png deleted file mode 100644 index 354ffeb..0000000 Binary files a/Assets.xcassets/Block Lists/marketing_icon.imageset/noun_Magnifying Glass_251109.png and /dev/null differ diff --git a/Assets.xcassets/Block Lists/ransomware_icon.imageset/Contents.json b/Assets.xcassets/Block Lists/ransomware_icon.imageset/Contents.json index 5a786cb..8072c85 100644 --- a/Assets.xcassets/Block Lists/ransomware_icon.imageset/Contents.json +++ b/Assets.xcassets/Block Lists/ransomware_icon.imageset/Contents.json @@ -2,7 +2,7 @@ "images" : [ { "idiom" : "universal", - "filename" : "ransomware-2.png", + "filename" : "ransomware.png", "scale" : "1x" }, { diff --git a/Assets.xcassets/Block Lists/ransomware_icon.imageset/ransomware-2.png b/Assets.xcassets/Block Lists/ransomware_icon.imageset/ransomware.png similarity index 100% rename from Assets.xcassets/Block Lists/ransomware_icon.imageset/ransomware-2.png rename to Assets.xcassets/Block Lists/ransomware_icon.imageset/ransomware.png diff --git a/Assets.xcassets/Block Lists/reporting_icon.imageset/Contents.json b/Assets.xcassets/Block Lists/reporting_icon.imageset/Contents.json new file mode 100644 index 0000000..803383c --- /dev/null +++ b/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/Assets.xcassets/Block Lists/reporting_icon.imageset/report-1.png b/Assets.xcassets/Block Lists/reporting_icon.imageset/report-1.png new file mode 100644 index 0000000..1a2deff Binary files /dev/null and b/Assets.xcassets/Block Lists/reporting_icon.imageset/report-1.png differ diff --git a/Assets.xcassets/Block Lists/reporting_icon.imageset/report-2.png b/Assets.xcassets/Block Lists/reporting_icon.imageset/report-2.png new file mode 100644 index 0000000..1a2deff Binary files /dev/null and b/Assets.xcassets/Block Lists/reporting_icon.imageset/report-2.png differ diff --git a/Assets.xcassets/Block Lists/reporting_icon.imageset/report.png b/Assets.xcassets/Block Lists/reporting_icon.imageset/report.png new file mode 100644 index 0000000..1a2deff Binary files /dev/null and b/Assets.xcassets/Block Lists/reporting_icon.imageset/report.png differ diff --git a/Assets.xcassets/menu.imageset/Contents.json b/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/Contents.json similarity index 69% rename from Assets.xcassets/menu.imageset/Contents.json rename to Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/Contents.json index 292b849..0a10d3c 100644 --- a/Assets.xcassets/menu.imageset/Contents.json +++ b/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/Contents.json @@ -2,17 +2,17 @@ "images" : [ { "idiom" : "universal", - "filename" : "menu-button.png", + "filename" : "ghost_1f47b.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "menu-button-1.png", + "filename" : "ghost_1f47b-1.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "menu-button-2.png", + "filename" : "ghost_1f47b-2.png", "scale" : "3x" } ], diff --git a/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-1.png b/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/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-1.png differ diff --git a/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-2.png b/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/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b-2.png differ diff --git a/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b.png b/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b.png new file mode 100644 index 0000000..c936ed8 Binary files /dev/null and b/Assets.xcassets/Block Lists/snapchat_analytics_icon.imageset/ghost_1f47b.png differ diff --git a/Assets.xcassets/Block Lists/user_data_icon.imageset/Contents.json b/Assets.xcassets/Block Lists/user_data_icon.imageset/Contents.json new file mode 100644 index 0000000..36425c7 --- /dev/null +++ b/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/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-1.png b/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/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-1.png differ diff --git a/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-2.png b/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/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon-2.png differ diff --git a/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon.png b/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/Assets.xcassets/Block Lists/user_data_icon.imageset/user-data-icon.png differ diff --git a/Assets.xcassets/Configure Blocking/Contents.json b/Assets.xcassets/Configure Blocking/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Configure Blocking/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Configure Blocking/icn_create_list.imageset/Contents.json b/Assets.xcassets/Configure Blocking/icn_create_list.imageset/Contents.json new file mode 100644 index 0000000..f36680e --- /dev/null +++ b/Assets.xcassets/Configure Blocking/icn_create_list.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_create_list.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Configure Blocking/icn_create_list.imageset/icn_create_list.pdf b/Assets.xcassets/Configure Blocking/icn_create_list.imageset/icn_create_list.pdf new file mode 100644 index 0000000..4f5144e Binary files /dev/null and b/Assets.xcassets/Configure Blocking/icn_create_list.imageset/icn_create_list.pdf differ diff --git a/Assets.xcassets/Configure Blocking/icn_csv_file.imageset/Contents.json b/Assets.xcassets/Configure Blocking/icn_csv_file.imageset/Contents.json new file mode 100644 index 0000000..0d49867 --- /dev/null +++ b/Assets.xcassets/Configure Blocking/icn_csv_file.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_csv_file.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Configure Blocking/icn_csv_file.imageset/icn_csv_file.pdf b/Assets.xcassets/Configure Blocking/icn_csv_file.imageset/icn_csv_file.pdf new file mode 100644 index 0000000..888e023 Binary files /dev/null and b/Assets.xcassets/Configure Blocking/icn_csv_file.imageset/icn_csv_file.pdf differ diff --git a/Assets.xcassets/Configure Blocking/icn_edit.imageset/Contents.json b/Assets.xcassets/Configure Blocking/icn_edit.imageset/Contents.json new file mode 100644 index 0000000..a833796 --- /dev/null +++ b/Assets.xcassets/Configure Blocking/icn_edit.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_edit.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Configure Blocking/icn_edit.imageset/icn_edit.pdf b/Assets.xcassets/Configure Blocking/icn_edit.imageset/icn_edit.pdf new file mode 100644 index 0000000..cf55e13 Binary files /dev/null and b/Assets.xcassets/Configure Blocking/icn_edit.imageset/icn_edit.pdf differ diff --git a/Assets.xcassets/Configure Blocking/icn_export_folder.imageset/Contents.json b/Assets.xcassets/Configure Blocking/icn_export_folder.imageset/Contents.json new file mode 100644 index 0000000..0ee3fd3 --- /dev/null +++ b/Assets.xcassets/Configure Blocking/icn_export_folder.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_export_folder.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Configure Blocking/icn_export_folder.imageset/icn_export_folder.pdf b/Assets.xcassets/Configure Blocking/icn_export_folder.imageset/icn_export_folder.pdf new file mode 100644 index 0000000..dc1563a Binary files /dev/null and b/Assets.xcassets/Configure Blocking/icn_export_folder.imageset/icn_export_folder.pdf differ diff --git a/Assets.xcassets/Configure Blocking/icn_import_list.imageset/Contents.json b/Assets.xcassets/Configure Blocking/icn_import_list.imageset/Contents.json new file mode 100644 index 0000000..39d43d8 --- /dev/null +++ b/Assets.xcassets/Configure Blocking/icn_import_list.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_import_list.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Configure Blocking/icn_import_list.imageset/icn_import_list.pdf b/Assets.xcassets/Configure Blocking/icn_import_list.imageset/icn_import_list.pdf new file mode 100644 index 0000000..b461e4b Binary files /dev/null and b/Assets.xcassets/Configure Blocking/icn_import_list.imageset/icn_import_list.pdf differ diff --git a/Assets.xcassets/Configure Blocking/icn_list_lock.imageset/Contents.json b/Assets.xcassets/Configure Blocking/icn_list_lock.imageset/Contents.json new file mode 100644 index 0000000..b4129a9 --- /dev/null +++ b/Assets.xcassets/Configure Blocking/icn_list_lock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_list_lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Configure Blocking/icn_list_lock.imageset/icn_list_lock.pdf b/Assets.xcassets/Configure Blocking/icn_list_lock.imageset/icn_list_lock.pdf new file mode 100644 index 0000000..e391f4a Binary files /dev/null and b/Assets.xcassets/Configure Blocking/icn_list_lock.imageset/icn_list_lock.pdf differ diff --git a/Assets.xcassets/Configure Blocking/icn_trash.imageset/Contents.json b/Assets.xcassets/Configure Blocking/icn_trash.imageset/Contents.json new file mode 100644 index 0000000..795b1b0 --- /dev/null +++ b/Assets.xcassets/Configure Blocking/icn_trash.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_trash.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Configure Blocking/icn_trash.imageset/icn_trash.pdf b/Assets.xcassets/Configure Blocking/icn_trash.imageset/icn_trash.pdf new file mode 100644 index 0000000..1951a0c Binary files /dev/null and b/Assets.xcassets/Configure Blocking/icn_trash.imageset/icn_trash.pdf differ diff --git a/Assets.xcassets/Confirmed Blue.colorset/Contents.json b/Assets.xcassets/Confirmed Blue.colorset/Contents.json new file mode 100644 index 0000000..82be4f6 --- /dev/null +++ b/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/Assets.xcassets/Contents.json b/Assets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/Assets.xcassets/Contents.json +++ b/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Assets.xcassets/Firewall/Contents.json b/Assets.xcassets/Firewall/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Firewall/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/Contents.json b/Assets.xcassets/Firewall/Subscription Plans/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Firewall/Subscription Plans/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/Contents.json b/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/Contents.json new file mode 100644 index 0000000..c1dba7d --- /dev/null +++ b/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "advanced.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "advanced_black_active.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/advanced.pdf b/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/advanced.pdf new file mode 100644 index 0000000..73ef19c Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/advanced.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/advanced_black_active.pdf b/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/advanced_black_active.pdf new file mode 100644 index 0000000..7c7ce87 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/advanced_active.imageset/advanced_black_active.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/Contents.json b/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/Contents.json new file mode 100644 index 0000000..c0f94b5 --- /dev/null +++ b/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "advanced_nonactive.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "advanced_black_nonactive.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/advanced_black_nonactive.pdf b/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/advanced_black_nonactive.pdf new file mode 100644 index 0000000..92c557a Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/advanced_black_nonactive.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/advanced_nonactive.pdf b/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/advanced_nonactive.pdf new file mode 100644 index 0000000..c9166b7 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/advanced_nonactive.imageset/advanced_nonactive.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/Contents.json b/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/Contents.json new file mode 100644 index 0000000..879bfa8 --- /dev/null +++ b/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "anonymous_active.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "anonymous_black_active.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/anonymous_active.pdf b/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/anonymous_active.pdf new file mode 100644 index 0000000..998de32 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/anonymous_active.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/anonymous_black_active.pdf b/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/anonymous_black_active.pdf new file mode 100644 index 0000000..7c7ce87 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/anonymous_active.imageset/anonymous_black_active.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/Contents.json b/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/Contents.json new file mode 100644 index 0000000..e8d440e --- /dev/null +++ b/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "anonymous.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "anonymous_black_nonactive.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/anonymous.pdf b/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/anonymous.pdf new file mode 100644 index 0000000..75f424a Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/anonymous.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/anonymous_black_nonactive.pdf b/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/anonymous_black_nonactive.pdf new file mode 100644 index 0000000..975d6c6 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/anonymous_nonactive.imageset/anonymous_black_nonactive.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/Contents.json b/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/Contents.json new file mode 100644 index 0000000..a4935dd --- /dev/null +++ b/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "basic.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "basic_black.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/basic.pdf b/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/basic.pdf new file mode 100644 index 0000000..baba656 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/basic.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/basic_black.pdf b/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/basic_black.pdf new file mode 100644 index 0000000..fe4f364 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/basic_active.imageset/basic_black.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/Contents.json b/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/Contents.json new file mode 100644 index 0000000..ce61930 --- /dev/null +++ b/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "universal_active.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "universal_black_active.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/universal_active.pdf b/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/universal_active.pdf new file mode 100644 index 0000000..b48ccfc Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/universal_active.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/universal_black_active.pdf b/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/universal_black_active.pdf new file mode 100644 index 0000000..8001959 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/universal_active.imageset/universal_black_active.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/Contents.json b/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/Contents.json new file mode 100644 index 0000000..e62cc47 --- /dev/null +++ b/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "universal.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "universal_black_nonactive.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/universal.pdf b/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/universal.pdf new file mode 100644 index 0000000..a2ce090 Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/universal.pdf differ diff --git a/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/universal_black_nonactive.pdf b/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/universal_black_nonactive.pdf new file mode 100644 index 0000000..8bd98ac Binary files /dev/null and b/Assets.xcassets/Firewall/Subscription Plans/universal_nonactive.imageset/universal_black_nonactive.pdf differ diff --git a/Assets.xcassets/Firewall/firewall-off-image.imageset/Contents.json b/Assets.xcassets/Firewall/firewall-off-image.imageset/Contents.json new file mode 100644 index 0000000..f7b6750 --- /dev/null +++ b/Assets.xcassets/Firewall/firewall-off-image.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "off-image.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "firewall-off-image-black.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/firewall-off-image.imageset/firewall-off-image-black.pdf b/Assets.xcassets/Firewall/firewall-off-image.imageset/firewall-off-image-black.pdf new file mode 100644 index 0000000..4ff5945 Binary files /dev/null and b/Assets.xcassets/Firewall/firewall-off-image.imageset/firewall-off-image-black.pdf differ diff --git a/Assets.xcassets/Firewall/firewall-off-image.imageset/off-image.pdf b/Assets.xcassets/Firewall/firewall-off-image.imageset/off-image.pdf new file mode 100644 index 0000000..198918a Binary files /dev/null and b/Assets.xcassets/Firewall/firewall-off-image.imageset/off-image.pdf differ diff --git a/Assets.xcassets/Firewall/firewall-on-image.imageset/Contents.json b/Assets.xcassets/Firewall/firewall-on-image.imageset/Contents.json new file mode 100644 index 0000000..263ba15 --- /dev/null +++ b/Assets.xcassets/Firewall/firewall-on-image.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "on-image.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "firewall-on-image-black.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/firewall-on-image.imageset/firewall-on-image-black.pdf b/Assets.xcassets/Firewall/firewall-on-image.imageset/firewall-on-image-black.pdf new file mode 100644 index 0000000..f1f893e Binary files /dev/null and b/Assets.xcassets/Firewall/firewall-on-image.imageset/firewall-on-image-black.pdf differ diff --git a/Assets.xcassets/Firewall/firewall-on-image.imageset/on-image.pdf b/Assets.xcassets/Firewall/firewall-on-image.imageset/on-image.pdf new file mode 100644 index 0000000..3e195a6 Binary files /dev/null and b/Assets.xcassets/Firewall/firewall-on-image.imageset/on-image.pdf differ diff --git a/Assets.xcassets/Firewall/icn_clickbait_trackers.imageset/Contents.json b/Assets.xcassets/Firewall/icn_clickbait_trackers.imageset/Contents.json new file mode 100644 index 0000000..12fcb9b --- /dev/null +++ b/Assets.xcassets/Firewall/icn_clickbait_trackers.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_clickbait_trackers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/icn_clickbait_trackers.imageset/icn_clickbait_trackers.pdf b/Assets.xcassets/Firewall/icn_clickbait_trackers.imageset/icn_clickbait_trackers.pdf new file mode 100644 index 0000000..a4db3f9 Binary files /dev/null and b/Assets.xcassets/Firewall/icn_clickbait_trackers.imageset/icn_clickbait_trackers.pdf differ diff --git a/Assets.xcassets/Firewall/icn_data_trackers.imageset/Contents.json b/Assets.xcassets/Firewall/icn_data_trackers.imageset/Contents.json new file mode 100644 index 0000000..83f28eb --- /dev/null +++ b/Assets.xcassets/Firewall/icn_data_trackers.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_data_trackers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/icn_data_trackers.imageset/icn_data_trackers.pdf b/Assets.xcassets/Firewall/icn_data_trackers.imageset/icn_data_trackers.pdf new file mode 100644 index 0000000..ab53b36 Binary files /dev/null and b/Assets.xcassets/Firewall/icn_data_trackers.imageset/icn_data_trackers.pdf differ diff --git a/Assets.xcassets/Firewall/icn_email_trackers.imageset/Contents.json b/Assets.xcassets/Firewall/icn_email_trackers.imageset/Contents.json new file mode 100644 index 0000000..7f661cf --- /dev/null +++ b/Assets.xcassets/Firewall/icn_email_trackers.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_email_trackers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/icn_email_trackers.imageset/icn_email_trackers.pdf b/Assets.xcassets/Firewall/icn_email_trackers.imageset/icn_email_trackers.pdf new file mode 100644 index 0000000..fe9ff8b Binary files /dev/null and b/Assets.xcassets/Firewall/icn_email_trackers.imageset/icn_email_trackers.pdf differ diff --git a/Assets.xcassets/Firewall/icn_facebook_trackers.imageset/Contents.json b/Assets.xcassets/Firewall/icn_facebook_trackers.imageset/Contents.json new file mode 100644 index 0000000..a37ecd0 --- /dev/null +++ b/Assets.xcassets/Firewall/icn_facebook_trackers.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_facebook_trackers.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/icn_facebook_trackers.imageset/icn_facebook_trackers.pdf b/Assets.xcassets/Firewall/icn_facebook_trackers.imageset/icn_facebook_trackers.pdf new file mode 100644 index 0000000..f914962 Binary files /dev/null and b/Assets.xcassets/Firewall/icn_facebook_trackers.imageset/icn_facebook_trackers.pdf differ diff --git a/Assets.xcassets/Firewall/icn_game_marketing.imageset/Contents.json b/Assets.xcassets/Firewall/icn_game_marketing.imageset/Contents.json new file mode 100644 index 0000000..19787c4 --- /dev/null +++ b/Assets.xcassets/Firewall/icn_game_marketing.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_game_marketing.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/icn_game_marketing.imageset/icn_game_marketing.pdf b/Assets.xcassets/Firewall/icn_game_marketing.imageset/icn_game_marketing.pdf new file mode 100644 index 0000000..87d6134 Binary files /dev/null and b/Assets.xcassets/Firewall/icn_game_marketing.imageset/icn_game_marketing.pdf differ diff --git a/Assets.xcassets/Firewall/icn_lock.imageset/Contents.json b/Assets.xcassets/Firewall/icn_lock.imageset/Contents.json new file mode 100644 index 0000000..960edaa --- /dev/null +++ b/Assets.xcassets/Firewall/icn_lock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/icn_lock.imageset/icn_lock.pdf b/Assets.xcassets/Firewall/icn_lock.imageset/icn_lock.pdf new file mode 100644 index 0000000..bd2a190 Binary files /dev/null and b/Assets.xcassets/Firewall/icn_lock.imageset/icn_lock.pdf differ diff --git a/Assets.xcassets/Firewall/icn_marketing_trackers.imageset/Contents.json b/Assets.xcassets/Firewall/icn_marketing_trackers.imageset/Contents.json new file mode 100644 index 0000000..d95e161 --- /dev/null +++ b/Assets.xcassets/Firewall/icn_marketing_trackers.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_marketing.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/icn_marketing_trackers.imageset/icn_marketing.pdf b/Assets.xcassets/Firewall/icn_marketing_trackers.imageset/icn_marketing.pdf new file mode 100644 index 0000000..c676776 Binary files /dev/null and b/Assets.xcassets/Firewall/icn_marketing_trackers.imageset/icn_marketing.pdf differ diff --git a/Assets.xcassets/Firewall/saveDiscount.imageset/Contents.json b/Assets.xcassets/Firewall/saveDiscount.imageset/Contents.json new file mode 100644 index 0000000..3c85e33 --- /dev/null +++ b/Assets.xcassets/Firewall/saveDiscount.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Status indicator.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Firewall/saveDiscount.imageset/Status indicator.png b/Assets.xcassets/Firewall/saveDiscount.imageset/Status indicator.png new file mode 100644 index 0000000..d3ff5be Binary files /dev/null and b/Assets.xcassets/Firewall/saveDiscount.imageset/Status indicator.png differ diff --git a/Assets.xcassets/Icon-Alt/Contents.json b/Assets.xcassets/Icon-Alt/Contents.json index da4a164..73c0059 100644 --- a/Assets.xcassets/Icon-Alt/Contents.json +++ b/Assets.xcassets/Icon-Alt/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Assets.xcassets/Onboarding/Contents.json b/Assets.xcassets/Onboarding/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Onboarding/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Onboarding/onboardingCheckmark.imageset/Contents.json b/Assets.xcassets/Onboarding/onboardingCheckmark.imageset/Contents.json new file mode 100644 index 0000000..c6f97a0 --- /dev/null +++ b/Assets.xcassets/Onboarding/onboardingCheckmark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Vector.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Onboarding/onboardingCheckmark.imageset/Vector.pdf b/Assets.xcassets/Onboarding/onboardingCheckmark.imageset/Vector.pdf new file mode 100644 index 0000000..1add02b Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingCheckmark.imageset/Vector.pdf differ diff --git a/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71.jpg b/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71.jpg new file mode 100644 index 0000000..3a2e550 Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71@2x.jpg b/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71@2x.jpg new file mode 100644 index 0000000..f004cb3 Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71@2x.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71@3x.jpg b/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71@3x.jpg new file mode 100644 index 0000000..6a30c70 Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingPaywall.imageset/71@3x.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingPaywall.imageset/Contents.json b/Assets.xcassets/Onboarding/onboardingPaywall.imageset/Contents.json new file mode 100644 index 0000000..c67239f --- /dev/null +++ b/Assets.xcassets/Onboarding/onboardingPaywall.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "71.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "71@2x.jpg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "71@3x.jpg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Onboarding/onboardingStep1.imageset/72.jpg b/Assets.xcassets/Onboarding/onboardingStep1.imageset/72.jpg new file mode 100644 index 0000000..904ab2a Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingStep1.imageset/72.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingStep1.imageset/72@2x.jpg b/Assets.xcassets/Onboarding/onboardingStep1.imageset/72@2x.jpg new file mode 100644 index 0000000..6b5759f Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingStep1.imageset/72@2x.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingStep1.imageset/72@3x.jpg b/Assets.xcassets/Onboarding/onboardingStep1.imageset/72@3x.jpg new file mode 100644 index 0000000..796c3f0 Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingStep1.imageset/72@3x.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingStep1.imageset/Contents.json b/Assets.xcassets/Onboarding/onboardingStep1.imageset/Contents.json new file mode 100644 index 0000000..e495ce7 --- /dev/null +++ b/Assets.xcassets/Onboarding/onboardingStep1.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "72.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "72@2x.jpg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "72@3x.jpg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Onboarding/onboardingStep2.imageset/73.jpg b/Assets.xcassets/Onboarding/onboardingStep2.imageset/73.jpg new file mode 100644 index 0000000..7dff8de Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingStep2.imageset/73.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingStep2.imageset/73@2x.jpg b/Assets.xcassets/Onboarding/onboardingStep2.imageset/73@2x.jpg new file mode 100644 index 0000000..bdc9c73 Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingStep2.imageset/73@2x.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingStep2.imageset/73@3x.jpg b/Assets.xcassets/Onboarding/onboardingStep2.imageset/73@3x.jpg new file mode 100644 index 0000000..bef30ef Binary files /dev/null and b/Assets.xcassets/Onboarding/onboardingStep2.imageset/73@3x.jpg differ diff --git a/Assets.xcassets/Onboarding/onboardingStep2.imageset/Contents.json b/Assets.xcassets/Onboarding/onboardingStep2.imageset/Contents.json new file mode 100644 index 0000000..c2eb643 --- /dev/null +++ b/Assets.xcassets/Onboarding/onboardingStep2.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "73.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "73@2x.jpg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "73@3x.jpg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Onboarding/splashBackground.imageset/Contents.json b/Assets.xcassets/Onboarding/splashBackground.imageset/Contents.json new file mode 100644 index 0000000..00d7fd1 --- /dev/null +++ b/Assets.xcassets/Onboarding/splashBackground.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "splashBackground.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "splashBackground@2x.jpg", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "splashBackground@3x.jpg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground.jpg b/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground.jpg new file mode 100644 index 0000000..f659bd9 Binary files /dev/null and b/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground.jpg differ diff --git a/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground@2x.jpg b/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground@2x.jpg new file mode 100644 index 0000000..1599514 Binary files /dev/null and b/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground@2x.jpg differ diff --git a/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground@3x.jpg b/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground@3x.jpg new file mode 100644 index 0000000..19fb101 Binary files /dev/null and b/Assets.xcassets/Onboarding/splashBackground.imageset/splashBackground@3x.jpg differ diff --git a/Assets.xcassets/Panel Background.colorset/Contents.json b/Assets.xcassets/Panel Background.colorset/Contents.json new file mode 100644 index 0000000..dc38378 --- /dev/null +++ b/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/Assets.xcassets/Panel Secondary Background.colorset/Contents.json b/Assets.xcassets/Panel Secondary Background.colorset/Contents.json new file mode 100644 index 0000000..ec3e8d4 --- /dev/null +++ b/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/Assets.xcassets/Paywall/Contents.json b/Assets.xcassets/Paywall/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Paywall/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/Feadback/Contents.json b/Assets.xcassets/Paywall/Feadback/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Paywall/Feadback/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Contents.json new file mode 100644 index 0000000..0f1e1b9 --- /dev/null +++ b/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Vector.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Vector.pdf b/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Vector.pdf new file mode 100644 index 0000000..92cbdd8 Binary files /dev/null and b/Assets.xcassets/Paywall/Feadback/feedback-checkmark.imageset/Vector.pdf differ diff --git a/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Arrow Ramp Right.svg b/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Arrow Ramp Right.svg new file mode 100644 index 0000000..2c3a6f0 --- /dev/null +++ b/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Arrow Ramp Right.svg @@ -0,0 +1,3 @@ + + + diff --git a/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Contents.json new file mode 100644 index 0000000..266c3f4 --- /dev/null +++ b/Assets.xcassets/Paywall/Feadback/feedback-paywall-arrow.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Arrow Ramp Right.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/AdobeStock_768605328 1.png b/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/AdobeStock_768605328 1.png new file mode 100644 index 0000000..e2a75cd Binary files /dev/null and b/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/AdobeStock_768605328 1.png differ diff --git a/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/Contents.json new file mode 100644 index 0000000..7ddb1d5 --- /dev/null +++ b/Assets.xcassets/Paywall/Feadback/feedback-paywall-banner.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AdobeStock_768605328 1.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/AdobeStock_776091887 2 (1).png b/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/AdobeStock_776091887 2 (1).png new file mode 100644 index 0000000..d33850c Binary files /dev/null and b/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/AdobeStock_776091887 2 (1).png differ diff --git a/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/Contents.json new file mode 100644 index 0000000..86fb01e --- /dev/null +++ b/Assets.xcassets/Paywall/Feadback/feedback-promo.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AdobeStock_776091887 2 (1).png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/Feadback/feedback.imageset/AdobeStock_811446718 1.png b/Assets.xcassets/Paywall/Feadback/feedback.imageset/AdobeStock_811446718 1.png new file mode 100644 index 0000000..e28a096 Binary files /dev/null and b/Assets.xcassets/Paywall/Feadback/feedback.imageset/AdobeStock_811446718 1.png differ diff --git a/Assets.xcassets/Paywall/Feadback/feedback.imageset/Contents.json b/Assets.xcassets/Paywall/Feadback/feedback.imageset/Contents.json new file mode 100644 index 0000000..201b4d7 --- /dev/null +++ b/Assets.xcassets/Paywall/Feadback/feedback.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AdobeStock_811446718 1.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/VPNPaywall/Checkbox.imageset/Checkbox.pdf b/Assets.xcassets/Paywall/VPNPaywall/Checkbox.imageset/Checkbox.pdf new file mode 100644 index 0000000..ff4c009 Binary files /dev/null and b/Assets.xcassets/Paywall/VPNPaywall/Checkbox.imageset/Checkbox.pdf differ diff --git a/Assets.xcassets/Paywall/VPNPaywall/Checkbox.imageset/Contents.json b/Assets.xcassets/Paywall/VPNPaywall/Checkbox.imageset/Contents.json new file mode 100644 index 0000000..7e26153 --- /dev/null +++ b/Assets.xcassets/Paywall/VPNPaywall/Checkbox.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Checkbox.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Paywall/VPNPaywall/Contents.json b/Assets.xcassets/Paywall/VPNPaywall/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Paywall/VPNPaywall/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/VPNPaywall/discount.imageset/Contents.json b/Assets.xcassets/Paywall/VPNPaywall/discount.imageset/Contents.json new file mode 100644 index 0000000..a667482 --- /dev/null +++ b/Assets.xcassets/Paywall/VPNPaywall/discount.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Label.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/VPNPaywall/discount.imageset/Label.png b/Assets.xcassets/Paywall/VPNPaywall/discount.imageset/Label.png new file mode 100644 index 0000000..abf51dd Binary files /dev/null and b/Assets.xcassets/Paywall/VPNPaywall/discount.imageset/Label.png differ diff --git a/Assets.xcassets/Paywall/VPNPaywall/fill-1.imageset/Contents.json b/Assets.xcassets/Paywall/VPNPaywall/fill-1.imageset/Contents.json new file mode 100644 index 0000000..60d74e9 --- /dev/null +++ b/Assets.xcassets/Paywall/VPNPaywall/fill-1.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "fill-1.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Paywall/VPNPaywall/fill-1.imageset/fill-1.png b/Assets.xcassets/Paywall/VPNPaywall/fill-1.imageset/fill-1.png new file mode 100644 index 0000000..4c6ee53 Binary files /dev/null and b/Assets.xcassets/Paywall/VPNPaywall/fill-1.imageset/fill-1.png differ diff --git a/Assets.xcassets/Paywall/VPNPaywall/fill-2.imageset/fill-2.png b/Assets.xcassets/Paywall/VPNPaywall/fill-2.imageset/fill-2.png new file mode 100644 index 0000000..61b4dc8 Binary files /dev/null and b/Assets.xcassets/Paywall/VPNPaywall/fill-2.imageset/fill-2.png differ diff --git a/Assets.xcassets/Paywall/VPNPaywall/grey-ellipse-1.imageset/Contents.json b/Assets.xcassets/Paywall/VPNPaywall/grey-ellipse-1.imageset/Contents.json new file mode 100644 index 0000000..00cd7e0 --- /dev/null +++ b/Assets.xcassets/Paywall/VPNPaywall/grey-ellipse-1.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "grey-ellipse-1.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Paywall/VPNPaywall/grey-ellipse-1.imageset/grey-ellipse-1.png b/Assets.xcassets/Paywall/VPNPaywall/grey-ellipse-1.imageset/grey-ellipse-1.png new file mode 100644 index 0000000..0263b5b Binary files /dev/null and b/Assets.xcassets/Paywall/VPNPaywall/grey-ellipse-1.imageset/grey-ellipse-1.png differ diff --git a/Assets.xcassets/Paywall/banner_70_percent.imageset/70% 1.svg b/Assets.xcassets/Paywall/banner_70_percent.imageset/70% 1.svg new file mode 100644 index 0000000..c44aac7 --- /dev/null +++ b/Assets.xcassets/Paywall/banner_70_percent.imageset/70% 1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Assets.xcassets/Paywall/banner_70_percent.imageset/Contents.json b/Assets.xcassets/Paywall/banner_70_percent.imageset/Contents.json new file mode 100644 index 0000000..0b1b2b5 --- /dev/null +++ b/Assets.xcassets/Paywall/banner_70_percent.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "70% 1.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/bg_paywall_onetime.imageset/Contents.json b/Assets.xcassets/Paywall/bg_paywall_onetime.imageset/Contents.json new file mode 100644 index 0000000..b4fa5a2 --- /dev/null +++ b/Assets.xcassets/Paywall/bg_paywall_onetime.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Group 1484762.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/bg_paywall_onetime.imageset/Group 1484762.jpg b/Assets.xcassets/Paywall/bg_paywall_onetime.imageset/Group 1484762.jpg new file mode 100644 index 0000000..cafe2cd Binary files /dev/null and b/Assets.xcassets/Paywall/bg_paywall_onetime.imageset/Group 1484762.jpg differ diff --git a/Assets.xcassets/Paywall/bg_paywall_onetime_ss.imageset/Contents.json b/Assets.xcassets/Paywall/bg_paywall_onetime_ss.imageset/Contents.json new file mode 100644 index 0000000..72725e1 --- /dev/null +++ b/Assets.xcassets/Paywall/bg_paywall_onetime_ss.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bg_paywall_onetime_ss.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/bg_paywall_onetime_ss.imageset/bg_paywall_onetime_ss.jpg b/Assets.xcassets/Paywall/bg_paywall_onetime_ss.imageset/bg_paywall_onetime_ss.jpg new file mode 100644 index 0000000..c829398 Binary files /dev/null and b/Assets.xcassets/Paywall/bg_paywall_onetime_ss.imageset/bg_paywall_onetime_ss.jpg differ diff --git a/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591.png b/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591.png new file mode 100644 index 0000000..fdb4461 Binary files /dev/null and b/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591.png differ diff --git a/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591@2x.png b/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591@2x.png new file mode 100644 index 0000000..3da9b8b Binary files /dev/null and b/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591@2x.png differ diff --git a/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591@3x.png b/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591@3x.png new file mode 100644 index 0000000..ced070b Binary files /dev/null and b/Assets.xcassets/Paywall/december_banner.imageset/AdobeStock_863702591@3x.png differ diff --git a/Assets.xcassets/Paywall/december_banner.imageset/Contents.json b/Assets.xcassets/Paywall/december_banner.imageset/Contents.json new file mode 100644 index 0000000..9196947 --- /dev/null +++ b/Assets.xcassets/Paywall/december_banner.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "AdobeStock_863702591.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "AdobeStock_863702591@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "AdobeStock_863702591@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/shield_checkmark.imageset/Contents.json b/Assets.xcassets/Paywall/shield_checkmark.imageset/Contents.json new file mode 100644 index 0000000..54ecd17 --- /dev/null +++ b/Assets.xcassets/Paywall/shield_checkmark.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "shield_checkmark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/shield_checkmark.imageset/shield_checkmark.pdf b/Assets.xcassets/Paywall/shield_checkmark.imageset/shield_checkmark.pdf new file mode 100644 index 0000000..aa3f864 Binary files /dev/null and b/Assets.xcassets/Paywall/shield_checkmark.imageset/shield_checkmark.pdf differ diff --git a/Assets.xcassets/Paywall/special_offer_2025.imageset/Contents.json b/Assets.xcassets/Paywall/special_offer_2025.imageset/Contents.json new file mode 100644 index 0000000..fd46bac --- /dev/null +++ b/Assets.xcassets/Paywall/special_offer_2025.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "special_offer_2025.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/special_offer_2025.imageset/special_offer_2025.png b/Assets.xcassets/Paywall/special_offer_2025.imageset/special_offer_2025.png new file mode 100644 index 0000000..9646049 Binary files /dev/null and b/Assets.xcassets/Paywall/special_offer_2025.imageset/special_offer_2025.png differ diff --git a/Assets.xcassets/Paywall/special_offer_stars.imageset/Contents.json b/Assets.xcassets/Paywall/special_offer_stars.imageset/Contents.json new file mode 100644 index 0000000..638045a --- /dev/null +++ b/Assets.xcassets/Paywall/special_offer_stars.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "special_offer_stars.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Paywall/special_offer_stars.imageset/special_offer_stars.png b/Assets.xcassets/Paywall/special_offer_stars.imageset/special_offer_stars.png new file mode 100644 index 0000000..c0ec1ff Binary files /dev/null and b/Assets.xcassets/Paywall/special_offer_stars.imageset/special_offer_stars.png differ diff --git a/Assets.xcassets/Power Button Shadow Color.colorset/Contents.json b/Assets.xcassets/Power Button Shadow Color.colorset/Contents.json new file mode 100644 index 0000000..742124e --- /dev/null +++ b/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/Assets.xcassets/Questionnaire/Contents.json b/Assets.xcassets/Questionnaire/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/Questionnaire/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Questionnaire/selectedRadioSwitcher.imageset/Contents.json b/Assets.xcassets/Questionnaire/selectedRadioSwitcher.imageset/Contents.json new file mode 100644 index 0000000..d7cb567 --- /dev/null +++ b/Assets.xcassets/Questionnaire/selectedRadioSwitcher.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Group 16680.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Questionnaire/selectedRadioSwitcher.imageset/Group 16680.pdf b/Assets.xcassets/Questionnaire/selectedRadioSwitcher.imageset/Group 16680.pdf new file mode 100644 index 0000000..a269c73 Binary files /dev/null and b/Assets.xcassets/Questionnaire/selectedRadioSwitcher.imageset/Group 16680.pdf differ diff --git a/Assets.xcassets/Questionnaire/unselectedRadioSwitcher.imageset/Contents.json b/Assets.xcassets/Questionnaire/unselectedRadioSwitcher.imageset/Contents.json new file mode 100644 index 0000000..ef9000c --- /dev/null +++ b/Assets.xcassets/Questionnaire/unselectedRadioSwitcher.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Group 16682.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/Questionnaire/unselectedRadioSwitcher.imageset/Group 16682.pdf b/Assets.xcassets/Questionnaire/unselectedRadioSwitcher.imageset/Group 16682.pdf new file mode 100644 index 0000000..f8aa97d Binary files /dev/null and b/Assets.xcassets/Questionnaire/unselectedRadioSwitcher.imageset/Group 16682.pdf differ diff --git a/Assets.xcassets/SheldIcon.imageset/Contents.json b/Assets.xcassets/SheldIcon.imageset/Contents.json new file mode 100644 index 0000000..8ea1e07 --- /dev/null +++ b/Assets.xcassets/SheldIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Tab Bar Icons.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/SheldIcon.imageset/Tab Bar Icons.pdf b/Assets.xcassets/SheldIcon.imageset/Tab Bar Icons.pdf new file mode 100644 index 0000000..b5735a2 Binary files /dev/null and b/Assets.xcassets/SheldIcon.imageset/Tab Bar Icons.pdf differ diff --git a/Assets.xcassets/VPN/Contents.json b/Assets.xcassets/VPN/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Assets.xcassets/VPN/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/VPN/icn_activity.imageset/Contents.json b/Assets.xcassets/VPN/icn_activity.imageset/Contents.json new file mode 100644 index 0000000..fb8d664 --- /dev/null +++ b/Assets.xcassets/VPN/icn_activity.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_activity.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/icn_activity.imageset/icn_activity.pdf b/Assets.xcassets/VPN/icn_activity.imageset/icn_activity.pdf new file mode 100644 index 0000000..291a4b6 Binary files /dev/null and b/Assets.xcassets/VPN/icn_activity.imageset/icn_activity.pdf differ diff --git a/Assets.xcassets/VPN/icn_checkmark.imageset/Contents.json b/Assets.xcassets/VPN/icn_checkmark.imageset/Contents.json new file mode 100644 index 0000000..8c449dc --- /dev/null +++ b/Assets.xcassets/VPN/icn_checkmark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_checkmark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/icn_checkmark.imageset/icn_checkmark.pdf b/Assets.xcassets/VPN/icn_checkmark.imageset/icn_checkmark.pdf new file mode 100644 index 0000000..8e29a35 Binary files /dev/null and b/Assets.xcassets/VPN/icn_checkmark.imageset/icn_checkmark.pdf differ diff --git a/Assets.xcassets/VPN/icn_checkmark_bold.imageset/Contents.json b/Assets.xcassets/VPN/icn_checkmark_bold.imageset/Contents.json new file mode 100644 index 0000000..ef446fd --- /dev/null +++ b/Assets.xcassets/VPN/icn_checkmark_bold.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_checkmark_bold.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/icn_checkmark_bold.imageset/icn_checkmark_bold.pdf b/Assets.xcassets/VPN/icn_checkmark_bold.imageset/icn_checkmark_bold.pdf new file mode 100644 index 0000000..3521ca7 Binary files /dev/null and b/Assets.xcassets/VPN/icn_checkmark_bold.imageset/icn_checkmark_bold.pdf differ diff --git a/Assets.xcassets/VPN/icn_configure_blocking.imageset/Contents.json b/Assets.xcassets/VPN/icn_configure_blocking.imageset/Contents.json new file mode 100644 index 0000000..536528d --- /dev/null +++ b/Assets.xcassets/VPN/icn_configure_blocking.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_configure_blocking.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/icn_configure_blocking.imageset/icn_configure_blocking.pdf b/Assets.xcassets/VPN/icn_configure_blocking.imageset/icn_configure_blocking.pdf new file mode 100644 index 0000000..74659c5 Binary files /dev/null and b/Assets.xcassets/VPN/icn_configure_blocking.imageset/icn_configure_blocking.pdf differ diff --git a/Assets.xcassets/VPN/icn_globe.imageset/Contents.json b/Assets.xcassets/VPN/icn_globe.imageset/Contents.json new file mode 100644 index 0000000..9f943b4 --- /dev/null +++ b/Assets.xcassets/VPN/icn_globe.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_globe.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/icn_globe.imageset/icn_globe.pdf b/Assets.xcassets/VPN/icn_globe.imageset/icn_globe.pdf new file mode 100644 index 0000000..b1c8e86 Binary files /dev/null and b/Assets.xcassets/VPN/icn_globe.imageset/icn_globe.pdf differ diff --git a/Assets.xcassets/VPN/icn_import.imageset/Contents.json b/Assets.xcassets/VPN/icn_import.imageset/Contents.json new file mode 100644 index 0000000..9da8012 --- /dev/null +++ b/Assets.xcassets/VPN/icn_import.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_import.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/icn_import.imageset/icn_import.pdf b/Assets.xcassets/VPN/icn_import.imageset/icn_import.pdf new file mode 100644 index 0000000..eaedc35 Binary files /dev/null and b/Assets.xcassets/VPN/icn_import.imageset/icn_import.pdf differ diff --git a/Assets.xcassets/VPN/icn_personalized_blocking.imageset/Contents.json b/Assets.xcassets/VPN/icn_personalized_blocking.imageset/Contents.json new file mode 100644 index 0000000..7edd911 --- /dev/null +++ b/Assets.xcassets/VPN/icn_personalized_blocking.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_personalized_blocking.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/icn_personalized_blocking.imageset/icn_personalized_blocking.pdf b/Assets.xcassets/VPN/icn_personalized_blocking.imageset/icn_personalized_blocking.pdf new file mode 100644 index 0000000..3954829 Binary files /dev/null and b/Assets.xcassets/VPN/icn_personalized_blocking.imageset/icn_personalized_blocking.pdf differ diff --git a/Assets.xcassets/VPN/icn_whitelist.imageset/Contents.json b/Assets.xcassets/VPN/icn_whitelist.imageset/Contents.json new file mode 100644 index 0000000..55f6c3a --- /dev/null +++ b/Assets.xcassets/VPN/icn_whitelist.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_whitelists.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/icn_whitelist.imageset/icn_whitelists.pdf b/Assets.xcassets/VPN/icn_whitelist.imageset/icn_whitelists.pdf new file mode 100644 index 0000000..e94a02f Binary files /dev/null and b/Assets.xcassets/VPN/icn_whitelist.imageset/icn_whitelists.pdf differ diff --git a/Assets.xcassets/VPN/vpn-off-image.imageset/Contents.json b/Assets.xcassets/VPN/vpn-off-image.imageset/Contents.json new file mode 100644 index 0000000..6655359 --- /dev/null +++ b/Assets.xcassets/VPN/vpn-off-image.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "vpn-off-image.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "vpn-off-image-black.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/vpn-off-image.imageset/vpn-off-image-black.pdf b/Assets.xcassets/VPN/vpn-off-image.imageset/vpn-off-image-black.pdf new file mode 100644 index 0000000..2a80ba3 Binary files /dev/null and b/Assets.xcassets/VPN/vpn-off-image.imageset/vpn-off-image-black.pdf differ diff --git a/Assets.xcassets/VPN/vpn-off-image.imageset/vpn-off-image.pdf b/Assets.xcassets/VPN/vpn-off-image.imageset/vpn-off-image.pdf new file mode 100644 index 0000000..41f0363 Binary files /dev/null and b/Assets.xcassets/VPN/vpn-off-image.imageset/vpn-off-image.pdf differ diff --git a/Assets.xcassets/VPN/vpn-on-image.imageset/Contents.json b/Assets.xcassets/VPN/vpn-on-image.imageset/Contents.json new file mode 100644 index 0000000..26e3b26 --- /dev/null +++ b/Assets.xcassets/VPN/vpn-on-image.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "vpn-on-image.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "vpn-on-image-black.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/VPN/vpn-on-image.imageset/vpn-on-image-black.pdf b/Assets.xcassets/VPN/vpn-on-image.imageset/vpn-on-image-black.pdf new file mode 100644 index 0000000..da5f46e Binary files /dev/null and b/Assets.xcassets/VPN/vpn-on-image.imageset/vpn-on-image-black.pdf differ diff --git a/Assets.xcassets/VPN/vpn-on-image.imageset/vpn-on-image.pdf b/Assets.xcassets/VPN/vpn-on-image.imageset/vpn-on-image.pdf new file mode 100644 index 0000000..49163e5 Binary files /dev/null and b/Assets.xcassets/VPN/vpn-on-image.imageset/vpn-on-image.pdf differ diff --git a/Assets.xcassets/VPNIcon.imageset/Contents.json b/Assets.xcassets/VPNIcon.imageset/Contents.json new file mode 100644 index 0000000..7d29a35 --- /dev/null +++ b/Assets.xcassets/VPNIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "VPN.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/VPNIcon.imageset/VPN.pdf b/Assets.xcassets/VPNIcon.imageset/VPN.pdf new file mode 100644 index 0000000..946db71 Binary files /dev/null and b/Assets.xcassets/VPNIcon.imageset/VPN.pdf differ diff --git a/Assets.xcassets/checkmark.imageset/checkmark-1.png b/Assets.xcassets/checkmark.imageset/checkmark-1.png deleted file mode 100644 index 113cb3c..0000000 Binary files a/Assets.xcassets/checkmark.imageset/checkmark-1.png and /dev/null differ diff --git a/Assets.xcassets/checkmark.imageset/checkmark-2.png b/Assets.xcassets/checkmark.imageset/checkmark-2.png deleted file mode 100644 index 113cb3c..0000000 Binary files a/Assets.xcassets/checkmark.imageset/checkmark-2.png and /dev/null differ diff --git a/Assets.xcassets/checkmark.imageset/checkmark.png b/Assets.xcassets/checkmark.imageset/checkmark.png deleted file mode 100644 index 113cb3c..0000000 Binary files a/Assets.xcassets/checkmark.imageset/checkmark.png and /dev/null differ diff --git a/Assets.xcassets/globe.imageset/Contents.json b/Assets.xcassets/globe.imageset/Contents.json new file mode 100644 index 0000000..053d53f --- /dev/null +++ b/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/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-1.png b/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-1.png new file mode 100644 index 0000000..51b5b4f Binary files /dev/null and b/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-1.png differ diff --git a/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-2.png b/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-2.png new file mode 100644 index 0000000..51b5b4f Binary files /dev/null and b/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e-2.png differ diff --git a/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e.png b/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e.png new file mode 100644 index 0000000..51b5b4f Binary files /dev/null and b/Assets.xcassets/globe.imageset/earth-globe-americas_1f30e.png differ diff --git a/Assets.xcassets/icn_close_filled.imageset/Contents.json b/Assets.xcassets/icn_close_filled.imageset/Contents.json new file mode 100644 index 0000000..b0a790e --- /dev/null +++ b/Assets.xcassets/icn_close_filled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_close_filled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/icn_close_filled.imageset/icn_close_filled.pdf b/Assets.xcassets/icn_close_filled.imageset/icn_close_filled.pdf new file mode 100644 index 0000000..db7617f Binary files /dev/null and b/Assets.xcassets/icn_close_filled.imageset/icn_close_filled.pdf differ diff --git a/Assets.xcassets/icn_configuration.imageset/Contents.json b/Assets.xcassets/icn_configuration.imageset/Contents.json new file mode 100644 index 0000000..be0da65 --- /dev/null +++ b/Assets.xcassets/icn_configuration.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_configuration.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/icn_configuration.imageset/icn_configuration.pdf b/Assets.xcassets/icn_configuration.imageset/icn_configuration.pdf new file mode 100644 index 0000000..ba7b23b Binary files /dev/null and b/Assets.xcassets/icn_configuration.imageset/icn_configuration.pdf differ diff --git a/Assets.xcassets/icn_firewall.imageset/Contents.json b/Assets.xcassets/icn_firewall.imageset/Contents.json new file mode 100644 index 0000000..aae1cd5 --- /dev/null +++ b/Assets.xcassets/icn_firewall.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_firewall.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/icn_firewall.imageset/icn_firewall.pdf b/Assets.xcassets/icn_firewall.imageset/icn_firewall.pdf new file mode 100644 index 0000000..47fa338 Binary files /dev/null and b/Assets.xcassets/icn_firewall.imageset/icn_firewall.pdf differ diff --git a/Assets.xcassets/icn_vpn.imageset/Contents.json b/Assets.xcassets/icn_vpn.imageset/Contents.json new file mode 100644 index 0000000..00e105c --- /dev/null +++ b/Assets.xcassets/icn_vpn.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "icn_vpn.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/icn_vpn.imageset/icn_vpn.pdf b/Assets.xcassets/icn_vpn.imageset/icn_vpn.pdf new file mode 100644 index 0000000..117a703 Binary files /dev/null and b/Assets.xcassets/icn_vpn.imageset/icn_vpn.pdf differ diff --git a/Assets.xcassets/ios-marketing.imageset/Contents.json b/Assets.xcassets/ios-marketing.imageset/Contents.json index 86e4d18..6d39844 100644 --- a/Assets.xcassets/ios-marketing.imageset/Contents.json +++ b/Assets.xcassets/ios-marketing.imageset/Contents.json @@ -1,23 +1,23 @@ { "images" : [ { + "filename" : "icon_512x512@2x.png", "idiom" : "universal", - "filename" : "Icon-1024.png", "scale" : "1x" }, { + "filename" : "icon_512x512@2x-1.png", "idiom" : "universal", - "filename" : "Icon-1025.png", "scale" : "2x" }, { + "filename" : "icon_512x512@2x-2.png", "idiom" : "universal", - "filename" : "Icon-1026.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Assets.xcassets/ios-marketing.imageset/Icon-1024.png b/Assets.xcassets/ios-marketing.imageset/Icon-1024.png deleted file mode 100644 index 6174ae3..0000000 Binary files a/Assets.xcassets/ios-marketing.imageset/Icon-1024.png and /dev/null differ diff --git a/Assets.xcassets/ios-marketing.imageset/Icon-1025.png b/Assets.xcassets/ios-marketing.imageset/Icon-1025.png deleted file mode 100644 index 6174ae3..0000000 Binary files a/Assets.xcassets/ios-marketing.imageset/Icon-1025.png and /dev/null differ diff --git a/Assets.xcassets/ios-marketing.imageset/Icon-1026.png b/Assets.xcassets/ios-marketing.imageset/Icon-1026.png deleted file mode 100644 index 6174ae3..0000000 Binary files a/Assets.xcassets/ios-marketing.imageset/Icon-1026.png and /dev/null differ diff --git a/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x-1.png b/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x-1.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x-1.png differ diff --git a/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x-2.png b/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x-2.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x-2.png differ diff --git a/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x.png b/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/Assets.xcassets/ios-marketing.imageset/icon_512x512@2x.png differ diff --git a/Assets.xcassets/lightBlue.colorset/Contents.json b/Assets.xcassets/lightBlue.colorset/Contents.json new file mode 100644 index 0000000..8048731 --- /dev/null +++ b/Assets.xcassets/lightBlue.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.984", + "green" : "0.952", + "red" : "0.875" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/lockdown_icon.imageset/Contents.json b/Assets.xcassets/lockdown_icon.imageset/Contents.json index fe848b5..6d39844 100644 --- a/Assets.xcassets/lockdown_icon.imageset/Contents.json +++ b/Assets.xcassets/lockdown_icon.imageset/Contents.json @@ -1,23 +1,23 @@ { "images" : [ { + "filename" : "icon_512x512@2x.png", "idiom" : "universal", - "filename" : "Icon-512.png", "scale" : "1x" }, { + "filename" : "icon_512x512@2x-1.png", "idiom" : "universal", - "filename" : "Icon-513.png", "scale" : "2x" }, { + "filename" : "icon_512x512@2x-2.png", "idiom" : "universal", - "filename" : "Icon-514.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Assets.xcassets/lockdown_icon.imageset/Icon-512.png b/Assets.xcassets/lockdown_icon.imageset/Icon-512.png deleted file mode 100644 index 9e843a6..0000000 Binary files a/Assets.xcassets/lockdown_icon.imageset/Icon-512.png and /dev/null differ diff --git a/Assets.xcassets/lockdown_icon.imageset/Icon-513.png b/Assets.xcassets/lockdown_icon.imageset/Icon-513.png deleted file mode 100644 index 9e843a6..0000000 Binary files a/Assets.xcassets/lockdown_icon.imageset/Icon-513.png and /dev/null differ diff --git a/Assets.xcassets/lockdown_icon.imageset/Icon-514.png b/Assets.xcassets/lockdown_icon.imageset/Icon-514.png deleted file mode 100644 index 9e843a6..0000000 Binary files a/Assets.xcassets/lockdown_icon.imageset/Icon-514.png and /dev/null differ diff --git a/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x-1.png b/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x-1.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x-1.png differ diff --git a/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x-2.png b/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x-2.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x-2.png differ diff --git a/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x.png b/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x.png new file mode 100644 index 0000000..a0608c3 Binary files /dev/null and b/Assets.xcassets/lockdown_icon.imageset/icon_512x512@2x.png differ diff --git a/Assets.xcassets/lockdown_icon_appstore.imageset/1024-1.png b/Assets.xcassets/lockdown_icon_appstore.imageset/1024-1.png deleted file mode 100644 index 23a35d5..0000000 Binary files a/Assets.xcassets/lockdown_icon_appstore.imageset/1024-1.png and /dev/null differ diff --git a/Assets.xcassets/lockdown_icon_appstore.imageset/1024-2.png b/Assets.xcassets/lockdown_icon_appstore.imageset/1024-2.png deleted file mode 100644 index 23a35d5..0000000 Binary files a/Assets.xcassets/lockdown_icon_appstore.imageset/1024-2.png and /dev/null differ diff --git a/Assets.xcassets/lockdown_icon_appstore.imageset/1024.png b/Assets.xcassets/lockdown_icon_appstore.imageset/1024.png deleted file mode 100644 index 23a35d5..0000000 Binary files a/Assets.xcassets/lockdown_icon_appstore.imageset/1024.png and /dev/null differ diff --git a/Assets.xcassets/lockdown_icon_appstore.imageset/Contents.json b/Assets.xcassets/lockdown_icon_appstore.imageset/Contents.json index 5c10c1b..e43a222 100644 --- a/Assets.xcassets/lockdown_icon_appstore.imageset/Contents.json +++ b/Assets.xcassets/lockdown_icon_appstore.imageset/Contents.json @@ -1,23 +1,23 @@ { "images" : [ { + "filename" : "ios-marketing.png", "idiom" : "universal", - "filename" : "1024.png", "scale" : "1x" }, { + "filename" : "ios-marketing-1.png", "idiom" : "universal", - "filename" : "1024-1.png", "scale" : "2x" }, { + "filename" : "ios-marketing-2.png", "idiom" : "universal", - "filename" : "1024-2.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing-1.png b/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing-1.png new file mode 100644 index 0000000..8a0250e Binary files /dev/null and b/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing-1.png differ diff --git a/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing-2.png b/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing-2.png new file mode 100644 index 0000000..8a0250e Binary files /dev/null and b/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing-2.png differ diff --git a/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing.png b/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing.png new file mode 100644 index 0000000..8a0250e Binary files /dev/null and b/Assets.xcassets/lockdown_icon_appstore.imageset/ios-marketing.png differ diff --git a/Assets.xcassets/menu.imageset/menu-button-1.png b/Assets.xcassets/menu.imageset/menu-button-1.png deleted file mode 100644 index 371c3f1..0000000 Binary files a/Assets.xcassets/menu.imageset/menu-button-1.png and /dev/null differ diff --git a/Assets.xcassets/menu.imageset/menu-button-2.png b/Assets.xcassets/menu.imageset/menu-button-2.png deleted file mode 100644 index 371c3f1..0000000 Binary files a/Assets.xcassets/menu.imageset/menu-button-2.png and /dev/null differ diff --git a/Assets.xcassets/menu.imageset/menu-button.png b/Assets.xcassets/menu.imageset/menu-button.png deleted file mode 100644 index 371c3f1..0000000 Binary files a/Assets.xcassets/menu.imageset/menu-button.png and /dev/null differ diff --git a/Assets.xcassets/message-circle.imageset/Contents.json b/Assets.xcassets/message-circle.imageset/Contents.json new file mode 100644 index 0000000..991d9d8 --- /dev/null +++ b/Assets.xcassets/message-circle.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "iconUpdated.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Assets.xcassets/message-circle.imageset/iconUpdated.pdf b/Assets.xcassets/message-circle.imageset/iconUpdated.pdf new file mode 100644 index 0000000..57ab9da Binary files /dev/null and b/Assets.xcassets/message-circle.imageset/iconUpdated.pdf differ diff --git a/Assets.xcassets/notification_example.imageset/Contents.json b/Assets.xcassets/notification_example.imageset/Contents.json new file mode 100644 index 0000000..812609e --- /dev/null +++ b/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/Assets.xcassets/notification_example.imageset/IMG_0004-1.png b/Assets.xcassets/notification_example.imageset/IMG_0004-1.png new file mode 100644 index 0000000..b571c9e Binary files /dev/null and b/Assets.xcassets/notification_example.imageset/IMG_0004-1.png differ diff --git a/Assets.xcassets/notification_example.imageset/IMG_0004-2.png b/Assets.xcassets/notification_example.imageset/IMG_0004-2.png new file mode 100644 index 0000000..b571c9e Binary files /dev/null and b/Assets.xcassets/notification_example.imageset/IMG_0004-2.png differ diff --git a/Assets.xcassets/notification_example.imageset/IMG_0004.png b/Assets.xcassets/notification_example.imageset/IMG_0004.png new file mode 100644 index 0000000..b571c9e Binary files /dev/null and b/Assets.xcassets/notification_example.imageset/IMG_0004.png differ diff --git a/Assets.xcassets/notification_example.imageset/IMG_0005-1.png b/Assets.xcassets/notification_example.imageset/IMG_0005-1.png new file mode 100644 index 0000000..d29c36b Binary files /dev/null and b/Assets.xcassets/notification_example.imageset/IMG_0005-1.png differ diff --git a/Assets.xcassets/notification_example.imageset/IMG_0005-2.png b/Assets.xcassets/notification_example.imageset/IMG_0005-2.png new file mode 100644 index 0000000..d29c36b Binary files /dev/null and b/Assets.xcassets/notification_example.imageset/IMG_0005-2.png differ diff --git a/Assets.xcassets/notification_example.imageset/IMG_0005.png b/Assets.xcassets/notification_example.imageset/IMG_0005.png new file mode 100644 index 0000000..d29c36b Binary files /dev/null and b/Assets.xcassets/notification_example.imageset/IMG_0005.png differ diff --git a/Assets.xcassets/power.imageset/Contents.json b/Assets.xcassets/power.imageset/Contents.json new file mode 100644 index 0000000..ad2ec5f --- /dev/null +++ b/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/Assets.xcassets/power.imageset/power.pdf b/Assets.xcassets/power.imageset/power.pdf new file mode 100644 index 0000000..6f539f6 Binary files /dev/null and b/Assets.xcassets/power.imageset/power.pdf differ diff --git a/Assets.xcassets/power_button.imageset/power-button-1.png b/Assets.xcassets/power_button.imageset/power-button-1.png deleted file mode 100644 index 2b44cb7..0000000 Binary files a/Assets.xcassets/power_button.imageset/power-button-1.png and /dev/null differ diff --git a/Assets.xcassets/power_button.imageset/power-button-2.png b/Assets.xcassets/power_button.imageset/power-button-2.png deleted file mode 100644 index 2b44cb7..0000000 Binary files a/Assets.xcassets/power_button.imageset/power-button-2.png and /dev/null differ diff --git a/Assets.xcassets/power_button.imageset/power-button.png b/Assets.xcassets/power_button.imageset/power-button.png deleted file mode 100644 index 2b44cb7..0000000 Binary files a/Assets.xcassets/power_button.imageset/power-button.png and /dev/null differ diff --git a/Assets.xcassets/safari.imageset/safari-1.png b/Assets.xcassets/safari.imageset/safari-1.png deleted file mode 100644 index 3e891c0..0000000 Binary files a/Assets.xcassets/safari.imageset/safari-1.png and /dev/null differ diff --git a/Assets.xcassets/safari.imageset/safari-2.png b/Assets.xcassets/safari.imageset/safari-2.png deleted file mode 100644 index 3e891c0..0000000 Binary files a/Assets.xcassets/safari.imageset/safari-2.png and /dev/null differ diff --git a/Assets.xcassets/safari.imageset/safari.png b/Assets.xcassets/safari.imageset/safari.png deleted file mode 100644 index 3e891c0..0000000 Binary files a/Assets.xcassets/safari.imageset/safari.png and /dev/null differ diff --git a/Assets.xcassets/checkmark.imageset/Contents.json b/Assets.xcassets/share.imageset/Contents.json similarity index 70% rename from Assets.xcassets/checkmark.imageset/Contents.json rename to Assets.xcassets/share.imageset/Contents.json index d9d5f9b..bfbe1c1 100644 --- a/Assets.xcassets/checkmark.imageset/Contents.json +++ b/Assets.xcassets/share.imageset/Contents.json @@ -2,17 +2,15 @@ "images" : [ { "idiom" : "universal", - "filename" : "checkmark.png", + "filename" : "share.png", "scale" : "1x" }, { "idiom" : "universal", - "filename" : "checkmark-1.png", "scale" : "2x" }, { "idiom" : "universal", - "filename" : "checkmark-2.png", "scale" : "3x" } ], diff --git a/Assets.xcassets/share.imageset/share.png b/Assets.xcassets/share.imageset/share.png new file mode 100644 index 0000000..3f83fa6 Binary files /dev/null and b/Assets.xcassets/share.imageset/share.png differ diff --git a/Assets.xcassets/tableCellBackground.colorset/Contents.json b/Assets.xcassets/tableCellBackground.colorset/Contents.json new file mode 100644 index 0000000..9c65033 --- /dev/null +++ b/Assets.xcassets/tableCellBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF6", + "green" : "0xF6", + "red" : "0xF6" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x6A", + "green" : "0x68", + "red" : "0x64" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/tableCellSelectedBackground.colorset/Contents.json b/Assets.xcassets/tableCellSelectedBackground.colorset/Contents.json new file mode 100644 index 0000000..cd71989 --- /dev/null +++ b/Assets.xcassets/tableCellSelectedBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFC", + "green" : "0xF4", + "red" : "0xDB" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x73", + "green" : "0x56", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Assets.xcassets/Block Lists/website_icon.imageset/Contents.json b/Assets.xcassets/website_icon.imageset/Contents.json similarity index 100% rename from Assets.xcassets/Block Lists/website_icon.imageset/Contents.json rename to Assets.xcassets/website_icon.imageset/Contents.json diff --git a/Assets.xcassets/Block Lists/website_icon.imageset/website_icon.png b/Assets.xcassets/website_icon.imageset/website_icon.png similarity index 100% rename from Assets.xcassets/Block Lists/website_icon.imageset/website_icon.png rename to Assets.xcassets/website_icon.imageset/website_icon.png diff --git a/Assets.xcassets/welcome-image.imageset/Contents.json b/Assets.xcassets/welcome-image.imageset/Contents.json new file mode 100644 index 0000000..a6dbe74 --- /dev/null +++ b/Assets.xcassets/welcome-image.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "welcome-image.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Assets.xcassets/welcome-image.imageset/welcome-image.pdf b/Assets.xcassets/welcome-image.imageset/welcome-image.pdf new file mode 100644 index 0000000..25a6993 Binary files /dev/null and b/Assets.xcassets/welcome-image.imageset/welcome-image.pdf differ diff --git a/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-1.png b/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-1.png index 4190aa9..f31ba04 100644 Binary files a/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-1.png and b/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-1.png differ diff --git a/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-2.png b/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-2.png index 4190aa9..f31ba04 100644 Binary files a/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-2.png and b/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown-2.png differ diff --git a/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown.png b/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown.png index 4190aa9..f31ba04 100644 Binary files a/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown.png and b/Assets.xcassets/whyTrustImage.imageset/whyTrustLockdown.png differ diff --git a/BlockDayLog.swift b/BlockDayLog.swift new file mode 100644 index 0000000..af8bc59 --- /dev/null +++ b/BlockDayLog.swift @@ -0,0 +1,89 @@ +// +// BlockDayLog.swift +// Lockdown +// +// Created by Oleg Dreyman on 22.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation + +final class BlockDayLog { + + static let shared = BlockDayLog() + + static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "h:mm a_" + return formatter + }() + + private let processingQueue = DispatchQueue(label: "LockdownBlockDayLogQueue") + + private static let kIsBlockLogDisabled = "LockdownIsBlockLogDisabled" + + private static let userDefaultsKey = "LockdownDayLogs" + private static let maxSize = 5000 + private static let maxReduction = 4500 + + private init() { } + + var isDisabled: Bool { + return defaults.bool(forKey: BlockDayLog.kIsBlockLogDisabled) + } + + var isEnabled: Bool { + return !isDisabled + } + + private var _dayLog: [Any]? { + #if DEBUG + dispatchPrecondition(condition: .onQueue(processingQueue)) + #endif + return defaults.array(forKey: BlockDayLog.userDefaultsKey) + } + + var strings: [String]? { + return processingQueue.sync { + self._dayLog as? [String] + } + } + + func clear() { + processingQueue.async { + defaults.set([], forKey: BlockDayLog.userDefaultsKey) + } + } + + func disable(shouldClear: Bool) { + defaults.set(true, forKey: BlockDayLog.kIsBlockLogDisabled) + if shouldClear { + clear() + } + } + + func enable() { + defaults.set(false, forKey: BlockDayLog.kIsBlockLogDisabled) + } + + func append(host: String, date: Date) { + guard isDisabled == false else { + // block log is disabled + return + } + + processingQueue.async { + let logString = BlockDayLog.dateFormatter.string(from: date) + host + // reduce log size if it's over the maxSize + if var dayLog = self._dayLog { + if dayLog.count > BlockDayLog.maxSize { + dayLog = dayLog.suffix(BlockDayLog.maxReduction) + } + dayLog.append(logString) + defaults.set(dayLog, forKey: BlockDayLog.userDefaultsKey) + } else { + defaults.set([logString], forKey: BlockDayLog.userDefaultsKey) + } + } + } +} diff --git a/Cartfile b/Cartfile index 4f39665..5c46a67 100755 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "zhuhaow/NEKit" +github "https://github.com/confirmedcode/NEKit.git" "master" \ No newline at end of file diff --git a/Cartfile.resolved b/Cartfile.resolved index 03e759f..82fa6da 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,8 +1,4 @@ -github "CocoaLumberjack/CocoaLumberjack" "3.5.3" -github "behrang/YamlSwift" "3.4.3" -github "lexrus/MMDB-Swift" "0.3.0" -github "robbiehanson/CocoaAsyncSocket" "7.6.3" -github "zhuhaow/NEKit" "0.14.0" +github "CocoaLumberjack/CocoaLumberjack" "3.8.5" +github "confirmedcode/CocoaAsyncSocket" "0cf7f247f8a5aef88da91c461612167d7382f47d" +github "confirmedcode/NEKit" "ed44edffd80af18c0695cf03f0f96d85781d93e3" github "zhuhaow/Resolver" "0.2.0" -github "zhuhaow/Sodium-framework" "v1.0.10.1" -github "zhuhaow/tun2socks" "0.7.0" diff --git a/Client.swift b/Client.swift index 86f88c1..f62444b 100644 --- a/Client.swift +++ b/Client.swift @@ -22,7 +22,9 @@ let kApiCodeEmailAlreadyUsed = 40 let kApiCodeReceiptAlreadyUsed = 48 let kApiCodeInvalidAuth = 401 let kApiCodeTooManyRequests = 999 +let kApiCodeSandboxReceiptNotAllowed = 9925 let kApiCodeUnknownError = 99999 +let kApiCodeNegativeError = -1 class Client { @@ -34,9 +36,10 @@ class Client { clearCookies() return getReceipt(forceRefresh: forceRefresh) .then { receipt -> Promise<(data: Data, response: URLResponse)> in - let parameters = [ + let parameters:[String : Any] = [ "authtype": "ios", - "authreceipt": receipt + "authreceipt": receipt, + "lockdown": true ] return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/signin", @@ -54,12 +57,148 @@ class Client { } } } + + static func signInWithEmail(email: String, password: String) throws -> Promise { + DDLogInfo("API CALL: test signIn with email") + URLCache.shared.removeAllCachedResponses() + clearCookies() + return firstly { () -> Promise<(data: Data, response: URLResponse)> in + let parameters:[String : Any] = [ + "email" : email, + "password" : password, + "lockdown": true + ] + return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/signin", parameters: parameters)) + } + .map { data, response -> SignIn in + try self.validateApiResponse(data: data, response: response) + let resp = response as! HTTPURLResponse // already validated the type in validateApiResponse + DDLogInfo("Got signin (with email) response with headers: \(resp.allHeaderFields)") + return try JSONDecoder().decode(SignIn.self, from: data) + } + } + + static func resendConfirmCode(email: String) throws -> Promise { + DDLogInfo("API CALL: resendConfirmCode") + return firstly { () -> Promise<(data: Data, response: URLResponse)> in + let parameters:[String : Any] = [ + "email" : email, + "lockdown": true + ] + return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/resend-confirm-code", parameters: parameters)) + } + .map { data, response -> Bool in + if let httpResponse = response as? HTTPURLResponse { + DDLogInfo("API RESULT: resend-confirm-code: \(httpResponse.statusCode)") + if httpResponse.statusCode < 400 { + return true + } + return false + } + DDLogInfo("API RESULT: error - resend-confirm-code: not HTTPURLResponse") + return false + } + } + + static func subscriptionEvent(forceRefresh: Bool = false) throws -> Promise { + DDLogInfo("API CALL: subscription-event") + return getReceipt(forceRefresh: forceRefresh) + .then { receipt -> Promise<(data: Data, response: URLResponse)> in + let parameters:[String : Any] = [ + "authtype": "ios", + "authreceipt": receipt, + "lockdown": true + ] + return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/subscription-event", parameters: parameters)) + } + .map { data, response -> SubscriptionEvent in + try self.validateApiResponse(data: data, response: response) + let subscriptionEvent = try JSONDecoder().decode(SubscriptionEvent.self, from: data) + DDLogInfo("API RESULT: subscriptionEvent: \(subscriptionEvent)") + return subscriptionEvent + } + .recover { error -> Promise in + DDLogInfo("Recovering from subscription-event error: \(error)") + return .value(SubscriptionEvent(message: "Recovery")) + } + } + + static func activeSubscriptions() throws -> Promise<[Subscription]> { + DDLogInfo("API CALL: active-subscriptions") + return firstly { + URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/active-subscriptions", parameters: [:])) + }.map { data, response -> [Subscription] in + try self.validateApiResponse(data: data, response: response) + let decoder = JSONDecoder() + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + decoder.dateDecodingStrategy = .formatted(formatter) + var subscriptions = try decoder.decode([Subscription].self, from: data) + DDLogInfo("API RESULT: active-subscriptions: \(subscriptions)") + // sort subscriptions with highest tier at the top + subscriptions.sort(by: { (sub1: Subscription, sub2: Subscription) -> Bool in + let p1 = Subscription.PlanType.precedence(p: sub1.planType) + let p2 = Subscription.PlanType.precedence(p: sub2.planType) + return p1 <= p2 + }) + DDLogInfo("API RESULT: sorted-active-subscriptions: \(subscriptions)") + return subscriptions + } + } + + // For creating email account only - not signing up with IAP receipt + static func signup(email: String, password: String) throws -> Promise { + DDLogInfo("API CALL: signup") + return firstly { () -> Promise<(data: Data, response: URLResponse)> in + let parameters:[String : Any] = [ + "email" : email, + "password" : password, + "lockdown": true + ] + return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/signup", parameters: parameters)) + } + .map { data, response -> Signup in + try self.validateApiResponse(data: data, response: response) + let signup = try JSONDecoder().decode(Signup.self, from: data) + DDLogInfo("API RESULT: signup: \(signup)") + return signup + } + } + + static func forgotPassword(email: String) throws -> Promise { + DDLogInfo("API CALL: forgot-password") + return firstly { () -> Promise<(data: Data, response: URLResponse)> in + let parameters:[String : Any] = [ + "email" : email, + "lockdown": true + ] + return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/forgot-password", parameters: parameters)) + } + .map { data, response -> Bool in + if let httpResponse = response as? HTTPURLResponse { + DDLogInfo("API RESULT: forgot-password: \(httpResponse.statusCode)") + if httpResponse.statusCode < 400 { + return true + } + if let error = try? JSONDecoder().decode(ApiError.self, from: data) { + throw error + } + throw ApiError( + code: httpResponse.statusCode, + message: "Unknown error" + ) + } + DDLogInfo("API RESULT: error - forgot-password: not HTTPURLResponse") + return false + } + } static func getKey() throws -> Promise { DDLogInfo("API CALL: getKey") return firstly { () -> Promise<(data: Data, response: URLResponse)> in - let parameters = [ - "platform" : "ios" + let parameters:[String : Any] = [ + "platform" : "ios", + "lockdown": true ] return URLSession.shared.dataTask(.promise, with: try makePostRequest(urlString: mainURL + "/get-key", parameters: parameters)) } @@ -98,16 +237,10 @@ class Client { } } - static func getBlockedDomainTest(connectionSuccessHandler: @escaping () -> Void, connectionFailedHandler: @escaping (_ error: Error?) -> Void) -> PMKFinalizer { + static func getBlockedDomainTest() -> Promise { return firstly { URLSession.shared.dataTask(.promise, with: try Client.makeGetRequest(urlString: "https://\(testFirewallDomain)")) - } - .done { _ in - connectionSuccessHandler() - } - .catch { error in - connectionFailedHandler(error) - } + }.asVoid() } // MARK: - Request Makers @@ -127,7 +260,7 @@ class Client { } static func makePostRequest(urlString: String, parameters: [String: Any]) throws -> URLRequest { - DDLogInfo("makePostRequest: \(urlString), parameters: \(parameters)") + DDLogInfo("makePostRequest: \(urlString)")//", parameters: \(parameters)") if let url = URL(string: urlString) { var rq = URLRequest(url: url) rq.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData @@ -144,7 +277,7 @@ class Client { // MARK: - Util - private static func getReceipt(forceRefresh: Bool) -> Promise { + static func getReceipt(forceRefresh: Bool) -> Promise { DDLogInfo("fetch and set latest receipt") return Promise { seal in SwiftyStoreKit.fetchReceipt(forceRefresh: forceRefresh) { result in @@ -198,7 +331,7 @@ class Client { return hasValidCookie } - private static func clearCookies() { + static func clearCookies() { DDLogInfo("clearing cookies") var cookiesToDelete:[HTTPCookie] = [] if let cookies = HTTPCookieStorage.shared.cookies { @@ -244,5 +377,4 @@ class Client { throw "Invalid URL Response received" } } - } diff --git a/ClientModels.swift b/ClientModels.swift index bb80356..7784880 100644 --- a/ClientModels.swift +++ b/ClientModels.swift @@ -21,12 +21,122 @@ struct GetKey: Codable { let b64: String } +struct SubscriptionEvent: Codable { + let message: String +} + +enum pt { + case advancedMonthly + case advancedAnnual + case anonymousMonthly + case anonymousAnnual + case universalMonthly + case universalAnnual + case universalWeekly +} + +struct Subscription: Codable { + let planType: PlanType + let receiptId: String + let expirationDate: Date + let expirationDateString: String + let expirationDateMs: Int + let cancellationDate: Date? + let cancellationDateString: String? + let cancellationDateMs: Int? + + struct PlanType: RawRepresentable, RawValueCodable, Hashable { + let rawValue: String + init(rawValue: String) { + self.rawValue = rawValue + } + + static func precedence(p: PlanType) -> Int { + switch p { + case Self.universalAnnual: + return 0 + case Self.universalMonthly: + return 1 + case Self.anonymousAnnual: + return 2 + case Self.anonymousWeekly: + return 3 + case Self.advancedAnnual: + return 4 + case Self.advancedMonthly: + return 5 + case Self.anonymousMonthly: + return 6 + default: + return 7 + } + } + + static let advancedMonthly = PlanType(rawValue: "ios-fw-monthly") + static let advancedAnnual = PlanType(rawValue: "ios-fw-annual") + static let anonymousMonthly = PlanType(rawValue: "ios-monthly") + static let anonymousAnnual = PlanType(rawValue: "ios-annual") + static let universalMonthly = PlanType(rawValue: "all-monthly") + static let universalAnnual = PlanType(rawValue: "all-annual") + static let anonymousWeekly = PlanType(rawValue: "ios-weekly") + + var isAdvanced: Bool { + self == Self.advancedAnnual || self == Self.advancedMonthly + } + + var isAnonymous: Bool { + self == Self.anonymousAnnual || self == Self.anonymousMonthly || self == Self.anonymousWeekly + } + + var isUniversal: Bool { + self == Self.universalAnnual || self == Self.universalMonthly + } + } + + func isSameType(_ subscrition: Subscription) -> Bool { + self.planType.isAdvanced == subscrition.planType.isAdvanced || + self.planType.isAnonymous == subscrition.planType.isAnonymous || + self.planType.isUniversal == subscrition.planType.isUniversal + } +} + struct SignIn: Codable { let code: Int let message: String } +struct Signup: Codable { + let code: Int + let message: String +} + struct ApiError: Codable, Error { let code: Int let message: String } + +// MARK: - Helpers + +public enum RawValueCodableError: Error { + case wrongRawValue +} + +public protocol RawValueCodable: RawRepresentable, Codable { +} + +public extension RawValueCodable where RawValue: Codable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(RawValue.self) + if let value = Self.init(rawValue: rawValue) { + self = value + } else { + throw RawValueCodableError.wrongRawValue + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } +} diff --git a/ConfirmedTunnel/PacketTunnelProvider.swift b/ConfirmedTunnel/PacketTunnelProvider.swift deleted file mode 100644 index 425d95b..0000000 --- a/ConfirmedTunnel/PacketTunnelProvider.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// PacketTunnelProvider.swift -// ConfirmedTunnel -// -// Created by Rahul Dewan on 3/29/18. -// Copyright © 2018 Trust Software. All rights reserved. -// - -import NetworkExtension - -class PacketTunnelProvider: NEPacketTunnelProvider { - - override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { - // Add code here to start the process of connecting the tunnel. - - var settings = NEPacketTunnelNetworkSettings.init(tunnelRemoteAddress: "192.0.2.2") - - var ipv4Settings = NEIPv4Settings.init(addresses: ["192.0.2.1"], subnetMasks: ["255.255.255.0"]) - var route = NEIPv4Route.init(destinationAddress: "10.0.0.0", subnetMask: "104.25.112.26") - - var excluded = NEIPv4Route.default()// NEIPv4Route.init(destinationAddress: "255.255.255.0", subnetMask: "255.255.255.0") - - ipv4Settings.includedRoutes = [route]; - ipv4Settings.excludedRoutes = [excluded] - //ipv4Settings.includedRoutes = @[[NEIPv4Route defaultRoute]]; - settings.ipv4Settings = ipv4Settings; - - - //settings.IPv4Settings = ipv4Settings; - settings.mtu = NSNumber.init(value: 1600) - var proxySettings = NEProxySettings.init() - - var proxyServerPort = 3838; - var proxyServerName = "localhost"; - - proxySettings.httpEnabled = true; - proxySettings.httpServer = NEProxyServer.init(address: proxyServerName, port: proxyServerPort) - proxySettings.httpsEnabled = true; - proxySettings.httpsServer = NEProxyServer.init(address: proxyServerName, port: proxyServerPort) - proxySettings.excludeSimpleHostnames = true; - proxySettings.exceptionList = ["*.ipchicken.com", "www.ipchicken.com"]; - proxySettings.matchDomains = ["*.google.com", "*.hulu.com"]; - - - settings.proxySettings = proxySettings; - - self.setTunnelNetworkSettings(settings, completionHandler: { error in - completionHandler(nil) - }) - } - - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - // Add code here to start the process of stopping the tunnel. - completionHandler() - } - - override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - // Add code here to handle the message. - if let handler = completionHandler { - handler(messageData) - } - } - - override func sleep(completionHandler: @escaping () -> Void) { - // Add code here to get ready to sleep. - completionHandler() - } - - override func wake() { - // Add code here to wake up. - } -} diff --git a/Defaults.swift b/Defaults.swift new file mode 100644 index 0000000..74d4cba --- /dev/null +++ b/Defaults.swift @@ -0,0 +1,55 @@ +// +// Defaults.swift +// Lockdown +// +// Created by Radu Lazar on 08.08.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import Foundation + +//MARK: Metrics +let kDayMetrics = "LockdownDayMetrics" +let kWeekMetrics = "LockdownWeekMetrics" +let kTotalMetrics = "LockdownTotalMetrics" +let kTotalEnabledMetrics = "LockdownTotalEnabledMetrics" +let kTotalDisabledMetrics = "LockdownTotalDisabledMetrics" + +let kActiveDay = "LockdownActiveDay" +let kActiveWeek = "LockdownActiveWeek" + +//MARK: Firewall utils + +let kLockdownBlockedDomains = "lockdown_domains" +let kUserBlockedDomains = "lockdown_domains_user" +let kUserBlockedLists = "lockdown_lists_user" + +//MARK: Whitelist + +let kLockdownWhitelistedDomains = "whitelisted_domains" +let kUserWhitelistedDomains = "whitelisted_domains_user" + +//MARK: Latest Knowledge + +let kLatestKnowledgeIsFirewallEnabled = "kLatestKnowledgeIsFirewallEnabled" +let kLatestKnowledgeIsVPNEnabled = "kLatestKnowledgeIsVPNEnabled" + +// MARK: - VPN Region + +let kSavedVPNRegionServerPrefix = "vpn_region_server_prefix" + +//MARK: Others + +let kAPICredentialsConfirmed = "APICredentialsConfirmed" + +let kUserWantsFirewallEnabled = "user_wants_firewall_enabled" +let kUserWantsVPNEnabled = "user_wants_vpn_enabled" + +let kAllowNotificationsAfterDate = "LockdownAllowNotificationsAfter" + +let kLockdownNotificationsEnergySavingCounter = "LockdownNotificationsEnergySavingCounter" + +let kAppActivateTime = "AppActivateTime" +let kOneTimeOfferShown = "OneTimeOfferShown" +let kSpecialOfferTimeDidReset = "SpecialOfferTimeDidReset_27_11_2024" +let kVersionOfLastRun = "VersionOfLastRun" diff --git a/Dnscryptproxy.xcframework/Info.plist b/Dnscryptproxy.xcframework/Info.plist new file mode 100644 index 0000000..6d46073 --- /dev/null +++ b/Dnscryptproxy.xcframework/Info.plist @@ -0,0 +1,40 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-arm64 + LibraryPath + Dnscryptproxy.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + LibraryIdentifier + ios-arm64_x86_64-simulator + LibraryPath + Dnscryptproxy.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Dnscryptproxy b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Dnscryptproxy new file mode 120000 index 0000000..64546e2 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Dnscryptproxy @@ -0,0 +1 @@ +Versions/Current/Dnscryptproxy \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Headers b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Headers new file mode 120000 index 0000000..a177d2a --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Modules b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Modules new file mode 120000 index 0000000..5736f31 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Modules @@ -0,0 +1 @@ +Versions/Current/Modules \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Resources b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Resources new file mode 120000 index 0000000..953ee36 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Dnscryptproxy b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Dnscryptproxy new file mode 100644 index 0000000..b5c435d Binary files /dev/null and b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Dnscryptproxy differ diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Dnscryptproxy.h b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Dnscryptproxy.h new file mode 100644 index 0000000..96b2bd0 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Dnscryptproxy.h @@ -0,0 +1,13 @@ + +// Objective-C API for talking to the following Go packages +// +// github.com/jedisct1/dnscrypt-proxy/dnscrypt-proxy/ios +// +// File is generated by gomobile bind. Do not edit. +#ifndef __Dnscryptproxy_FRAMEWORK_H__ +#define __Dnscryptproxy_FRAMEWORK_H__ + +#include "Dnscryptproxy.objc.h" +#include "Universe.objc.h" + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Dnscryptproxy.objc.h b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Dnscryptproxy.objc.h new file mode 100644 index 0000000..a3de93d --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Dnscryptproxy.objc.h @@ -0,0 +1,81 @@ +// Objective-C API for talking to github.com/jedisct1/dnscrypt-proxy/dnscrypt-proxy/ios Go package. +// gobind -lang=objc github.com/jedisct1/dnscrypt-proxy/dnscrypt-proxy/ios +// +// File is generated by gobind. Do not edit. + +#ifndef __Dnscryptproxy_H__ +#define __Dnscryptproxy_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + + +@class DnscryptproxyApp; +@class DnscryptproxyConfig; +@protocol DnscryptproxyCloakCallback; +@class DnscryptproxyCloakCallback; + +@protocol DnscryptproxyCloakCallback +- (void)proxyReady; +@end + +@interface DnscryptproxyApp : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +- (void)closeIdleConnections; +- (void)logCritical:(NSString* _Nullable)s; +- (void)logDebug:(NSString* _Nullable)s; +- (void)logError:(NSString* _Nullable)s; +- (void)logFatal:(NSString* _Nullable)s; +- (void)logInfo:(NSString* _Nullable)s; +- (void)logNotice:(NSString* _Nullable)s; +- (void)logWarn:(NSString* _Nullable)s; +- (long)refreshServersInfo; +- (void)run:(id _Nullable)cloakCallback; +- (BOOL)stop:(NSError* _Nullable* _Nullable)error; +@end + +@interface DnscryptproxyConfig : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nullable instancetype)init; +- (NSString* _Nonnull)listServers:(NSError* _Nullable* _Nullable)error; +- (BOOL)loadJson:(NSString* _Nullable)cfg error:(NSError* _Nullable* _Nullable)error; +- (BOOL)loadToml:(NSString* _Nullable)cfg error:(NSError* _Nullable* _Nullable)error; +- (NSString* _Nonnull)toJson:(NSError* _Nullable* _Nullable)error; +- (NSString* _Nonnull)toToml:(NSError* _Nullable* _Nullable)error; +@end + +FOUNDATION_EXPORT NSString* _Nonnull const DnscryptproxyAppVersion; + +FOUNDATION_EXPORT DnscryptproxyConfig* _Nullable DnscryptproxyDefaultConfig(void); + +FOUNDATION_EXPORT BOOL DnscryptproxyFillIpBlacklistTrees(NSString* _Nullable filepath, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT BOOL DnscryptproxyFillPatternlistTrees(NSString* _Nullable filepath, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT DnscryptproxyApp* _Nullable DnscryptproxyMain(NSString* _Nullable configFile); + +FOUNDATION_EXPORT DnscryptproxyConfig* _Nullable DnscryptproxyNewConfig(void); + +FOUNDATION_EXPORT BOOL DnscryptproxyPrefetchSourceURLCloak(long timeout_int, BOOL useIPv4, BOOL useIPv6, NSString* _Nullable fallbackResolver, BOOL ignoreSystemDNS, NSString* _Nullable url, NSString* _Nullable cacheFile, NSString* _Nullable minisignKey, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT void DnscryptproxyRefreshServersInfoCloak(DnscryptproxyApp* _Nullable app); + +@class DnscryptproxyCloakCallback; + +@interface DnscryptproxyCloakCallback : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (void)proxyReady; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Universe.objc.h b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Universe.objc.h new file mode 100644 index 0000000..019e750 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/Universe.objc.h @@ -0,0 +1,29 @@ +// Objective-C API for talking to Go package. +// gobind -lang=objc +// +// File is generated by gobind. Do not edit. + +#ifndef __Universe_H__ +#define __Universe_H__ + +@import Foundation; +#include "ref.h" + +@protocol Universeerror; +@class Universeerror; + +@protocol Universeerror +- (NSString* _Nonnull)error; +@end + +@class Universeerror; + +@interface Universeerror : NSError { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (NSString* _Nonnull)error; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/ref.h b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/ref.h new file mode 100644 index 0000000..b8036a4 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Headers/ref.h @@ -0,0 +1,35 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef __GO_REF_HDR__ +#define __GO_REF_HDR__ + +#include + +// GoSeqRef is an object tagged with an integer for passing back and +// forth across the language boundary. A GoSeqRef may represent either +// an instance of a Go object, or an Objective-C object passed to Go. +// The explicit allocation of a GoSeqRef is used to pin a Go object +// when it is passed to Objective-C. The Go seq package maintains a +// reference to the Go object in a map keyed by the refnum along with +// a reference count. When the reference count reaches zero, the Go +// seq package will clear the corresponding entry in the map. +@interface GoSeqRef : NSObject { +} +@property(readonly) int32_t refnum; +@property(strong) id obj; // NULL when representing a Go object. + +// new GoSeqRef object to proxy a Go object. The refnum must be +// provided from Go side. +- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; + +- (int32_t)incNum; + +@end + +@protocol goSeqRefInterface +-(GoSeqRef*) _ref; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Modules/module.modulemap b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Modules/module.modulemap new file mode 100644 index 0000000..8fa78da --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Modules/module.modulemap @@ -0,0 +1,8 @@ +framework module "Dnscryptproxy" { + header "ref.h" + header "Dnscryptproxy.objc.h" + header "Universe.objc.h" + header "Dnscryptproxy.h" + + export * +} \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Resources/Info.plist b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Resources/Info.plist new file mode 100644 index 0000000..0d1a4b8 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/Current b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/Current new file mode 120000 index 0000000..8c7e5a6 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy-old.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Dnscryptproxy b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Dnscryptproxy new file mode 120000 index 0000000..64546e2 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Dnscryptproxy @@ -0,0 +1 @@ +Versions/Current/Dnscryptproxy \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Headers b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Headers new file mode 120000 index 0000000..a177d2a --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Modules b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Modules new file mode 120000 index 0000000..5736f31 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Modules @@ -0,0 +1 @@ +Versions/Current/Modules \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Resources b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Resources new file mode 120000 index 0000000..953ee36 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Dnscryptproxy b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Dnscryptproxy new file mode 100644 index 0000000..cdf2275 Binary files /dev/null and b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Dnscryptproxy differ diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Dnscryptproxy-not-working b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Dnscryptproxy-not-working new file mode 100644 index 0000000..7cf7d92 Binary files /dev/null and b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Dnscryptproxy-not-working differ diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.h b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.h new file mode 100644 index 0000000..4d0dca9 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.h @@ -0,0 +1,13 @@ + +// Objective-C API for talking to the following Go packages +// +// github.com/DNSCrypt/dnscrypt-proxy/dnscrypt-proxy/ios +// +// File is generated by gomobile bind. Do not edit. +#ifndef __Dnscryptproxy_FRAMEWORK_H__ +#define __Dnscryptproxy_FRAMEWORK_H__ + +#include "Dnscryptproxy.objc.h" +#include "Universe.objc.h" + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.objc.h b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.objc.h new file mode 100644 index 0000000..7bd31f6 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.objc.h @@ -0,0 +1,81 @@ +// Objective-C API for talking to github.com/DNSCrypt/dnscrypt-proxy/dnscrypt-proxy/ios Go package. +// gobind -lang=objc github.com/DNSCrypt/dnscrypt-proxy/dnscrypt-proxy/ios +// +// File is generated by gobind. Do not edit. + +#ifndef __Dnscryptproxy_H__ +#define __Dnscryptproxy_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + + +@class DnscryptproxyApp; +@class DnscryptproxyConfig; +@protocol DnscryptproxyCloakCallback; +@class DnscryptproxyCloakCallback; + +@protocol DnscryptproxyCloakCallback +- (void)proxyReady; +@end + +@interface DnscryptproxyApp : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +- (void)closeIdleConnections; +- (void)logCritical:(NSString* _Nullable)s; +- (void)logDebug:(NSString* _Nullable)s; +- (void)logError:(NSString* _Nullable)s; +- (void)logFatal:(NSString* _Nullable)s; +- (void)logInfo:(NSString* _Nullable)s; +- (void)logNotice:(NSString* _Nullable)s; +- (void)logWarn:(NSString* _Nullable)s; +- (long)refreshServersInfo; +- (void)run:(id _Nullable)cloakCallback; +- (BOOL)stop:(NSError* _Nullable* _Nullable)error; +@end + +@interface DnscryptproxyConfig : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nullable instancetype)init; +- (NSString* _Nonnull)listServers:(NSError* _Nullable* _Nullable)error; +- (BOOL)loadJson:(NSString* _Nullable)cfg error:(NSError* _Nullable* _Nullable)error; +- (BOOL)loadToml:(NSString* _Nullable)cfg error:(NSError* _Nullable* _Nullable)error; +- (NSString* _Nonnull)toJson:(NSError* _Nullable* _Nullable)error; +- (NSString* _Nonnull)toToml:(NSError* _Nullable* _Nullable)error; +@end + +FOUNDATION_EXPORT NSString* _Nonnull const DnscryptproxyAppVersion; + +FOUNDATION_EXPORT DnscryptproxyConfig* _Nullable DnscryptproxyDefaultConfig(void); + +FOUNDATION_EXPORT BOOL DnscryptproxyFillIpBlacklistTrees(NSString* _Nullable filepath, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT BOOL DnscryptproxyFillPatternlistTrees(NSString* _Nullable filepath, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT DnscryptproxyApp* _Nullable DnscryptproxyMain(NSString* _Nullable configFile); + +FOUNDATION_EXPORT DnscryptproxyConfig* _Nullable DnscryptproxyNewConfig(void); + +FOUNDATION_EXPORT BOOL DnscryptproxyPrefetchSourceURLCloak(long timeout_int, BOOL useIPv4, BOOL useIPv6, NSString* _Nullable fallbackResolver, BOOL ignoreSystemDNS, NSString* _Nullable url, NSString* _Nullable cacheFile, NSString* _Nullable minisignKey, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT void DnscryptproxyRefreshServersInfoCloak(DnscryptproxyApp* _Nullable app); + +@class DnscryptproxyCloakCallback; + +@interface DnscryptproxyCloakCallback : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (void)proxyReady; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Universe.objc.h b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Universe.objc.h new file mode 100644 index 0000000..019e750 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/Universe.objc.h @@ -0,0 +1,29 @@ +// Objective-C API for talking to Go package. +// gobind -lang=objc +// +// File is generated by gobind. Do not edit. + +#ifndef __Universe_H__ +#define __Universe_H__ + +@import Foundation; +#include "ref.h" + +@protocol Universeerror; +@class Universeerror; + +@protocol Universeerror +- (NSString* _Nonnull)error; +@end + +@class Universeerror; + +@interface Universeerror : NSError { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (NSString* _Nonnull)error; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/ref.h b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/ref.h new file mode 100644 index 0000000..b8036a4 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Headers/ref.h @@ -0,0 +1,35 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef __GO_REF_HDR__ +#define __GO_REF_HDR__ + +#include + +// GoSeqRef is an object tagged with an integer for passing back and +// forth across the language boundary. A GoSeqRef may represent either +// an instance of a Go object, or an Objective-C object passed to Go. +// The explicit allocation of a GoSeqRef is used to pin a Go object +// when it is passed to Objective-C. The Go seq package maintains a +// reference to the Go object in a map keyed by the refnum along with +// a reference count. When the reference count reaches zero, the Go +// seq package will clear the corresponding entry in the map. +@interface GoSeqRef : NSObject { +} +@property(readonly) int32_t refnum; +@property(strong) id obj; // NULL when representing a Go object. + +// new GoSeqRef object to proxy a Go object. The refnum must be +// provided from Go side. +- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; + +- (int32_t)incNum; + +@end + +@protocol goSeqRefInterface +-(GoSeqRef*) _ref; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Modules/module.modulemap b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Modules/module.modulemap new file mode 100644 index 0000000..8fa78da --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Modules/module.modulemap @@ -0,0 +1,8 @@ +framework module "Dnscryptproxy" { + header "ref.h" + header "Dnscryptproxy.objc.h" + header "Universe.objc.h" + header "Dnscryptproxy.h" + + export * +} \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Resources/Info.plist b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Resources/Info.plist new file mode 100644 index 0000000..0d1a4b8 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/Current b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/Current new file mode 120000 index 0000000..8c7e5a6 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64/Dnscryptproxy.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Dnscryptproxy b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Dnscryptproxy new file mode 120000 index 0000000..64546e2 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Dnscryptproxy @@ -0,0 +1 @@ +Versions/Current/Dnscryptproxy \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Headers b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Headers new file mode 120000 index 0000000..a177d2a --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Headers @@ -0,0 +1 @@ +Versions/Current/Headers \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Modules b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Modules new file mode 120000 index 0000000..5736f31 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Modules @@ -0,0 +1 @@ +Versions/Current/Modules \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Resources b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Resources new file mode 120000 index 0000000..953ee36 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Resources @@ -0,0 +1 @@ +Versions/Current/Resources \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Dnscryptproxy b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Dnscryptproxy new file mode 100644 index 0000000..e646ecf Binary files /dev/null and b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Dnscryptproxy differ diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.h b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.h new file mode 100644 index 0000000..96b2bd0 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.h @@ -0,0 +1,13 @@ + +// Objective-C API for talking to the following Go packages +// +// github.com/jedisct1/dnscrypt-proxy/dnscrypt-proxy/ios +// +// File is generated by gomobile bind. Do not edit. +#ifndef __Dnscryptproxy_FRAMEWORK_H__ +#define __Dnscryptproxy_FRAMEWORK_H__ + +#include "Dnscryptproxy.objc.h" +#include "Universe.objc.h" + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.objc.h b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.objc.h new file mode 100644 index 0000000..a3de93d --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Dnscryptproxy.objc.h @@ -0,0 +1,81 @@ +// Objective-C API for talking to github.com/jedisct1/dnscrypt-proxy/dnscrypt-proxy/ios Go package. +// gobind -lang=objc github.com/jedisct1/dnscrypt-proxy/dnscrypt-proxy/ios +// +// File is generated by gobind. Do not edit. + +#ifndef __Dnscryptproxy_H__ +#define __Dnscryptproxy_H__ + +@import Foundation; +#include "ref.h" +#include "Universe.objc.h" + + +@class DnscryptproxyApp; +@class DnscryptproxyConfig; +@protocol DnscryptproxyCloakCallback; +@class DnscryptproxyCloakCallback; + +@protocol DnscryptproxyCloakCallback +- (void)proxyReady; +@end + +@interface DnscryptproxyApp : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +- (void)closeIdleConnections; +- (void)logCritical:(NSString* _Nullable)s; +- (void)logDebug:(NSString* _Nullable)s; +- (void)logError:(NSString* _Nullable)s; +- (void)logFatal:(NSString* _Nullable)s; +- (void)logInfo:(NSString* _Nullable)s; +- (void)logNotice:(NSString* _Nullable)s; +- (void)logWarn:(NSString* _Nullable)s; +- (long)refreshServersInfo; +- (void)run:(id _Nullable)cloakCallback; +- (BOOL)stop:(NSError* _Nullable* _Nullable)error; +@end + +@interface DnscryptproxyConfig : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nullable instancetype)init; +- (NSString* _Nonnull)listServers:(NSError* _Nullable* _Nullable)error; +- (BOOL)loadJson:(NSString* _Nullable)cfg error:(NSError* _Nullable* _Nullable)error; +- (BOOL)loadToml:(NSString* _Nullable)cfg error:(NSError* _Nullable* _Nullable)error; +- (NSString* _Nonnull)toJson:(NSError* _Nullable* _Nullable)error; +- (NSString* _Nonnull)toToml:(NSError* _Nullable* _Nullable)error; +@end + +FOUNDATION_EXPORT NSString* _Nonnull const DnscryptproxyAppVersion; + +FOUNDATION_EXPORT DnscryptproxyConfig* _Nullable DnscryptproxyDefaultConfig(void); + +FOUNDATION_EXPORT BOOL DnscryptproxyFillIpBlacklistTrees(NSString* _Nullable filepath, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT BOOL DnscryptproxyFillPatternlistTrees(NSString* _Nullable filepath, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT DnscryptproxyApp* _Nullable DnscryptproxyMain(NSString* _Nullable configFile); + +FOUNDATION_EXPORT DnscryptproxyConfig* _Nullable DnscryptproxyNewConfig(void); + +FOUNDATION_EXPORT BOOL DnscryptproxyPrefetchSourceURLCloak(long timeout_int, BOOL useIPv4, BOOL useIPv6, NSString* _Nullable fallbackResolver, BOOL ignoreSystemDNS, NSString* _Nullable url, NSString* _Nullable cacheFile, NSString* _Nullable minisignKey, NSError* _Nullable* _Nullable error); + +FOUNDATION_EXPORT void DnscryptproxyRefreshServersInfoCloak(DnscryptproxyApp* _Nullable app); + +@class DnscryptproxyCloakCallback; + +@interface DnscryptproxyCloakCallback : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (void)proxyReady; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Universe.objc.h b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Universe.objc.h new file mode 100644 index 0000000..019e750 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/Universe.objc.h @@ -0,0 +1,29 @@ +// Objective-C API for talking to Go package. +// gobind -lang=objc +// +// File is generated by gobind. Do not edit. + +#ifndef __Universe_H__ +#define __Universe_H__ + +@import Foundation; +#include "ref.h" + +@protocol Universeerror; +@class Universeerror; + +@protocol Universeerror +- (NSString* _Nonnull)error; +@end + +@class Universeerror; + +@interface Universeerror : NSError { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (NSString* _Nonnull)error; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/ref.h b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/ref.h new file mode 100644 index 0000000..b8036a4 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Headers/ref.h @@ -0,0 +1,35 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#ifndef __GO_REF_HDR__ +#define __GO_REF_HDR__ + +#include + +// GoSeqRef is an object tagged with an integer for passing back and +// forth across the language boundary. A GoSeqRef may represent either +// an instance of a Go object, or an Objective-C object passed to Go. +// The explicit allocation of a GoSeqRef is used to pin a Go object +// when it is passed to Objective-C. The Go seq package maintains a +// reference to the Go object in a map keyed by the refnum along with +// a reference count. When the reference count reaches zero, the Go +// seq package will clear the corresponding entry in the map. +@interface GoSeqRef : NSObject { +} +@property(readonly) int32_t refnum; +@property(strong) id obj; // NULL when representing a Go object. + +// new GoSeqRef object to proxy a Go object. The refnum must be +// provided from Go side. +- (instancetype)initWithRefnum:(int32_t)refnum obj:(id)obj; + +- (int32_t)incNum; + +@end + +@protocol goSeqRefInterface +-(GoSeqRef*) _ref; +@end + +#endif diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Modules/module.modulemap b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Modules/module.modulemap new file mode 100644 index 0000000..8fa78da --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Modules/module.modulemap @@ -0,0 +1,8 @@ +framework module "Dnscryptproxy" { + header "ref.h" + header "Dnscryptproxy.objc.h" + header "Universe.objc.h" + header "Dnscryptproxy.h" + + export * +} \ No newline at end of file diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Resources/Info.plist b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Resources/Info.plist new file mode 100644 index 0000000..0d1a4b8 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/A/Resources/Info.plist @@ -0,0 +1,6 @@ + + + + + + diff --git a/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/Current b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/Current new file mode 120000 index 0000000..8c7e5a6 --- /dev/null +++ b/Dnscryptproxy.xcframework/ios-arm64_x86_64-simulator/Dnscryptproxy.framework/Versions/Current @@ -0,0 +1 @@ +A \ No newline at end of file diff --git a/DomainNameValidator.swift b/DomainNameValidator.swift new file mode 100644 index 0000000..7b2e334 --- /dev/null +++ b/DomainNameValidator.swift @@ -0,0 +1,60 @@ +// +// DomainNameValidator.swift +// Lockdown +// +// Created by Oleg Dreyman on 19.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation + +enum DomainNameValidator { + + enum Status { + case valid + case notValid(FailureReason) + + // Not currently shown to the user, but can be leveraged in the future + enum FailureReason { + case emptyString + case noDots + case invalidCharacters(in: String) + case labelEmpty + } + } + + /// All "url host allowed" characters with the exception of the wildcard symbol + static let allowedChars = CharacterSet.urlHostAllowed + .subtracting(CharacterSet(charactersIn: "*")) + + static func validate(_ domainName: String) -> Status { + + guard domainName.isEmpty == false else { + return .notValid(.emptyString) + } + + var labels = domainName.components(separatedBy: ".") + guard labels.count > 1 else { + return .notValid(.noDots) + } + + // Wildcard is allowed only as a first label. + // If first label is a wildcard, we're removing + // it from the elements to validate + if labels.first == "*" { + labels.removeFirst() + } + + for label in labels { + guard label.isEmpty == false else { + return .notValid(.labelEmpty) + } + + guard allowedChars.isSuperset(of: CharacterSet(charactersIn: label)) else { + return .notValid(.invalidCharacters(in: label)) + } + } + + return .valid + } +} diff --git a/Environment.swift b/Environment.swift index e3a4e7f..e3ea051 100644 --- a/Environment.swift +++ b/Environment.swift @@ -7,14 +7,34 @@ // import Foundation +import CocoaLumberjackSwift -let vpnSourceID = "-111818" -let vpnDomain = "confirmedvpn.com" -let vpnRemoteIdentifier = "www" + vpnSourceID + "." + vpnDomain -let mainDomain = "confirmedvpn.com" +//Prod Environment +let vpnSourceID = "-111818" //getEnvironmentVariable(key: "vpnSourceID", default: "-111818") +let vpnDomain = "confirmedvpn.com" //getEnvironmentVariable(key: "vpnDomain", default: "confirmedvpn.com") +let vpnRemoteIdentifier = "www" + vpnSourceID + "." + vpnDomain +let mainDomain = "confirmedvpn.com" //getEnvironmentVariable(key: "mainDomain", default: "confirmedvpn.com") let mainURL = "https://www." + mainDomain +// Dev Environment +// US-East US-West +//let vpnSourceID = "-111618" +//let vpnDomain = "trusty-ap.science" +//let vpnRemoteIdentifier = "www" + vpnSourceID + "." + vpnDomain +//let mainDomain = "trusty-ap.science" +//let mainURL = "https://www." + mainDomain + let testFirewallDomain = "example.com" -let lastVersionToAskForRating = "020" +let lastVersionToAskForRating = "024" + +func getEnvironmentVariable(key: String, default: String) -> String { + if let value = ProcessInfo.processInfo.environment[key] { + return value + } + else { + DDLogError("ERROR: Could not find environment variable key \(key)") + return "" + } +} diff --git a/FirewallController.swift b/FirewallController.swift index 7717cb2..ce17249 100644 --- a/FirewallController.swift +++ b/FirewallController.swift @@ -9,6 +9,7 @@ import UIKit import NetworkExtension import CocoaLumberjackSwift import PromiseKit +import WidgetKit let kFirewallTunnelLocalizedDescription = "Lockdown Configuration" @@ -27,46 +28,96 @@ class FirewallController: NSObject { // get the reference to the latest manager in Settings NETunnelProviderManager.loadAllFromPreferences { (managers, error) -> Void in if let managers = managers, managers.count > 0 { - self.manager = nil - self.manager = managers[0] + if (self.manager == managers[0]) { + DDLogInfo("Encountered same manager while refreshing manager, not replacing it.") + } else { + self.manager = nil + self.manager = managers[0] + } + completion(nil) } completion(error) } } + func existingManagerCount(completion: @escaping (Int?) -> Void) { + NETunnelProviderManager.loadAllFromPreferences { (managers, error) in + completion(managers?.count) + } + } + func status() -> NEVPNStatus { - if manager != nil { - return manager!.connection.status + if let manager { + return manager.connection.status } else { return .invalid } } + + func deleteConfigurationAndAddAgain() { + refreshManager { (error) in + self.manager?.removeFromPreferences(completionHandler: { (removeError) in + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.setEnabled(true, isUserExplicitToggle: true) + } + }) + } + } func restart(completion: @escaping (_ error: Error?) -> Void = {_ in }) { + DDLogInfo("FirewallController.restart called") // Don't let this affect userWantsFirewallOn/Off config FirewallController.shared.setEnabled(false, completion: { error in + DDLogInfo("FirewallController.restart completed disabling") // TODO: Handle the error (throw?) if error != nil { DDLogError("Error disabling on Firewall restart: \(error!)") } - FirewallController.shared.setEnabled(true, completion: { - error in - if error != nil { - DDLogError("Error enabling on Firewall restart: \(error!)") - } - completion(error) - }) + // waiting for a little bit before re-enabling: + // without it, sometimes Firewall fails to enable + DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { + DDLogInfo("FirewallController.restart wait completed") + FirewallController.shared.setEnabled(true, completion: { + error in + DDLogInfo("FirewallController.restart completed enabling") + if error != nil { + DDLogError("Error enabling on Firewall restart: \(error!)") + } + completion(error) + }) + } }) } + struct CombinedBlockListEmptyError: Error { } + + private func handleUserDeniedAccessToFirewallConfiguration() { + setUserWantsFirewallEnabled(false) + if #available(iOSApplicationExtension 14.0, iOS 14.0, *) { + WidgetCenter.shared.reloadAllTimelines() + } + manager = nil + } + func setEnabled(_ enabled: Bool, isUserExplicitToggle: Bool = false, completion: @escaping (_ error: Error?) -> Void = {_ in }) { DDLogInfo("FirewallController set enabled: \(enabled)") // only change this boolean if it's user action if (isUserExplicitToggle) { setUserWantsFirewallEnabled(enabled) + if #available(iOSApplicationExtension 14.0, iOS 14.0, *) { + WidgetCenter.shared.reloadAllTimelines() + } + } + + if enabled && getIsCombinedBlockListEmpty() { + DDLogError("Trying to enable Firewall when combined block list is empty; not allowing") + completion(FirewallController.CombinedBlockListEmptyError()) + assertionFailure("Trying to enable Firewall when combined block list is empty; not allowing. This crash only happens in DEBUG mode") + return } + // just to be sure, reload the managers to make sure we don't make multiple configs NETunnelProviderManager.loadAllFromPreferences { (managers, error) -> Void in if let managers = managers, managers.count > 0 { @@ -76,17 +127,18 @@ class FirewallController: NSObject { else { self.manager = nil self.manager = NETunnelProviderManager() - self.manager!.protocolConfiguration = NETunnelProviderProtocol() + self.manager?.protocolConfiguration = NETunnelProviderProtocol() } - self.manager!.localizedDescription = kFirewallTunnelLocalizedDescription - self.manager!.protocolConfiguration?.serverAddress = kFirewallTunnelLocalizedDescription - self.manager!.isEnabled = enabled - self.manager!.isOnDemandEnabled = enabled + self.manager?.localizedDescription = kFirewallTunnelLocalizedDescription + self.manager?.protocolConfiguration?.serverAddress = kFirewallTunnelLocalizedDescription + self.manager?.isEnabled = enabled + self.manager?.isOnDemandEnabled = enabled + self.manager?.protocolConfiguration?.disconnectOnSleep = false let connectRule = NEOnDemandRuleConnect() connectRule.interfaceTypeMatch = .any - self.manager!.onDemandRules = [connectRule] - self.manager!.saveToPreferences(completionHandler: { (error) -> Void in + self.manager?.onDemandRules = [connectRule] + self.manager?.saveToPreferences(completionHandler: { [weak self] (error) -> Void in // TODO: Handle each case specifically if let e = error as? NEVPNError { DDLogError("VPN Error while saving state: \(enabled) \(e)") @@ -96,7 +148,8 @@ class FirewallController: NSObject { case .configurationInvalid: break; case .configurationReadWriteFailed: - break; + self?.handleUserDeniedAccessToFirewallConfiguration() + return case .configurationStale: break; case .configurationUnknown: @@ -104,18 +157,73 @@ class FirewallController: NSObject { case .connectionFailed: break; } + completion(e) } else if let e = error { DDLogError("Error saving config for enabled state: \(enabled): \(e)") + completion(e) } else { - DDLogInfo("Successfully saved config for enabled state: \(enabled)") + self?.loadFromPreferenceAndStartFirewall(enabled, completion: completion) } - self.refreshManager(completion: { error in - completion(nil) - }) }) } } + private func loadFromPreferenceAndStartFirewall(_ enabled: Bool, completion: @escaping (_ error: Error?) -> Void) { + manager?.loadFromPreferences { [weak self] error in + if let error { + DDLogError("Read preference error before start firewall: " + error.localizedDescription) + } + DDLogInfo("Successfully saved config for enabled state: \(enabled)") + // manually activate the starting of the tunnel, and also do a dummy connect to a nonexistant, invalid URL to force enabling + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + if (enabled) { + self?.startFirewallTunnel(completion: completion) + } + else { + DDLogInfo("FirewallController.setEnabled not enabled, no need to call startVPNTunnel") + completion(nil) + } + } + } + } + + private func startFirewallTunnel(completion: @escaping (_ error: Error?) -> Void) { + guard let manager else { + DDLogInfo("FirewallController.setEnabled ignore: empty manager") + completion(nil) + return + } + DDLogInfo("FirewallController.setEnabled enabled, calling startVPNTunnel") + do { + try manager.connection.startVPNTunnel() + let config = URLSessionConfiguration.default + config.requestCachePolicy = .reloadIgnoringLocalCacheData + config.urlCache = nil + let session = URLSession.init(configuration: config) + if let url = URL(string: "https://nonexistant_invalid_url") { + let task = session.dataTask(with: url) { (data, response, error) in + DDLogInfo("FirewallController.setEnabled response from calling nonexistant url") + return + } + DDLogInfo("FirewallController.setEnabled calling nonexistant url") + task.resume() + } + DDLogInfo("FirewallController.setEnabled refreshing manager") + refreshManager(completion: { error in + if let error { + DDLogInfo("FirewallController.setEnabled error response from refreshing manager: \(error)") + } + else { + DDLogInfo("FirewallController.setEnabled no error from refreshing manager") + } + completion(nil) + }) + } + catch { + DDLogError("Unable to start the tunnel after saving: " + error.localizedDescription) + completion(error.localizedDescription) + } + } } diff --git a/FirewallUtilities.swift b/FirewallUtilities.swift index 58611ea..b6734af 100644 --- a/FirewallUtilities.swift +++ b/FirewallUtilities.swift @@ -8,15 +8,14 @@ import Foundation import NetworkExtension +import WidgetKit +import UniformTypeIdentifiers // MARK: - Constants -let kLockdownBlockedDomains = "lockdown_domains" -let kUserBlockedDomains = "lockdown_domains_user" - // MARK: - data structures -struct IPRange : Codable { +struct ConfirmedIPRange : Codable { var subnetMask : String var enabled : Bool var IPv6 : Bool @@ -33,13 +32,31 @@ struct LockdownGroup : Codable { var iconURL : String var enabled : Bool var domains : Dictionary - var ipRanges : Dictionary + var ipRanges : Dictionary + var warning: String? + var accessLevel: String = "advanced" } struct LockdownDefaults : Codable { var lockdownDefaults : Dictionary } +struct UserBlockListsGroup: Codable { + var name: String + var enabled: Bool = false + var domains: Set = [] + var description: String? +} + +struct UserBlockListsDefaults: Codable { + var userBlockListsDefaults: Dictionary +} + +//struct Domains: Codable { +// var name: String +// var isBlocked: Bool = true +//} + // MARK: - Block Metrics & Block Log let currentCalendar = Calendar.current @@ -49,23 +66,55 @@ let blockLogDateFormatter : DateFormatter = { return formatter }() -let kDayMetrics = "LockdownDayMetrics" -let kWeekMetrics = "LockdownWeekMetrics" -let kTotalMetrics = "LockdownTotalMetrics" - -let kActiveDay = "LockdownActiveDay" -let kActiveWeek = "LockdownActiveWeek" - -let kDayLogs = "LockdownDayLogs" -let kDayLogsMaxSize = 5000; -let kDayLogsMaxReduction = 4500; +enum MetricsUpdate { + + enum Mode { + case incrementAndLog(host: String) + case resetIfNeeded + + var incrementBy: Int { + switch self { + case .incrementAndLog: + return 1 + case .resetIfNeeded: + return 0 + } + } + } + + enum RescheduleNotifications { + case always + case never + case withEnergySaving + + var allowsScheduling: Bool { + switch self { + case .always, .withEnergySaving: + return true + case .never: + return false + } + } + } +} -func incrementMetricsAndLog(host: String) { +func updateMetrics(_ mode: MetricsUpdate.Mode, rescheduleNotifications: MetricsUpdate.RescheduleNotifications) { let date = Date() // TOTAL - increment total - defaults.set(Int(getTotalMetrics() + 1), forKey: kTotalMetrics) + let totalMetrics = getTotalMetrics() + let updatedTotal = totalMetrics + mode.incrementBy + + defaults.set(updatedTotal, forKey: kTotalMetrics) + + if (100 ... 200) ~= updatedTotal, rescheduleNotifications.allowsScheduling { + OneTimeActions.performOnce(ifHasNotSeen: .oneHundredTrackingAttemptsBlockedNotification) { + PushNotifications.shared.scheduleOnboardingNotification( + options: rescheduleNotifications == .withEnergySaving ? [.energySaving] : [] + ) + } + } // WEEKLY - reset metrics on new week and increment week let currentWeek = currentCalendar.component(.weekOfYear, from: date) @@ -73,7 +122,8 @@ func incrementMetricsAndLog(host: String) { defaults.set(0, forKey: kWeekMetrics) defaults.set(currentWeek, forKey: kActiveWeek) } - defaults.set(Int(getWeekMetrics() + 1), forKey: kWeekMetrics) + let weekMetrics = getWeekMetrics() + defaults.set(Int(weekMetrics + mode.incrementBy), forKey: kWeekMetrics) // DAY - reset metric on new day and increment day and log // set day metric @@ -81,58 +131,36 @@ func incrementMetricsAndLog(host: String) { if currentDay != defaults.integer(forKey: kActiveDay) { defaults.set(0, forKey: kDayMetrics) defaults.set(currentDay, forKey: kActiveDay) - defaults.set([], forKey:kDayLogs); - } - defaults.set(Int(getDayMetrics() + 1), forKey: kDayMetrics) - // set log - let logString = blockLogDateFormatter.string(from: date) + host; - // reduce log size if it's over the maxSize - if var dayLog = defaults.array(forKey: kDayLogs) { - if dayLog.count > kDayLogsMaxSize { - dayLog = dayLog.suffix(kDayLogsMaxReduction); - } - dayLog.append(logString); - defaults.set(dayLog, forKey: kDayLogs); - } - else { - defaults.set([logString], forKey: kDayLogs); + BlockDayLog.shared.clear() } + defaults.set(Int(getDayMetrics() + mode.incrementBy), forKey: kDayMetrics) -} - -func getDayMetrics() -> Int { - return defaults.integer(forKey: kDayMetrics) -} - -func getDayMetricsString() -> String { - return metricsToString(metric: getDayMetrics()) -} - -func getWeekMetrics() -> Int { - return defaults.integer(forKey: kWeekMetrics) -} - -func getWeekMetricsString() -> String { - return metricsToString(metric: getWeekMetrics()) -} - -func getTotalMetrics() -> Int { - return defaults.integer(forKey: kTotalMetrics) -} - -func getTotalMetricsString() -> String { - return metricsToString(metric: getTotalMetrics()) -} - -func metricsToString(metric : Int) -> String { - if metric < 1000 { - return "\(metric)" + if #available(iOSApplicationExtension 14.0, iOS 14.0, *) { + WidgetCenter.shared.reloadAllTimelines() } - else if metric < 1000000 { - return "\(Int(metric / 1000))k" + + switch mode { + case .incrementAndLog(host: let host): + guard BlockDayLog.shared.isDisabled == false else { + // block log disabled + break + } + + // set log + BlockDayLog.shared.append(host: host, date: date) + case .resetIfNeeded: + // no-act + break } - else { - return "\(Int(metric / 1000000))m" + + switch rescheduleNotifications { + case .always: + PushNotifications.shared.rescheduleWeeklyUpdate(options: []) + case .withEnergySaving: + PushNotifications.shared.rescheduleWeeklyUpdate(options: [.energySaving]) + case .never: + // no-act + break } } @@ -141,84 +169,218 @@ func metricsToString(metric : Int) -> String { func setupFirewallDefaultBlockLists() { var lockdownBlockedDomains = getLockdownBlockedDomains() - let clickbait = LockdownGroup.init( - version: 20, - internalID: "clickbait", - name: "Clickbait", - iconURL: "clickbait_icon", + let snapchatAnalytics = LockdownGroup.init( + version: 28, + internalID: "snapchatAnalytics", + name: NSLocalizedString("Snapchat Trackers", comment: "The title of a list of trackers"), + iconURL: "snapchat_analytics_icon", enabled: false, - domains: getDomainBlockList(filename: "clickbait"), + domains: getDomainBlockList(filename: "snapchat_analytics"), ipRanges: [:]) - let crypto = LockdownGroup.init( - version: 20, - internalID: "crypto_mining", - name: "Crypto Mining", - iconURL: "crypto_icon", + let gameAds = LockdownGroup.init( + version: 31, + internalID: "gameAds", + name: NSLocalizedString("Game Marketing", comment: "The title of a list of trackers"), + iconURL: "game_ads_icon", enabled: true, - domains: getDomainBlockList(filename: "crypto_mining"), - ipRanges: [:]) + domains: getDomainBlockList(filename: "game_ads"), + ipRanges: [:], + accessLevel: "basic") + + let clickbait = LockdownGroup.init( + version: 30, + internalID: "clickbait", + name: NSLocalizedString("Clickbait", comment: "The title of a list of trackers"), + iconURL: "clickbait_icon", + enabled: true, + domains: getDomainBlockList(filename: "clickbait"), + ipRanges: [:], + accessLevel: "basic") let emailOpens = LockdownGroup.init( - version: 20, + version: 31, internalID: "email_opens", - name: "Email Opens (Beta)", + name: NSLocalizedString("Email Trackers", comment: "The title of a list of trackers"), iconURL: "email_icon", - enabled: false, + enabled: true, domains: getDomainBlockList(filename: "email_opens"), - ipRanges: [:]) + ipRanges: [:], + accessLevel: "basic") let facebookInc = LockdownGroup.init( - version: 20, + version: 34, internalID: "facebook_inc", - name: "Facebook Inc (Beta)", + name: NSLocalizedString("Facebook & WhatsApp", comment: "The title of a list of trackers"), iconURL: "facebook_icon", enabled: false, domains: getDomainBlockList(filename: "facebook_inc"), - ipRanges: [:]) + ipRanges: [:], + warning: "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.") let facebookSDK = LockdownGroup.init( - version: 20, + version: 29, internalID: "facebook_sdk", - name: "Facebook SDK", + name: NSLocalizedString("Facebook Trackers", comment: "The title of a list of trackers"), iconURL: "facebook_white_icon", enabled: true, domains: getDomainBlockList(filename: "facebook_sdk"), - ipRanges: [:]) + ipRanges: [:], + accessLevel: "basic") let marketingScripts = LockdownGroup.init( - version: 20, + version: 32, internalID: "marketing_scripts", - name: "Marketing Scripts", + name: NSLocalizedString("Marketing Trackers", comment: "The title of a list of trackers"), iconURL: "marketing_icon", enabled: true, domains: getDomainBlockList(filename: "marketing"), + ipRanges: [:], + accessLevel: "basic") + + let marketingScriptsII = LockdownGroup.init( + version: 31, + internalID: "marketing_beta_scripts", + name: NSLocalizedString("Marketing Trackers II", comment: "The title of a list of trackers"), + iconURL: "marketing_icon", + enabled: false, + domains: getDomainBlockList(filename: "marketing_beta"), ipRanges: [:]) + + let googleShoppingAds = LockdownGroup.init( + version: 36, + internalID: "google_shopping_ads", + name: NSLocalizedString("Google Shopping", comment: "The title of a list of trackers"), + iconURL: "google_icon", + enabled: false, + domains: getDomainBlockList(filename: "google_shopping_ads"), + ipRanges: [:], + warning: "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.") - let ransomware = LockdownGroup.init( - version: 20, - internalID: "ransomware", - name: "Ransomware", - iconURL: "ransomware_icon", + let dataTrackers = LockdownGroup.init( + version: 36, + internalID: "data_trackers", + name: NSLocalizedString("Data Trackers", comment: "The title of a list of trackers"), + iconURL: "user_data_icon", + enabled: true, + domains: getDomainBlockList(filename: "data_trackers"), + ipRanges: [:], + accessLevel: "basic") + + let generalAds = LockdownGroup.init( + version: 41, + internalID: "general_ads", + name: NSLocalizedString("General Marketing", comment: "The title of a list of trackers"), + iconURL: "ads_icon", + enabled: true, + domains: getDomainBlockList(filename: "general_ads"), + ipRanges: [:], + accessLevel: "basic") + + let reporting = LockdownGroup.init( + version: 30, + internalID: "reporting", + name: NSLocalizedString("Reporting", comment: "The title of a list of trackers"), + iconURL: "reporting_icon", + enabled: true, + domains: getDomainBlockList(filename: "reporting"), + ipRanges: [:], + accessLevel: "basic") + + let amazonTrackers = LockdownGroup.init( + version: 33, + internalID: "amazon_trackers", + name: NSLocalizedString("Amazon Trackers", comment: "The title of a list of trackers"), + iconURL: "amazon_icon", + enabled: true, + domains: getDomainBlockList(filename: "amazon_trackers"), + ipRanges: [:], + warning: "This blocks Amazon referral link tracking too, so enabling this list may cause errors when clicking certain links on Amazon.", + accessLevel: "basic") + + let ifunnyTrackers = LockdownGroup.init( + version: 4, + internalID: "ifunnyTrackers", + name: NSLocalizedString("iFunny Trackers", comment: "The title of a list of trackers"), + iconURL: "icn_ifunny", enabled: false, - domains: getDomainBlockList(filename: "ransomware"), + domains: getDomainBlockList(filename: "ifunny_trackers"), ipRanges: [:]) - let defaultLockdownSettings = [clickbait, - crypto, - emailOpens, - facebookInc, - facebookSDK, - marketingScripts, - ransomware]; - - for var def in defaultLockdownSettings { - if let current = lockdownBlockedDomains.lockdownDefaults[def.internalID], current.version >= def.version {} - else { - if let current = lockdownBlockedDomains.lockdownDefaults[def.internalID] { - def.enabled = current.enabled // don't replace whether it was disabled + let advancedGaming = LockdownGroup.init( + version: 5, + internalID: "advancedGaming", + name: NSLocalizedString("Advanced Gaming", comment: "The title of a list of trackers"), + iconURL: "icn_advanced_gaming", + enabled: false, + domains: getDomainBlockList(filename: "advanced_gaming"), + ipRanges: [:]) + + let tiktokTrackers = LockdownGroup.init( + version: 4, + internalID: "tiktokTrackers", + name: NSLocalizedString("Tiktok Trackers", comment: "The title of a list of trackers"), + iconURL: "icn_tiktok", + enabled: false, + domains: getDomainBlockList(filename: "tiktok_trackers"), + ipRanges: [:]) + + let scams = LockdownGroup.init( + version: 4, + internalID: "scams", + name: NSLocalizedString("Scams", comment: "The title of a list of trackers"), + iconURL: "icn_scams", + enabled: false, + domains: getDomainBlockList(filename: "scams"), + ipRanges: [:]) + + let junesJourneyTrackers = LockdownGroup.init( + version: 5, + internalID: "junesJourneyTrackers", + name: NSLocalizedString("Junes Journey Trackers", comment: "The title of a list of trackers"), + iconURL: "icn-junes-journey", + enabled: false, + domains: getDomainBlockList(filename: "junes_journey_trackers"), + ipRanges: [:]) + + let advancedAnalytics = LockdownGroup.init( + version: 4, + internalID: "advancedAnalytics", + name: NSLocalizedString("Advanced Analytics", comment: "The title of a list of trackers"), + iconURL: "icn_advanced_analytics", + enabled: false, + domains: getDomainBlockList(filename: "advanced_analytics"), + ipRanges: [:]) + + let defaultLockdownSettings = [ + advancedAnalytics, + junesJourneyTrackers, + scams, + tiktokTrackers, + advancedGaming, + ifunnyTrackers, + snapchatAnalytics, + gameAds, + clickbait, + emailOpens, + facebookInc, + facebookSDK, + marketingScripts, + marketingScriptsII, + googleShoppingAds, + dataTrackers, + generalAds, + reporting, + amazonTrackers]; + + for var defaultGroup in defaultLockdownSettings { + if let current = lockdownBlockedDomains.lockdownDefaults[defaultGroup.internalID], current.version >= defaultGroup.version { + // no version change, no action needed + } else { + if let current = lockdownBlockedDomains.lockdownDefaults[defaultGroup.internalID] { + defaultGroup.enabled = current.enabled // don't replace whether it was disabled } - lockdownBlockedDomains.lockdownDefaults[def.internalID] = def + lockdownBlockedDomains.lockdownDefaults[defaultGroup.internalID] = defaultGroup } } @@ -252,6 +414,7 @@ func getDomainBlockList(filename: String) -> Dictionary { func getAllBlockedDomains() -> Array { let lockdownBlockedDomains = getLockdownBlockedDomains() let userBlockedDomains = getUserBlockedDomains() + let userListsBlockedDomains = getBlockedLists() var allBlockedDomains = Array() for (_, ldValue) in lockdownBlockedDomains.lockdownDefaults { @@ -269,9 +432,67 @@ func getAllBlockedDomains() -> Array { } } + for (_, value) in userListsBlockedDomains.userBlockListsDefaults { + if value.enabled { + for domain in value.domains { + allBlockedDomains.append(domain) + } + } + } + return allBlockedDomains } +func getTotalEnabled() -> Array { + let lockdownBlockedDomains = getLockdownBlockedDomains() + + var total = Array() + for (_, ldValue) in lockdownBlockedDomains.lockdownDefaults { + if ldValue.enabled { + for (key, value) in ldValue.domains { + if value { + total.append(key) + } + } + } + } + + return total +} + +func getTotalDisabled() -> Array { + let lockdownBlockedDomains = getLockdownBlockedDomains() + + var total = Array() + for (_, ldValue) in lockdownBlockedDomains.lockdownDefaults { + if !ldValue.enabled { + for (key, value) in ldValue.domains { + if value { + total.append(key) + } + } + } + } + + return total +} + +func getIsCombinedBlockListEmpty() -> Bool { + return (getAllBlockedDomains() + getAllWhitelistedDomains()).isEmpty +} + +// MARK: - Curated Lockdown blocked domains + +func getLockdownBlockedDomains() -> LockdownDefaults { + guard let lockdownDefaultsData = defaults.object(forKey: kLockdownBlockedDomains) as? Data else { + return LockdownDefaults(lockdownDefaults: [:]) + } + guard let lockdownDefaults = try? PropertyListDecoder().decode(LockdownDefaults.self, from: lockdownDefaultsData) else { + return LockdownDefaults(lockdownDefaults: [:]) + } + return lockdownDefaults +} + // MARK: - User blocked domains func getUserBlockedDomains() -> Dictionary { @@ -299,67 +520,185 @@ func deleteUserBlockedDomain(domain: String) { defaults.set(domains, forKey: kUserBlockedDomains) } -// MARK: - Lockdown blocked domains +// MARK: - User blocked lists -func getLockdownBlockedDomains() -> LockdownDefaults { - guard let lockdownDefaultsData = defaults.object(forKey: kLockdownBlockedDomains) as? Data else { - return LockdownDefaults(lockdownDefaults: [:]) +func getBlockedLists() -> UserBlockListsDefaults { + guard let listsDefaultsData = defaults.object(forKey: kUserBlockedLists) as? Data else { + return UserBlockListsDefaults(userBlockListsDefaults: [:]) } - guard let lockdownDefaults = try? PropertyListDecoder().decode(LockdownDefaults.self, from: lockdownDefaultsData) else { - return LockdownDefaults(lockdownDefaults: [:]) + + guard let listDefaults = try? JSONDecoder().decode(UserBlockListsDefaults.self, from: listsDefaultsData) else { + return UserBlockListsDefaults(userBlockListsDefaults: [:]) + } + return listDefaults +} + +func addBlockedList(listName: String) { + var data = getBlockedLists() + if !data.userBlockListsDefaults.keys.contains(listName) { + data.userBlockListsDefaults[listName] = UserBlockListsGroup(name: listName, enabled: false) + let encodedData = try? JSONEncoder().encode(data) + defaults.set(encodedData, forKey: kUserBlockedLists) } - return lockdownDefaults } -// MARK: - Unused +func changeBlockedListName(from listName: String, to newListName: String) { + var data = getBlockedLists() + data.userBlockListsDefaults[newListName] = data.userBlockListsDefaults[listName] + data.userBlockListsDefaults[newListName]?.name = newListName + data.userBlockListsDefaults[listName] = nil + let encodedData = try? JSONEncoder().encode(data) + defaults.set(encodedData, forKey: kUserBlockedLists) +} + +func deleteBlockedList(listName: String) { + var data = getBlockedLists() + data.userBlockListsDefaults[listName] = nil + let encodedData = try? JSONEncoder().encode(data) + defaults.set(encodedData, forKey: kUserBlockedLists) +} + +func addDomainToBlockedList(domain: String, for listName: String) { + var data = getBlockedLists() + data.userBlockListsDefaults[listName]?.domains.insert(domain) + let encodedData = try? JSONEncoder().encode(data) + defaults.set(encodedData, forKey: kUserBlockedLists) +} + +func deleteDoman(domain: String, inBlockedListName listName: String) -> UserBlockListsGroup? { + var data = getBlockedLists() + guard let index = data.userBlockListsDefaults[listName]?.domains.firstIndex(of: domain) else { + return nil + } + data.userBlockListsDefaults[listName]?.domains.remove(at: index) + let encodedData = try? JSONEncoder().encode(data) + defaults.set(encodedData, forKey: kUserBlockedLists) + return data.userBlockListsDefaults[listName] +} + +extension UserBlockListsGroup { + func generateCurrentTimeStamp () -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy_MM_dd_hh_mm_ss" + return (formatter.string(from: Date()) as NSString) as String + } + + func exportToURL() -> URL? { + var csvString = "" + + for domain in domains { + csvString = csvString.appending("\(domain)\r\n") + } + + let documents = FileManager.default.urls( + for: .documentDirectory, + in: .userDomainMask + ).first + + let fileName = "LOCKDOWN_\(NSDate.now)" + + guard let path = documents?.appendingPathComponent("/\(fileName).csv") else { + return nil + } + + do { + try csvString.write(to: path, atomically: true, encoding: .utf8) + return path + } catch { + print(error.localizedDescription) + return nil + } + + + + + +// guard let encoded = try? JSONEncoder().encode(self) else { return nil } // -//func getBlockedIPv4List(filename: String) -> Dictionary { -// var domains = [String : IPRange]() -// guard let path = Bundle.main.path(forResource: filename, ofType: "txt") else { -// return domains -// } -// do { -// let content = try String(contentsOfFile:path, encoding: String.Encoding.utf8) -// let lines = content.components(separatedBy: "\n") -// for line in lines { -// // CIDR -// if line.contains("/") { -// if let subnetBits = Int(line.split(separator: "/")[1]) { -// let d = String(line.split(separator: "/")[0]) -// let mask = 0xffffffff ^ ((1 << (32 - subnetBits)) - 1) -// let subnetMask = String.init(format: "%d.%d.%d.%d", (mask & 0x00ff000000) >> 24, (mask & 0x00ff0000) >> 16, (mask & 0x0000ff00) >> 8, (mask & 0xff)) +// let documents = FileManager.default.urls( +// for: .documentDirectory, +// in: .userDomainMask +// ).first // -// domains[d] = IPRange.init(subnetMask: subnetMask, enabled: true, IPv6: false, subnetBits: subnetBits) -// } -// } -// // not CIDR, just feed the IP itself -// else { -// domains[line] = IPRange.init(subnetMask: "255.255.255.255", enabled: true, IPv6: false, subnetBits: 0) -// } +// guard let path = documents?.appendingPathComponent("/\(fileName).csv") else { +// return nil // } -// } catch _ as NSError { -// } -// return domains -//} // -//func getBlockedIPv6List(filename: String) -> Dictionary { -// var domains = [String : IPRange]() -// guard let ipv6Path = Bundle.main.path(forResource: filename, ofType: "txt") else { -// return domains +// do { +// try encoded.write(to: path, options: .atomicWrite) +// print(fileName) +// return path +// } catch { +// print(error.localizedDescription) +// return nil +// } + } + + static func importData(from url: URL) { + +// if #available(iOSApplicationExtension 14.0, *) { +// let supportedFiles: [UTType] = [UTType.data] +// +//// let controller = UIDocumentPickerViewController +// +// +// +// } else { +// // Fallback on earlier versions +// } + + guard let data = try? Data(contentsOf: url) +// let list = try? JSONDecoder().decode(UserBlockListsGroup.self, from: data) + + else { return } + defaults.set(data, forKey: kUserBlockedLists) + try? FileManager.default.removeItem(at: url) + } +} + +//extension Domains { +// +// func generateCurrentTimeStamp () -> String { +// let formatter = DateFormatter() +// formatter.dateFormat = "yyyy_MM_dd_hh_mm_ss" +// return (formatter.string(from: Date()) as NSString) as String // } -// do { -// let content = try String(contentsOfFile:ipv6Path, encoding: String.Encoding.utf8) -// let lines = content.components(separatedBy: "\n") -// for line in lines { -// if line.contains("/") { -// if let subnetBits = Int(line.split(separator: "/")[1]) { -// let d = String(line.split(separator: "/")[0]) -// let subnetMask = "\(subnetBits)" -// domains[d] = IPRange.init(subnetMask: subnetMask, enabled: true, IPv6: true, subnetBits: subnetBits) -// } -// } +// +// func exportToURL() -> URL? { +// +// let timeStamp = generateCurrentTimeStamp() +// let fileName = "LOCKDOWN_\(NSDate.now)" +// guard let encoded = try? JSONEncoder().encode(self) else { return nil } +// +// let documents = FileManager.default.urls( +// for: .documentDirectory, +// in: .userDomainMask +// ).first +// +// guard let path = documents?.appendingPathComponent("/\(fileName).csv") else { +// return nil // } -// } catch _ as NSError { +// +// do { +// try encoded.write(to: path, options: .atomicWrite) +// print(fileName) +// return path +// } catch { +// print(error.localizedDescription) +// return nil +// } +// } +// +// static func importData(from url: URL) { +// guard +// let data = try? Data(contentsOf: url), +// let domain = try? JSONDecoder().decode(Domains.self, from: data) +// +// +// else { return } +// addDomainToBlockedList(domain: domain.name, for: "oop") +// +// +// try? FileManager.default.removeItem(at: url) // } -// return domains //} diff --git a/Fonts/Juana-SemiBold.ttf b/Fonts/Juana-SemiBold.ttf new file mode 100644 index 0000000..417808c Binary files /dev/null and b/Fonts/Juana-SemiBold.ttf differ diff --git a/Fonts/KumbhSans-Bold.ttf b/Fonts/KumbhSans-Bold.ttf new file mode 100644 index 0000000..87891ca Binary files /dev/null and b/Fonts/KumbhSans-Bold.ttf differ diff --git a/Fonts/KumbhSans-Regular.ttf b/Fonts/KumbhSans-Regular.ttf new file mode 100644 index 0000000..55fd144 Binary files /dev/null and b/Fonts/KumbhSans-Regular.ttf differ diff --git a/Fonts/SF-Pro-Rounded-Bold.otf b/Fonts/SF-Pro-Rounded-Bold.otf new file mode 100755 index 0000000..b5c2aed Binary files /dev/null and b/Fonts/SF-Pro-Rounded-Bold.otf differ diff --git a/Fonts/SF-Pro-Rounded-Medium.otf b/Fonts/SF-Pro-Rounded-Medium.otf new file mode 100755 index 0000000..1a0a7e4 Binary files /dev/null and b/Fonts/SF-Pro-Rounded-Medium.otf differ diff --git a/Fonts/SF-Pro-Rounded-Regular.otf b/Fonts/SF-Pro-Rounded-Regular.otf new file mode 100755 index 0000000..4d32308 Binary files /dev/null and b/Fonts/SF-Pro-Rounded-Regular.otf differ diff --git a/Fonts/SF-Pro-Rounded-Semibold.otf b/Fonts/SF-Pro-Rounded-Semibold.otf new file mode 100755 index 0000000..973f92d Binary files /dev/null and b/Fonts/SF-Pro-Rounded-Semibold.otf differ diff --git a/Localizable.strings b/Localizable.strings new file mode 100644 index 0000000..b935fa2 Binary files /dev/null and b/Localizable.strings differ diff --git a/Lockdown Blocker/BlockerPrivacyInfo.xcprivacy b/Lockdown Blocker/BlockerPrivacyInfo.xcprivacy new file mode 100644 index 0000000..cfbe279 --- /dev/null +++ b/Lockdown Blocker/BlockerPrivacyInfo.xcprivacy @@ -0,0 +1,8 @@ + + + + + NSPrivacyTracking + + + diff --git a/Lockdown Blocker/ContentBlockerRequestHandler.swift b/Lockdown Blocker/ContentBlockerRequestHandler.swift index 45143ba..6aa80dc 100644 --- a/Lockdown Blocker/ContentBlockerRequestHandler.swift +++ b/Lockdown Blocker/ContentBlockerRequestHandler.swift @@ -66,5 +66,4 @@ class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling { context.completeRequest(returningItems: [item], completionHandler: nil) } - } diff --git a/Lockdown Blocker/Info.plist b/Lockdown Blocker/Info.plist index 92f18fa..82fabf7 100644 --- a/Lockdown Blocker/Info.plist +++ b/Lockdown Blocker/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.2.0 + $(MARKETING_VERSION) CFBundleVersion - 16 + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionPointIdentifier diff --git a/Lockdown Blocker/Lockdown Blocker.entitlements b/Lockdown Blocker/Lockdown Blocker.entitlements index 562d4b0..2c41926 100644 --- a/Lockdown Blocker/Lockdown Blocker.entitlements +++ b/Lockdown Blocker/Lockdown Blocker.entitlements @@ -2,8 +2,6 @@ - com.apple.developer.icloud-container-identifiers - com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.application-groups diff --git a/Lockdown Firewall Today/Base.lproj/MainInterface.storyboard b/Lockdown Firewall Today/Base.lproj/MainInterface.storyboard index 60321d3..9d8d128 100644 --- a/Lockdown Firewall Today/Base.lproj/MainInterface.storyboard +++ b/Lockdown Firewall Today/Base.lproj/MainInterface.storyboard @@ -1,11 +1,9 @@ - - - - + + - + @@ -18,14 +16,15 @@ - + @@ -86,7 +86,6 @@ - @@ -103,6 +102,6 @@ - + diff --git a/Lockdown Firewall Today/FireWallPrivacyInfo.xcprivacy b/Lockdown Firewall Today/FireWallPrivacyInfo.xcprivacy new file mode 100644 index 0000000..454777d --- /dev/null +++ b/Lockdown Firewall Today/FireWallPrivacyInfo.xcprivacy @@ -0,0 +1,8 @@ + + + + + NSPrivacyTracking + + + diff --git a/Lockdown Firewall Today/FirewallTodayViewController.swift b/Lockdown Firewall Today/FirewallTodayViewController.swift index 01374fc..96ffcfb 100644 --- a/Lockdown Firewall Today/FirewallTodayViewController.swift +++ b/Lockdown Firewall Today/FirewallTodayViewController.swift @@ -47,26 +47,28 @@ class FirewallTodayViewController: UIViewController, NCWidgetProviding { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - setupFirewallButtons() - - if getUserWantsFirewallEnabled() && FirewallController.shared.status() == .connected { - DDLogInfo("Widget Firewall Test: user wants firewall enabled and connected, testing blocking with widget") - _ = Client.getBlockedDomainTest(connectionSuccessHandler: { - DDLogError("Widget Firewall Test: Connected to \(testFirewallDomain) even though it's supposed to be blocked, restart the Firewall") - self.restartFirewall() - }, connectionFailedHandler: { - error in - if error != nil { - let nsError = error! as NSError + FirewallController.shared.refreshManager(completion: { error in + if let e = error { + DDLogError("Error refreshing Manager in background check: \(e)") + return + } + self.setupFirewallButtons() + if getUserWantsFirewallEnabled() && (FirewallController.shared.status() == .connected || FirewallController.shared.status() == .invalid) { + DDLogInfo("Widget Firewall Test: user wants firewall enabled and connected, testing blocking with widget") + Client.getBlockedDomainTest().done { + DDLogError("Widget Firewall Test: Connected to \(testFirewallDomain) even though it's supposed to be blocked, restart the Firewall") + self.restartFirewall() + }.catch { error in + let nsError = error as NSError if nsError.domain == NSURLErrorDomain { DDLogInfo("Widget Firewall Test: Successful blocking of \(testFirewallDomain) with NSURLErrorDomain error: \(nsError)") } else { - DDLogInfo("Widget Firewall Test: Successful blocking of \(testFirewallDomain), but seeing non-NSURLErrorDomain error: \(error!)") + DDLogInfo("Widget Firewall Test: Successful blocking of \(testFirewallDomain), but seeing non-NSURLErrorDomain error: \(error)") } } - }) - } + } + }) } func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { @@ -83,7 +85,7 @@ class FirewallTodayViewController: UIViewController, NCWidgetProviding { @objc func updateMetrics() { DispatchQueue.main.async { - self.blockedTodayLabel.text = getDayMetricsString() + " Blocked Today" + self.blockedTodayLabel.text = getDayMetricsString() + NSLocalizedString(" Blocked Today", comment: "refers to number of connections blocked today, as in '34 Blocked Today'") } } @@ -130,23 +132,23 @@ class FirewallTodayViewController: UIViewController, NCWidgetProviding { } func setFirewallButtonConnected() { - firewallStatusLabel.text = "Firewall Active" + firewallStatusLabel.text = NSLocalizedString("Firewall Active", comment: "") toggleFirewall?.tintColor = .tunnelsBlue toggleFirewall.layer.borderColor = UIColor.tunnelsBlue.cgColor } func setFirewallButtonDisconnected() { - firewallStatusLabel.text = "Firewall Not Active" + firewallStatusLabel.text = NSLocalizedString("Firewall Not Active", comment: "") toggleFirewall?.tintColor = UIColor.darkGray toggleFirewall?.layer.borderColor = UIColor.darkGray.cgColor } func setFirewallButtonConnecting() { - firewallStatusLabel.text = "Activating..." + firewallStatusLabel.text = NSLocalizedString("Activating", comment: "") } func setFirewallButtonDisconnecting() { - firewallStatusLabel.text = "Deactivating..." + firewallStatusLabel.text = NSLocalizedString("Deactivating", comment: "") } // MARK: - Helpers @@ -168,6 +170,7 @@ class FirewallTodayViewController: UIViewController, NCWidgetProviding { func createRemoteRecord(recordName: String, shouldOpenAppOnFailure: Bool = false) { let privateDatabase = CKContainer.init(identifier: kICloudContainer).privateCloudDatabase + // even though this is deprecated, we're still using this for now out of concerns about compatibility let myRecord = CKRecord(recordType: recordName, zoneID: CKRecordZone.default().zoneID) privateDatabase.save(myRecord, completionHandler: ({returnRecord, error in @@ -178,7 +181,7 @@ class FirewallTodayViewController: UIViewController, NCWidgetProviding { self.openApp() } } else { - DDLogInfo("Successfully saved record") + DDLogInfo("Successfully saved record: \(returnRecord as Any)") } })) } @@ -190,5 +193,4 @@ class FirewallTodayViewController: UIViewController, NCWidgetProviding { @IBAction func openLockdown(sender: UIButton) { openApp() } - } diff --git a/Lockdown Firewall Today/Info.plist b/Lockdown Firewall Today/Info.plist index d99f1d6..a9b7927 100644 --- a/Lockdown Firewall Today/Info.plist +++ b/Lockdown Firewall Today/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.2.0 + $(MARKETING_VERSION) CFBundleVersion - 16 + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionMainStoryboard diff --git a/Lockdown Firewall Today/de.lproj/MainInterface.strings b/Lockdown Firewall Today/de.lproj/MainInterface.strings new file mode 100644 index 0000000..04880b0 --- /dev/null +++ b/Lockdown Firewall Today/de.lproj/MainInterface.strings @@ -0,0 +1,8 @@ +/* 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/en.lproj/MainInterface.strings b/Lockdown Firewall Today/en.lproj/MainInterface.strings new file mode 100644 index 0000000..04880b0 --- /dev/null +++ b/Lockdown Firewall Today/en.lproj/MainInterface.strings @@ -0,0 +1,8 @@ +/* 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/es.lproj/MainInterface.strings b/Lockdown Firewall Today/es.lproj/MainInterface.strings index 9b0029d..173f46f 100644 --- a/Lockdown Firewall Today/es.lproj/MainInterface.strings +++ b/Lockdown Firewall Today/es.lproj/MainInterface.strings @@ -1,12 +1,8 @@ +/* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "Firewall Activo"; -/* Class = "UIButton"; normalTitle = "Speed: Tap to test"; ObjectID = "Apy-GL-W9N"; */ -"Apy-GL-W9N.normalTitle" = "Velocidad: Pulse para probar"; +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Abrir Lockdown"; -/* Class = "UIButton"; normalTitle = "Change country"; ObjectID = "Nlw-t0-1Mi"; */ -"Nlw-t0-1Mi.normalTitle" = "Cambiar Pais"; - -/* Class = "UILabel"; text = "Connected"; ObjectID = "XJH-bI-G1N"; */ -"XJH-bI-G1N.text" = "PROTEGIDO"; - -/* Class = "UILabel"; text = "IP: Finding..."; ObjectID = "Zgn-9l-eoa"; */ -"Zgn-9l-eoa.text" = "Direccion IP: ..."; +/* Class = "UILabel"; text = "-"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "-"; diff --git a/Lockdown Firewall Today/fr.lproj/MainInterface.strings b/Lockdown Firewall Today/fr.lproj/MainInterface.strings new file mode 100644 index 0000000..4c7f1f7 --- /dev/null +++ b/Lockdown Firewall Today/fr.lproj/MainInterface.strings @@ -0,0 +1,8 @@ +/* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "Pare-feu Activé"; + +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Ouvrir Lockdown"; + +/* Class = "UILabel"; text = "-"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "-"; diff --git a/Lockdown Firewall Today/ja.lproj/MainInterface.strings b/Lockdown Firewall Today/ja.lproj/MainInterface.strings index 3e6c4fc..5df1f02 100644 --- a/Lockdown Firewall Today/ja.lproj/MainInterface.strings +++ b/Lockdown Firewall Today/ja.lproj/MainInterface.strings @@ -1,12 +1,8 @@ +/* Class = "UILabel"; text = "Firewall Active"; ObjectID = "1pC-80-fVF"; */ +"1pC-80-fVF.text" = "ファイアウォールがアクティブ"; -/* Class = "UIButton"; normalTitle = "Speed: Tap to test"; ObjectID = "Apy-GL-W9N"; */ -"Apy-GL-W9N.normalTitle" = "速度: タップしてください(テスト)"; +/* Class = "UIButton"; normalTitle = "Open Lockdown"; ObjectID = "PT0-Mi-sJG"; */ +"PT0-Mi-sJG.normalTitle" = "Lockdownを開く"; -/* Class = "UIButton"; normalTitle = "Change country"; ObjectID = "Nlw-t0-1Mi"; */ -"Nlw-t0-1Mi.normalTitle" = "地域を変更する"; - -/* Class = "UILabel"; text = "Connected"; ObjectID = "XJH-bI-G1N"; */ -"XJH-bI-G1N.text" = "保護されています"; - -/* Class = "UILabel"; text = "IP: Finding..."; ObjectID = "Zgn-9l-eoa"; */ -"Zgn-9l-eoa.text" = "IP: ..."; +/* Class = "UILabel"; text = "-"; ObjectID = "Vbb-eR-jl5"; */ +"Vbb-eR-jl5.text" = "-"; diff --git a/Lockdown Tunnel/DNSCryptThread.swift b/Lockdown Tunnel/DNSCryptThread.swift new file mode 100644 index 0000000..9fc4aae --- /dev/null +++ b/Lockdown Tunnel/DNSCryptThread.swift @@ -0,0 +1,76 @@ +// +// DNSCryptThread.swift +// LockdowniOS +// +// Created by Johnny Lin on 3/31/22. +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import Foundation +import Dnscryptproxy + +let kDNSCryptProxyReady = "DNSCryptProxyReady"; + +class DNSCryptThread: Thread, DnscryptproxyCloakCallbackProtocol { + let dnsApp: DnscryptproxyApp + + init?(arguments: [String]?) { + guard let dnsApp = DnscryptproxyMain(arguments?[0]) else { return nil } + + self.dnsApp = dnsApp + super.init() + name = "DNSCloak" + } + + override func main() { + dnsApp.run(self) + } + + @objc func proxyReady() { + NotificationCenter.default.post(name: NSNotification.Name(kDNSCryptProxyReady), object: self) + } + + func closeIdleConnections() { + dnsApp.closeIdleConnections() + } + + func refreshServersInfo() { + dnsApp.refreshServersInfo() + } + + func stopApp() { + do { + try dnsApp.stop() + } catch { + print("Error stopping app") + } + } + + func logDebug(_ str: String) { + dnsApp.logDebug(str) + } + + func logInfo(_ str: String) { + dnsApp.logInfo(str) + } + + func logNotice(_ str: String) { + dnsApp.logNotice(str) + } + + func logWarn(_ str: String) { + dnsApp.logWarn(str) + } + + func logError(_ str: String) { + dnsApp.logError(str) + } + + func logCritical(_ str: String) { + dnsApp.logCritical(str) + } + + func logFatal(_ str: String) { + dnsApp.logFatal(str) + } +} diff --git a/Lockdown Tunnel/Info.plist b/Lockdown Tunnel/Info.plist index 96baef4..2d3be5e 100644 --- a/Lockdown Tunnel/Info.plist +++ b/Lockdown Tunnel/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.2.0 + $(MARKETING_VERSION) CFBundleVersion - 16 + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionPointIdentifier diff --git a/Lockdown Tunnel/LockdownTunnelBridgingHeader.h b/Lockdown Tunnel/LockdownTunnelBridgingHeader.h new file mode 100644 index 0000000..6c60c04 --- /dev/null +++ b/Lockdown Tunnel/LockdownTunnelBridgingHeader.h @@ -0,0 +1,14 @@ +// +// LockdownTunnelBridgingHeader.h +// LockdownTunnel +// +// Created by Johnny Lin on 4/12/22. +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +#ifndef LockdownTunnelBridgingHeader_h +#define LockdownTunnelBridgingHeader_h + +#import + +#endif /* LockdownTunnelBridgingHeader_h */ diff --git a/Lockdown Tunnel/PacketTunnelProvider.swift b/Lockdown Tunnel/PacketTunnelProvider.swift index 969d5cb..399d887 100644 --- a/Lockdown Tunnel/PacketTunnelProvider.swift +++ b/Lockdown Tunnel/PacketTunnelProvider.swift @@ -7,65 +7,139 @@ import NetworkExtension import NEKit -import CocoaLumberjackSwift +import Dnscryptproxy +import Network +import PromiseKit +import CocoaLumberjack -class LDObserverFactory: ObserverFactory { +var latestBlockedDomains = getAllBlockedDomains() + +class PacketTunnelProvider: NEPacketTunnelProvider { - override func getObserverForProxySocket(_ socket: ProxySocket) -> Observer? { - return LDProxySocketObserver(); - } + let dnsServerAddress = "127.0.0.1" + var dns: DNSCryptThread? + + let proxyServerAddress = "127.0.0.1" + let proxyServerPort: UInt16 = 9090 + var proxyServer: GCDHTTPProxyServer? + + let monitor = NWPathMonitor() + let fileManager = FileManager.default + let groupContainer = "group.com.confirmed" - class LDProxySocketObserver: Observer { + let lastReachabilityKillKey = "lastReachabilityKillTime" + private var token: NSObjectProtocol? + private let center = NotificationCenter.default + private var proxyError: Error? - let whitelistedDomains = getAllWhitelistedDomains() + func log(_ str: String) { + PacketTunnelProviderLogs.log(str) + NSLog("ptplog - " + str) + } + + override func cancelTunnelWithError(_ error: Error?) { + self.log("===== ERROR - cancelTunnelWithError \(error?.localizedDescription ?? "")") + } + + override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { + log("+++++ startTunnel NEW") + + // reachability check + monitor.pathUpdateHandler = { [weak self] path in + self?.pathUpdateHandler(path: path) + } - override func signal(_ event: ProxySocketEvent) { - switch event { - case .receivedRequest(let session, let socket): - // this is for testing if the blocking is working correctly - always block this - if (session.host == testFirewallDomain) { - socket.forceDisconnect() - return - } - // if domain is in whitelist, just return (user probably didn't whitelist something they want to block - for whitelistedDomain in whitelistedDomains { - if (session.host.hasSuffix("." + whitelistedDomain) || session.host == whitelistedDomain) { - DDLogInfo("whitelisted \(session.host), not blocking") - return - } + + log("Calling setTunnelNetworkSettings") + initializeDns(); + initializeProxy(); + + setupObserverDNSCryptProxyReady(completionHandler: completionHandler) + + startDns(); + proxyError = startProxy() + } + + private func setupObserverDNSCryptProxyReady(completionHandler: @escaping (Error?) -> Void) { + token = center.addObserver( + forName: Notification.Name(kDNSCryptProxyReady), + object: nil, + queue: .main + ) { [weak self] notification in + self?.dnsCryptProxyReady(completionHandler: completionHandler) + } + } + + private func dnsCryptProxyReady(completionHandler: @escaping (Error?) -> Void) { + log("Found available resolvers, tell iOS we are ready") + if let token { + center.removeObserver(token) + } + updateTunnelSetting(completionHandler: completionHandler) + let queue = DispatchQueue(label: "Monitor") + monitor.start(queue: queue) + } + + private func updateTunnelSetting(completionHandler: @escaping (Error?) -> Void) { + let networkSettings = getNetworkSettings(); + + self.setTunnelNetworkSettings(networkSettings) { [weak self] error in + guard let self else { return } + if let error { + self.log("ERROR - StartTunnel \(error.localizedDescription)") + completionHandler(error); + } else { + self.log("No error on setTunnelNetworkSettings, starting dns and proxy") + + if let proxyError = self.proxyError { + self.log("ERROR - Failed to start proxy: \(proxyError)") + completionHandler(proxyError) } - // else if firewall on, then block - if (getUserWantsFirewallEnabled()) { - incrementMetricsAndLog(host: session.host); - DDLogInfo("session host: \(session.host)") - socket.forceDisconnect() - return + else { + self.log("SUCCESS - startTunnel") + completionHandler(nil) } - default: - break; + self.proxyError = nil } } - } -} - -class PacketTunnelProvider: NEPacketTunnelProvider { - - let proxyServerPort: UInt16 = 9090; - let proxyServerAddress = "127.0.0.1"; - var proxyServer: GCDHTTPProxyServer! + private func refreshServers() { + stopProxyServer() + dns?.closeIdleConnections() + dns?.refreshServersInfo() + initializeProxy() + _ = startProxy() + } - //MARK: - OVERRIDES + override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + self.log("+++++ stopTunnel with reason: \(reason)") + monitor.cancel() + stopProxyServer() + stopDnsServer() + self.log("stopTunnel completionHandler, exit") + completionHandler(); + exit(EXIT_SUCCESS); + } + +// override func wake() { +// log("===== wake") +// flushBlockLog(log: log) +// log("wake setting tunnel network settings to nil") +// self.setTunnelNetworkSettings(nil, completionHandler: { error in +// if (error != nil) { +// self.log("error setting tunnelnetworksettings to nil: \(error)") +// } +// self.log("wake calling reactivate tunnel") +// self.reactivateTunnel() +// }) +// } - override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { - if proxyServer != nil { - proxyServer.stop() - } - proxyServer = nil + func getNetworkSettings() -> NEPacketTunnelNetworkSettings { + log("===== getNetworkSettings") - let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: proxyServerAddress) - settings.mtu = NSNumber(value: 1500) + let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: dnsServerAddress) + networkSettings.mtu = 1500 let proxySettings = NEProxySettings() proxySettings.httpEnabled = true; @@ -74,37 +148,387 @@ class PacketTunnelProvider: NEPacketTunnelProvider { proxySettings.httpsServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort)) proxySettings.excludeSimpleHostnames = false; proxySettings.exceptionList = [] - let combined: Array = getAllBlockedDomains() + getAllWhitelistedDomains() + [testFirewallDomain] // probably not blocking whitelisted so this is safe, example.com is used to ensure firewall is still working - proxySettings.matchDomains = combined + proxySettings.matchDomains = getAllWhitelistedDomains() + networkSettings.proxySettings = proxySettings; - settings.dnsSettings = NEDNSSettings(servers: ["127.0.0.1"]) - settings.proxySettings = proxySettings; - RawSocketFactory.TunnelProvider = self - ObserverFactory.currentFactory = LDObserverFactory() + let dnsSettings = NEDNSSettings(servers: [dnsServerAddress]) + dnsSettings.matchDomains = [""]; + networkSettings.dnsSettings = dnsSettings; - self.setTunnelNetworkSettings(settings, completionHandler: { error in - self.proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort)) + return networkSettings; + } + + func initializeAndReturnConfigPath() -> String { + log("===== initializeAndReturnConfigPath") + + let configFile = Bundle.main.url(forResource: "dnscrypt-proxy", withExtension: "toml") + let sharedDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupContainer) + + // remove blocklist if it exists + let newBlocklistFile = sharedDir!.appendingPathComponent("blocklist.txt") + if fileManager.fileExists(atPath: newBlocklistFile.path) { + log("blocklist.txt exists") + do { + try fileManager.removeItem(atPath: newBlocklistFile.path) + log("removed old blocklist.txt") + } catch { + log("ERROR - couldnt remove old blocklist.txt: \(error)") + } + } + + // generate text for new blocklist + let blockedDomainsArray = getAllBlockedDomains() + var blockedDomains: String = testFirewallDomain + for blockedDomain in blockedDomainsArray { + blockedDomains = blockedDomains + "\n" + blockedDomain + } + + // copy blocklist file into shared dir + do { + try blockedDomains.write(to: newBlocklistFile, atomically: false, encoding: .utf8) + log("wrote content to blocklist.txt") + } + catch { + log("ERROR - couldnt write content to blocklist.txt: \(error)") + } + + // clear prefix suffix files + let prefixFile = sharedDir!.appendingPathComponent("blacklist.txt.prefixes") + let suffixFile = sharedDir!.appendingPathComponent("blacklist.txt.suffixes") + if fileManager.fileExists(atPath: prefixFile.path){ + log("prefix file exists at: \(prefixFile.path)") do { - try self.proxyServer.start() - completionHandler(nil) + try fileManager.removeItem(atPath: prefixFile.path) + log("prefix file removed at: \(prefixFile.path)") + } catch { } - catch { - DDLogError("Error starting proxy server \(error)") - completionHandler(error) + } + if fileManager.fileExists(atPath: suffixFile.path){ + do { + try fileManager.removeItem(atPath: suffixFile.path) + log("suffix file removed at: \(suffixFile.path)") + } catch { + log("ERROR - error removing suffix file: \(error)") } - }) + } + + // create new prefix/suffix files + let errorPtr: NSErrorPointer = nil + log("filling in prefix/suffix files at: \(newBlocklistFile.path)") + DnscryptproxyFillPatternlistTrees(newBlocklistFile.path, errorPtr) + if let error = errorPtr?.pointee { + log("ERROR - filling in prefix/suffix files: \(error)") + } + + // read config file template + var configFileText = "" + do { + configFileText = try String(contentsOf: configFile!, encoding: .utf8) + log("Read config file template") + } + catch { + log("ERROR - couldn't read config file template text at: \(configFile!.path)") + } + + // replace BLOCKLIST_FILE_HERE and BLOCKLIST_LOG_HERE with urls of blocklist file/log + let replacedConfig = configFileText.replacingOccurrences(of: "BLOCKLIST_FILE_HERE", with: "\(newBlocklistFile.path)").replacingOccurrences(of: "BLOCKLIST_LOG_HERE", with: "\(sharedDir!.appendingPathComponent("blocklist.log").path)") + + // write replaced string to new file + let replacedConfigURL = sharedDir!.appendingPathComponent("replaced-config.toml") + log("replaced config file url: \(replacedConfigURL.path)") + do { + try replacedConfig.write(to: replacedConfigURL, atomically: false, encoding: .utf8) + log("replaced config written") + } + catch { + log("ERROR - couldn't write replaced config: \(error)") + } + log("returning replacedConfigURL \(replacedConfigURL)") + return replacedConfigURL.path } - override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { - DNSServer.currentServer = nil - RawSocketFactory.TunnelProvider = nil - ObserverFactory.currentFactory = nil + func initializeDns() { + log("===== initialize DNS server") + // stopDnsServer() + log("initializing DNSCryptThread") + dns = DNSCryptThread(arguments: [initializeAndReturnConfigPath()]); + } + + func initializeProxy() { + log("===== initialize proxy server") + // stopProxyServer() + log("initializing GCDHTTPProxyServer") + proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort)) + } + + func startProxy() -> Error? { + log("===== startProxy") + do { + try self.proxyServer?.start() + log("started proxyServer") + return nil + } catch { + log("ERROR - couldnt start proxyServer") + return error + } + } + + func startDns() { + log("===== startDns") + dns?.start() + } + + func stopDnsServer() { + log("===== stopDnsServer") + guard let dns else { return } + + log("dns is not nil") + log("dns closing idle connections") + dns.closeIdleConnections() + log("dns stopApp") + dns.stopApp() + + log("dns set to nil") + self.dns = nil + } + + func stopProxyServer() { + log("===== stopProxyServer") + guard let proxyServer else { return } + + log("proxyServer is not nil") + log("proxyServer stop") proxyServer.stop() - proxyServer = nil - DDLogError("LockdownTunnel: error on stopping: \(reason)") + + log("proxyServer nil") + self.proxyServer = nil + } + +// func reactivateTunnel() { +// log("===== reactivateTunnel, reasserting true") +// reasserting = true +// +// let networkSettings = getNetworkSettings() +// +// self.setTunnelNetworkSettings(networkSettings) { [weak self] error in +// guard let self else { return } +// if let error { +// self.log("ERROR - reactivateTunnel setTunnelNetworkSettings: \(error.localizedDescription)") +// } +// self.log("reactivateTunnel setTunnelNetworkSettings complete, reasserting false") +// self.reasserting = false +// +// self._dns.closeIdleConnections() +// self.log("closed idle connections") +// +// self.log("||||| reactivate AFTER - checking availability to apple.com") +// self.checkNetworkConnection { [weak self] success in +// guard let self else { return } +// self.log("ReactivateTunnel checkNetworkConnection result: \(success)") +// } +// } +// +// startDns() +// } + + + // MARK: - reachability + + func pathUpdateHandler(path: Network.NWPath) { + log("REACHABILITY - Connected: \(path.status == .satisfied) - NWPATH: \(path.debugDescription)") + if path.usesInterfaceType(.wifi) { + log("REACHABILITY - have connection to wifi") + } + if path.usesInterfaceType(.cellular) { + log("REACHABILITY - have connection to cellular") + } + let servers = Resolver().getservers().map(Resolver.getnameinfo) + log("REACHABILITY DNS Servers: \(servers)") - completionHandler() - exit(EXIT_SUCCESS) + log("reachability testing network") + +// self.checkNetworkConnection { [weak self] success in +// guard let self else { return } +// self.log("reachability network check result: \(success)") +// if( success == false ) { +// self.log("ERROR - network check failed, killing PTP if not killed in the last 30 seconds") +// +// // only kill PTP if it hasnt been killed in the last 30 seconds - to avoid race conditions/infinite loop +// // TODO: maybe force VPN restart too? +// // TODO: maybe force wait a second on stopping? +// // TODO: make this smarter e.g- if PTP has been killed in the last 30 seconds, wait 10 seconds to kill it +// let timeIntervalOfLastReachabilityKill = defaults.double(forKey: self.lastReachabilityKillKey) +// let dateOfLastReachabilityKill = Date(timeIntervalSince1970: timeIntervalOfLastReachabilityKill) +// let timeSinceLastReachabilityKill = Date().timeIntervalSince(dateOfLastReachabilityKill) +// self.log("REACHABILITY kill - time since last kill: \(timeSinceLastReachabilityKill)") +// if (timeSinceLastReachabilityKill < 60) { +// self.log("REACHABILITY kill - did this < 30 seconds ago, not calling it again") +// return +// } +// else { +// // do the kill +// defaults.set(Date().timeIntervalSince1970, forKey: self.lastReachabilityKillKey) +// } +// } +// } } + + func checkNetworkConnection(callback: @escaping (Bool) -> Void, attempt: Int = 1) { + log("===== checkNetworkConnection - attempt #\(attempt)") + URLCache.shared.removeAllCachedResponses() + firstly { + try makeNetworkConnection() + } + .map { [weak self] data, response -> Void in + guard let self else { return } + try self.validateNetworkResponse(response: response) + callback(true) + } + .catch { [weak self] error in + guard let self else { return } + self.log("ERROR - failed checkNetworkConnection attempt #\(attempt): \(error)") + if attempt < 3 { + DispatchQueue.global(qos: .default).asyncAfter(deadline: DispatchTime.now() + (attempt == 1 ? 5 : 15)) { + self.checkNetworkConnection(callback: callback, attempt: attempt + 1) + } + } else { + self.log("ERROR - failed checkNetworkConnection attempt #\(attempt): \(error)") + callback(false) + } + } + } + + func makeNetworkConnection() throws -> Promise<(data: Data, response: URLResponse)> { + return URLSession.shared.dataTask(.promise, with: try Client.makeGetRequest(urlString: "https://apple.com")) + } + + func validateNetworkResponse(response: URLResponse?) throws { + self.log("validating checkNetworkConnection response") + if let resp = response as? HTTPURLResponse { + if (resp.statusCode >= 400 || resp.statusCode <= 0) { + self.log("response has bad status code \(resp.statusCode)") + throw "response has bad status code \(resp.statusCode)" + } + else { + self.log("response has good status code (2xx, 3xx) and no error code") + } + } + else { + throw "Invalid URL Response received: \(String(describing: response))" + } + } + +} + +extension PacketTunnelProvider { + + #if DEBUG + static let debugLogsKey = AppGroupStorage.Key<[String]>(rawValue: "com.confirmed.packettunnelprovider.debuglogs") + + func debugLog(_ string: String) { + let string = "DEBUG LOG \(PacketTunnelProviderLogs.dateFormatter.string(from: Date())) \(string)" + if var existing = AppGroupStorage.shared.read(key: PacketTunnelProvider.debugLogsKey) { + existing.append(string) + AppGroupStorage.shared.write(content: existing, key: PacketTunnelProvider.debugLogsKey) + } else { + AppGroupStorage.shared.write(content: [string], key: PacketTunnelProvider.debugLogsKey) + } + } + + func flushDebugLogsToPacketTunnelProviderLogs() { + if let existing = AppGroupStorage.shared.read(key: PacketTunnelProvider.debugLogsKey) { + for entry in existing { + PacketTunnelProviderLogs.log(entry) + } + AppGroupStorage.shared.delete(forKey: PacketTunnelProvider.debugLogsKey) + } + } + #endif +} + +open class Resolver { + + fileprivate var state = __res_9_state() + + public init() { + res_9_ninit(&state) + } + + deinit { + res_9_ndestroy(&state) + } + + public final func getservers() -> [res_9_sockaddr_union] { + + let maxServers = 10 + var servers = [res_9_sockaddr_union](repeating: res_9_sockaddr_union(), count: maxServers) + let found = Int(res_9_getservers(&state, &servers, Int32(maxServers))) + + // filter is to remove the erroneous empty entry when there's no real servers + return Array(servers[0 ..< found]).filter() { $0.sin.sin_len > 0 } + } +} + +extension Resolver { + public static func getnameinfo(_ s: res_9_sockaddr_union) -> String { + var s = s + var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST)) + + let sinlen = socklen_t(s.sin.sin_len) + let _ = withUnsafePointer(to: &s) { + $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { + Darwin.getnameinfo($0, sinlen, + &hostBuffer, socklen_t(hostBuffer.count), + nil, 0, + NI_NUMERICHOST) + } + } + + return String(cString: hostBuffer) + } +} + +extension NEProviderStopReason: CustomDebugStringConvertible { + + public var debugDescription: String { + switch self { + case .none: + return "none" + case .userInitiated: + return "userInitiated" + case .providerFailed: + return "providerFailed" + case .noNetworkAvailable: + return "noNetworkAvailable" + case .unrecoverableNetworkChange: + return "unrecoverableNetworkChange" + case .providerDisabled: + return "providerDisabled" + case .authenticationCanceled: + return "authenticationCanceled" + case .configurationFailed: + return "configurationFailed" + case .idleTimeout: + return "idleTimeout" + case .configurationDisabled: + return "configurationDisabled" + case .configurationRemoved: + return "configurationRemoved" + case .superceded: + return "superceded" + case .userLogout: + return "userLogout" + case .userSwitch: + return "userSwitch" + case .connectionFailed: + return "connectionFailed" + case .sleep: + return "sleep" + case .appUpdate: + return "appUpdate" + case .internalError: + return "internalError" + } + } } diff --git a/Lockdown Tunnel/TunnelPrivacyInfo.xcprivacy b/Lockdown Tunnel/TunnelPrivacyInfo.xcprivacy new file mode 100644 index 0000000..454777d --- /dev/null +++ b/Lockdown Tunnel/TunnelPrivacyInfo.xcprivacy @@ -0,0 +1,8 @@ + + + + + NSPrivacyTracking + + + diff --git a/LockdownFirewallWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/LockdownFirewallWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/LockdownFirewallWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdownFirewallWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/LockdownFirewallWidget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/LockdownFirewallWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdownFirewallWidget/Assets.xcassets/Contents.json b/LockdownFirewallWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LockdownFirewallWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdownFirewallWidget/Assets.xcassets/Main Background.colorset/Contents.json b/LockdownFirewallWidget/Assets.xcassets/Main Background.colorset/Contents.json new file mode 100644 index 0000000..36d7d76 --- /dev/null +++ b/LockdownFirewallWidget/Assets.xcassets/Main Background.colorset/Contents.json @@ -0,0 +1,31 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "extended-gray", + "components" : { + "alpha" : "1.000", + "white" : "0.930" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "platform" : "osx", + "reference" : "underPageBackgroundColor" + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdownFirewallWidget/Assets.xcassets/Panel Background.colorset/Contents.json b/LockdownFirewallWidget/Assets.xcassets/Panel Background.colorset/Contents.json new file mode 100644 index 0000000..ca11ee7 --- /dev/null +++ b/LockdownFirewallWidget/Assets.xcassets/Panel 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" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.118", + "green" : "0.110", + "red" : "0.110" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdownFirewallWidget/Assets.xcassets/Panel Secondary Background.colorset/Contents.json b/LockdownFirewallWidget/Assets.xcassets/Panel Secondary Background.colorset/Contents.json new file mode 100644 index 0000000..ec3e8d4 --- /dev/null +++ b/LockdownFirewallWidget/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/LockdownFirewallWidget/Assets.xcassets/Power Button Shadow Color.colorset/Contents.json b/LockdownFirewallWidget/Assets.xcassets/Power Button Shadow Color.colorset/Contents.json new file mode 100644 index 0000000..742124e --- /dev/null +++ b/LockdownFirewallWidget/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/LockdownFirewallWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/LockdownFirewallWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/LockdownFirewallWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LockdownFirewallWidget/Assets.xcassets/power.imageset/Contents.json b/LockdownFirewallWidget/Assets.xcassets/power.imageset/Contents.json new file mode 100644 index 0000000..0a60853 --- /dev/null +++ b/LockdownFirewallWidget/Assets.xcassets/power.imageset/Contents.json @@ -0,0 +1,19 @@ +{ + "images" : [ + { + "filename" : "power.pdf", + "idiom" : "universal", + "language-direction" : "left-to-right" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "auto-scaling" : "auto", + "compression-type" : "lossless", + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/LockdownFirewallWidget/Assets.xcassets/power.imageset/power.pdf b/LockdownFirewallWidget/Assets.xcassets/power.imageset/power.pdf new file mode 100644 index 0000000..6f539f6 Binary files /dev/null and b/LockdownFirewallWidget/Assets.xcassets/power.imageset/power.pdf differ diff --git a/LockdownFirewallWidget/CombinedProvider.swift b/LockdownFirewallWidget/CombinedProvider.swift new file mode 100644 index 0000000..758d61e --- /dev/null +++ b/LockdownFirewallWidget/CombinedProvider.swift @@ -0,0 +1,80 @@ +// +// CombinedProvider.swift +// LockdownFirewallWidgetExtension +// +// Created by Oleg Dreyman on 28.09.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation +import WidgetKit + +struct CombinedProvider: TimelineProvider { + + let main: Main + let supplemental: Supplemental + + struct Entry: TimelineEntry { + var main: Main.Entry + var supplemental: Supplemental.Entry + + var date: Date { + return min(main.date, supplemental.date) + } + } + + func placeholder(in context: Context) -> Entry { + let leftPlaceholder = main.placeholder(in: context) + let rightPlaceholder = supplemental.placeholder(in: context) + + return Entry(main: leftPlaceholder, supplemental: rightPlaceholder) + } + + func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) { + var leftSnapshot: Main.Entry? + var rightSnapshot: Supplemental.Entry? + let group = DispatchGroup() + + group.enter() + main.getSnapshot(in: context) { (leftEntry) in + leftSnapshot = leftEntry + group.leave() + } + + group.enter() + supplemental.getSnapshot(in: context) { (rightEntry) in + rightSnapshot = rightEntry + group.leave() + } + + dispatchPrecondition(condition: .onQueue(.main)) + group.notify(queue: .main) { + completion(Entry(main: leftSnapshot!, supplemental: rightSnapshot!)) + } + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + var leftTimeline: Timeline? + var rightTimeline: Timeline? + let group = DispatchGroup() + + group.enter() + main.getTimeline(in: context) { (leftEntry) in + leftTimeline = leftEntry + group.leave() + } + + group.enter() + supplemental.getTimeline(in: context) { (rightEntry) in + rightTimeline = rightEntry + group.leave() + } + + dispatchPrecondition(condition: .onQueue(.main)) + group.notify(queue: .main) { + let zippedEntries = zip(leftTimeline!.entries, rightTimeline!.entries) + let timeline = Timeline(entries: zippedEntries.map({ Entry.init(main: $0, supplemental: $1) }), policy: leftTimeline!.policy) + completion(timeline) + } + } +} diff --git a/LockdownFirewallWidget/FireWallWidgetPrivacyInfo.xcprivacy b/LockdownFirewallWidget/FireWallWidgetPrivacyInfo.xcprivacy new file mode 100644 index 0000000..454777d --- /dev/null +++ b/LockdownFirewallWidget/FireWallWidgetPrivacyInfo.xcprivacy @@ -0,0 +1,8 @@ + + + + + NSPrivacyTracking + + + diff --git a/ConfirmedTunnel/Info.plist b/LockdownFirewallWidget/Info.plist similarity index 68% rename from ConfirmedTunnel/Info.plist rename to LockdownFirewallWidget/Info.plist index a2b4380..355748c 100644 --- a/ConfirmedTunnel/Info.plist +++ b/LockdownFirewallWidget/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - ConfirmedTunnel + LockdownFirewallWidget CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -15,17 +15,20 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - XPC! + $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + $(MARKETING_VERSION) CFBundleVersion - 1 + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionPointIdentifier - com.apple.networkextension.packet-tunnel - NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).PacketTunnelProvider + com.apple.widgetkit-extension + UIAppFonts + + Montserrat-Medium.ttf + Montserrat-Bold.ttf + diff --git a/LockdownFirewallWidget/LoadingCircle.swift b/LockdownFirewallWidget/LoadingCircle.swift new file mode 100644 index 0000000..3aa4c44 --- /dev/null +++ b/LockdownFirewallWidget/LoadingCircle.swift @@ -0,0 +1,122 @@ +// +// LoadingCircle.swift +// Lockdown +// +// Created by Johnny Lin on 1/21/20. +// Copyright © 2020 Confirmed, Inc. All rights reserved. +// + +import Foundation +import SwiftUI + +struct TunnelState { + var color: Color = Color.gray + var circleColor: Color = Color.gray + + init() { + } + + init(color: Color, circleColor: Color) { + self.color = color + self.circleColor = circleColor + } +} + +struct StatusLabel: View { + + let text: String + let color: Color + + var body: some View { + Text(text) + .font(.system(size: 10, weight: .semibold)) + .foregroundColor(.white) + .padding(EdgeInsets(top: 0, leading: 7, bottom: 0, trailing: 7)) + .background(color) + .overlay( + RoundedRectangle(cornerRadius: 3) + .stroke(color, lineWidth: 4) + ) + } +} + +struct LoadingCircle: View { + + let side: CGFloat + let tunnelState: TunnelState + let link: String + + init(tunnelState: TunnelState, side: CGFloat, link: String) { + self.tunnelState = tunnelState + self.side = side + self.link = link + } + + var body: some View { + ZStack { + Circle() + .stroke(lineWidth: 2.5) + .frame(width: side * 0.4, height: side * 0.4) + .padding(4) + .foregroundColor(tunnelState.circleColor) + .zIndex(10) + Circle() + .fill() + .frame(width: side * 0.4, height: side * 0.4) + .shadow(color: .powerButtonShadowColor, radius: 8, x: 0, y: 3.5) + .padding(4) + .foregroundColor(Color.panelSecondaryBackground) + .background(Color.panelBackground) + .zIndex(1) + Link.init(destination: URL.init(string: self.link)!, label: { + Image(uiImage: UIImage(named: "power")!.withRenderingMode(.alwaysTemplate).withTintColor(UIColor(tunnelState.circleColor)).resized(toFit: CGSize(width: side * 0.30, height: side * 0.30))) + .foregroundColor(tunnelState.circleColor) + .zIndex(40) + }) + } + } +} + +struct BlankButtonStyle: ButtonStyle { + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + } +} + +extension Color { + static let confirmedBlue = Color(red: 0/255.0, green: 173/255.0, blue: 231/255.0) + static let panelBackground = Color("Panel Background") + static let panelSecondaryBackground = Color("Panel Secondary Background") + static let powerButtonShadowColor = Color("Power Button Shadow Color") + static let mainBackground = Color("Main Background") + + static let lightGray = Color(UIColor.lightGray) + static let flatRed = Color(red: 231/255, green: 76/255, blue: 60/255) +} + +extension UIImage { + func resized(toFit size: CGSize) -> UIImage { + assert(size.width > 0 && size.height > 0, "You cannot safely scale an image to a zero width or height") + + let imageAspectRatio = self.size.width / self.size.height + let canvasAspectRatio = size.width / size.height + + var resizeFactor: CGFloat + + if imageAspectRatio > canvasAspectRatio { + resizeFactor = size.width / self.size.width + } else { + resizeFactor = size.height / self.size.height + } + + let scaledSize = CGSize(width: self.size.width * resizeFactor, height: self.size.height * resizeFactor) + + UIGraphicsBeginImageContextWithOptions(scaledSize, false, 0.0) + draw(in: CGRect(origin: .zero, size: scaledSize)) + + let scaledImage = UIGraphicsGetImageFromCurrentImageContext() ?? self + UIGraphicsEndImageContext() + + return scaledImage + } +} diff --git a/LockdownFirewallWidget/LockdownFirewallWidget.swift b/LockdownFirewallWidget/LockdownFirewallWidget.swift new file mode 100644 index 0000000..2737a75 --- /dev/null +++ b/LockdownFirewallWidget/LockdownFirewallWidget.swift @@ -0,0 +1,239 @@ +// +// LockdownFirewallWidget.swift +// LockdownFirewallWidget +// +// Created by Oleg Dreyman on 25.09.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import WidgetKit +import SwiftUI + +struct FirewallProvider: TimelineProvider { + func placeholder(in context: Context) -> FirewallEntry { + FirewallEntry(date: Date(), size: context.displaySize, isFirewallEnabled: false, dayMetricsString: "--") + } + + func getSnapshot(in context: Context, completion: @escaping (FirewallEntry) -> ()) { + let entry = FirewallEntry(date: Date(), size: context.displaySize, isFirewallEnabled: getUserWantsFirewallEnabled(), dayMetricsString: getDayMetricsString(commas: true)) + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { + var entries: [FirewallEntry] = [] + + let currentDate = Date() + let entry = FirewallEntry(date: Date(), size: context.displaySize, isFirewallEnabled: getUserWantsFirewallEnabled(), dayMetricsString: getDayMetricsString(commas: true)) + entries.append(entry) + + let timeline = Timeline( + entries: entries, + policy: .after(Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!) + ) + completion(timeline) + } +} + +struct FirewallEntry: TimelineEntry { + let date: Date + let size: CGSize + let isFirewallEnabled: Bool + let dayMetricsString: String + + var buttonColor: Color { + if isFirewallEnabled { + return .confirmedBlue + } else { + return Color(.systemGray) + } + } +} + +struct LockdownFirewallWidgetEntryView : View { + var entry: FirewallProvider.Entry + + var body: some View { + VStack(spacing: 0) { + LoadingCircle( + tunnelState: TunnelState( + color: entry.buttonColor, + circleColor: entry.buttonColor + ), + side: entry.size.height, + link: "lockdown://" + ) + .padding(EdgeInsets(top: 12, leading: 0, bottom: 2, trailing: 0)) + if entry.isFirewallEnabled { + StatusLabel(text: NSLocalizedString("FIREWALL ON", comment: ""), color: .confirmedBlue) + } else { + StatusLabel(text: NSLocalizedString("FIREWALL OFF", comment: ""), color: .flatRed) + } + if entry.size.height < 160 { + Spacer().frame(minHeight: 4) + } else { + Spacer() + } + Link(destination: URL(string: "lockdown://showMetrics")!, label: { + VStack(spacing: 0) { + Text(entry.dayMetricsString) + .font(.system(size: 21, weight: .semibold)) + Text(NSLocalizedString("Blocked Today", comment: "")) + .font(.system(size: 12, weight: .medium)) + } + .padding(.bottom, 12) + }) + }.frame(width: entry.size.height, height: entry.size.height) + } +} + +struct VPNProvider: TimelineProvider { + func placeholder(in context: Context) -> VPNEntry { + VPNEntry(date: Date(), size: context.displaySize, isVPNEnabled: false, vpnRegion: VPNRegion()) + } + + func getSnapshot(in context: Context, completion: @escaping (VPNEntry) -> ()) { + let entry = VPNEntry(date: Date(), size: context.displaySize, isVPNEnabled: LatestKnowledge.isVPNEnabled, vpnRegion: getSavedVPNRegion()) + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { + var entries: [VPNEntry] = [] + + let currentDate = Date() + let entry = VPNEntry(date: Date(), size: context.displaySize, isVPNEnabled: LatestKnowledge.isVPNEnabled, vpnRegion: getSavedVPNRegion()) + entries.append(entry) + + let timeline = Timeline( + entries: entries, + policy: .after(Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!) + ) + completion(timeline) + } +} + +struct VPNEntry: TimelineEntry { + let date: Date + let size: CGSize + let isVPNEnabled: Bool + let vpnRegion: VPNRegion + + var buttonColor: Color { + if isVPNEnabled { + return .confirmedBlue + } else { + return Color(.systemGray) + } + } +} + +struct LockdownVPNWidgetEntryView : View { + var entry: VPNProvider.Entry + + var body: some View { + VStack(spacing: 0) { + LoadingCircle( + tunnelState: TunnelState( + color: entry.buttonColor, + circleColor: entry.buttonColor + ), + side: entry.size.height, + link: "lockdown://" + ) + .padding(EdgeInsets(top: 12, leading: 0, bottom: 2, trailing: 0)) + if entry.isVPNEnabled { + StatusLabel(text: NSLocalizedString("Tunnel On", comment: "").uppercased(), color: .confirmedBlue) + } else { + StatusLabel(text: NSLocalizedString("Tunnel Off", comment: "").uppercased(), color: .flatRed) + } + if entry.size.height < 160 { + Spacer().frame(minHeight: 4) + } else { + Spacer() + } + Link(destination: URL(string: "lockdown://changeVPNregion")!, label: { + VStack(spacing: 0) { + Text(entry.vpnRegion.regionFlagEmoji) + .font(.system(size: 21, weight: .semibold)) + Text(entry.vpnRegion.regionDisplayNameShort) + .font(.system(size: 12, weight: .medium)) + } + .padding(.bottom, 12) + }) + }.frame(width: entry.size.height, height: entry.size.height) + } +} + +struct CombinedWidgetView: View { + let firewall: FirewallEntry + let vpn: VPNEntry + + var body: some View { + HStack(alignment: .top, spacing: 0) { + LockdownFirewallWidgetEntryView(entry: firewall) + LockdownVPNWidgetEntryView(entry: vpn) + } + .frame(minWidth: firewall.size.width, minHeight: firewall.size.height) + } +} + +@main +struct LockdownWidgetBundle: WidgetBundle { + + @WidgetBundleBuilder + var body: some Widget { + LockdownFirewallWidget() + LockdownVPNWidget() + LockdownCombinedWidget() + } +} + +struct LockdownFirewallWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration( + kind: "LockdownFirewallWidget", + provider: FirewallProvider(), + content: { entry in + ZStack { + Color.panelBackground + LockdownFirewallWidgetEntryView(entry: entry) + } + } + ) + .configurationDisplayName("Firewall") + .supportedFamilies([.systemSmall]) + } +} + +struct LockdownVPNWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration( + kind: "LockdownVPNWidget", + provider: VPNProvider(), + content: { entry in + ZStack { + Color.panelBackground + LockdownVPNWidgetEntryView(entry: entry) + } + } + ) + .configurationDisplayName("Secure Tunnel") + .supportedFamilies([.systemSmall]) + } +} + +struct LockdownCombinedWidget: Widget { + var body: some WidgetConfiguration { + StaticConfiguration( + kind: "LockdownCombinedWidget", + provider: CombinedProvider(main: FirewallProvider(), supplemental: VPNProvider()), + content: { entry in + ZStack { + Color.panelBackground + CombinedWidgetView(firewall: entry.main, vpn: entry.supplemental) + } + } + ) + .configurationDisplayName("Firewall + Tunnel") + .supportedFamilies([.systemMedium]) + } +} diff --git a/ConfirmedTunnel/ConfirmedTunnel.entitlements b/LockdownFirewallWidgetExtension.entitlements similarity index 53% rename from ConfirmedTunnel/ConfirmedTunnel.entitlements rename to LockdownFirewallWidgetExtension.entitlements index d05c0ec..291240c 100644 --- a/ConfirmedTunnel/ConfirmedTunnel.entitlements +++ b/LockdownFirewallWidgetExtension.entitlements @@ -2,11 +2,9 @@ - com.apple.developer.networking.networkextension + com.apple.security.application-groups - app-proxy-provider - content-filter-provider - packet-tunnel-provider + group.com.confirmed diff --git a/LockdowniOS.xcodeproj/project.pbxproj b/LockdowniOS.xcodeproj/project.pbxproj index 4f1ccf6..ed4bc58 100644 --- a/LockdowniOS.xcodeproj/project.pbxproj +++ b/LockdowniOS.xcodeproj/project.pbxproj @@ -3,16 +3,35 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ - 08799CF7AFE70CC200E47EDB /* Pods_Lockdown_Firewall_Widget.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A890BF9C9CF89A7E923EDDA /* Pods_Lockdown_Firewall_Widget.framework */; }; + 065F043966BC72234D8E0073 /* Pods_LockdownTunnel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F07BBAE85FFEFFCD9706CF39 /* Pods_LockdownTunnel.framework */; }; + 1579100974C8086B190B35BB /* Pods-Lockdown VPN Widget-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = A19DA148E491FF88E4B0B408 /* Pods-Lockdown VPN Widget-settings-metadata.plist */; }; + 180AC5905ADA404C13B1D170 /* Pods_Lockdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6FEED2B9A31E4C2EF288E61 /* Pods_Lockdown.framework */; }; 20816D1FD569053C0994232B /* Pods-Lockdown-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = E4A025BF9012D4E6454AE1D6 /* Pods-Lockdown-metadata.plist */; }; + 388CD7581B88A7E496467546 /* Pods-Lockdown Firewall Widget-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2DF472CA81A935DEF14D7039 /* Pods-Lockdown Firewall Widget-settings-metadata.plist */; }; + 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 */; }; 3D0711B822FE79BE00391C6E /* WhyTrustViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0711B722FE79BE00391C6E /* WhyTrustViewController.swift */; }; 3D0711BB22FE7B5100391C6E /* TitleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D0711BA22FE7B5100391C6E /* TitleViewController.swift */; }; 3D0971D822EBAD1000CCD326 /* facebook_sdk.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3D0971D722EBAD1000CCD326 /* facebook_sdk.txt */; }; 3D0971DA22EBAD4C00CCD326 /* marketing.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3D0971D922EBAD4C00CCD326 /* marketing.txt */; }; + 3D3BF4CC233D5E9100D0C482 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3D3BF4D0233D5E9100D0C482 /* Localizable.strings */; }; + 3D3BF4CD233D5E9100D0C482 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3D3BF4D0233D5E9100D0C482 /* Localizable.strings */; }; + 3D3BF4CE233D5E9100D0C482 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3D3BF4D0233D5E9100D0C482 /* Localizable.strings */; }; + 3D40826327F675F6004C146B /* dnscrypt-proxy.toml in Resources */ = {isa = PBXBuildFile; fileRef = 3D40826227F675F6004C146B /* dnscrypt-proxy.toml */; }; + 3D40826427F675F6004C146B /* dnscrypt-proxy.toml in Resources */ = {isa = PBXBuildFile; fileRef = 3D40826227F675F6004C146B /* dnscrypt-proxy.toml */; }; + 3D40826527F675F6004C146B /* dnscrypt-proxy.toml in Resources */ = {isa = PBXBuildFile; fileRef = 3D40826227F675F6004C146B /* dnscrypt-proxy.toml */; }; + 3D40826627F675F6004C146B /* dnscrypt-proxy.toml in Resources */ = {isa = PBXBuildFile; fileRef = 3D40826227F675F6004C146B /* dnscrypt-proxy.toml */; }; + 3D40826727F675F6004C146B /* dnscrypt-proxy.toml in Resources */ = {isa = PBXBuildFile; fileRef = 3D40826227F675F6004C146B /* dnscrypt-proxy.toml */; }; + 3D40826927F6A03F004C146B /* DNSCryptThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D40826827F6A03F004C146B /* DNSCryptThread.swift */; }; + 3D40826A27F6A03F004C146B /* DNSCryptThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D40826827F6A03F004C146B /* DNSCryptThread.swift */; }; + 3D40826B27F6A03F004C146B /* DNSCryptThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D40826827F6A03F004C146B /* DNSCryptThread.swift */; }; + 3D40826C27F6A03F004C146B /* DNSCryptThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D40826827F6A03F004C146B /* DNSCryptThread.swift */; }; + 3D40826D27F6A03F004C146B /* DNSCryptThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D40826827F6A03F004C146B /* DNSCryptThread.swift */; }; 3D44378022DFB22600908CDC /* Montserrat-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377A22DFB22600908CDC /* Montserrat-Medium.ttf */; }; 3D44378122DFB22600908CDC /* Montserrat-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377B22DFB22600908CDC /* Montserrat-Light.ttf */; }; 3D44378222DFB22600908CDC /* Montserrat-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377C22DFB22600908CDC /* Montserrat-Thin.ttf */; }; @@ -58,24 +77,26 @@ 3D47CDD422F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScalePulseOutRapid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D47CDAC22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScalePulseOutRapid.swift */; }; 3D47CDD522F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulseRise.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D47CDAD22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulseRise.swift */; }; 3D47CDD622F3C3F3003BD7F7 /* NVActivityIndicatorAnimationOrbit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D47CDAE22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationOrbit.swift */; }; + 3D4D7FEC247F2435000369FD /* google_shopping_ads.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3D4D7FEB247F22AE000369FD /* google_shopping_ads.txt */; }; 3D5464D323037CCA00AE1F73 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 3D5464D223037CCA00AE1F73 /* Settings.bundle */; }; 3D5464D42303839200AE1F73 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 3D5464D223037CCA00AE1F73 /* Settings.bundle */; }; 3D5464D52303839400AE1F73 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 3D5464D223037CCA00AE1F73 /* Settings.bundle */; }; 3D5464D62303839500AE1F73 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 3D5464D223037CCA00AE1F73 /* Settings.bundle */; }; 3D5561D4230B58F30062001D /* PrivacyPolicyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5561D3230B58F30062001D /* PrivacyPolicyViewController.swift */; }; + 3D5F5A0823107C1E004C3860 /* game_ads.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3D5F5A0723107C1E004C3860 /* game_ads.txt */; }; + 3D5F5A0A23107EB8004C3860 /* snapchat_analytics.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3D5F5A0923107EB8004C3860 /* snapchat_analytics.txt */; }; + 3D5F5A0C23109ABB004C3860 /* WhatIsVpnViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D5F5A0B23109ABB004C3860 /* WhatIsVpnViewController.swift */; }; + 3D752C342357FA3B00C163E4 /* SF-Pro-Rounded-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3D752C302357FA3B00C163E4 /* SF-Pro-Rounded-Regular.otf */; }; + 3D752C352357FA3B00C163E4 /* SF-Pro-Rounded-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3D752C312357FA3B00C163E4 /* SF-Pro-Rounded-Medium.otf */; }; + 3D752C362357FA3B00C163E4 /* SF-Pro-Rounded-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3D752C322357FA3B00C163E4 /* SF-Pro-Rounded-Bold.otf */; }; + 3D752C372357FA3B00C163E4 /* SF-Pro-Rounded-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3D752C332357FA3B00C163E4 /* SF-Pro-Rounded-Semibold.otf */; }; 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 */; }; - 3D94AAF322FDEAC50012B0DE /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DABD9FE22F7AD4D00480AAC /* FirewallUtilities.swift */; }; 3D94AAF422FDEAC80012B0DE /* WhitelistUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */; }; 3D94AAF522FDEACD0012B0DE /* VPNController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18921B77C80008A9322 /* VPNController.swift */; }; - 3D94AAF622FDEAD60012B0DE /* FirewallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F4022F252720017740D /* FirewallController.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 */; }; - 3D94AB0D22FE05090012B0DE /* CocoaLumberjack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E1206D982B0060D945 /* CocoaLumberjack.framework */; }; - 3D94AB0E22FE05090012B0DE /* CocoaLumberjackSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E2206D982B0060D945 /* CocoaLumberjackSwift.framework */; }; - 3D94AB0F22FE0CF60012B0DE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1141A1B1F46230500F54698 /* Assets.xcassets */; }; 3D94AB1022FE0CFB0012B0DE /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3D94AB0222FDEDEB0012B0DE /* MainInterface.storyboard */; }; 3D94AB1222FE3A460012B0DE /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D94AB1122FE3A460012B0DE /* Environment.swift */; }; 3D94AB1322FE3BA10012B0DE /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D94AB1122FE3A460012B0DE /* Environment.swift */; }; @@ -83,13 +104,22 @@ 3D94AB1522FE3BA40012B0DE /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D94AB1122FE3A460012B0DE /* Environment.swift */; }; 3D970DAD22EC149D00F9CC93 /* BlockLogCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D970DAC22EC149D00F9CC93 /* BlockLogCell.swift */; }; 3D970DAF22EC15D800F9CC93 /* BlockLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D970DAE22EC15D800F9CC93 /* BlockLogViewController.swift */; }; + 3D9FC67723E503DF004122D3 /* EmailSignInViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9FC67623E503DF004122D3 /* EmailSignInViewController.swift */; }; + 3D9FC67923E521DE004122D3 /* ForgotPasswordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9FC67823E521DE004122D3 /* ForgotPasswordViewController.swift */; }; 3DAA6B4F22EA76420018FC09 /* clickbait.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3DAA6B4E22EA76420018FC09 /* clickbait.txt */; }; 3DAA6B5322EA988F0018FC09 /* ransomware.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3DAA6B5222EA988F0018FC09 /* ransomware.txt */; }; 3DABD9FD22F7961F00480AAC /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F2D22F190720017740D /* Client.swift */; }; - 3DABD9FF22F7AD4D00480AAC /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DABD9FE22F7AD4D00480AAC /* FirewallUtilities.swift */; }; - 3DABDA0022F7AD4D00480AAC /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DABD9FE22F7AD4D00480AAC /* FirewallUtilities.swift */; }; - 3DABDA0122F7AD4D00480AAC /* FirewallUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DABD9FE22F7AD4D00480AAC /* FirewallUtilities.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 */; }; @@ -114,6 +144,212 @@ 3DCA4F3122F190AE0017740D /* ClientModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F3022F190AE0017740D /* ClientModels.swift */; }; 3DCA4F3322F22CB40017740D /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F3222F22CB40017740D /* HomeViewController.swift */; }; 3DCA4F4122F252720017740D /* FirewallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCA4F4022F252720017740D /* FirewallController.swift */; }; + 3DCBC8F22542544A00446C98 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3D3BF4D0233D5E9100D0C482 /* Localizable.strings */; }; + 3DCFE6FA24493F9000EA9B35 /* marketing_beta.txt in Sources */ = {isa = PBXBuildFile; fileRef = 3DCFE6F924493F9000EA9B35 /* marketing_beta.txt */; }; + 3DCFE6FB244945A100EA9B35 /* marketing_beta.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3DCFE6F924493F9000EA9B35 /* marketing_beta.txt */; }; + 3DD545D628068AC5005E140C /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 3DD545CD280681AA005E140C /* libresolv.9.tbd */; }; + 3DD545DB2808C2F6005E140C /* 5000_dummy_list.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3DD545DA2808C2F6005E140C /* 5000_dummy_list.txt */; }; + 3DF2455423A2F8A400E46613 /* EmailSignUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF2455323A2F8A400E46613 /* EmailSignUpViewController.swift */; }; + 3DF2455623A306DB00E46613 /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF2455523A306DB00E46613 /* Loader.swift */; }; + 3DF5D75F2633B1E100F77D79 /* amazon_trackers.txt in Resources */ = {isa = PBXBuildFile; fileRef = 3DF5D75E2633B1E100F77D79 /* amazon_trackers.txt */; }; + 40098E2A29FDA6A800886474 /* BulletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40098E2929FDA6A800886474 /* BulletView.swift */; }; + 40098E2C29FDA6CC00886474 /* PaywallDescriptionLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40098E2B29FDA6CC00886474 /* PaywallDescriptionLabel.swift */; }; + 40098E2E29FDA6E500886474 /* PlanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40098E2D29FDA6E500886474 /* PlanView.swift */; }; + 40098E3129FDA73300886474 /* VPNPaywallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40098E3029FDA73200886474 /* VPNPaywallViewController.swift */; }; + 40098E3B29FF378F00886474 /* FirewallPaywallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40098E3529FF378F00886474 /* FirewallPaywallViewController.swift */; }; + 40098E3C29FF378F00886474 /* AnnualPlanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40098E3829FF378F00886474 /* AnnualPlanView.swift */; }; + 40098E3D29FF378F00886474 /* MonthlyPlanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40098E3929FF378F00886474 /* MonthlyPlanView.swift */; }; + 40098E3E29FF378F00886474 /* AdvancedPlansViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40098E3A29FF378F00886474 /* AdvancedPlansViews.swift */; }; + 4015B4F729EFD9AC004102E0 /* AccessLevelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4015B4F629EFD9AC004102E0 /* AccessLevelView.swift */; }; + 4015B4FD29F00DD8004102E0 /* LDVpnViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4015B4FC29F00DD8004102E0 /* LDVpnViewController.swift */; }; + 4015B4FF29F14C95004102E0 /* LDCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4015B4FE29F14C95004102E0 /* LDCardView.swift */; }; + 4015B50329F16E1A004102E0 /* LDConfigurationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4015B50229F16E1A004102E0 /* LDConfigurationViewController.swift */; }; + 402BAD252A0B675B009B8820 /* LockedListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402BAD242A0B675B009B8820 /* LockedListsView.swift */; }; + 402BAD362A0CD37C009B8820 /* ConnectivityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402BAD352A0CD37C009B8820 /* ConnectivityService.swift */; }; + 402BAD382A0CD3B0009B8820 /* ConnectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402BAD372A0CD3B0009B8820 /* ConnectionState.swift */; }; + 402D24B829D59B4400A5AB60 /* EmptyListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D24B729D59B4400A5AB60 /* EmptyListsView.swift */; }; + 402D24CB29D87B5A00A5AB60 /* ListsSubmenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D24CA29D87B5A00A5AB60 /* ListsSubmenuView.swift */; }; + 402D24D429D87F4500A5AB60 /* CustomBlockedTableHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D24D329D87F4500A5AB60 /* CustomBlockedTableHeader.swift */; }; + 402D251629E514CF00A5AB60 /* MoveToListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D251529E514CF00A5AB60 /* MoveToListViewController.swift */; }; + 402D251929E517E100A5AB60 /* ConfiguredNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D251829E517E100A5AB60 /* ConfiguredNavigationView.swift */; }; + 402D251B29E519B500A5AB60 /* CustomTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D251A29E519B500A5AB60 /* CustomTableView.swift */; }; + 402D251F29E52D6A00A5AB60 /* EditDomainsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D251E29E52D6A00A5AB60 /* EditDomainsViewController.swift */; }; + 402D252129E52D7600A5AB60 /* BottomMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D252029E52D7600A5AB60 /* BottomMenu.swift */; }; + 402D252329E5473E00A5AB60 /* EditDomainsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D252229E5473E00A5AB60 /* EditDomainsCell.swift */; }; + 402D252729E5843300A5AB60 /* ImportBlockListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D252629E5843300A5AB60 /* ImportBlockListViewController.swift */; }; + 402D252929E632F300A5AB60 /* ListSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D252829E632F300A5AB60 /* ListSettingsViewController.swift */; }; + 402D252B29E6335100A5AB60 /* SwitchBlockingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D252A29E6335100A5AB60 /* SwitchBlockingView.swift */; }; + 402D252D29E6346900A5AB60 /* ListBlockedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D252C29E6346900A5AB60 /* ListBlockedTableViewCell.swift */; }; + 402D252F29E6357700A5AB60 /* ListDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D252E29E6357700A5AB60 /* ListDetailViewController.swift */; }; + 402D253129E635CB00A5AB60 /* DomainsBlockedTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D253029E635CB00A5AB60 /* DomainsBlockedTableViewCell.swift */; }; + 402D253329E6588000A5AB60 /* ListDescriptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D253229E6588000A5AB60 /* ListDescriptionViewController.swift */; }; + 402D253B29E8F9A400A5AB60 /* JSONSerialization+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D253A29E8F9A400A5AB60 /* JSONSerialization+Extensions.swift */; }; + 402D254829EE112E00A5AB60 /* LDFirewallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D254729EE112E00A5AB60 /* LDFirewallViewController.swift */; }; + 402D254A29EE1C6E00A5AB60 /* DescriptionLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D254929EE1C6E00A5AB60 /* DescriptionLabel.swift */; }; + 402D254E29EE598D00A5AB60 /* TrackersGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D254D29EE598D00A5AB60 /* TrackersGroupView.swift */; }; + 402D255029EE78D600A5AB60 /* OverallStatiscticView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 402D254F29EE78D600A5AB60 /* OverallStatiscticView.swift */; }; + 408E7A9429F88C9200B2F587 /* CustomUISwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408E7A9329F88C9200B2F587 /* CustomUISwitch.swift */; }; + 408E7A9729FA698C00B2F587 /* UIVIew+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 408E7A9629FA698C00B2F587 /* UIVIew+Extensions.swift */; }; + 409481AE2A431D78001F11EB /* VPNController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18921B77C80008A9322 /* VPNController.swift */; }; + 40960AE22A029A7D000F82EB /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960AE12A029A7D000F82EB /* UIApplication+Extension.swift */; }; + 40960AE92A033514000F82EB /* AccessLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960AE82A033514000F82EB /* AccessLevel.swift */; }; + 40960AEB2A03396F000F82EB /* ProductPurchasable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960AEA2A03396F000F82EB /* ProductPurchasable.swift */; }; + 40960AED2A0339E2000F82EB /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960AEC2A0339E2000F82EB /* UserService.swift */; }; + 40960AF02A033A41000F82EB /* LockdownUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960AEF2A033A41000F82EB /* LockdownUser.swift */; }; + 40960AF22A033AF9000F82EB /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960AF12A033AF9000F82EB /* String+Localized.swift */; }; + 40960AFB2A033E46000F82EB /* PaywallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960AFA2A033E46000F82EB /* PaywallService.swift */; }; + 40960B032A033EA9000F82EB /* CountdownDisplayService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960B022A033EA9000F82EB /* CountdownDisplayService.swift */; }; + 40960B072A033F0B000F82EB /* WeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960B062A033F0B000F82EB /* WeakObject.swift */; }; + 40960B0A2A03400E000F82EB /* UserDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960B092A03400E000F82EB /* UserDefault.swift */; }; + 40960B0D2A034054000F82EB /* LockdownStorageIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960B0C2A034054000F82EB /* LockdownStorageIdentifier.swift */; }; + 40960B0E2A034054000F82EB /* LockdownStorageIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960B0C2A034054000F82EB /* LockdownStorageIdentifier.swift */; }; + 40960B152A034400000F82EB /* Keychainable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40960B142A034400000F82EB /* Keychainable.swift */; }; + 409B59E02A14CB7B0010242C /* SignUpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409B59DF2A14CB7B0010242C /* SignUpViewController.swift */; }; + 409B59E42A15CC900010242C /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409B59E32A15CC900010242C /* WelcomeView.swift */; }; + 409B59E62A15D00C0010242C /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409B59E52A15D00C0010242C /* WelcomeViewController.swift */; }; + 40CC816E2A14B25C00F9805E /* DeleteMyAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC816D2A14B25C00F9805E /* DeleteMyAccountViewController.swift */; }; + 40CC81702A14B29100F9805E /* EmailComposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC816F2A14B29100F9805E /* EmailComposable.swift */; }; + 40CC81792A14BA8800F9805E /* EmailAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC81782A14BA8800F9805E /* EmailAddress.swift */; }; + 40CC817B2A14BAA600F9805E /* EmailValidatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC817A2A14BAA600F9805E /* EmailValidatable.swift */; }; + 40CC817E2A14BB3600F9805E /* UIView+Corners.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC817D2A14BB3600F9805E /* UIView+Corners.swift */; }; + 40CC81822A14BD2200F9805E /* DeleteMyAccountViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40CC81812A14BD2100F9805E /* DeleteMyAccountViewController.xib */; }; + 40CC849E2A14BEA000F9805E /* SignUpViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40CC849D2A14BEA000F9805E /* SignUpViewController.xib */; }; + 40CC84A12A14BECF00F9805E /* EnableNotificationsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84A02A14BECF00F9805E /* EnableNotificationsViewController.swift */; }; + 40CC84A32A14BED800F9805E /* EnableNotificationsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40CC84A22A14BED800F9805E /* EnableNotificationsViewController.xib */; }; + 40CC84A52A14BEFA00F9805E /* SplashScreenViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 40CC84A42A14BEFA00F9805E /* SplashScreenViewController.xib */; }; + 40CC84A82A14C09600F9805E /* String+URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84A72A14C09600F9805E /* String+URL.swift */; }; + 40CC84AA2A14C0A300F9805E /* String+Attributed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84A92A14C0A200F9805E /* String+Attributed.swift */; }; + 40CC84AF2A14C0D800F9805E /* Date+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84AB2A14C0D700F9805E /* Date+Ext.swift */; }; + 40CC84B02A14C0D800F9805E /* CALayer+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84AC2A14C0D800F9805E /* CALayer+Ext.swift */; }; + 40CC84B12A14C0D800F9805E /* UIStackView+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84AD2A14C0D800F9805E /* UIStackView+Ext.swift */; }; + 40CC84B22A14C0D800F9805E /* UIAppearance+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84AE2A14C0D800F9805E /* UIAppearance+Ext.swift */; }; + 40CC84B82A14C0EA00F9805E /* UIViewController+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84B32A14C0E900F9805E /* UIViewController+Ext.swift */; }; + 40CC84B92A14C0EA00F9805E /* Font+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84B42A14C0E900F9805E /* Font+Ext.swift */; }; + 40CC84BA2A14C0EA00F9805E /* UICollectionView+Dequeue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84B52A14C0EA00F9805E /* UICollectionView+Dequeue.swift */; }; + 40CC84BB2A14C0EA00F9805E /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84B62A14C0EA00F9805E /* NibLoadable.swift */; }; + 40CC84BC2A14C0EA00F9805E /* UIView+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84B72A14C0EA00F9805E /* UIView+Ext.swift */; }; + 40CC84BE2A14C15400F9805E /* LockdownGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84BD2A14C15400F9805E /* LockdownGradient.swift */; }; + 40CC84C52A14C2B800F9805E /* FloatingTextInputTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84C12A14C2B700F9805E /* FloatingTextInputTextField.swift */; }; + 40CC84C62A14C2B800F9805E /* TextBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84C22A14C2B800F9805E /* TextBox.swift */; }; + 40CC84C72A14C2B800F9805E /* TextInputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84C32A14C2B800F9805E /* TextInputState.swift */; }; + 40CC84C82A14C2B800F9805E /* TextBoxLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40CC84C42A14C2B800F9805E /* TextBoxLabel.swift */; }; + 40E04A222A26708200000E8C /* WhatsNewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E04A212A26708200000E8C /* WhatsNewViewController.swift */; }; + 40E04A242A26758200000E8C /* WhatsNewDescriptionLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E04A232A26758200000E8C /* WhatsNewDescriptionLabel.swift */; }; + 40E04A502A28B4A000000E8C /* dnscrypt-proxy.toml in Resources */ = {isa = PBXBuildFile; fileRef = 3D40826227F675F6004C146B /* dnscrypt-proxy.toml */; }; + 40E04A532A29D79100000E8C /* BlockListContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E04A522A29D79100000E8C /* BlockListContainerViewController.swift */; }; + 40E04A552A29D7AB00000E8C /* CuratedListsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E04A542A29D7AB00000E8C /* CuratedListsViewController.swift */; }; + 40E04A572A29D7BC00000E8C /* CustomListsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E04A562A29D7BC00000E8C /* CustomListsViewController.swift */; }; + 40E04A592A2A1B4C00000E8C /* CTAView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E04A582A2A1B4C00000E8C /* CTAView.swift */; }; + 40E7A2F22A0CE8AE00E0231A /* advanced_gaming.txt in Resources */ = {isa = PBXBuildFile; fileRef = 40E7A2EC2A0CE8AE00E0231A /* advanced_gaming.txt */; }; + 40E7A2F82A0CE92900E0231A /* ifunny_trackers.txt in Resources */ = {isa = PBXBuildFile; fileRef = 40E7A2F32A0CE92800E0231A /* ifunny_trackers.txt */; }; + 40E7A2F92A0CE92900E0231A /* junes_journey_trackers.txt in Resources */ = {isa = PBXBuildFile; fileRef = 40E7A2F42A0CE92800E0231A /* junes_journey_trackers.txt */; }; + 40E7A2FA2A0CE92900E0231A /* scams.txt in Resources */ = {isa = PBXBuildFile; fileRef = 40E7A2F52A0CE92800E0231A /* scams.txt */; }; + 40E7A2FB2A0CE92900E0231A /* tiktok_trackers.txt in Resources */ = {isa = PBXBuildFile; fileRef = 40E7A2F62A0CE92800E0231A /* tiktok_trackers.txt */; }; + 40E7A2FC2A0CE92900E0231A /* advanced_analytics.txt in Resources */ = {isa = PBXBuildFile; fileRef = 40E7A2F72A0CE92900E0231A /* advanced_analytics.txt */; }; + 40E7A3012A0E1C7A00E0231A /* SplashScreenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E7A3002A0E1C7A00E0231A /* SplashScreenViewController.swift */; }; + 40FC414329F74C7900BD7396 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40FC414229F74C7900BD7396 /* String+Extensions.swift */; }; + 4A86219093026DE70A097E79 /* Pods-LockdownTests-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 8DA68459884385F76BF86234 /* Pods-LockdownTests-metadata.plist */; }; + 51006F192CBD0E7400F5142C /* ImageBannerWithTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51006F182CBD0E7400F5142C /* ImageBannerWithTitleView.swift */; }; + 510AA2F62CB8222100E53560 /* FeedbackPaywallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510AA2F52CB8222100E53560 /* FeedbackPaywallViewModel.swift */; }; + 5117DE882CBD4A6600C4A61B /* SectionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5117DE872CBD4A6600C4A61B /* SectionTitleView.swift */; }; + 5145A19A2CBE37C40074C562 /* FeedbackFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5145A1992CBE37C40074C562 /* FeedbackFlow.swift */; }; + 51DD89E52CB7DA770028B4FE /* FeedbackPaywallViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DD89E42CB7DA770028B4FE /* FeedbackPaywallViewController.swift */; }; + 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 */; }; + 5E13011D2D5E0131003896BD /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E13011C2D5E012E003896BD /* OnboardingView.swift */; }; + 5E9AF7302CF5D198001239A0 /* SpecialOfferPaywallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9AF72F2CF5D18F001239A0 /* SpecialOfferPaywallView.swift */; }; + 5E9BD5882D787D8500E8DE4F /* ReviewAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9BD5852D787D8400E8DE4F /* ReviewAlertManager.swift */; }; + 5EA97B2F2CF5F83D0082D3FD /* KumbhSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5EA97B2E2CF5F83D0082D3FD /* KumbhSans-Regular.ttf */; }; + 5EA97B312CF5F83D0082D3FD /* KumbhSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5EA97B2C2CF5F83D0082D3FD /* KumbhSans-Bold.ttf */; }; + 5EA97B322CF5F83D0082D3FD /* KumbhSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5EA97B2E2CF5F83D0082D3FD /* KumbhSans-Regular.ttf */; }; + 5EA97B342CF5F83D0082D3FD /* KumbhSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5EA97B2C2CF5F83D0082D3FD /* KumbhSans-Bold.ttf */; }; + 5EA97B362CF5F86A0082D3FD /* Juana-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5EA97B352CF5F86A0082D3FD /* Juana-SemiBold.ttf */; }; + 5EA97B372CF5F86A0082D3FD /* Juana-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5EA97B352CF5F86A0082D3FD /* Juana-SemiBold.ttf */; }; + 5EA97B392CF604F20082D3FD /* SpecialOfferPaywallModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EA97B382CF604D60082D3FD /* SpecialOfferPaywallModel.swift */; }; + 601BF3ED11EB7CBF95BF5720 /* Pods-Lockdown Firewall Widget-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 12884CAB7C53B842E9E3745C /* Pods-Lockdown Firewall Widget-metadata.plist */; }; + 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 */; }; + 7C0156562521C2F200670CB5 /* Montserrat-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377E22DFB22600908CDC /* Montserrat-SemiBold.ttf */; }; + 7C0156572521C2F200670CB5 /* Montserrat-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377F22DFB22600908CDC /* Montserrat-Regular.ttf */; }; + 7C0156582521C2F200670CB5 /* Montserrat-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377B22DFB22600908CDC /* Montserrat-Light.ttf */; }; + 7C0156592521C2F200670CB5 /* Montserrat-Thin.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 3D44377C22DFB22600908CDC /* Montserrat-Thin.ttf */; }; + 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 */; }; + 7C422EA525279724007F9C22 /* Align.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C422EA425279724007F9C22 /* Align.swift */; }; + 7C422EAF252797A6007F9C22 /* AccountVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C422EAE252797A6007F9C22 /* AccountVC.swift */; }; + 7C422EB72527A2D1007F9C22 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C422EB62527A2D1007F9C22 /* MainTabBarViewController.swift */; }; + 7C44081B2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C44081A2539BCCE003FAD1E /* ProtectedFileAccess.swift */; }; + 7C44081C2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C44081A2539BCCE003FAD1E /* ProtectedFileAccess.swift */; }; + 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 */; }; + 7C9A9371251E1EC700DA5721 /* LockdownFirewallWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9A9370251E1EC700DA5721 /* LockdownFirewallWidget.swift */; }; + 7C9A9373251E1EC700DA5721 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7C9A9372251E1EC700DA5721 /* Assets.xcassets */; }; + 7C9A9377251E1EC700DA5721 /* LockdownFirewallWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7C9A936A251E1EC700DA5721 /* LockdownFirewallWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 7C9A9384251E1F9C00DA5721 /* LoadingCircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C9A9383251E1F9C00DA5721 /* LoadingCircle.swift */; }; + 7CAB283F254336230087AAF4 /* CustomNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CAB283E254336230087AAF4 /* CustomNavigationView.swift */; }; + 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 */; }; + 7CE91C992521ED5E009D8269 /* VPNRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C952521ED5E009D8269 /* VPNRegion.swift */; }; + 7CE91C9A2521ED5E009D8269 /* VPNRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91C952521ED5E009D8269 /* VPNRegion.swift */; }; + 7CE91CA8252214C9009D8269 /* CombinedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CE91CA7252214C9009D8269 /* CombinedProvider.swift */; }; + 7F5F41C69EE479F38F028B45 /* Pods_Lockdown_Firewall_Widget.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 171F1A80C5E933902B1708CE /* Pods_Lockdown_Firewall_Widget.framework */; }; + 90728B81560C790FD5A02A6B /* Pods-Lockdown VPN Widget-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 92D3DD81205F17D004056D79 /* Pods-Lockdown VPN Widget-metadata.plist */; }; + 92635DD62BF2764E0044673D /* Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92635DD52BF2764E0044673D /* Availability.swift */; }; + 92635DD72BF2764E0044673D /* Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92635DD52BF2764E0044673D /* Availability.swift */; }; + 92635DD82BF2764E0044673D /* Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92635DD52BF2764E0044673D /* Availability.swift */; }; + 92635DDA2BF2764E0044673D /* Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92635DD52BF2764E0044673D /* Availability.swift */; }; + 92635DDC2BF2764E0044673D /* Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92635DD52BF2764E0044673D /* Availability.swift */; }; + 92635DDF2BF2780A0044673D /* Availability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92635DD52BF2764E0044673D /* Availability.swift */; }; + 92635DE12BF784FF0044673D /* BlockerPrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 92635DE02BF784FF0044673D /* BlockerPrivacyInfo.xcprivacy */; }; + 92635DE32BF786330044673D /* FireWallPrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 92635DE22BF786330044673D /* FireWallPrivacyInfo.xcprivacy */; }; + 92635DE52BF7869C0044673D /* TunnelPrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 92635DE42BF7869C0044673D /* TunnelPrivacyInfo.xcprivacy */; }; + 92635DE82BF787220044673D /* WidgetExtensionPrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 92635DE72BF787220044673D /* WidgetExtensionPrivacyInfo.xcprivacy */; }; + 92635DEA2BF788E50044673D /* FireWallWidgetPrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 92635DE92BF788E50044673D /* FireWallWidgetPrivacyInfo.xcprivacy */; }; + 92635DEC2BF78A9F0044673D /* VPNVidgetPrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 92635DEB2BF78A9F0044673D /* VPNVidgetPrivacyInfo.xcprivacy */; }; + 92CCC17B2BEE40C900C38E1C /* ProductButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92CCC17A2BEE40C900C38E1C /* ProductButton.swift */; }; + 92CCC17D2BF22ABE00C38E1C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 92CCC17C2BF22ABE00C38E1C /* PrivacyInfo.xcprivacy */; }; A101106D202B9D4300807612 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A101106C202B9D4300807612 /* BaseViewController.swift */; }; A1141A151F46230500F54698 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1141A141F46230500F54698 /* AppDelegate.swift */; }; A1141A1A1F46230500F54698 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1141A181F46230500F54698 /* Main.storyboard */; }; @@ -128,27 +364,14 @@ 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 */; }; 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 */; }; A12473FA1FE44285008493B8 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A12473F81FE44285008493B8 /* MainInterface.storyboard */; }; A12473FE1FE44285008493B8 /* Lockdown VPN Widget.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A12473F31FE44284008493B8 /* Lockdown VPN Widget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - A1342E8C20B0B87D0045E9DF /* CocoaLumberjackSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E2206D982B0060D945 /* CocoaLumberjackSwift.framework */; }; - A1342E8D20B0B8870045E9DF /* CocoaLumberjack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E1206D982B0060D945 /* CocoaLumberjack.framework */; }; A1359FDA20AF6E32008C4BF7 /* LocalLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1359FD920AF6E31008C4BF7 /* LocalLogger.swift */; }; A154A07E215C78180010FFCC /* BlockListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154A07D215C78180010FFCC /* BlockListCell.swift */; }; A154A080215C7A8C0010FFCC /* BlockListAddCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A154A07F215C7A8C0010FFCC /* BlockListAddCell.swift */; }; - A15939C0206D965D0060D945 /* tun2socks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939B9206D965C0060D945 /* tun2socks.framework */; }; - A15939C1206D965D0060D945 /* lwip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BA206D965D0060D945 /* lwip.framework */; }; - A15939C2206D965D0060D945 /* MMDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BB206D965D0060D945 /* MMDB.framework */; }; - A15939C3206D965D0060D945 /* NEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BC206D965D0060D945 /* NEKit.framework */; }; - A15939C4206D965D0060D945 /* Resolver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BD206D965D0060D945 /* Resolver.framework */; }; - A15939C5206D965D0060D945 /* Yaml.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BE206D965D0060D945 /* Yaml.framework */; }; - A15939C6206D965D0060D945 /* Sodium.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BF206D965D0060D945 /* Sodium.framework */; }; - A15939E3206D982B0060D945 /* CocoaAsyncSocket.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E0206D982B0060D945 /* CocoaAsyncSocket.framework */; }; - A15939E4206D982B0060D945 /* CocoaLumberjack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E1206D982B0060D945 /* CocoaLumberjack.framework */; }; - A15939E5206D982B0060D945 /* CocoaLumberjackSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E2206D982B0060D945 /* CocoaLumberjackSwift.framework */; }; A15F3C751F79DC8F00B07F03 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A15F3C731F79D90500B07F03 /* LaunchScreen.storyboard */; }; A174CCAE22B15B1000F1B840 /* BlockListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A174CCAD22B15B1000F1B840 /* BlockListViewController.swift */; }; A18B31F92087ED7900C0FFAA /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1E78D12207BE58C007FAE70 /* CloudKit.framework */; }; @@ -162,9 +385,7 @@ A1DBA18621B77C66008A9322 /* VPNSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18521B77C66008A9322 /* VPNSubscription.swift */; }; A1DBA18A21B77C80008A9322 /* VPNController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18921B77C80008A9322 /* VPNController.swift */; }; A1DBA18B21B77C88008A9322 /* VPNController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18921B77C80008A9322 /* VPNController.swift */; }; - A1DBA18E21B77C8E008A9322 /* VPNSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1DBA18521B77C66008A9322 /* VPNSubscription.swift */; }; A1DBA19621B82F73008A9322 /* LICENSE.md in Resources */ = {isa = PBXBuildFile; fileRef = A1DBA19521B82F72008A9322 /* LICENSE.md */; }; - A1DD82BE1FE446CA00482632 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1141A1B1F46230500F54698 /* Assets.xcassets */; }; A1E7481A1F9108B6004B8021 /* SpeedTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1E748191F9108B6004B8021 /* SpeedTest.swift */; }; A1E78D13207BE58C007FAE70 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1E78D12207BE58C007FAE70 /* CloudKit.framework */; }; A1EBEACB2097AE6E002B9087 /* M13CheckboxDisclosurePathGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1EBEAB82097AE5B002B9087 /* M13CheckboxDisclosurePathGenerator.swift */; }; @@ -188,16 +409,6 @@ A1EBEADD2097AE6E002B9087 /* M13CheckboxStrokeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1EBEACA2097AE6D002B9087 /* M13CheckboxStrokeController.swift */; }; A1FCDA4422C0651300C928BC /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FCDA4322C0651300C928BC /* PacketTunnelProvider.swift */; }; A1FCDA4922C0651300C928BC /* LockdownTunnel.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A1FCDA4122C0651300C928BC /* LockdownTunnel.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - A1FCDA4E22C0666A00C928BC /* CocoaAsyncSocket.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E0206D982B0060D945 /* CocoaAsyncSocket.framework */; }; - A1FCDA4F22C066B900C928BC /* CocoaLumberjack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E1206D982B0060D945 /* CocoaLumberjack.framework */; }; - A1FCDA5022C066B900C928BC /* CocoaLumberjackSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939E2206D982B0060D945 /* CocoaLumberjackSwift.framework */; }; - A1FCDA5122C066B900C928BC /* lwip.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BA206D965D0060D945 /* lwip.framework */; }; - A1FCDA5222C066B900C928BC /* MMDB.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BB206D965D0060D945 /* MMDB.framework */; }; - A1FCDA5322C066B900C928BC /* Resolver.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BD206D965D0060D945 /* Resolver.framework */; }; - A1FCDA5422C066B900C928BC /* Sodium.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BF206D965D0060D945 /* Sodium.framework */; }; - A1FCDA5522C066B900C928BC /* tun2socks.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939B9206D965C0060D945 /* tun2socks.framework */; }; - A1FCDA5622C066B900C928BC /* Yaml.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BE206D965D0060D945 /* Yaml.framework */; }; - A1FCDA5722C066F300C928BC /* NEKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A15939BC206D965D0060D945 /* NEKit.framework */; }; A1FCDA5D22C1301A00C928BC /* BlockListGroupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FCDA5C22C1301900C928BC /* BlockListGroupViewController.swift */; }; A1FCDA5F22C14EB800C928BC /* BlockListGroupCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FCDA5E22C14EB800C928BC /* BlockListGroupCell.swift */; }; A1FCDA6322C7616400C928BC /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1FCDA6222C7616400C928BC /* NetworkExtension.framework */; }; @@ -207,9 +418,79 @@ 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 */; }; - D37198BBCE0A226BD9071F4A /* Pods_LockdownTunnel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A0C2DF90344891424A626067 /* Pods_LockdownTunnel.framework */; }; - E0C51E8814F43CD752AB740D /* Pods_Lockdown.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 31E2DCBA5F0A1C82E81F2D44 /* Pods_Lockdown.framework */; }; - F6529FCC7BC553DB6372DE40 /* Pods_Lockdown_VPN_Widget.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B555BB9C945AD99E970BE3A /* Pods_Lockdown_VPN_Widget.framework */; }; + B1062A2D2A447B2F00FA9E8B /* RadioSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1062A2C2A447B2F00FA9E8B /* RadioSwitcher.swift */; }; + B1062A2F2A448F1700FA9E8B /* SelectableRadioSwitcherWithTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1062A2E2A448F1700FA9E8B /* SelectableRadioSwitcherWithTitle.swift */; }; + B1062A312A449F5200FA9E8B /* TitleAndSubtitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1062A302A449F5200FA9E8B /* TitleAndSubtitleView.swift */; }; + B1062A332A459AC800FA9E8B /* TextViewWithPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1062A322A459AC800FA9E8B /* TextViewWithPlaceholder.swift */; }; + B1062A362A45BD7000FA9E8B /* StepsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1062A352A45BD7000FA9E8B /* StepsViewModel.swift */; }; + B1062A382A45BEBE00FA9E8B /* WhatProblemStepViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1062A372A45BEBE00FA9E8B /* WhatProblemStepViewModel.swift */; }; + B157DE312A56B7F7003BA0AB /* CodableUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = B157DE302A56B7F7003BA0AB /* CodableUserDefaults.swift */; }; + B163A2F72A2F29A100FD7C5E /* CocoaAsyncSocket.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2F62A2F29A000FD7C5E /* CocoaAsyncSocket.xcframework */; }; + B163A2F82A2F29A100FD7C5E /* CocoaAsyncSocket.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2F62A2F29A000FD7C5E /* CocoaAsyncSocket.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B163A2F92A2F29BB00FD7C5E /* CocoaAsyncSocket.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2F62A2F29A000FD7C5E /* CocoaAsyncSocket.xcframework */; }; + B163A2FF2A2F2A1800FD7C5E /* CocoaLumberjack.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2FC2A2F29FC00FD7C5E /* CocoaLumberjack.xcframework */; }; + B163A3002A2F2A1800FD7C5E /* CocoaLumberjack.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2FC2A2F29FC00FD7C5E /* CocoaLumberjack.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B163A3012A2F2A3200FD7C5E /* CocoaLumberjack.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2FC2A2F29FC00FD7C5E /* CocoaLumberjack.xcframework */; }; + B163A3042A2F2A4900FD7C5E /* CocoaLumberjack.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2FC2A2F29FC00FD7C5E /* CocoaLumberjack.xcframework */; }; + B163A3072A2F2A6000FD7C5E /* CocoaLumberjack.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2FC2A2F29FC00FD7C5E /* CocoaLumberjack.xcframework */; }; + B163A30A2A2F2A7300FD7C5E /* CocoaLumberjack.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A2FC2A2F29FC00FD7C5E /* CocoaLumberjack.xcframework */; }; + B163A30E2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A30D2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework */; }; + B163A30F2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B163A30D2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B163A3102A2F2ACB00FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A30D2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework */; }; + B163A3132A2F2AE300FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A30D2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework */; }; + B163A3162A2F2AF600FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A30D2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework */; }; + B163A3192A2F2B0800FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A30D2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework */; }; + B163A31D2A2F2B4000FD7C5E /* lwip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A31C2A2F2B4000FD7C5E /* lwip.xcframework */; }; + B163A31E2A2F2B4000FD7C5E /* lwip.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B163A31C2A2F2B4000FD7C5E /* lwip.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B163A31F2A2F2B5200FD7C5E /* lwip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A31C2A2F2B4000FD7C5E /* lwip.xcframework */; }; + B163A3222A2F2B6800FD7C5E /* lwip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A31C2A2F2B4000FD7C5E /* lwip.xcframework */; }; + B163A3252A2F2B7900FD7C5E /* lwip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A31C2A2F2B4000FD7C5E /* lwip.xcframework */; }; + B163A3282A2F2B9300FD7C5E /* lwip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A31C2A2F2B4000FD7C5E /* lwip.xcframework */; }; + B163A32E2A2F2C0C00FD7C5E /* Resolver.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A32B2A2F2BE700FD7C5E /* Resolver.xcframework */; }; + B163A3322A2F2C7800FD7C5E /* NEKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A3312A2F2C7800FD7C5E /* NEKit.xcframework */; }; + B163A3332A2F2C7900FD7C5E /* NEKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B163A3312A2F2C7800FD7C5E /* NEKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B163A3342A2F2C8B00FD7C5E /* NEKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A3312A2F2C7800FD7C5E /* NEKit.xcframework */; }; + B163A3382A2F2CBC00FD7C5E /* tun2socks.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A3372A2F2CBC00FD7C5E /* tun2socks.xcframework */; }; + B163A3392A2F2CBC00FD7C5E /* tun2socks.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B163A3372A2F2CBC00FD7C5E /* tun2socks.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B163A33A2A2F2CD100FD7C5E /* tun2socks.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A3372A2F2CBC00FD7C5E /* tun2socks.xcframework */; }; + B163A3402A2F498600FD7C5E /* Dnscryptproxy.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A33F2A2F498600FD7C5E /* Dnscryptproxy.xcframework */; }; + B163A3422A2F4AE800FD7C5E /* Dnscryptproxy.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A33F2A2F498600FD7C5E /* Dnscryptproxy.xcframework */; }; + B163A3452A2F4B6B00FD7C5E /* Dnscryptproxy.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A33F2A2F498600FD7C5E /* Dnscryptproxy.xcframework */; }; + B163A3482A2F4B8300FD7C5E /* Dnscryptproxy.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A33F2A2F498600FD7C5E /* Dnscryptproxy.xcframework */; }; + B163A34B2A2F4BBA00FD7C5E /* Dnscryptproxy.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A33F2A2F498600FD7C5E /* Dnscryptproxy.xcframework */; }; + B163A34E2A2F4C2C00FD7C5E /* Resolver.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B163A32B2A2F2BE700FD7C5E /* Resolver.xcframework */; }; + B163A34F2A2F4C2C00FD7C5E /* Resolver.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B163A32B2A2F2BE700FD7C5E /* Resolver.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B17492C72B87424E005D9601 /* PaywallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17492C62B87424E005D9601 /* PaywallViewModel.swift */; }; + B17492C92B8742B6005D9601 /* PaywallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B17492C82B8742B6005D9601 /* PaywallView.swift */; }; + B1A01CA52A4328E1004D43EE /* StepsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A01CA42A4328E1004D43EE /* StepsViewController.swift */; }; + B1A01CA92A432926004D43EE /* StepModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A01CA82A432926004D43EE /* StepModel.swift */; }; + B1A01CAC2A4343F1004D43EE /* StepsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A01CAB2A4343F1004D43EE /* StepsView.swift */; }; + B1BA87012A4C4BC400D141A8 /* QuestionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1BA87002A4C4BC400D141A8 /* QuestionModel.swift */; }; + B1F11C732A45DBF900A137A3 /* DomainListSaveable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C722A45DBF900A137A3 /* DomainListSaveable.swift */; }; + B1F11C7A2A498C5300A137A3 /* QuestionsStepViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C792A498C5300A137A3 /* QuestionsStepViewModel.swift */; }; + B1F11C7C2A498CBF00A137A3 /* BaseStepViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C7B2A498CBF00A137A3 /* BaseStepViewModel.swift */; }; + B1F11C7E2A49AE4000A137A3 /* YesNoRadioSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C7D2A49AE4000A137A3 /* YesNoRadioSwitcherView.swift */; }; + B1F11C802A49E35800A137A3 /* QuestionTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C7F2A49E35800A137A3 /* QuestionTitleView.swift */; }; + B1F11C822A49E63400A137A3 /* NavigationLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C812A49E63400A137A3 /* NavigationLinkView.swift */; }; + B1F11C842A4B029800A137A3 /* SelectCountryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C832A4B029800A137A3 /* SelectCountryViewController.swift */; }; + B1F11C862A4B02EE00A137A3 /* SelectCountryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C852A4B02EE00A137A3 /* SelectCountryViewModel.swift */; }; + B1F11C882A4B033000A137A3 /* Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C872A4B033000A137A3 /* Country.swift */; }; + B1F11C8A2A4B050500A137A3 /* CountryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C892A4B050500A137A3 /* CountryView.swift */; }; + B1F11C8C2A4C273500A137A3 /* SelectRegionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C8B2A4C273500A137A3 /* SelectRegionViewModel.swift */; }; + B1F11C8E2A4C27B800A137A3 /* BaseSelectCountryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1F11C8D2A4C27B800A137A3 /* BaseSelectCountryViewModel.swift */; }; + C9E66BB880A29A48D055FBFF /* Pods-Lockdown-settings-metadata.plist in Resources */ = {isa = PBXBuildFile; fileRef = 50F9BE503587CE4933CB7983 /* Pods-Lockdown-settings-metadata.plist */; }; + E06F1E09E754223BF6E5D372 /* Pods_LockdownTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6F35024B0B1EB324BC94470 /* Pods_LockdownTests.framework */; }; + F01CAB7C2C61106F009C19CF /* SUI+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01CAB7B2C61106F009C19CF /* SUI+Extensions.swift */; }; + F01CAB7E2C61316C009C19CF /* OneTimePaywallModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01CAB7D2C61316C009C19CF /* OneTimePaywallModel.swift */; }; + F09235DA0DB40BE2B52F6D96 /* Pods_Lockdown_VPN_Widget.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95D421359DD51FC306D3B4C9 /* Pods_Lockdown_VPN_Widget.framework */; }; + F0A8E0412C64E977001303C6 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8E0402C64E977001303C6 /* Defaults.swift */; }; + F0A8E0432C64E9A5001303C6 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8E0402C64E977001303C6 /* Defaults.swift */; }; + F0A8E0442C64E9A6001303C6 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8E0402C64E977001303C6 /* Defaults.swift */; }; + F0A8E0452C64E9A6001303C6 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8E0402C64E977001303C6 /* Defaults.swift */; }; + F0A8E0462C64E9A7001303C6 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8E0402C64E977001303C6 /* Defaults.swift */; }; + F0A8E0472C64E9AA001303C6 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A8E0402C64E977001303C6 /* Defaults.swift */; }; + F0B12AF42C60CA63008EF8AA /* PaywallRoundContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B12AF32C60CA63008EF8AA /* PaywallRoundContainer.swift */; }; + F0B12AF82C60D602008EF8AA /* OneTimePaywallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B12AF72C60D602008EF8AA /* OneTimePaywallView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -220,6 +501,20 @@ remoteGlobalIDString = 3DBD57BA22FD727900DE189F; remoteInfo = "Lockdown Firewall Today"; }; + 7C0D111F2473FC7E00A26E04 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A1141A091F46230500F54698 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A1141A101F46230500F54698; + remoteInfo = Lockdown; + }; + 7C9A9375251E1EC700DA5721 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A1141A091F46230500F54698 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7C9A9369251E1EC700DA5721; + remoteInfo = LockdownFirewallWidgetExtension; + }; A118F63520B33F44009A64E7 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = A1141A091F46230500F54698 /* Project object */; @@ -244,13 +539,21 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - A15939D8206D97C40060D945 /* CopyFiles */ = { + 3DD3D09826CC8714002238E8 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - ); + B163A3332A2F2C7900FD7C5E /* NEKit.xcframework in Embed Frameworks */, + B163A3392A2F2CBC00FD7C5E /* tun2socks.xcframework in Embed Frameworks */, + B163A34F2A2F4C2C00FD7C5E /* Resolver.xcframework in Embed Frameworks */, + B163A2F82A2F29A100FD7C5E /* CocoaAsyncSocket.xcframework in Embed Frameworks */, + B163A30F2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework in Embed Frameworks */, + B163A31E2A2F2B4000FD7C5E /* lwip.xcframework in Embed Frameworks */, + B163A3002A2F2A1800FD7C5E /* CocoaLumberjack.xcframework in Embed Frameworks */, + ); + name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; A18B79571F8C36460042A4EF /* Embed App Extensions */ = { @@ -263,6 +566,7 @@ 3DBD57C622FD727900DE189F /* Lockdown Firewall Widget.appex in Embed App Extensions */, A12473FE1FE44285008493B8 /* Lockdown VPN Widget.appex in Embed App Extensions */, A1FCDA4922C0651300C928BC /* LockdownTunnel.appex in Embed App Extensions */, + 7C9A9377251E1EC700DA5721 /* LockdownFirewallWidgetExtension.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -272,11 +576,22 @@ /* Begin PBXFileReference section */ 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 = ""; }; 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 = ""; }; - 31E2DCBA5F0A1C82E81F2D44 /* Pods_Lockdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 171F1A80C5E933902B1708CE /* Pods_Lockdown_Firewall_Widget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown_Firewall_Widget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 173D3911239ED434E2139981 /* 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 = ""; }; + 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 = ""; }; + 2C5C889B3C3F01CB4B730A22 /* 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 = ""; }; + 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 = ""; }; + 383AB71E3748C73C2FE45B7D /* 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 = ""; }; + 3D01D97A2480DBED003A710C /* data_trackers.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = data_trackers.txt; sourceTree = ""; }; + 3D01D99C2481E241003A710C /* general_ads.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = general_ads.txt; sourceTree = ""; }; + 3D01D99D2481E252003A710C /* reporting.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = reporting.txt; sourceTree = ""; }; 3D0711B722FE79BE00391C6E /* WhyTrustViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhyTrustViewController.swift; sourceTree = ""; }; 3D0711BA22FE7B5100391C6E /* TitleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleViewController.swift; sourceTree = ""; }; 3D0971D722EBAD1000CCD326 /* facebook_sdk.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = facebook_sdk.txt; sourceTree = ""; }; 3D0971D922EBAD4C00CCD326 /* marketing.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = marketing.txt; sourceTree = ""; }; + 3D3BF4CF233D5E9100D0C482 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 3D40826227F675F6004C146B /* dnscrypt-proxy.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "dnscrypt-proxy.toml"; sourceTree = ""; }; + 3D40826827F6A03F004C146B /* DNSCryptThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSCryptThread.swift; sourceTree = ""; }; 3D44377A22DFB22600908CDC /* Montserrat-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-Medium.ttf"; sourceTree = ""; }; 3D44377B22DFB22600908CDC /* Montserrat-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-Light.ttf"; sourceTree = ""; }; 3D44377C22DFB22600908CDC /* Montserrat-Thin.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Montserrat-Thin.ttf"; sourceTree = ""; }; @@ -323,15 +638,30 @@ 3D47CDAC22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScalePulseOutRapid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NVActivityIndicatorAnimationLineScalePulseOutRapid.swift; sourceTree = ""; }; 3D47CDAD22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulseRise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NVActivityIndicatorAnimationBallPulseRise.swift; sourceTree = ""; }; 3D47CDAE22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationOrbit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NVActivityIndicatorAnimationOrbit.swift; sourceTree = ""; }; + 3D4D7FEB247F22AE000369FD /* google_shopping_ads.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = google_shopping_ads.txt; sourceTree = ""; }; 3D5464D223037CCA00AE1F73 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; 3D5561D3230B58F30062001D /* PrivacyPolicyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyViewController.swift; sourceTree = ""; }; + 3D5F5A0723107C1E004C3860 /* game_ads.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = game_ads.txt; sourceTree = ""; }; + 3D5F5A0923107EB8004C3860 /* snapchat_analytics.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = snapchat_analytics.txt; sourceTree = ""; }; + 3D5F5A0B23109ABB004C3860 /* WhatIsVpnViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatIsVpnViewController.swift; sourceTree = ""; }; + 3D752C302357FA3B00C163E4 /* SF-Pro-Rounded-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Rounded-Regular.otf"; sourceTree = ""; }; + 3D752C312357FA3B00C163E4 /* SF-Pro-Rounded-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Rounded-Medium.otf"; sourceTree = ""; }; + 3D752C322357FA3B00C163E4 /* SF-Pro-Rounded-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Rounded-Bold.otf"; sourceTree = ""; }; + 3D752C332357FA3B00C163E4 /* SF-Pro-Rounded-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Rounded-Semibold.otf"; sourceTree = ""; }; + 3D89610D253527B1006D8C12 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = ""; }; + 3D896110253527B2006D8C12 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 3D94AB0322FDEDEB0012B0DE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 3D94AB1122FE3A460012B0DE /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 3D970DAC22EC149D00F9CC93 /* BlockLogCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockLogCell.swift; sourceTree = ""; }; 3D970DAE22EC15D800F9CC93 /* BlockLogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockLogViewController.swift; sourceTree = ""; }; + 3D9FC67623E503DF004122D3 /* EmailSignInViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignInViewController.swift; sourceTree = ""; }; + 3D9FC67823E521DE004122D3 /* ForgotPasswordViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForgotPasswordViewController.swift; sourceTree = ""; }; + 3DA14D34255DF56E00A3658E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Main.strings; sourceTree = ""; }; + 3DA14D3C255DF5CF00A3658E /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainInterface.strings; sourceTree = ""; }; + 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 = ""; }; - 3DABD9FE22F7AD4D00480AAC /* FirewallUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallUtilities.swift; 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 = ""; }; @@ -348,32 +678,189 @@ 3DCA4F3022F190AE0017740D /* ClientModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientModels.swift; sourceTree = ""; }; 3DCA4F3222F22CB40017740D /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 3DCA4F4022F252720017740D /* FirewallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirewallController.swift; sourceTree = ""; }; - 428B4B342E5EA9720C08F150 /* Pods-Today.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Today.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Today/Pods-Today.debug.xcconfig"; sourceTree = ""; }; - 4C50BEAA399D6FDF2C2672C6 /* Pods-Confirmed VPN.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Confirmed VPN.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Confirmed VPN/Pods-Confirmed VPN.debug.xcconfig"; sourceTree = ""; }; - 4D422CB6539443825E5CD91B /* Pods-Confirmed Tunnels.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Confirmed Tunnels.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Confirmed Tunnels/Pods-Confirmed Tunnels.debug.xcconfig"; sourceTree = ""; }; + 3DCBC8FF25425AB200446C98 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + 3DCBC90025425AB200446C98 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Main.strings; sourceTree = ""; }; + 3DCBC90125425AB200446C98 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/MainInterface.strings; sourceTree = ""; }; + 3DCBC90225425AB200446C98 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/MainInterface.strings; sourceTree = ""; }; + 3DCBC90925425BC900446C98 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Main.strings; sourceTree = ""; }; + 3DCBC90A25425BC900446C98 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 3DCBC90B25425BC900446C98 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainInterface.strings; sourceTree = ""; }; + 3DCBC90C25425BC900446C98 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/MainInterface.strings; sourceTree = ""; }; + 3DCFE6F924493F9000EA9B35 /* marketing_beta.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = marketing_beta.txt; sourceTree = ""; }; + 3DD545CD280681AA005E140C /* libresolv.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.9.tbd; path = usr/lib/libresolv.9.tbd; sourceTree = SDKROOT; }; + 3DD545D428068233005E140C /* LockdownTunnelBridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LockdownTunnelBridgingHeader.h; sourceTree = ""; }; + 3DD545DA2808C2F6005E140C /* 5000_dummy_list.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = 5000_dummy_list.txt; sourceTree = ""; }; + 3DE443FA25353453006DF67D /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/MainInterface.strings; sourceTree = ""; }; + 3DE443FE253534C7006DF67D /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/MainInterface.strings; sourceTree = ""; }; + 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 = ""; }; + 40098E2929FDA6A800886474 /* BulletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletView.swift; sourceTree = ""; }; + 40098E2B29FDA6CC00886474 /* PaywallDescriptionLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallDescriptionLabel.swift; sourceTree = ""; }; + 40098E2D29FDA6E500886474 /* PlanView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlanView.swift; sourceTree = ""; }; + 40098E3029FDA73200886474 /* VPNPaywallViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNPaywallViewController.swift; sourceTree = ""; }; + 40098E3529FF378F00886474 /* FirewallPaywallViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FirewallPaywallViewController.swift; sourceTree = ""; }; + 40098E3829FF378F00886474 /* AnnualPlanView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnnualPlanView.swift; sourceTree = ""; }; + 40098E3929FF378F00886474 /* MonthlyPlanView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonthlyPlanView.swift; sourceTree = ""; }; + 40098E3A29FF378F00886474 /* AdvancedPlansViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvancedPlansViews.swift; sourceTree = ""; }; + 4015B4F629EFD9AC004102E0 /* AccessLevelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessLevelView.swift; sourceTree = ""; }; + 4015B4FC29F00DD8004102E0 /* LDVpnViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDVpnViewController.swift; sourceTree = ""; }; + 4015B4FE29F14C95004102E0 /* LDCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDCardView.swift; sourceTree = ""; }; + 4015B50229F16E1A004102E0 /* LDConfigurationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDConfigurationViewController.swift; sourceTree = ""; }; + 402BAD242A0B675B009B8820 /* LockedListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedListsView.swift; sourceTree = ""; }; + 402BAD352A0CD37C009B8820 /* ConnectivityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityService.swift; sourceTree = ""; }; + 402BAD372A0CD3B0009B8820 /* ConnectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionState.swift; sourceTree = ""; }; + 402D24B729D59B4400A5AB60 /* EmptyListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyListsView.swift; sourceTree = ""; }; + 402D24CA29D87B5A00A5AB60 /* ListsSubmenuView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListsSubmenuView.swift; sourceTree = ""; }; + 402D24D329D87F4500A5AB60 /* CustomBlockedTableHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomBlockedTableHeader.swift; sourceTree = ""; }; + 402D251529E514CF00A5AB60 /* MoveToListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MoveToListViewController.swift; sourceTree = ""; }; + 402D251829E517E100A5AB60 /* ConfiguredNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfiguredNavigationView.swift; sourceTree = ""; }; + 402D251A29E519B500A5AB60 /* CustomTableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTableView.swift; sourceTree = ""; }; + 402D251E29E52D6A00A5AB60 /* EditDomainsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditDomainsViewController.swift; sourceTree = ""; }; + 402D252029E52D7600A5AB60 /* BottomMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomMenu.swift; sourceTree = ""; }; + 402D252229E5473E00A5AB60 /* EditDomainsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditDomainsCell.swift; sourceTree = ""; }; + 402D252629E5843300A5AB60 /* ImportBlockListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportBlockListViewController.swift; sourceTree = ""; }; + 402D252829E632F300A5AB60 /* ListSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListSettingsViewController.swift; sourceTree = ""; }; + 402D252A29E6335100A5AB60 /* SwitchBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchBlockingView.swift; sourceTree = ""; }; + 402D252C29E6346900A5AB60 /* ListBlockedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBlockedTableViewCell.swift; sourceTree = ""; }; + 402D252E29E6357700A5AB60 /* ListDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDetailViewController.swift; sourceTree = ""; }; + 402D253029E635CB00A5AB60 /* DomainsBlockedTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainsBlockedTableViewCell.swift; sourceTree = ""; }; + 402D253229E6588000A5AB60 /* ListDescriptionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListDescriptionViewController.swift; sourceTree = ""; }; + 402D253A29E8F9A400A5AB60 /* JSONSerialization+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONSerialization+Extensions.swift"; sourceTree = ""; }; + 402D254729EE112E00A5AB60 /* LDFirewallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LDFirewallViewController.swift; sourceTree = ""; }; + 402D254929EE1C6E00A5AB60 /* DescriptionLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionLabel.swift; sourceTree = ""; }; + 402D254D29EE598D00A5AB60 /* TrackersGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackersGroupView.swift; sourceTree = ""; }; + 402D254F29EE78D600A5AB60 /* OverallStatiscticView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverallStatiscticView.swift; sourceTree = ""; }; + 408E7A9329F88C9200B2F587 /* CustomUISwitch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomUISwitch.swift; sourceTree = ""; }; + 408E7A9629FA698C00B2F587 /* UIVIew+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIVIew+Extensions.swift"; sourceTree = ""; }; + 40960AE12A029A7D000F82EB /* UIApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; + 40960AE82A033514000F82EB /* AccessLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessLevel.swift; sourceTree = ""; }; + 40960AEA2A03396F000F82EB /* ProductPurchasable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPurchasable.swift; sourceTree = ""; }; + 40960AEC2A0339E2000F82EB /* UserService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = ""; }; + 40960AEF2A033A41000F82EB /* LockdownUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockdownUser.swift; sourceTree = ""; }; + 40960AF12A033AF9000F82EB /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; + 40960AFA2A033E46000F82EB /* PaywallService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallService.swift; sourceTree = ""; }; + 40960B022A033EA9000F82EB /* CountdownDisplayService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownDisplayService.swift; sourceTree = ""; }; + 40960B062A033F0B000F82EB /* WeakObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakObject.swift; sourceTree = ""; }; + 40960B092A03400E000F82EB /* UserDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefault.swift; sourceTree = ""; }; + 40960B0C2A034054000F82EB /* LockdownStorageIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockdownStorageIdentifier.swift; sourceTree = ""; }; + 40960B142A034400000F82EB /* Keychainable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychainable.swift; sourceTree = ""; }; + 409B59DF2A14CB7B0010242C /* SignUpViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpViewController.swift; sourceTree = ""; }; + 409B59E32A15CC900010242C /* WelcomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; + 409B59E52A15D00C0010242C /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; + 40CC816D2A14B25C00F9805E /* DeleteMyAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteMyAccountViewController.swift; sourceTree = ""; }; + 40CC816F2A14B29100F9805E /* EmailComposable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailComposable.swift; sourceTree = ""; }; + 40CC81782A14BA8800F9805E /* EmailAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailAddress.swift; sourceTree = ""; }; + 40CC817A2A14BAA600F9805E /* EmailValidatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmailValidatable.swift; sourceTree = ""; }; + 40CC817D2A14BB3600F9805E /* UIView+Corners.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Corners.swift"; sourceTree = ""; }; + 40CC81812A14BD2100F9805E /* DeleteMyAccountViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DeleteMyAccountViewController.xib; sourceTree = ""; }; + 40CC849D2A14BEA000F9805E /* SignUpViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SignUpViewController.xib; sourceTree = ""; }; + 40CC84A02A14BECF00F9805E /* EnableNotificationsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnableNotificationsViewController.swift; sourceTree = ""; }; + 40CC84A22A14BED800F9805E /* EnableNotificationsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EnableNotificationsViewController.xib; sourceTree = ""; }; + 40CC84A42A14BEFA00F9805E /* SplashScreenViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SplashScreenViewController.xib; sourceTree = ""; }; + 40CC84A72A14C09600F9805E /* String+URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+URL.swift"; sourceTree = ""; }; + 40CC84A92A14C0A200F9805E /* String+Attributed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Attributed.swift"; sourceTree = ""; }; + 40CC84AB2A14C0D700F9805E /* Date+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Ext.swift"; sourceTree = ""; }; + 40CC84AC2A14C0D800F9805E /* CALayer+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CALayer+Ext.swift"; sourceTree = ""; }; + 40CC84AD2A14C0D800F9805E /* UIStackView+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStackView+Ext.swift"; sourceTree = ""; }; + 40CC84AE2A14C0D800F9805E /* UIAppearance+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAppearance+Ext.swift"; sourceTree = ""; }; + 40CC84B32A14C0E900F9805E /* UIViewController+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+Ext.swift"; sourceTree = ""; }; + 40CC84B42A14C0E900F9805E /* Font+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Font+Ext.swift"; sourceTree = ""; }; + 40CC84B52A14C0EA00F9805E /* UICollectionView+Dequeue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UICollectionView+Dequeue.swift"; sourceTree = ""; }; + 40CC84B62A14C0EA00F9805E /* NibLoadable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NibLoadable.swift; sourceTree = ""; }; + 40CC84B72A14C0EA00F9805E /* UIView+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Ext.swift"; sourceTree = ""; }; + 40CC84BD2A14C15400F9805E /* LockdownGradient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockdownGradient.swift; sourceTree = ""; }; + 40CC84C12A14C2B700F9805E /* FloatingTextInputTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingTextInputTextField.swift; sourceTree = ""; }; + 40CC84C22A14C2B800F9805E /* TextBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextBox.swift; sourceTree = ""; }; + 40CC84C32A14C2B800F9805E /* TextInputState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextInputState.swift; sourceTree = ""; }; + 40CC84C42A14C2B800F9805E /* TextBoxLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextBoxLabel.swift; sourceTree = ""; }; + 40E04A212A26708200000E8C /* WhatsNewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewViewController.swift; sourceTree = ""; }; + 40E04A232A26758200000E8C /* WhatsNewDescriptionLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewDescriptionLabel.swift; sourceTree = ""; }; + 40E04A522A29D79100000E8C /* BlockListContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListContainerViewController.swift; sourceTree = ""; }; + 40E04A542A29D7AB00000E8C /* CuratedListsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CuratedListsViewController.swift; sourceTree = ""; }; + 40E04A562A29D7BC00000E8C /* CustomListsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomListsViewController.swift; sourceTree = ""; }; + 40E04A582A2A1B4C00000E8C /* CTAView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CTAView.swift; sourceTree = ""; }; + 40E7A2EC2A0CE8AE00E0231A /* advanced_gaming.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = advanced_gaming.txt; sourceTree = ""; }; + 40E7A2F32A0CE92800E0231A /* ifunny_trackers.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ifunny_trackers.txt; sourceTree = ""; }; + 40E7A2F42A0CE92800E0231A /* junes_journey_trackers.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = junes_journey_trackers.txt; sourceTree = ""; }; + 40E7A2F52A0CE92800E0231A /* scams.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = scams.txt; sourceTree = ""; }; + 40E7A2F62A0CE92800E0231A /* tiktok_trackers.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = tiktok_trackers.txt; sourceTree = ""; }; + 40E7A2F72A0CE92900E0231A /* advanced_analytics.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = advanced_analytics.txt; sourceTree = ""; }; + 40E7A3002A0E1C7A00E0231A /* SplashScreenViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewController.swift; sourceTree = ""; }; + 40FC414229F74C7900BD7396 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + 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 = ""; }; - 65F695578DAA62084B36A513 /* Pods-Lockdown Tunnels.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown Tunnels.release.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown Tunnels/Pods-Lockdown Tunnels.release.xcconfig"; sourceTree = ""; }; - 6A890BF9C9CF89A7E923EDDA /* Pods_Lockdown_Firewall_Widget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown_Firewall_Widget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 726E8CFC747C13F896CA72B6 /* 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 = ""; }; - 7B555BB9C945AD99E970BE3A /* Pods_Lockdown_VPN_Widget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown_VPN_Widget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7CDB1F5AC85EB2D826BB00C2 /* 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 = ""; }; - 7E013E3207564A64E3A1BD49 /* Pods-Lockdown Tunnels.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Lockdown Tunnels.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Lockdown Tunnels/Pods-Lockdown Tunnels.debug.xcconfig"; sourceTree = ""; }; - 8827B5DAD4A819CDC5115562 /* 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 = ""; }; + 51006F182CBD0E7400F5142C /* ImageBannerWithTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBannerWithTitleView.swift; sourceTree = ""; }; + 510AA2F52CB8222100E53560 /* FeedbackPaywallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPaywallViewModel.swift; sourceTree = ""; }; + 5117DE872CBD4A6600C4A61B /* SectionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionTitleView.swift; sourceTree = ""; }; + 5145A1992CBE37C40074C562 /* FeedbackFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackFlow.swift; sourceTree = ""; }; + 51DD89E42CB7DA770028B4FE /* FeedbackPaywallViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPaywallViewController.swift; sourceTree = ""; }; + 5E13011C2D5E012E003896BD /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + 5E9AF72F2CF5D18F001239A0 /* SpecialOfferPaywallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialOfferPaywallView.swift; sourceTree = ""; }; + 5E9BD5852D787D8400E8DE4F /* ReviewAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewAlertManager.swift; sourceTree = ""; }; + 5EA97B2C2CF5F83D0082D3FD /* KumbhSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "KumbhSans-Bold.ttf"; sourceTree = ""; }; + 5EA97B2E2CF5F83D0082D3FD /* KumbhSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "KumbhSans-Regular.ttf"; sourceTree = ""; }; + 5EA97B352CF5F86A0082D3FD /* Juana-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Juana-SemiBold.ttf"; sourceTree = ""; }; + 5EA97B382CF604D60082D3FD /* SpecialOfferPaywallModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpecialOfferPaywallModel.swift; sourceTree = ""; }; + 66424506768B2196F870B04C /* 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 = ""; }; + 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 = ""; }; + 71D50056A5E2E1F6486369F9 /* 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 = ""; }; + 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 = ""; }; + 7C422EA425279724007F9C22 /* Align.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Align.swift; sourceTree = ""; }; + 7C422EAE252797A6007F9C22 /* AccountVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountVC.swift; sourceTree = ""; }; + 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; }; + 7C9A936D251E1EC700DA5721 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 7C9A9370251E1EC700DA5721 /* LockdownFirewallWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockdownFirewallWidget.swift; sourceTree = ""; }; + 7C9A9372251E1EC700DA5721 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 7C9A9374251E1EC700DA5721 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7C9A9383251E1F9C00DA5721 /* LoadingCircle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingCircle.swift; sourceTree = ""; }; + 7CAB283E254336230087AAF4 /* CustomNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNavigationView.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + 92635DD52BF2764E0044673D /* Availability.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Availability.swift; sourceTree = ""; }; + 92635DE02BF784FF0044673D /* BlockerPrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = BlockerPrivacyInfo.xcprivacy; sourceTree = ""; }; + 92635DE22BF786330044673D /* FireWallPrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = FireWallPrivacyInfo.xcprivacy; sourceTree = ""; }; + 92635DE42BF7869C0044673D /* TunnelPrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = TunnelPrivacyInfo.xcprivacy; sourceTree = ""; }; + 92635DE72BF787220044673D /* WidgetExtensionPrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = WidgetExtensionPrivacyInfo.xcprivacy; sourceTree = ""; }; + 92635DE92BF788E50044673D /* FireWallWidgetPrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = FireWallWidgetPrivacyInfo.xcprivacy; sourceTree = ""; }; + 92635DEB2BF78A9F0044673D /* VPNVidgetPrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = VPNVidgetPrivacyInfo.xcprivacy; sourceTree = ""; }; + 92CCC17A2BEE40C900C38E1C /* ProductButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductButton.swift; sourceTree = ""; }; + 92CCC17C2BF22ABE00C38E1C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; 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 = ""; }; - 953709B6B9D85B15EF763F5B /* 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 = ""; }; - 96179E9445306C33ADBDDFAB /* Pods-Today.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Today.release.xcconfig"; path = "Pods/Target Support Files/Pods-Today/Pods-Today.release.xcconfig"; sourceTree = ""; }; - A0C2DF90344891424A626067 /* Pods_LockdownTunnel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LockdownTunnel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 95D421359DD51FC306D3B4C9 /* Pods_Lockdown_VPN_Widget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown_VPN_Widget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 982620F84D937F22C4824017 /* 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 = ""; }; 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 = ""; }; A1141A191F46230500F54698 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; A1141A1B1F46230500F54698 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A1141A201F46230500F54698 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A1141A291F46230600F54698 /* ConfirmediOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmediOSTests.swift; sourceTree = ""; }; - A1141A2B1F46230600F54698 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A1141A341F46230600F54698 /* ConfirmediOSUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmediOSUITests.swift; sourceTree = ""; }; - A1141A361F46230600F54698 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A1159FC8207C201900DA4670 /* socialBlockList.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = socialBlockList.json; sourceTree = ""; }; A1159FC9207C201A00DA4670 /* privacyBlockList.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = privacyBlockList.json; sourceTree = ""; }; A118F63C20B33FED009A64E7 /* TransitionSubmitButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionSubmitButton.swift; sourceTree = ""; }; @@ -381,7 +868,6 @@ 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 = ""; }; 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 = ""; }; @@ -391,16 +877,6 @@ A1359FD920AF6E31008C4BF7 /* LocalLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalLogger.swift; sourceTree = ""; }; A154A07D215C78180010FFCC /* BlockListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListCell.swift; sourceTree = ""; }; A154A07F215C7A8C0010FFCC /* BlockListAddCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListAddCell.swift; sourceTree = ""; }; - A15939B9206D965C0060D945 /* tun2socks.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = tun2socks.framework; path = Carthage/Build/iOS/tun2socks.framework; sourceTree = ""; }; - A15939BA206D965D0060D945 /* lwip.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = lwip.framework; path = Carthage/Build/iOS/lwip.framework; sourceTree = ""; }; - A15939BB206D965D0060D945 /* MMDB.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MMDB.framework; path = Carthage/Build/iOS/MMDB.framework; sourceTree = ""; }; - A15939BC206D965D0060D945 /* NEKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NEKit.framework; path = Carthage/Build/iOS/NEKit.framework; sourceTree = ""; }; - A15939BD206D965D0060D945 /* Resolver.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Resolver.framework; path = Carthage/Build/iOS/Resolver.framework; sourceTree = ""; }; - A15939BE206D965D0060D945 /* Yaml.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Yaml.framework; path = Carthage/Build/iOS/Yaml.framework; sourceTree = ""; }; - A15939BF206D965D0060D945 /* Sodium.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sodium.framework; path = Carthage/Build/iOS/Sodium.framework; sourceTree = ""; }; - A15939E0206D982B0060D945 /* CocoaAsyncSocket.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaAsyncSocket.framework; path = Carthage/Build/iOS/CocoaAsyncSocket.framework; sourceTree = ""; }; - A15939E1206D982B0060D945 /* CocoaLumberjack.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaLumberjack.framework; path = Carthage/Build/iOS/CocoaLumberjack.framework; sourceTree = ""; }; - A15939E2206D982B0060D945 /* CocoaLumberjackSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CocoaLumberjackSwift.framework; path = Carthage/Build/iOS/CocoaLumberjackSwift.framework; sourceTree = ""; }; A15F3C741F79D90500B07F03 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; A174CCAD22B15B1000F1B840 /* BlockListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListViewController.swift; sourceTree = ""; }; A1912FE91F58B2D00007F6D4 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; @@ -409,6 +885,7 @@ A1931CFF20791F5900E695EB /* ContentBlockerRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBlockerRequestHandler.swift; sourceTree = ""; }; A1931D0120791F5900E695EB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; A1931D0820791F6100E695EB /* Lockdown Blocker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Lockdown Blocker.entitlements"; sourceTree = ""; }; + A19DA148E491FF88E4B0B408 /* Pods-Lockdown VPN Widget-settings-metadata.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "Pods-Lockdown VPN Widget-settings-metadata.plist"; path = "Settings.bundle/Pods-Lockdown VPN Widget-settings-metadata.plist"; sourceTree = ""; }; A1D85F06207C4C8300B766E0 /* adBlockListTwo.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = adBlockListTwo.json; sourceTree = ""; }; A1D85F08207C52A000B766E0 /* adBlockListThree.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = adBlockListThree.json; sourceTree = ""; }; A1DBA18521B77C66008A9322 /* VPNSubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VPNSubscription.swift; path = LockdowniOS/VPNSubscription.swift; sourceTree = ""; }; @@ -450,15 +927,54 @@ 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 = ""; }; - A6822C9110BC5F2F96454261 /* 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 = ""; }; 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 = ""; }; - AC978E3E9830282F26277011 /* Pods-Confirmed Tunnels.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Confirmed Tunnels.release.xcconfig"; path = "Pods/Target Support Files/Pods-Confirmed Tunnels/Pods-Confirmed Tunnels.release.xcconfig"; sourceTree = ""; }; + AF8AFE376E3ABA6AFC1C0A48 /* 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 = ""; }; + B1062A2C2A447B2F00FA9E8B /* RadioSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioSwitcher.swift; sourceTree = ""; }; + B1062A2E2A448F1700FA9E8B /* SelectableRadioSwitcherWithTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableRadioSwitcherWithTitle.swift; sourceTree = ""; }; + B1062A302A449F5200FA9E8B /* TitleAndSubtitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleAndSubtitleView.swift; sourceTree = ""; }; + B1062A322A459AC800FA9E8B /* TextViewWithPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWithPlaceholder.swift; sourceTree = ""; }; + B1062A352A45BD7000FA9E8B /* StepsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsViewModel.swift; sourceTree = ""; }; + B1062A372A45BEBE00FA9E8B /* WhatProblemStepViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatProblemStepViewModel.swift; sourceTree = ""; }; + B157DE302A56B7F7003BA0AB /* CodableUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableUserDefaults.swift; sourceTree = ""; }; + B163A2F62A2F29A000FD7C5E /* CocoaAsyncSocket.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CocoaAsyncSocket.xcframework; path = ThirdPartyFrameworks/CocoaAsyncSocket.xcframework; sourceTree = ""; }; + B163A2FC2A2F29FC00FD7C5E /* CocoaLumberjack.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CocoaLumberjack.xcframework; path = ThirdPartyFrameworks/CocoaLumberjack.xcframework; sourceTree = ""; }; + B163A30D2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CocoaLumberjackSwift.xcframework; path = ThirdPartyFrameworks/CocoaLumberjackSwift.xcframework; sourceTree = ""; }; + B163A31C2A2F2B4000FD7C5E /* lwip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = lwip.xcframework; path = ThirdPartyFrameworks/lwip.xcframework; sourceTree = ""; }; + B163A32B2A2F2BE700FD7C5E /* Resolver.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Resolver.xcframework; path = ThirdPartyFrameworks/Resolver.xcframework; sourceTree = ""; }; + B163A3312A2F2C7800FD7C5E /* NEKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = NEKit.xcframework; path = ThirdPartyFrameworks/NEKit.xcframework; sourceTree = ""; }; + B163A3372A2F2CBC00FD7C5E /* tun2socks.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = tun2socks.xcframework; path = ThirdPartyFrameworks/tun2socks.xcframework; sourceTree = ""; }; + B163A33F2A2F498600FD7C5E /* Dnscryptproxy.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = Dnscryptproxy.xcframework; sourceTree = ""; }; + B17492C62B87424E005D9601 /* PaywallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallViewModel.swift; sourceTree = ""; }; + B17492C82B8742B6005D9601 /* PaywallView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaywallView.swift; sourceTree = ""; }; + B1A01CA42A4328E1004D43EE /* StepsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsViewController.swift; sourceTree = ""; }; + B1A01CA82A432926004D43EE /* StepModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepModel.swift; sourceTree = ""; }; + B1A01CAB2A4343F1004D43EE /* StepsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepsView.swift; sourceTree = ""; }; + B1BA87002A4C4BC400D141A8 /* QuestionModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuestionModel.swift; sourceTree = ""; }; + B1F11C722A45DBF900A137A3 /* DomainListSaveable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListSaveable.swift; sourceTree = ""; }; + B1F11C792A498C5300A137A3 /* QuestionsStepViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionsStepViewModel.swift; sourceTree = ""; }; + B1F11C7B2A498CBF00A137A3 /* BaseStepViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseStepViewModel.swift; sourceTree = ""; }; + B1F11C7D2A49AE4000A137A3 /* YesNoRadioSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YesNoRadioSwitcherView.swift; sourceTree = ""; }; + B1F11C7F2A49E35800A137A3 /* QuestionTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionTitleView.swift; sourceTree = ""; }; + B1F11C812A49E63400A137A3 /* NavigationLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationLinkView.swift; sourceTree = ""; }; + B1F11C832A4B029800A137A3 /* SelectCountryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryViewController.swift; sourceTree = ""; }; + B1F11C852A4B02EE00A137A3 /* SelectCountryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectCountryViewModel.swift; sourceTree = ""; }; + B1F11C872A4B033000A137A3 /* Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Country.swift; sourceTree = ""; }; + B1F11C892A4B050500A137A3 /* CountryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryView.swift; sourceTree = ""; }; + B1F11C8B2A4C273500A137A3 /* SelectRegionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectRegionViewModel.swift; sourceTree = ""; }; + B1F11C8D2A4C27B800A137A3 /* BaseSelectCountryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseSelectCountryViewModel.swift; 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 = ""; }; - C184E908CB776A3C52800606 /* 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 = ""; }; - D996266D8EF26A3162182E10 /* Pods-Confirmed VPN.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Confirmed VPN.release.xcconfig"; path = "Pods/Target Support Files/Pods-Confirmed VPN/Pods-Confirmed VPN.release.xcconfig"; sourceTree = ""; }; + C06D645D3C224C044075C2B2 /* 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 = ""; }; + CA9718C85DF38D42AFFA32FA /* 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 = ""; }; + D6F35024B0B1EB324BC94470 /* Pods_LockdownTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LockdownTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E49CFBA25B9560416CF24373 /* 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 = ""; }; 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 = ""; }; - EE344B485CF03034ED1715B2 /* 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 = ""; }; - FDC72127CE99C59603A65899 /* 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 = ""; }; + F01CAB7B2C61106F009C19CF /* SUI+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SUI+Extensions.swift"; sourceTree = ""; }; + F01CAB7D2C61316C009C19CF /* OneTimePaywallModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneTimePaywallModel.swift; sourceTree = ""; }; + F07BBAE85FFEFFCD9706CF39 /* Pods_LockdownTunnel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LockdownTunnel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F0A8E0402C64E977001303C6 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; + F0B12AF32C60CA63008EF8AA /* PaywallRoundContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallRoundContainer.swift; sourceTree = ""; }; + F0B12AF72C60D602008EF8AA /* OneTimePaywallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneTimePaywallView.swift; sourceTree = ""; }; + F6FEED2B9A31E4C2EF288E61 /* Pods_Lockdown.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Lockdown.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -467,11 +983,34 @@ buildActionMask = 2147483647; files = ( 3DBD57CD22FD7AE400DE189F /* CloudKit.framework in Frameworks */, - 3D94AB0D22FE05090012B0DE /* CocoaLumberjack.framework in Frameworks */, - 3D94AB0E22FE05090012B0DE /* CocoaLumberjackSwift.framework in Frameworks */, + B163A3422A2F4AE800FD7C5E /* Dnscryptproxy.xcframework in Frameworks */, 3D94AAF022FD7BFA0012B0DE /* NetworkExtension.framework in Frameworks */, + B163A3072A2F2A6000FD7C5E /* CocoaLumberjack.xcframework in Frameworks */, + B163A3252A2F2B7900FD7C5E /* lwip.xcframework in Frameworks */, 3DBD57BC22FD727900DE189F /* NotificationCenter.framework in Frameworks */, - 08799CF7AFE70CC200E47EDB /* Pods_Lockdown_Firewall_Widget.framework in Frameworks */, + B163A3162A2F2AF600FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */, + 7F5F41C69EE479F38F028B45 /* Pods_Lockdown_Firewall_Widget.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C0D11172473FC7E00A26E04 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E06F1E09E754223BF6E5D372 /* Pods_LockdownTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C9A9367251E1EC700DA5721 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B163A3192A2F2B0800FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */, + B163A3282A2F2B9300FD7C5E /* lwip.xcframework in Frameworks */, + 7C9A936E251E1EC700DA5721 /* SwiftUI.framework in Frameworks */, + B163A34B2A2F4BBA00FD7C5E /* Dnscryptproxy.xcframework in Frameworks */, + B163A30A2A2F2A7300FD7C5E /* CocoaLumberjack.xcframework in Frameworks */, + 7C9A936C251E1EC700DA5721 /* WidgetKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -479,20 +1018,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A15939E3206D982B0060D945 /* CocoaAsyncSocket.framework in Frameworks */, A12229AB22C014CB00BFF624 /* StoreKit.framework in Frameworks */, - A15939E4206D982B0060D945 /* CocoaLumberjack.framework in Frameworks */, - A15939E5206D982B0060D945 /* CocoaLumberjackSwift.framework in Frameworks */, - A15939C0206D965D0060D945 /* tun2socks.framework in Frameworks */, - A15939C1206D965D0060D945 /* lwip.framework in Frameworks */, - A15939C2206D965D0060D945 /* MMDB.framework in Frameworks */, - A15939C3206D965D0060D945 /* NEKit.framework in Frameworks */, - A15939C4206D965D0060D945 /* Resolver.framework in Frameworks */, + B163A34E2A2F4C2C00FD7C5E /* Resolver.xcframework in Frameworks */, A18B31F92087ED7900C0FFAA /* CloudKit.framework in Frameworks */, - A15939C5206D965D0060D945 /* Yaml.framework in Frameworks */, - A15939C6206D965D0060D945 /* Sodium.framework in Frameworks */, - E0C51E8814F43CD752AB740D /* Pods_Lockdown.framework in Frameworks */, 3DAF907922EFD70200FB29E0 /* NetworkExtension.framework in Frameworks */, + B163A31D2A2F2B4000FD7C5E /* lwip.xcframework in Frameworks */, + B163A30E2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */, + B163A3382A2F2CBC00FD7C5E /* tun2socks.xcframework in Frameworks */, + B163A2FF2A2F2A1800FD7C5E /* CocoaLumberjack.xcframework in Frameworks */, + B163A3322A2F2C7800FD7C5E /* NEKit.xcframework in Frameworks */, + B163A3402A2F498600FD7C5E /* Dnscryptproxy.xcframework in Frameworks */, + B163A2F72A2F29A100FD7C5E /* CocoaAsyncSocket.xcframework in Frameworks */, + 180AC5905ADA404C13B1D170 /* Pods_Lockdown.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -500,12 +1037,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A1342E8D20B0B8870045E9DF /* CocoaLumberjack.framework in Frameworks */, 3DAF907A22EFD70900FB29E0 /* NetworkExtension.framework in Frameworks */, - A1342E8C20B0B87D0045E9DF /* CocoaLumberjackSwift.framework in Frameworks */, + B163A3482A2F4B8300FD7C5E /* Dnscryptproxy.xcframework in Frameworks */, A12473F41FE44285008493B8 /* NotificationCenter.framework in Frameworks */, + B163A3102A2F2ACB00FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */, + B163A31F2A2F2B5200FD7C5E /* lwip.xcframework in Frameworks */, + B163A3012A2F2A3200FD7C5E /* CocoaLumberjack.xcframework in Frameworks */, A1E78D13207BE58C007FAE70 /* CloudKit.framework in Frameworks */, - F6529FCC7BC553DB6372DE40 /* Pods_Lockdown_VPN_Widget.framework in Frameworks */, + F09235DA0DB40BE2B52F6D96 /* Pods_Lockdown_VPN_Widget.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -520,18 +1059,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A1FCDA5722C066F300C928BC /* NEKit.framework in Frameworks */, + 3DD545D628068AC5005E140C /* libresolv.9.tbd in Frameworks */, + B163A32E2A2F2C0C00FD7C5E /* Resolver.xcframework in Frameworks */, + B163A3452A2F4B6B00FD7C5E /* Dnscryptproxy.xcframework in Frameworks */, + B163A3132A2F2AE300FD7C5E /* CocoaLumberjackSwift.xcframework in Frameworks */, + B163A2F92A2F29BB00FD7C5E /* CocoaAsyncSocket.xcframework in Frameworks */, + B163A3222A2F2B6800FD7C5E /* lwip.xcframework in Frameworks */, + B163A3042A2F2A4900FD7C5E /* CocoaLumberjack.xcframework in Frameworks */, + B163A33A2A2F2CD100FD7C5E /* tun2socks.xcframework in Frameworks */, A1FCDA6322C7616400C928BC /* NetworkExtension.framework in Frameworks */, - A1FCDA4E22C0666A00C928BC /* CocoaAsyncSocket.framework in Frameworks */, - A1FCDA4F22C066B900C928BC /* CocoaLumberjack.framework in Frameworks */, - A1FCDA5022C066B900C928BC /* CocoaLumberjackSwift.framework in Frameworks */, - A1FCDA5122C066B900C928BC /* lwip.framework in Frameworks */, - A1FCDA5222C066B900C928BC /* MMDB.framework in Frameworks */, - A1FCDA5322C066B900C928BC /* Resolver.framework in Frameworks */, - A1FCDA5422C066B900C928BC /* Sodium.framework in Frameworks */, - A1FCDA5522C066B900C928BC /* tun2socks.framework in Frameworks */, - A1FCDA5622C066B900C928BC /* Yaml.framework in Frameworks */, - D37198BBCE0A226BD9071F4A /* Pods_LockdownTunnel.framework in Frameworks */, + B163A3342A2F2C8B00FD7C5E /* NEKit.xcframework in Frameworks */, + 065F043966BC72234D8E0073 /* Pods_LockdownTunnel.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -549,12 +1087,27 @@ 3D0971D522EBAAEE00CCD326 /* Domains */ = { isa = PBXGroup; children = ( + 40E7A2F72A0CE92900E0231A /* advanced_analytics.txt */, + 40E7A2F32A0CE92800E0231A /* ifunny_trackers.txt */, + 40E7A2F42A0CE92800E0231A /* junes_journey_trackers.txt */, + 40E7A2F52A0CE92800E0231A /* scams.txt */, + 40E7A2F62A0CE92800E0231A /* tiktok_trackers.txt */, + 40E7A2EC2A0CE8AE00E0231A /* advanced_gaming.txt */, + 3DD545DA2808C2F6005E140C /* 5000_dummy_list.txt */, 3DAA6B4E22EA76420018FC09 /* clickbait.txt */, + 3D5F5A0723107C1E004C3860 /* game_ads.txt */, A1FCDA8422CDE60800C928BC /* crypto_mining.txt */, + 3D01D97A2480DBED003A710C /* data_trackers.txt */, + 3D01D99C2481E241003A710C /* general_ads.txt */, + 3DF5D75E2633B1E100F77D79 /* amazon_trackers.txt */, + 3D01D99D2481E252003A710C /* reporting.txt */, + 3D5F5A0923107EB8004C3860 /* snapchat_analytics.txt */, A1FCDA8C22D3C50A00C928BC /* email_opens.txt */, A1FCDA8922D3BA1900C928BC /* facebook_inc.txt */, 3D0971D722EBAD1000CCD326 /* facebook_sdk.txt */, 3D0971D922EBAD4C00CCD326 /* marketing.txt */, + 3D4D7FEB247F22AE000369FD /* google_shopping_ads.txt */, + 3DCFE6F924493F9000EA9B35 /* marketing_beta.txt */, 3DAA6B5222EA988F0018FC09 /* ransomware.txt */, ); name = Domains; @@ -573,6 +1126,13 @@ 3D44377922DFB22600908CDC /* Fonts */ = { isa = PBXGroup; children = ( + 3D752C322357FA3B00C163E4 /* SF-Pro-Rounded-Bold.otf */, + 3D752C312357FA3B00C163E4 /* SF-Pro-Rounded-Medium.otf */, + 3D752C302357FA3B00C163E4 /* SF-Pro-Rounded-Regular.otf */, + 3D752C332357FA3B00C163E4 /* SF-Pro-Rounded-Semibold.otf */, + 5EA97B352CF5F86A0082D3FD /* Juana-SemiBold.ttf */, + 5EA97B2C2CF5F83D0082D3FD /* KumbhSans-Bold.ttf */, + 5EA97B2E2CF5F83D0082D3FD /* KumbhSans-Regular.ttf */, 3D44377A22DFB22600908CDC /* Montserrat-Medium.ttf */, 3D44377B22DFB22600908CDC /* Montserrat-Light.ttf */, 3D44377C22DFB22600908CDC /* Montserrat-Thin.ttf */, @@ -646,6 +1206,16 @@ path = Animations; sourceTree = ""; }; + 3D9FC67A23E521E5004122D3 /* Account */ = { + isa = PBXGroup; + children = ( + 3DF2455323A2F8A400E46613 /* EmailSignUpViewController.swift */, + 3D9FC67623E503DF004122D3 /* EmailSignInViewController.swift */, + 3D9FC67823E521DE004122D3 /* ForgotPasswordViewController.swift */, + ); + path = Account; + sourceTree = ""; + }; 3DBD57A322FBB97D00DE189F /* Firewall */ = { isa = PBXGroup; children = ( @@ -653,7 +1223,6 @@ A154A07D215C78180010FFCC /* BlockListCell.swift */, A1FCDA5E22C14EB800C928BC /* BlockListGroupCell.swift */, A1FCDA5C22C1301900C928BC /* BlockListGroupViewController.swift */, - A174CCAD22B15B1000F1B840 /* BlockListViewController.swift */, 3D970DAC22EC149D00F9CC93 /* BlockLogCell.swift */, 3D970DAE22EC15D800F9CC93 /* BlockLogViewController.swift */, ); @@ -663,9 +1232,9 @@ 3DBD57A422FBBA4600DE189F /* VPN */ = { isa = PBXGroup; children = ( - A12186261FB8F691007058B3 /* SignupViewController.swift */, 3DBD57B522FD00BB00DE189F /* SetRegionCell.swift */, 3DBD57B322FCFF2400DE189F /* SetRegionViewController.swift */, + 3D5F5A0B23109ABB004C3860 /* WhatIsVpnViewController.swift */, 3DBD57AD22FBE04300DE189F /* WhitelistAddCell.swift */, 3DBD57AB22FBDFE200DE189F /* WhitelistCell.swift */, 3DBD57A522FBCD7A00DE189F /* WhitelistViewController.swift */, @@ -680,6 +1249,7 @@ 3DBD57BE22FD727900DE189F /* FirewallTodayViewController.swift */, 3D94AB0222FDEDEB0012B0DE /* MainInterface.storyboard */, 3DBD57C322FD727900DE189F /* Info.plist */, + 92635DE22BF786330044673D /* FireWallPrivacyInfo.xcprivacy */, ); path = "Lockdown Firewall Today"; sourceTree = ""; @@ -687,8 +1257,7 @@ 3DBD57CB22FD74D700DE189F /* Tests */ = { isa = PBXGroup; children = ( - A1141A281F46230600F54698 /* LockdowniOSTests */, - A1141A331F46230600F54698 /* LockdowniOSUITests */, + 7C0D111B2473FC7E00A26E04 /* LockdownTests */, ); path = Tests; sourceTree = ""; @@ -699,32 +1268,656 @@ A1141A141F46230500F54698 /* AppDelegate.swift */, A101106C202B9D4300807612 /* BaseViewController.swift */, A1141A201F46230500F54698 /* Info.plist */, - 3DCA4F3222F22CB40017740D /* HomeViewController.swift */, A15F3C731F79D90500B07F03 /* LaunchScreen.storyboard */, - A1359FD920AF6E31008C4BF7 /* LocalLogger.swift */, A1DBA19421B77CE9008A9322 /* LockdowniOS.entitlements */, A1141A181F46230500F54698 /* Main.storyboard */, - 3D0711BA22FE7B5100391C6E /* TitleViewController.swift */, 3D5561D3230B58F30062001D /* PrivacyPolicyViewController.swift */, + 3D0711BA22FE7B5100391C6E /* TitleViewController.swift */, 3DBD57A122FBB0D900DE189F /* WebViewViewController.swift */, ); name = Main; sourceTree = ""; }; + 40098E1A29FDA61900886474 /* Paywalls */ = { + isa = PBXGroup; + children = ( + 51DD89E32CB7D9E10028B4FE /* FeedbackFormPaywall */, + 40960AEE2A033A16000F82EB /* Models */, + 40098E3329FF376900886474 /* FirewallPaywall */, + 40098E2029FDA63100886474 /* VPNPaywall */, + 40960AEA2A03396F000F82EB /* ProductPurchasable.swift */, + ); + name = Paywalls; + sourceTree = ""; + }; + 40098E2029FDA63100886474 /* VPNPaywall */ = { + isa = PBXGroup; + children = ( + 40098E2F29FDA6F400886474 /* Controllers */, + 40098E2229FDA66100886474 /* Views */, + ); + name = VPNPaywall; + sourceTree = ""; + }; + 40098E2229FDA66100886474 /* Views */ = { + isa = PBXGroup; + children = ( + 40098E2D29FDA6E500886474 /* PlanView.swift */, + B17492C62B87424E005D9601 /* PaywallViewModel.swift */, + 40098E2B29FDA6CC00886474 /* PaywallDescriptionLabel.swift */, + 40098E2929FDA6A800886474 /* BulletView.swift */, + B17492C82B8742B6005D9601 /* PaywallView.swift */, + 92CCC17A2BEE40C900C38E1C /* ProductButton.swift */, + F0B12AF32C60CA63008EF8AA /* PaywallRoundContainer.swift */, + ); + name = Views; + sourceTree = ""; + }; + 40098E2F29FDA6F400886474 /* Controllers */ = { + isa = PBXGroup; + children = ( + 40098E3029FDA73200886474 /* VPNPaywallViewController.swift */, + F0B12AF72C60D602008EF8AA /* OneTimePaywallView.swift */, + F01CAB7D2C61316C009C19CF /* OneTimePaywallModel.swift */, + 5E9AF72F2CF5D18F001239A0 /* SpecialOfferPaywallView.swift */, + 5EA97B382CF604D60082D3FD /* SpecialOfferPaywallModel.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + 40098E3329FF376900886474 /* FirewallPaywall */ = { + isa = PBXGroup; + children = ( + 40098E3429FF378F00886474 /* Controllers */, + 40098E3629FF378F00886474 /* Models */, + 40098E3729FF378F00886474 /* Views */, + ); + name = FirewallPaywall; + sourceTree = ""; + }; + 40098E3429FF378F00886474 /* Controllers */ = { + isa = PBXGroup; + children = ( + 40098E3529FF378F00886474 /* FirewallPaywallViewController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 40098E3629FF378F00886474 /* Models */ = { + isa = PBXGroup; + children = ( + ); + path = Models; + sourceTree = ""; + }; + 40098E3729FF378F00886474 /* Views */ = { + isa = PBXGroup; + children = ( + 40098E3829FF378F00886474 /* AnnualPlanView.swift */, + 40098E3929FF378F00886474 /* MonthlyPlanView.swift */, + 40098E3A29FF378F00886474 /* AdvancedPlansViews.swift */, + ); + path = Views; + sourceTree = ""; + }; + 4015B4F829F00D74004102E0 /* VPN */ = { + isa = PBXGroup; + children = ( + 4015B4FB29F00D8C004102E0 /* Models */, + 4015B4FA29F00D87004102E0 /* Views */, + 4015B4F929F00D7E004102E0 /* Controllers */, + ); + name = VPN; + sourceTree = ""; + }; + 4015B4F929F00D7E004102E0 /* Controllers */ = { + isa = PBXGroup; + children = ( + 4015B4FC29F00DD8004102E0 /* LDVpnViewController.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + 4015B4FA29F00D87004102E0 /* Views */ = { + isa = PBXGroup; + children = ( + 4015B4FE29F14C95004102E0 /* LDCardView.swift */, + ); + name = Views; + sourceTree = ""; + }; + 4015B4FB29F00D8C004102E0 /* Models */ = { + isa = PBXGroup; + children = ( + ); + name = Models; + sourceTree = ""; + }; + 4015B50029F16DD9004102E0 /* Configuration */ = { + isa = PBXGroup; + children = ( + 4015B50129F16DEB004102E0 /* Controllers */, + ); + name = Configuration; + sourceTree = ""; + }; + 4015B50129F16DEB004102E0 /* Controllers */ = { + isa = PBXGroup; + children = ( + 4015B50229F16E1A004102E0 /* LDConfigurationViewController.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + 402BAD2E2A0CD353009B8820 /* Services */ = { + isa = PBXGroup; + children = ( + 402BAD342A0CD35B009B8820 /* ConnectivityService */, + ); + name = Services; + sourceTree = ""; + }; + 402BAD342A0CD35B009B8820 /* ConnectivityService */ = { + isa = PBXGroup; + children = ( + 402BAD352A0CD37C009B8820 /* ConnectivityService.swift */, + 402BAD372A0CD3B0009B8820 /* ConnectionState.swift */, + ); + name = ConnectivityService; + sourceTree = ""; + }; + 402D24CE29D87EF300A5AB60 /* Scenes */ = { + isa = PBXGroup; + children = ( + 5E13011B2D5E0118003896BD /* Onboarding */, + 40098E1A29FDA61900886474 /* Paywalls */, + 4015B50029F16DD9004102E0 /* Configuration */, + 4015B4F829F00D74004102E0 /* VPN */, + 402D253E29EE10A100A5AB60 /* Firewall */, + 402D252429E5840500A5AB60 /* Import Block List */, + 402D251129E5142A00A5AB60 /* Edit Domains */, + 402D24CF29D87EF900A5AB60 /* Configure Blocking */, + B1A01CA32A432826004D43EE /* Questionnaire */, + ); + name = Scenes; + sourceTree = ""; + }; + 402D24CF29D87EF900A5AB60 /* Configure Blocking */ = { + isa = PBXGroup; + children = ( + 402D24D229D87F1E00A5AB60 /* Models */, + 402D24D129D87F1600A5AB60 /* Controllers */, + 402D24D029D87F0A00A5AB60 /* Views */, + ); + name = "Configure Blocking"; + sourceTree = ""; + }; + 402D24D029D87F0A00A5AB60 /* Views */ = { + isa = PBXGroup; + children = ( + 402D24CA29D87B5A00A5AB60 /* ListsSubmenuView.swift */, + 402D24D329D87F4500A5AB60 /* CustomBlockedTableHeader.swift */, + 402D24B729D59B4400A5AB60 /* EmptyListsView.swift */, + 402D252A29E6335100A5AB60 /* SwitchBlockingView.swift */, + 402D252C29E6346900A5AB60 /* ListBlockedTableViewCell.swift */, + 402D253029E635CB00A5AB60 /* DomainsBlockedTableViewCell.swift */, + 402BAD242A0B675B009B8820 /* LockedListsView.swift */, + ); + name = Views; + sourceTree = ""; + }; + 402D24D129D87F1600A5AB60 /* Controllers */ = { + isa = PBXGroup; + children = ( + A174CCAD22B15B1000F1B840 /* BlockListViewController.swift */, + 402D252829E632F300A5AB60 /* ListSettingsViewController.swift */, + 402D252E29E6357700A5AB60 /* ListDetailViewController.swift */, + 402D253229E6588000A5AB60 /* ListDescriptionViewController.swift */, + 40E04A522A29D79100000E8C /* BlockListContainerViewController.swift */, + 40E04A542A29D7AB00000E8C /* CuratedListsViewController.swift */, + 40E04A562A29D7BC00000E8C /* CustomListsViewController.swift */, + B1F11C722A45DBF900A137A3 /* DomainListSaveable.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + 402D24D229D87F1E00A5AB60 /* Models */ = { + isa = PBXGroup; + children = ( + ); + name = Models; + sourceTree = ""; + }; + 402D251129E5142A00A5AB60 /* Edit Domains */ = { + isa = PBXGroup; + children = ( + 402D251729E515D300A5AB60 /* Views */, + 402D251229E5144300A5AB60 /* Controllers */, + ); + name = "Edit Domains"; + sourceTree = ""; + }; + 402D251229E5144300A5AB60 /* Controllers */ = { + isa = PBXGroup; + children = ( + 402D251E29E52D6A00A5AB60 /* EditDomainsViewController.swift */, + 402D251529E514CF00A5AB60 /* MoveToListViewController.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + 402D251729E515D300A5AB60 /* Views */ = { + isa = PBXGroup; + children = ( + 402D252029E52D7600A5AB60 /* BottomMenu.swift */, + 402D251A29E519B500A5AB60 /* CustomTableView.swift */, + 402D251829E517E100A5AB60 /* ConfiguredNavigationView.swift */, + 402D252229E5473E00A5AB60 /* EditDomainsCell.swift */, + 4015B4F629EFD9AC004102E0 /* AccessLevelView.swift */, + ); + name = Views; + sourceTree = ""; + }; + 402D252429E5840500A5AB60 /* Import Block List */ = { + isa = PBXGroup; + children = ( + 402D252529E5842100A5AB60 /* Controllers */, + ); + name = "Import Block List"; + sourceTree = ""; + }; + 402D252529E5842100A5AB60 /* Controllers */ = { + isa = PBXGroup; + children = ( + 402D252629E5843300A5AB60 /* ImportBlockListViewController.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + 402D253E29EE10A100A5AB60 /* Firewall */ = { + isa = PBXGroup; + children = ( + 402D254629EE10BB00A5AB60 /* Models */, + 402D254529EE10B500A5AB60 /* Views */, + 402D254429EE10AD00A5AB60 /* Controllers */, + ); + name = Firewall; + sourceTree = ""; + }; + 402D254429EE10AD00A5AB60 /* Controllers */ = { + isa = PBXGroup; + children = ( + 402D254729EE112E00A5AB60 /* LDFirewallViewController.swift */, + ); + name = Controllers; + sourceTree = ""; + }; + 402D254529EE10B500A5AB60 /* Views */ = { + isa = PBXGroup; + children = ( + 402D254929EE1C6E00A5AB60 /* DescriptionLabel.swift */, + 402D254D29EE598D00A5AB60 /* TrackersGroupView.swift */, + 402D254F29EE78D600A5AB60 /* OverallStatiscticView.swift */, + 408E7A9329F88C9200B2F587 /* CustomUISwitch.swift */, + ); + name = Views; + sourceTree = ""; + }; + 402D254629EE10BB00A5AB60 /* Models */ = { + isa = PBXGroup; + children = ( + ); + name = Models; + sourceTree = ""; + }; + 40960AEE2A033A16000F82EB /* Models */ = { + isa = PBXGroup; + children = ( + 40960AEF2A033A41000F82EB /* LockdownUser.swift */, + ); + name = Models; + sourceTree = ""; + }; + 40960AF92A033E2E000F82EB /* Business Logic */ = { + isa = PBXGroup; + children = ( + 402BAD2E2A0CD353009B8820 /* Services */, + 40960AFA2A033E46000F82EB /* PaywallService.swift */, + 40960B022A033EA9000F82EB /* CountdownDisplayService.swift */, + ); + name = "Business Logic"; + sourceTree = ""; + }; + 40960B042A033EE8000F82EB /* Core */ = { + isa = PBXGroup; + children = ( + 40CC84BF2A14C29600F9805E /* CommonUI */, + 40CC817C2A14BAF800F9805E /* Extensions */, + 40960B132A0343EA000F82EB /* Protocols */, + 40960B0B2A034037000F82EB /* Constants */, + 40960B082A033FEF000F82EB /* PropertyWrappers */, + 40960B052A033EED000F82EB /* Helpers */, + ); + name = Core; + sourceTree = ""; + }; + 40960B052A033EED000F82EB /* Helpers */ = { + isa = PBXGroup; + children = ( + 40960B062A033F0B000F82EB /* WeakObject.swift */, + ); + name = Helpers; + sourceTree = ""; + }; + 40960B082A033FEF000F82EB /* PropertyWrappers */ = { + isa = PBXGroup; + children = ( + 40960B092A03400E000F82EB /* UserDefault.swift */, + B157DE302A56B7F7003BA0AB /* CodableUserDefaults.swift */, + ); + name = PropertyWrappers; + sourceTree = ""; + }; + 40960B0B2A034037000F82EB /* Constants */ = { + isa = PBXGroup; + children = ( + 40CC84BD2A14C15400F9805E /* LockdownGradient.swift */, + 40CC81782A14BA8800F9805E /* EmailAddress.swift */, + 40960B0C2A034054000F82EB /* LockdownStorageIdentifier.swift */, + ); + name = Constants; + sourceTree = ""; + }; + 40960B132A0343EA000F82EB /* Protocols */ = { + isa = PBXGroup; + children = ( + 40CC817A2A14BAA600F9805E /* EmailValidatable.swift */, + 40CC816F2A14B29100F9805E /* EmailComposable.swift */, + 40960B142A034400000F82EB /* Keychainable.swift */, + ); + name = Protocols; + sourceTree = ""; + }; + 409B59E22A15CC7C0010242C /* WelcomeScreen */ = { + isa = PBXGroup; + children = ( + 409B59E32A15CC900010242C /* WelcomeView.swift */, + 409B59E52A15D00C0010242C /* WelcomeViewController.swift */, + ); + name = WelcomeScreen; + sourceTree = ""; + }; + 40CC817C2A14BAF800F9805E /* Extensions */ = { + isa = PBXGroup; + children = ( + 40CC84A62A14C08500F9805E /* String */, + 40CC817D2A14BB3600F9805E /* UIView+Corners.swift */, + 40CC84B42A14C0E900F9805E /* Font+Ext.swift */, + 40CC84B62A14C0EA00F9805E /* NibLoadable.swift */, + 40CC84B52A14C0EA00F9805E /* UICollectionView+Dequeue.swift */, + 40CC84B72A14C0EA00F9805E /* UIView+Ext.swift */, + 40CC84B32A14C0E900F9805E /* UIViewController+Ext.swift */, + 40CC84AC2A14C0D800F9805E /* CALayer+Ext.swift */, + 40CC84AB2A14C0D700F9805E /* Date+Ext.swift */, + 40CC84AE2A14C0D800F9805E /* UIAppearance+Ext.swift */, + 40CC84AD2A14C0D800F9805E /* UIStackView+Ext.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 40CC817F2A14BCEA00F9805E /* DeleteAccount */ = { + isa = PBXGroup; + children = ( + 40CC81802A14BCF700F9805E /* ViewController */, + ); + name = DeleteAccount; + sourceTree = ""; + }; + 40CC81802A14BCF700F9805E /* ViewController */ = { + isa = PBXGroup; + children = ( + 40CC81812A14BD2100F9805E /* DeleteMyAccountViewController.xib */, + 40CC816D2A14B25C00F9805E /* DeleteMyAccountViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 40CC84992A14BE6400F9805E /* SignUpAndLogin */ = { + isa = PBXGroup; + children = ( + 40CC849A2A14BE8600F9805E /* ViewController */, + ); + name = SignUpAndLogin; + sourceTree = ""; + }; + 40CC849A2A14BE8600F9805E /* ViewController */ = { + isa = PBXGroup; + children = ( + 40CC849D2A14BEA000F9805E /* SignUpViewController.xib */, + 409B59DF2A14CB7B0010242C /* SignUpViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 40CC849F2A14BEC200F9805E /* EnableNotifications */ = { + isa = PBXGroup; + children = ( + 40CC84A22A14BED800F9805E /* EnableNotificationsViewController.xib */, + 40CC84A02A14BECF00F9805E /* EnableNotificationsViewController.swift */, + ); + name = EnableNotifications; + sourceTree = ""; + }; + 40CC84A62A14C08500F9805E /* String */ = { + isa = PBXGroup; + children = ( + 40CC84A92A14C0A200F9805E /* String+Attributed.swift */, + 40CC84A72A14C09600F9805E /* String+URL.swift */, + 40960AF12A033AF9000F82EB /* String+Localized.swift */, + ); + name = String; + sourceTree = ""; + }; + 40CC84BF2A14C29600F9805E /* CommonUI */ = { + isa = PBXGroup; + children = ( + 40CC84C02A14C2A800F9805E /* FloatingInput */, + ); + name = CommonUI; + sourceTree = ""; + }; + 40CC84C02A14C2A800F9805E /* FloatingInput */ = { + isa = PBXGroup; + children = ( + 40CC84C12A14C2B700F9805E /* FloatingTextInputTextField.swift */, + 40CC84C22A14C2B800F9805E /* TextBox.swift */, + 40CC84C42A14C2B800F9805E /* TextBoxLabel.swift */, + 40CC84C32A14C2B800F9805E /* TextInputState.swift */, + ); + name = FloatingInput; + sourceTree = ""; + }; + 40E04A1B2A26705600000E8C /* WhatsNewScreen */ = { + isa = PBXGroup; + children = ( + 40E04A212A26708200000E8C /* WhatsNewViewController.swift */, + 40E04A232A26758200000E8C /* WhatsNewDescriptionLabel.swift */, + ); + name = WhatsNewScreen; + sourceTree = ""; + }; + 40E7A2FD2A0E1C3200E0231A /* Presentation */ = { + isa = PBXGroup; + children = ( + 40E04A1B2A26705600000E8C /* WhatsNewScreen */, + 409B59E22A15CC7C0010242C /* WelcomeScreen */, + 40CC849F2A14BEC200F9805E /* EnableNotifications */, + 40CC84992A14BE6400F9805E /* SignUpAndLogin */, + 40CC817F2A14BCEA00F9805E /* DeleteAccount */, + 40E7A2FE2A0E1C4500E0231A /* SplashScreen */, + ); + name = Presentation; + sourceTree = ""; + }; + 40E7A2FE2A0E1C4500E0231A /* SplashScreen */ = { + isa = PBXGroup; + children = ( + 40E7A2FF2A0E1C5600E0231A /* ViewController */, + ); + name = SplashScreen; + sourceTree = ""; + }; + 40E7A2FF2A0E1C5600E0231A /* ViewController */ = { + isa = PBXGroup; + children = ( + 40CC84A42A14BEFA00F9805E /* SplashScreenViewController.xib */, + 40E7A3002A0E1C7A00E0231A /* SplashScreenViewController.swift */, + ); + name = ViewController; + sourceTree = ""; + }; + 51DD89E32CB7D9E10028B4FE /* FeedbackFormPaywall */ = { + isa = PBXGroup; + children = ( + 51DD89E42CB7DA770028B4FE /* FeedbackPaywallViewController.swift */, + 510AA2F52CB8222100E53560 /* FeedbackPaywallViewModel.swift */, + ); + name = FeedbackFormPaywall; + sourceTree = ""; + }; + 5E13011B2D5E0118003896BD /* Onboarding */ = { + isa = PBXGroup; + children = ( + 5E13011C2D5E012E003896BD /* OnboardingView.swift */, + ); + path = Onboarding; + sourceTree = ""; + }; + 7C0D11102473EDFD00A26E04 /* Services */ = { + isa = PBXGroup; + children = ( + 7C0D11112473EE2E00A26E04 /* DomainNameValidator.swift */, + 7C6619BB247810E2005E8BB1 /* BlockDayLog.swift */, + 7C3E8D20247D8057004B81D6 /* PushNotifications.swift */, + 7C1AE072247FD82A0000A7D3 /* PushNotificationsAuthorization.swift */, + 7C1AE074247FE1FB0000A7D3 /* PushNotificationsAuthorizationUI.swift */, + 7C1AE079247FF87E0000A7D3 /* OneTimeActions.swift */, + 7C44081A2539BCCE003FAD1E /* ProtectedFileAccess.swift */, + ); + name = Services; + sourceTree = ""; + }; + 7C0D111B2473FC7E00A26E04 /* LockdownTests */ = { + isa = PBXGroup; + children = ( + 7C0D111C2473FC7E00A26E04 /* LockdownTests.swift */, + 7C0D11242473FD6500A26E04 /* DomainNameValidatorTests.swift */, + 7C0D111E2473FC7E00A26E04 /* Info.plist */, + 7CD52D80247E850D00D0530F /* SnapshotTests.swift */, + 7CD1435E248798D4009206A9 /* TrackerInfoTests.swift */, + ); + path = LockdownTests; + sourceTree = ""; + }; + 7C1AE07E248028E40000A7D3 /* Extensions */ = { + isa = PBXGroup; + children = ( + 7C1AE07F248028F40000A7D3 /* UIKit+Extensions.swift */, + 7C422EA425279724007F9C22 /* Align.swift */, + 402D253A29E8F9A400A5AB60 /* JSONSerialization+Extensions.swift */, + 40FC414229F74C7900BD7396 /* String+Extensions.swift */, + 408E7A9629FA698C00B2F587 /* UIVIew+Extensions.swift */, + 40960AE12A029A7D000F82EB /* UIApplication+Extension.swift */, + F01CAB7B2C61106F009C19CF /* SUI+Extensions.swift */, + ); + name = Extensions; + sourceTree = ""; + }; + 7C3EFA0024867DD600719D96 /* Services */ = { + isa = PBXGroup; + children = ( + 3DF2455523A306DB00E46613 /* Loader.swift */, + A1359FD920AF6E31008C4BF7 /* LocalLogger.swift */, + 7C3EFA0124867DEE00719D96 /* TrackerInfo.swift */, + 7C798A1925409F8100A99695 /* Mailto.swift */, + 7CC8EFEC254036050005054C /* FirewallRepair.swift */, + 40960AEC2A0339E2000F82EB /* UserService.swift */, + ); + name = Services; + sourceTree = ""; + }; + 7C422E95252796E2007F9C22 /* Views */ = { + isa = PBXGroup; + children = ( + 7C422E96252796EE007F9C22 /* StaticTableView.swift */, + 7CAB283E254336230087AAF4 /* CustomNavigationView.swift */, + ); + name = Views; + sourceTree = ""; + }; + 7C422EAC25279755007F9C22 /* Screens */ = { + isa = PBXGroup; + children = ( + 3D9FC67A23E521E5004122D3 /* Account */, + 7C422EAD2527975E007F9C22 /* Main */, + 7C4D9BBA252C8748004175EA /* AccountUI.swift */, + ); + name = Screens; + sourceTree = ""; + }; + 7C422EAD2527975E007F9C22 /* Main */ = { + isa = PBXGroup; + children = ( + 3DCA4F3222F22CB40017740D /* HomeViewController.swift */, + 7C422EAE252797A6007F9C22 /* AccountVC.swift */, + 7C422EB62527A2D1007F9C22 /* MainTabBarViewController.swift */, + 40E04A582A2A1B4C00000E8C /* CTAView.swift */, + ); + name = Main; + sourceTree = ""; + }; + 7C9A936F251E1EC700DA5721 /* LockdownFirewallWidget */ = { + isa = PBXGroup; + children = ( + 7C9A9370251E1EC700DA5721 /* LockdownFirewallWidget.swift */, + 7C9A9383251E1F9C00DA5721 /* LoadingCircle.swift */, + 7C9A9372251E1EC700DA5721 /* Assets.xcassets */, + 7C9A9374251E1EC700DA5721 /* Info.plist */, + 7CE91CA7252214C9009D8269 /* CombinedProvider.swift */, + 92635DE92BF788E50044673D /* FireWallWidgetPrivacyInfo.xcprivacy */, + ); + path = LockdownFirewallWidget; + sourceTree = ""; + }; + 92635DE62BF786D20044673D /* WidgetExtension */ = { + isa = PBXGroup; + children = ( + 92635DE72BF787220044673D /* WidgetExtensionPrivacyInfo.xcprivacy */, + ); + path = WidgetExtension; + sourceTree = ""; + }; A1141A081F46230500F54698 = { isa = PBXGroup; children = ( + 7CE91C8E2521D6CF009D8269 /* LockdownFirewallWidgetExtension.entitlements */, + 92CCC17C2BF22ABE00C38E1C /* PrivacyInfo.xcprivacy */, + 3D40826227F675F6004C146B /* dnscrypt-proxy.toml */, + 3D3BF4D0233D5E9100D0C482 /* Localizable.strings */, 3D5464D223037CCA00AE1F73 /* Settings.bundle */, A11E78A21F6A33C5007499CA /* Shared */, A1141A131F46230500F54698 /* LockdowniOS */, A1931CFC20791F5900E695EB /* Lockdown Blocker */, 3DBD57BD22FD727900DE189F /* Lockdown Firewall Today */, A12473F51FE44285008493B8 /* Today */, + 92635DE62BF786D20044673D /* WidgetExtension */, A1FCDA4222C0651300C928BC /* Lockdown Tunnel */, A1DBA19521B82F72008A9322 /* LICENSE.md */, A1141A1B1F46230500F54698 /* Assets.xcassets */, A1FCDA8322CDE5ED00C928BC /* Block Lists */, 3D44377922DFB22600908CDC /* Fonts */, + 7C9A936F251E1EC700DA5721 /* LockdownFirewallWidget */, + B163A2F52A2F298400FD7C5E /* ThirdPartyFrameworks */, A1141A421F46233600F54698 /* Frameworks */, A17A6A2C202B44BB00657B9E /* Modified Pods */, 3DBD57CB22FD74D700DE189F /* Tests */, @@ -741,6 +1934,8 @@ A1931CFB20791F5800E695EB /* Lockdown Blocker.appex */, A1FCDA4122C0651300C928BC /* LockdownTunnel.appex */, 3DBD57BB22FD727900DE189F /* Lockdown Firewall Widget.appex */, + 7C0D111A2473FC7E00A26E04 /* LockdownTests.xctest */, + 7C9A936A251E1EC700DA5721 /* LockdownFirewallWidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -748,54 +1943,47 @@ A1141A131F46230500F54698 /* LockdowniOS */ = { isa = PBXGroup; children = ( + 92635DD52BF2764E0044673D /* Availability.swift */, + 40E7A2FD2A0E1C3200E0231A /* Presentation */, + 40960B042A033EE8000F82EB /* Core */, + 40960AF92A033E2E000F82EB /* Business Logic */, + 402D24CE29D87EF300A5AB60 /* Scenes */, + 7C422EAC25279755007F9C22 /* Screens */, + 7C422E95252796E2007F9C22 /* Views */, + 7C1AE07E248028E40000A7D3 /* Extensions */, 3DFD0F8422F0F773002A3F25 /* Main */, 3DBD57A322FBB97D00DE189F /* Firewall */, 3D0711B922FE79FF00391C6E /* Why Trust */, 3DBD57A422FBBA4600DE189F /* VPN */, + 7C3EFA0024867DD600719D96 /* Services */, ); path = LockdowniOS; sourceTree = ""; }; - A1141A281F46230600F54698 /* LockdowniOSTests */ = { - isa = PBXGroup; - children = ( - A1141A291F46230600F54698 /* ConfirmediOSTests.swift */, - A1141A2B1F46230600F54698 /* Info.plist */, - ); - path = LockdowniOSTests; - sourceTree = ""; - }; - A1141A331F46230600F54698 /* LockdowniOSUITests */ = { - isa = PBXGroup; - children = ( - A1141A341F46230600F54698 /* ConfirmediOSUITests.swift */, - A1141A361F46230600F54698 /* Info.plist */, - ); - path = LockdowniOSUITests; - sourceTree = ""; - }; A1141A421F46233600F54698 /* Frameworks */ = { isa = PBXGroup; children = ( + B163A33F2A2F498600FD7C5E /* Dnscryptproxy.xcframework */, + B163A3372A2F2CBC00FD7C5E /* tun2socks.xcframework */, + B163A3312A2F2C7800FD7C5E /* NEKit.xcframework */, + B163A32B2A2F2BE700FD7C5E /* Resolver.xcframework */, + B163A31C2A2F2B4000FD7C5E /* lwip.xcframework */, + B163A30D2A2F2AB500FD7C5E /* CocoaLumberjackSwift.xcframework */, + B163A2FC2A2F29FC00FD7C5E /* CocoaLumberjack.xcframework */, + B163A2F62A2F29A000FD7C5E /* CocoaAsyncSocket.xcframework */, + 3DD545CD280681AA005E140C /* libresolv.9.tbd */, A1FCDA6222C7616400C928BC /* NetworkExtension.framework */, A12229AA22C014CA00BFF624 /* StoreKit.framework */, A1F07D4720A37FA8007CBA1B /* AdSupport.framework */, A1E78D12207BE58C007FAE70 /* CloudKit.framework */, - A15939E0206D982B0060D945 /* CocoaAsyncSocket.framework */, - A15939E1206D982B0060D945 /* CocoaLumberjack.framework */, - A15939E2206D982B0060D945 /* CocoaLumberjackSwift.framework */, - A15939BA206D965D0060D945 /* lwip.framework */, - A15939BB206D965D0060D945 /* MMDB.framework */, - A15939BC206D965D0060D945 /* NEKit.framework */, - A15939BD206D965D0060D945 /* Resolver.framework */, - A15939BF206D965D0060D945 /* Sodium.framework */, - A15939B9206D965C0060D945 /* tun2socks.framework */, - A15939BE206D965D0060D945 /* Yaml.framework */, A1912FE91F58B2D00007F6D4 /* NotificationCenter.framework */, - 31E2DCBA5F0A1C82E81F2D44 /* Pods_Lockdown.framework */, - A0C2DF90344891424A626067 /* Pods_LockdownTunnel.framework */, - 6A890BF9C9CF89A7E923EDDA /* Pods_Lockdown_Firewall_Widget.framework */, - 7B555BB9C945AD99E970BE3A /* Pods_Lockdown_VPN_Widget.framework */, + 7C9A936B251E1EC700DA5721 /* WidgetKit.framework */, + 7C9A936D251E1EC700DA5721 /* SwiftUI.framework */, + F6FEED2B9A31E4C2EF288E61 /* Pods_Lockdown.framework */, + 171F1A80C5E933902B1708CE /* Pods_Lockdown_Firewall_Widget.framework */, + 95D421359DD51FC306D3B4C9 /* Pods_Lockdown_VPN_Widget.framework */, + D6F35024B0B1EB324BC94470 /* Pods_LockdownTests.framework */, + F07BBAE85FFEFFCD9706CF39 /* Pods_LockdownTunnel.framework */, ); name = Frameworks; sourceTree = ""; @@ -815,16 +2003,23 @@ A11E78A21F6A33C5007499CA /* Shared */ = { isa = PBXGroup; children = ( + 5E9BD5852D787D8400E8DE4F /* ReviewAlertManager.swift */, + 3DAF734C2768572300D97BB0 /* FirewallUtilities.swift */, 3DCA4F2D22F190720017740D /* Client.swift */, 3DCA4F3022F190AE0017740D /* ClientModels.swift */, 3D94AB1122FE3A460012B0DE /* Environment.swift */, 3DCA4F4022F252720017740D /* FirewallController.swift */, - 3DABD9FE22F7AD4D00480AAC /* FirewallUtilities.swift */, 3DBD57AF22FC14CC00DE189F /* Shared.swift */, A1E748191F9108B6004B8021 /* SpeedTest.swift */, A1DBA18921B77C80008A9322 /* VPNController.swift */, A1DBA18521B77C66008A9322 /* VPNSubscription.swift */, 3DBD57A722FBD7A100DE189F /* WhitelistUtilities.swift */, + 7CE91C582521D54F009D8269 /* UserDefaults.swift */, + 7CE91C702521D58C009D8269 /* Metrics.swift */, + 7CE91C952521ED5E009D8269 /* VPNRegion.swift */, + 40960AE82A033514000F82EB /* AccessLevel.swift */, + 7C0D11102473EDFD00A26E04 /* Services */, + F0A8E0402C64E977001303C6 /* Defaults.swift */, ); name = Shared; sourceTree = ""; @@ -836,6 +2031,7 @@ A12473F61FE44285008493B8 /* VPNTodayViewController.swift */, A12473F81FE44285008493B8 /* MainInterface.storyboard */, A12473FB1FE44285008493B8 /* Info.plist */, + 92635DEB2BF78A9F0044673D /* VPNVidgetPrivacyInfo.xcprivacy */, ); path = Today; sourceTree = ""; @@ -861,6 +2057,7 @@ A1D85F08207C52A000B766E0 /* adBlockListThree.json */, A1931CFF20791F5900E695EB /* ContentBlockerRequestHandler.swift */, A1931D0120791F5900E695EB /* Info.plist */, + 92635DE02BF784FF0044673D /* BlockerPrivacyInfo.xcprivacy */, ); path = "Lockdown Blocker"; sourceTree = ""; @@ -895,8 +2092,11 @@ isa = PBXGroup; children = ( A1FCDA4322C0651300C928BC /* PacketTunnelProvider.swift */, + 3D40826827F6A03F004C146B /* DNSCryptThread.swift */, A1FCDA4522C0651300C928BC /* Info.plist */, A1FCDA4622C0651300C928BC /* LockdownTunnel.entitlements */, + 3DD545D428068233005E140C /* LockdownTunnelBridgingHeader.h */, + 92635DE42BF7869C0044673D /* TunnelPrivacyInfo.xcprivacy */, ); path = "Lockdown Tunnel"; sourceTree = ""; @@ -906,30 +2106,86 @@ children = ( 3D0971D622EBAB0200CCD326 /* IPs */, 3D0971D522EBAAEE00CCD326 /* Domains */, + 7C3EFA032486879800719D96 /* tracker_info.json */, ); name = "Block Lists"; path = LockdowniOS; sourceTree = ""; }; + B1062A342A45BD5800FA9E8B /* ViewModel */ = { + isa = PBXGroup; + children = ( + B1062A352A45BD7000FA9E8B /* StepsViewModel.swift */, + B1062A372A45BEBE00FA9E8B /* WhatProblemStepViewModel.swift */, + B1F11C792A498C5300A137A3 /* QuestionsStepViewModel.swift */, + B1F11C7B2A498CBF00A137A3 /* BaseStepViewModel.swift */, + B1F11C852A4B02EE00A137A3 /* SelectCountryViewModel.swift */, + B1F11C8B2A4C273500A137A3 /* SelectRegionViewModel.swift */, + B1F11C8D2A4C27B800A137A3 /* BaseSelectCountryViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + B163A2F52A2F298400FD7C5E /* ThirdPartyFrameworks */ = { + isa = PBXGroup; + children = ( + ); + path = ThirdPartyFrameworks; + sourceTree = ""; + }; + B1A01CA32A432826004D43EE /* Questionnaire */ = { + isa = PBXGroup; + children = ( + 5145A1992CBE37C40074C562 /* FeedbackFlow.swift */, + B1A01CA62A432902004D43EE /* Controllers */, + B1A01CA72A432919004D43EE /* Models */, + B1062A342A45BD5800FA9E8B /* ViewModel */, + B1A01CAA2A4343D0004D43EE /* Views */, + ); + name = Questionnaire; + path = Scenes/Questionnaire; + sourceTree = ""; + }; + B1A01CA62A432902004D43EE /* Controllers */ = { + isa = PBXGroup; + children = ( + B1A01CA42A4328E1004D43EE /* StepsViewController.swift */, + B1F11C832A4B029800A137A3 /* SelectCountryViewController.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + B1A01CA72A432919004D43EE /* Models */ = { + isa = PBXGroup; + children = ( + B1A01CA82A432926004D43EE /* StepModel.swift */, + B1F11C872A4B033000A137A3 /* Country.swift */, + B1BA87002A4C4BC400D141A8 /* QuestionModel.swift */, + ); + path = Models; + sourceTree = ""; + }; + B1A01CAA2A4343D0004D43EE /* Views */ = { + isa = PBXGroup; + children = ( + B1A01CAB2A4343F1004D43EE /* StepsView.swift */, + B1062A2C2A447B2F00FA9E8B /* RadioSwitcher.swift */, + B1062A2E2A448F1700FA9E8B /* SelectableRadioSwitcherWithTitle.swift */, + B1062A302A449F5200FA9E8B /* TitleAndSubtitleView.swift */, + B1062A322A459AC800FA9E8B /* TextViewWithPlaceholder.swift */, + B1F11C7D2A49AE4000A137A3 /* YesNoRadioSwitcherView.swift */, + B1F11C7F2A49E35800A137A3 /* QuestionTitleView.swift */, + B1F11C812A49E63400A137A3 /* NavigationLinkView.swift */, + B1F11C892A4B050500A137A3 /* CountryView.swift */, + 51006F182CBD0E7400F5142C /* ImageBannerWithTitleView.swift */, + 5117DE872CBD4A6600C4A61B /* SectionTitleView.swift */, + ); + path = Views; + sourceTree = ""; + }; D4B0457BD6E109D891101985 /* Pods */ = { isa = PBXGroup; children = ( - 4C50BEAA399D6FDF2C2672C6 /* Pods-Confirmed VPN.debug.xcconfig */, - D996266D8EF26A3162182E10 /* Pods-Confirmed VPN.release.xcconfig */, - 428B4B342E5EA9720C08F150 /* Pods-Today.debug.xcconfig */, - 96179E9445306C33ADBDDFAB /* Pods-Today.release.xcconfig */, - 4D422CB6539443825E5CD91B /* Pods-Confirmed Tunnels.debug.xcconfig */, - AC978E3E9830282F26277011 /* Pods-Confirmed Tunnels.release.xcconfig */, - 8827B5DAD4A819CDC5115562 /* Pods-Lockdown.debug.xcconfig */, - C184E908CB776A3C52800606 /* Pods-Lockdown.release.xcconfig */, - 7E013E3207564A64E3A1BD49 /* Pods-Lockdown Tunnels.debug.xcconfig */, - 65F695578DAA62084B36A513 /* Pods-Lockdown Tunnels.release.xcconfig */, - 953709B6B9D85B15EF763F5B /* Pods-LockdownTunnel.debug.xcconfig */, - 726E8CFC747C13F896CA72B6 /* Pods-LockdownTunnel.release.xcconfig */, - 7CDB1F5AC85EB2D826BB00C2 /* Pods-Lockdown Firewall Widget.debug.xcconfig */, - FDC72127CE99C59603A65899 /* Pods-Lockdown Firewall Widget.release.xcconfig */, - A6822C9110BC5F2F96454261 /* Pods-Lockdown VPN Widget.debug.xcconfig */, - EE344B485CF03034ED1715B2 /* Pods-Lockdown VPN Widget.release.xcconfig */, E4A025BF9012D4E6454AE1D6 /* Pods-Lockdown-metadata.plist */, B2AFAE1E2F56A1CA9EC153D4 /* Pods-LockdownTunnel-metadata.plist */, 92D3DD81205F17D004056D79 /* Pods-Lockdown VPN Widget-metadata.plist */, @@ -938,6 +2194,22 @@ 0CDA77C17BF2DEC43E3D56EA /* Pods-LockdownTunnel-settings-metadata.plist */, 8ED8D7A5DFFEEA5E9BD7FD20 /* Pods-Lockdown VPN Widget-settings-metadata.plist */, 50FB8ADA1D444FD9486F2D44 /* Pods-Lockdown Firewall Widget-settings-metadata.plist */, + 50F9BE503587CE4933CB7983 /* Pods-Lockdown-settings-metadata.plist */, + 6F089C7008AB8F59DE3EA7BD /* Pods-LockdownTunnel-settings-metadata.plist */, + A19DA148E491FF88E4B0B408 /* Pods-Lockdown VPN Widget-settings-metadata.plist */, + 2DF472CA81A935DEF14D7039 /* Pods-Lockdown Firewall Widget-settings-metadata.plist */, + 8DA68459884385F76BF86234 /* Pods-LockdownTests-metadata.plist */, + 2ADD2E8AC036859E49987E8B /* Pods-LockdownTests-settings-metadata.plist */, + C06D645D3C224C044075C2B2 /* Pods-Lockdown.debug.xcconfig */, + 173D3911239ED434E2139981 /* Pods-Lockdown.release.xcconfig */, + 982620F84D937F22C4824017 /* Pods-Lockdown Firewall Widget.debug.xcconfig */, + 383AB71E3748C73C2FE45B7D /* Pods-Lockdown Firewall Widget.release.xcconfig */, + 71D50056A5E2E1F6486369F9 /* Pods-Lockdown VPN Widget.debug.xcconfig */, + 2C5C889B3C3F01CB4B730A22 /* Pods-Lockdown VPN Widget.release.xcconfig */, + CA9718C85DF38D42AFFA32FA /* Pods-LockdownTests.debug.xcconfig */, + 66424506768B2196F870B04C /* Pods-LockdownTests.release.xcconfig */, + E49CFBA25B9560416CF24373 /* Pods-LockdownTunnel.debug.xcconfig */, + AF8AFE376E3ABA6AFC1C0A48 /* Pods-LockdownTunnel.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -949,7 +2221,7 @@ isa = PBXNativeTarget; buildConfigurationList = 3DBD57C722FD727900DE189F /* Build configuration list for PBXNativeTarget "Lockdown Firewall Widget" */; buildPhases = ( - FCE1317D723F4940F4E41C9C /* [CP] Check Pods Manifest.lock */, + 7AB1A2DC4BBD6078CEE6FCA2 /* [CP] Check Pods Manifest.lock */, 3DBD57B722FD727900DE189F /* Sources */, 3DBD57B822FD727900DE189F /* Frameworks */, 3DBD57B922FD727900DE189F /* Resources */, @@ -963,18 +2235,56 @@ productReference = 3DBD57BB22FD727900DE189F /* Lockdown Firewall Widget.appex */; productType = "com.apple.product-type.app-extension"; }; + 7C0D11192473FC7E00A26E04 /* LockdownTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C0D11232473FC7E00A26E04 /* Build configuration list for PBXNativeTarget "LockdownTests" */; + buildPhases = ( + 8FA9E46080A10D1A0A2DC20F /* [CP] Check Pods Manifest.lock */, + 7C0D11162473FC7E00A26E04 /* Sources */, + 7C0D11172473FC7E00A26E04 /* Frameworks */, + 7C0D11182473FC7E00A26E04 /* Resources */, + 08F1591636E665AA506616E2 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 7C0D11202473FC7E00A26E04 /* PBXTargetDependency */, + ); + name = LockdownTests; + productName = LockdownTests; + productReference = 7C0D111A2473FC7E00A26E04 /* LockdownTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7C9A9369251E1EC700DA5721 /* LockdownFirewallWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7C9A937A251E1EC700DA5721 /* Build configuration list for PBXNativeTarget "LockdownFirewallWidgetExtension" */; + buildPhases = ( + 7C9A9366251E1EC700DA5721 /* Sources */, + 7C9A9367251E1EC700DA5721 /* Frameworks */, + 7C9A9368251E1EC700DA5721 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LockdownFirewallWidgetExtension; + productName = LockdownFirewallWidgetExtension; + productReference = 7C9A936A251E1EC700DA5721 /* LockdownFirewallWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; A1141A101F46230500F54698 /* Lockdown */ = { isa = PBXNativeTarget; buildConfigurationList = A1141A391F46230600F54698 /* Build configuration list for PBXNativeTarget "Lockdown" */; buildPhases = ( - 08EEEAB72DD0E9CF5B6B4DE3 /* [CP] Check Pods Manifest.lock */, + 40AD9D4B7B30B7F2200BDBC8 /* [CP] Check Pods Manifest.lock */, + 3D94AEC12542A859005FDC0E /* ShellScript */, A1141A0D1F46230500F54698 /* Sources */, A1141A0E1F46230500F54698 /* Frameworks */, A1141A0F1F46230500F54698 /* Resources */, A18B79571F8C36460042A4EF /* Embed App Extensions */, - B2A70E191D2C5336B6A98613 /* [CP] Embed Pods Frameworks */, - A15939D8206D97C40060D945 /* CopyFiles */, - A1863898206FD7A200EF4511 /* ShellScript */, + 3DD3D09826CC8714002238E8 /* Embed Frameworks */, + 40F495132A17A3A100AB33A0 /* Script Localizarion OFF */, + 2505B9F94FC7347581A566AF /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -983,6 +2293,7 @@ A1931D0320791F5900E695EB /* PBXTargetDependency */, A1FCDA4822C0651300C928BC /* PBXTargetDependency */, 3DBD57C522FD727900DE189F /* PBXTargetDependency */, + 7C9A9376251E1EC700DA5721 /* PBXTargetDependency */, ); name = Lockdown; productName = TrustiOS; @@ -993,7 +2304,7 @@ isa = PBXNativeTarget; buildConfigurationList = A12473FF1FE44285008493B8 /* Build configuration list for PBXNativeTarget "Lockdown VPN Widget" */; buildPhases = ( - E20A95E02AF7431818AFB079 /* [CP] Check Pods Manifest.lock */, + 9CF42E72FC5A546DC821ECAD /* [CP] Check Pods Manifest.lock */, A12473EF1FE44284008493B8 /* Sources */, A12473F01FE44284008493B8 /* Frameworks */, A12473F11FE44284008493B8 /* Resources */, @@ -1028,7 +2339,7 @@ isa = PBXNativeTarget; buildConfigurationList = A1FCDA4A22C0651300C928BC /* Build configuration list for PBXNativeTarget "LockdownTunnel" */; buildPhases = ( - 6659B63A2377451DC09AC180 /* [CP] Check Pods Manifest.lock */, + 32E1FE379DCFB29A1102E7C0 /* [CP] Check Pods Manifest.lock */, A1FCDA3D22C0651300C928BC /* Sources */, A1FCDA3E22C0651300C928BC /* Frameworks */, A1FCDA3F22C0651300C928BC /* Resources */, @@ -1048,13 +2359,14 @@ A1141A091F46230500F54698 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1030; + LastSwiftUpdateCheck = 1310; LastUpgradeCheck = 1010; ORGANIZATIONNAME = "Confirmed Inc."; TargetAttributes = { 3DBD57BA22FD727900DE189F = { CreatedOnToolsVersion = 10.3; DevelopmentTeam = V8J3Z26F6Z; + LastSwiftMigration = ""; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { @@ -1077,10 +2389,20 @@ }; }; }; + 7C0D11192473FC7E00A26E04 = { + CreatedOnToolsVersion = 11.3; + DevelopmentTeam = V8J3Z26F6Z; + ProvisioningStyle = Automatic; + TestTargetID = A1141A101F46230500F54698; + }; + 7C9A9369251E1EC700DA5721 = { + CreatedOnToolsVersion = 12.0; + DevelopmentTeam = V8J3Z26F6Z; + ProvisioningStyle = Automatic; + }; A1141A101F46230500F54698 = { CreatedOnToolsVersion = 8.3.3; - DevelopmentTeam = V8J3Z26F6Z; - LastSwiftMigration = 1010; + LastSwiftMigration = ""; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { @@ -1112,7 +2434,7 @@ A12473F21FE44284008493B8 = { CreatedOnToolsVersion = 9.2; DevelopmentTeam = V8J3Z26F6Z; - LastSwiftMigration = 1010; + LastSwiftMigration = ""; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { @@ -1138,7 +2460,7 @@ A1931CFA20791F5800E695EB = { CreatedOnToolsVersion = 9.2; DevelopmentTeam = V8J3Z26F6Z; - LastSwiftMigration = 1010; + LastSwiftMigration = ""; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { @@ -1152,6 +2474,7 @@ A1FCDA4022C0651300C928BC = { CreatedOnToolsVersion = 10.2; DevelopmentTeam = V8J3Z26F6Z; + LastSwiftMigration = ""; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.NetworkExtensions.iOS = { @@ -1168,6 +2491,15 @@ knownRegions = ( en, Base, + fr, + ja, + es, + de, + ar, + "zh-Hans", + "zh-Hant", + vi, + ru, ); mainGroup = A1141A081F46230500F54698; productRefGroup = A1141A121F46230500F54698 /* Products */; @@ -1179,6 +2511,8 @@ A12473F21FE44284008493B8 /* Lockdown VPN Widget */, A1FCDA4022C0651300C928BC /* LockdownTunnel */, 3DBD57BA22FD727900DE189F /* Lockdown Firewall Widget */, + 7C0D11192473FC7E00A26E04 /* LockdownTests */, + 7C9A9369251E1EC700DA5721 /* LockdownFirewallWidgetExtension */, ); }; /* End PBXProject section */ @@ -1188,9 +2522,43 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3D94AB0F22FE0CF60012B0DE /* Assets.xcassets in Resources */, + 3D3BF4CE233D5E9100D0C482 /* Localizable.strings in Resources */, + 3D40826627F675F6004C146B /* dnscrypt-proxy.toml in Resources */, 3D94AB1022FE0CFB0012B0DE /* MainInterface.storyboard in Resources */, 3D5464D62303839500AE1F73 /* Settings.bundle in Resources */, + 601BF3ED11EB7CBF95BF5720 /* Pods-Lockdown Firewall Widget-metadata.plist in Resources */, + 92635DE32BF786330044673D /* FireWallPrivacyInfo.xcprivacy in Resources */, + 92635DEA2BF788E50044673D /* FireWallWidgetPrivacyInfo.xcprivacy in Resources */, + 388CD7581B88A7E496467546 /* Pods-Lockdown Firewall Widget-settings-metadata.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C0D11182473FC7E00A26E04 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A86219093026DE70A097E79 /* Pods-LockdownTests-metadata.plist in Resources */, + 78010EFC9ED40D77BD40C924 /* Pods-LockdownTests-settings-metadata.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C9A9368251E1EC700DA5721 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5EA97B322CF5F83D0082D3FD /* KumbhSans-Regular.ttf in Resources */, + 5EA97B372CF5F86A0082D3FD /* Juana-SemiBold.ttf in Resources */, + 5EA97B342CF5F83D0082D3FD /* KumbhSans-Bold.ttf in Resources */, + 7C0156582521C2F200670CB5 /* Montserrat-Light.ttf in Resources */, + 7C0156542521C2F200670CB5 /* Montserrat-Medium.ttf in Resources */, + 7C9A9373251E1EC700DA5721 /* Assets.xcassets in Resources */, + 7C0156562521C2F200670CB5 /* Montserrat-SemiBold.ttf in Resources */, + 92635DE82BF787220044673D /* WidgetExtensionPrivacyInfo.xcprivacy in Resources */, + 7C0156572521C2F200670CB5 /* Montserrat-Regular.ttf in Resources */, + 3DCBC8F22542544A00446C98 /* Localizable.strings in Resources */, + 7C0156552521C2F200670CB5 /* Montserrat-Bold.ttf in Resources */, + 7C0156592521C2F200670CB5 /* Montserrat-Thin.ttf in Resources */, + 3D40826727F675F6004C146B /* dnscrypt-proxy.toml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1198,33 +2566,64 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3D01D99F2481E42E003A710C /* general_ads.txt in Resources */, A1D85F0A207C562F00B766E0 /* adBlockListTwo.json in Resources */, + 3D01D99E2481E42B003A710C /* reporting.txt in Resources */, A1D85F0B207C562F00B766E0 /* adBlockListThree.json in Resources */, A1159FCC207C228300DA4670 /* privacyBlockList.json in Resources */, + 40CC81822A14BD2200F9805E /* DeleteMyAccountViewController.xib in Resources */, A1159FCD207C228300DA4670 /* socialBlockList.json in Resources */, + 40E7A2F22A0CE8AE00E0231A /* advanced_gaming.txt in Resources */, A1FCDA8722CDE8C000C928BC /* crypto_mining_ips.txt in Resources */, + 3D752C342357FA3B00C163E4 /* SF-Pro-Rounded-Regular.otf in Resources */, A1159FCE207C228300DA4670 /* adBlockList.json in Resources */, 3D44378522DFB22600908CDC /* Montserrat-Regular.ttf in Resources */, 3D44378122DFB22600908CDC /* Montserrat-Light.ttf in Resources */, 3DAA6B5322EA988F0018FC09 /* ransomware.txt in Resources */, + 40CC84A32A14BED800F9805E /* EnableNotificationsViewController.xib in Resources */, 3D44378422DFB22600908CDC /* Montserrat-SemiBold.ttf in Resources */, + 3D01D97B2480DCB3003A710C /* data_trackers.txt in Resources */, 3D44378022DFB22600908CDC /* Montserrat-Medium.ttf in Resources */, + 40E7A2F82A0CE92900E0231A /* ifunny_trackers.txt in Resources */, A15F3C751F79DC8F00B07F03 /* LaunchScreen.storyboard in Resources */, + 3DD545DB2808C2F6005E140C /* 5000_dummy_list.txt in Resources */, A1FCDA8522CDE60800C928BC /* crypto_mining.txt in Resources */, A1FCDA8B22D3BA1900C928BC /* facebook_inc.txt in Resources */, + 92CCC17D2BF22ABE00C38E1C /* PrivacyInfo.xcprivacy in Resources */, 3D0971D822EBAD1000CCD326 /* facebook_sdk.txt in Resources */, A1DBA19621B82F73008A9322 /* LICENSE.md in Resources */, + 3D4D7FEC247F2435000369FD /* google_shopping_ads.txt in Resources */, + 3D3BF4CC233D5E9100D0C482 /* Localizable.strings in Resources */, + 3D752C352357FA3B00C163E4 /* SF-Pro-Rounded-Medium.otf in Resources */, + 3DF5D75F2633B1E100F77D79 /* amazon_trackers.txt in Resources */, + 3D752C372357FA3B00C163E4 /* SF-Pro-Rounded-Semibold.otf in Resources */, + 3D752C362357FA3B00C163E4 /* SF-Pro-Rounded-Bold.otf in Resources */, 3D44378222DFB22600908CDC /* Montserrat-Thin.ttf in Resources */, A1FCDA8A22D3BA1900C928BC /* facebook_inc_ips.txt in Resources */, + 40E7A2F92A0CE92900E0231A /* junes_journey_trackers.txt in Resources */, 3DAA6B4F22EA76420018FC09 /* clickbait.txt in Resources */, A1FCDA8D22D3C50A00C928BC /* email_opens.txt in Resources */, + 40E7A2FB2A0CE92900E0231A /* tiktok_trackers.txt in Resources */, A1141A1C1F46230500F54698 /* Assets.xcassets in Resources */, 3D44378322DFB22600908CDC /* Montserrat-Bold.ttf in Resources */, A1FCDA9122D3D52C00C928BC /* facebook_inc_ipv6.txt in Resources */, + 5EA97B362CF5F86A0082D3FD /* Juana-SemiBold.ttf in Resources */, + 3D5F5A0823107C1E004C3860 /* game_ads.txt in Resources */, + 3D5F5A0A23107EB8004C3860 /* snapchat_analytics.txt in Resources */, + 3DCFE6FB244945A100EA9B35 /* marketing_beta.txt in Resources */, + 5EA97B2F2CF5F83D0082D3FD /* KumbhSans-Regular.ttf in Resources */, + 5EA97B312CF5F83D0082D3FD /* KumbhSans-Bold.ttf in Resources */, + 40CC84A52A14BEFA00F9805E /* SplashScreenViewController.xib in Resources */, + 40CC849E2A14BEA000F9805E /* SignUpViewController.xib in Resources */, 3D0971DA22EBAD4C00CCD326 /* marketing.txt in Resources */, + 7C3EFA042486879800719D96 /* tracker_info.json in Resources */, A1141A1A1F46230500F54698 /* Main.storyboard in Resources */, + 3D40826327F675F6004C146B /* dnscrypt-proxy.toml in Resources */, 3D5464D323037CCA00AE1F73 /* Settings.bundle in Resources */, 20816D1FD569053C0994232B /* Pods-Lockdown-metadata.plist in Resources */, + 40E7A2FC2A0CE92900E0231A /* advanced_analytics.txt in Resources */, + 40E7A2FA2A0CE92900E0231A /* scams.txt in Resources */, + C9E66BB880A29A48D055FBFF /* Pods-Lockdown-settings-metadata.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1232,9 +2631,13 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - A1DD82BE1FE446CA00482632 /* Assets.xcassets in Resources */, + 3D3BF4CD233D5E9100D0C482 /* Localizable.strings in Resources */, + 3D40826427F675F6004C146B /* dnscrypt-proxy.toml in Resources */, A12473FA1FE44285008493B8 /* MainInterface.storyboard in Resources */, 3D5464D42303839200AE1F73 /* Settings.bundle in Resources */, + 90728B81560C790FD5A02A6B /* Pods-Lockdown VPN Widget-metadata.plist in Resources */, + 92635DEC2BF78A9F0044673D /* VPNVidgetPrivacyInfo.xcprivacy in Resources */, + 1579100974C8086B190B35BB /* Pods-Lockdown VPN Widget-settings-metadata.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1247,6 +2650,8 @@ A1D85F09207C52A000B766E0 /* adBlockListThree.json in Resources */, A1159FCB207C201A00DA4670 /* privacyBlockList.json in Resources */, A1931CFE20791F5900E695EB /* adBlockList.json in Resources */, + 92635DE12BF784FF0044673D /* BlockerPrivacyInfo.xcprivacy in Resources */, + 40E04A502A28B4A000000E8C /* dnscrypt-proxy.toml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1255,31 +2660,110 @@ buildActionMask = 2147483647; files = ( 3D5464D52303839400AE1F73 /* Settings.bundle in Resources */, + 5666ABC4D0064E4669D1943F /* Pods-LockdownTunnel-metadata.plist in Resources */, + 92635DE52BF7869C0044673D /* TunnelPrivacyInfo.xcprivacy in Resources */, + 5647ACFEBBAB001FAE27CAF9 /* Pods-LockdownTunnel-settings-metadata.plist in Resources */, + 3D40826527F675F6004C146B /* dnscrypt-proxy.toml in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 08EEEAB72DD0E9CF5B6B4DE3 /* [CP] Check Pods Manifest.lock */ = { + 08F1591636E665AA506616E2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-LockdownTests/Pods-LockdownTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/SnapshotTesting/SnapshotTesting.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapshotTesting.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LockdownTests/Pods-LockdownTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 2505B9F94FC7347581A566AF /* [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}/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}/SwiftCSV/SwiftCSV.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}/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}/SwiftCSV.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; + }; + 32E1FE379DCFB29A1102E7C0 /* [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-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; }; - 6659B63A2377451DC09AC180 /* [CP] Check Pods Manifest.lock */ = { + 3D94AEC12542A859005FDC0E /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#if which bartycrouch > /dev/null; then\n# bartycrouch update -x\n# bartycrouch lint -x\n#else\n# echo \"warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch\"\n#fi\n"; + }; + 40AD9D4B7B30B7F2200BDBC8 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1294,91 +2778,76 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-LockdownTunnel-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Lockdown-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; }; - A1863898206FD7A200EF4511 /* ShellScript */ = { + 40F495132A17A3A100AB33A0 /* Script Localizarion OFF */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 8; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "$(SRCROOT)/Carthage/Build/iOS/CocoaAsyncSocket.framework", - "$(SRCROOT)/Carthage/Build/iOS/CocoaLumberjackSwift.framework", - "$(SRCROOT)/Carthage/Build/iOS/CocoaLumberjack.framework", - "$(SRCROOT)/Carthage/Build/iOS/lwip.framework", - "$(SRCROOT)/Carthage/Build/iOS/MMDB.framework", - "$(SRCROOT)/Carthage/Build/iOS/NEKit.framework", - "$(SRCROOT)/Carthage/Build/iOS/Resolver.framework", - "$(SRCROOT)/Carthage/Build/iOS/Sodium.framework", - "$(SRCROOT)/Carthage/Build/iOS/tun2socks.framework", - "$(SRCROOT)/Carthage/Build/iOS/Yaml.framework", + ); + name = "Script Localizarion OFF"; + outputFileListPaths = ( ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 0; + runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; - shellScript = "/usr/local/bin/carthage copy-frameworks\n"; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n# Remove German\nrm -r \"${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app/de.lproj\"\n# Remove French\nrm -r \"${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app/fr.lproj\"\n# Remove Japaneese\nrm -r \"${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app/ja.lproj\"\n# Remove Spanish\nrm -r \"${TARGET_BUILD_DIR}/${PRODUCT_NAME}.app/es.lproj\"\n"; }; - B2A70E191D2C5336B6A98613 /* [CP] Embed Pods Frameworks */ = { + 7AB1A2DC4BBD6078CEE6FCA2 /* [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}/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}/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 Firewall Widget-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; }; - E20A95E02AF7431818AFB079 /* [CP] Check Pods Manifest.lock */ = { + 8FA9E46080A10D1A0A2DC20F /* [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", + "$(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; }; - FCE1317D723F4940F4E41C9C /* [CP] Check Pods Manifest.lock */ = { + 9CF42E72FC5A546DC821ECAD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1393,7 +2862,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Lockdown Firewall Widget-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Lockdown VPN Widget-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1408,15 +2877,58 @@ buildActionMask = 2147483647; files = ( 3D94AAF422FDEAC80012B0DE /* WhitelistUtilities.swift in Sources */, - 3D94AAF322FDEAC50012B0DE /* FirewallUtilities.swift in Sources */, + 7C1AE078247FE2010000A7D3 /* PushNotificationsAuthorization.swift in Sources */, + 92635DDA2BF2764E0044673D /* Availability.swift in Sources */, + 7C6619BE247810EE005E8BB1 /* BlockDayLog.swift in Sources */, 3D94AAF522FDEACD0012B0DE /* VPNController.swift in Sources */, 3D94AAF122FDEAC00012B0DE /* Client.swift in Sources */, + 7C1AE07D247FF87F0000A7D3 /* OneTimeActions.swift in Sources */, + 3DAF73552768572300D97BB0 /* FirewallUtilities.swift in Sources */, 3D94AAF722FDEAD70012B0DE /* FirewallController.swift in Sources */, - 3D94AAFD22FDEB460012B0DE /* VPNSubscription.swift in Sources */, + 7CE91C992521ED5E009D8269 /* VPNRegion.swift in Sources */, + 7CD52D83247EC18900D0530F /* PushNotifications.swift in Sources */, 3DBD57BF22FD727900DE189F /* FirewallTodayViewController.swift in Sources */, + 7CE91C682521D565009D8269 /* UserDefaults.swift in Sources */, 3D94AB1422FE3BA20012B0DE /* Environment.swift in Sources */, 3D94AAF822FDEADC0012B0DE /* Shared.swift in Sources */, 3D94AAF222FDEAC20012B0DE /* ClientModels.swift in Sources */, + 7CE91C862521D5B7009D8269 /* Metrics.swift in Sources */, + F0A8E0452C64E9A6001303C6 /* Defaults.swift in Sources */, + 3D40826C27F6A03F004C146B /* DNSCryptThread.swift in Sources */, + 7C44081E2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C0D11162473FC7E00A26E04 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7CD1435F248798D4009206A9 /* TrackerInfoTests.swift in Sources */, + 7C0D111D2473FC7E00A26E04 /* LockdownTests.swift in Sources */, + 7CD52D81247E850D00D0530F /* SnapshotTests.swift in Sources */, + 7C0D11252473FD6500A26E04 /* DomainNameValidatorTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C9A9366251E1EC700DA5721 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 92635DDC2BF2764E0044673D /* Availability.swift in Sources */, + 7C9A9384251E1F9C00DA5721 /* LoadingCircle.swift in Sources */, + 3DAF73642768586200D97BB0 /* WhitelistUtilities.swift in Sources */, + 7CE91CA8252214C9009D8269 /* CombinedProvider.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 */, + 3D40826D27F6A03F004C146B /* DNSCryptThread.swift in Sources */, + 7CE91C9A2521ED5E009D8269 /* VPNRegion.swift in Sources */, + 7CE91C692521D566009D8269 /* UserDefaults.swift in Sources */, + F0A8E0462C64E9A7001303C6 /* Defaults.swift in Sources */, + 3DAF73602768583700D97BB0 /* OneTimeActions.swift in Sources */, + 7CE91C872521D5B8009D8269 /* Metrics.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1425,63 +2937,155 @@ buildActionMask = 2147483647; files = ( 3D94AB1222FE3A460012B0DE /* Environment.swift in Sources */, + 5EA97B392CF604F20082D3FD /* SpecialOfferPaywallModel.swift in Sources */, A1DBA18A21B77C80008A9322 /* VPNController.swift in Sources */, 3DBD57B622FD00BC00DE189F /* SetRegionCell.swift in Sources */, 3D47CDB622F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallGridPulse.swift in Sources */, + 7C6619BC247810E2005E8BB1 /* BlockDayLog.swift in Sources */, + 3D40826927F6A03F004C146B /* DNSCryptThread.swift in Sources */, + 40960B032A033EA9000F82EB /* CountdownDisplayService.swift in Sources */, 3D47CDD622F3C3F3003BD7F7 /* NVActivityIndicatorAnimationOrbit.swift in Sources */, 3D5561D4230B58F30062001D /* PrivacyPolicyViewController.swift in Sources */, 3D47CDBB22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScalePulseOut.swift in Sources */, A118F64920B33FED009A64E7 /* CGRectEx.swift in Sources */, 3D47CDBF22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallZigZag.swift in Sources */, + B157DE312A56B7F7003BA0AB /* CodableUserDefaults.swift in Sources */, + 402D251B29E519B500A5AB60 /* CustomTableView.swift in Sources */, + 7C3E8D21247D8057004B81D6 /* PushNotifications.swift in Sources */, 3DBD57A222FBB0D900DE189F /* WebViewViewController.swift in Sources */, + 402D255029EE78D600A5AB60 /* OverallStatiscticView.swift in Sources */, A118F64320B33FED009A64E7 /* FadeTransition.swift in Sources */, + 7CE91C592521D54F009D8269 /* UserDefaults.swift in Sources */, 3D47CDC822F3C3F3003BD7F7 /* NVActivityIndicatorAnimationCircleStrokeSpin.swift in Sources */, + 40098E2C29FDA6CC00886474 /* PaywallDescriptionLabel.swift in Sources */, + 3DF2455623A306DB00E46613 /* Loader.swift in Sources */, A118F64720B33FED009A64E7 /* SpinerLayer.swift in Sources */, + 4015B4FF29F14C95004102E0 /* LDCardView.swift in Sources */, + 402D254E29EE598D00A5AB60 /* TrackersGroupView.swift in Sources */, 3D47CDC522F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallRotateChase.swift in Sources */, + B1F11C882A4B033000A137A3 /* Country.swift in Sources */, + 40CC84BC2A14C0EA00F9805E /* UIView+Ext.swift in Sources */, A1EBEACD2097AE6E002B9087 /* M13CheckboxFadeController.swift in Sources */, + 409B59E62A15D00C0010242C /* WelcomeViewController.swift in Sources */, + 40CC81792A14BA8800F9805E /* EmailAddress.swift in Sources */, 3D47CDCF22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationPacman.swift in Sources */, 3D47CDC422F3C3F3003BD7F7 /* NVActivityIndicatorAnimationSquareSpin.swift in Sources */, + B1A01CA52A4328E1004D43EE /* StepsViewController.swift in Sources */, + 7C1AE07A247FF87F0000A7D3 /* OneTimeActions.swift in Sources */, + 40CC84AF2A14C0D800F9805E /* Date+Ext.swift in Sources */, A1EBEAD72097AE6E002B9087 /* M13CheckboxAddRemovePathGenerator.swift in Sources */, + B1A01CAC2A4343F1004D43EE /* StepsView.swift in Sources */, + B1062A2F2A448F1700FA9E8B /* SelectableRadioSwitcherWithTitle.swift in Sources */, 3D970DAD22EC149D00F9CC93 /* BlockLogCell.swift in Sources */, A1EBEACF2097AE6E002B9087 /* M13Checkbox.swift in Sources */, + 40E04A222A26708200000E8C /* WhatsNewViewController.swift in Sources */, + 40960AFB2A033E46000F82EB /* PaywallService.swift in Sources */, + 40098E3129FDA73300886474 /* VPNPaywallViewController.swift in Sources */, 3D47CDD522F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulseRise.swift in Sources */, 3D47CDCD22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScale.swift in Sources */, + 7CAB283F254336230087AAF4 /* CustomNavigationView.swift in Sources */, A1EBEAD22097AE6E002B9087 /* M13CheckboxCheckPathGenerator.swift in Sources */, + B1F11C8C2A4C273500A137A3 /* SelectRegionViewModel.swift in Sources */, 3DCA4F3122F190AE0017740D /* ClientModels.swift in Sources */, + 3DCFE6FA24493F9000EA9B35 /* marketing_beta.txt in Sources */, 3D0711B822FE79BE00391C6E /* WhyTrustViewController.swift in Sources */, A118F64520B33FED009A64E7 /* TimerEx.swift in Sources */, + 408E7A9429F88C9200B2F587 /* CustomUISwitch.swift in Sources */, + B1062A362A45BD7000FA9E8B /* StepsViewModel.swift in Sources */, + 7C422EAF252797A6007F9C22 /* AccountVC.swift in Sources */, + 4015B50329F16E1A004102E0 /* LDConfigurationViewController.swift in Sources */, + B1F11C7C2A498CBF00A137A3 /* BaseStepViewModel.swift in Sources */, 3D47CDD222F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallSpinFadeLoader.swift in Sources */, + 3D9FC67723E503DF004122D3 /* EmailSignInViewController.swift in Sources */, + 51DD89E52CB7DA770028B4FE /* FeedbackPaywallViewController.swift in Sources */, 3DCA4F3322F22CB40017740D /* HomeViewController.swift in Sources */, + 7CE91C962521ED5E009D8269 /* VPNRegion.swift in Sources */, + F0A8E0412C64E977001303C6 /* Defaults.swift in Sources */, 3DCA4F4122F252720017740D /* FirewallController.swift in Sources */, A1359FDA20AF6E32008C4BF7 /* LocalLogger.swift in Sources */, + 402D252929E632F300A5AB60 /* ListSettingsViewController.swift in Sources */, + 40098E3E29FF378F00886474 /* AdvancedPlansViews.swift in Sources */, + 40CC84BB2A14C0EA00F9805E /* NibLoadable.swift in Sources */, 3D47CDD122F3C3F3003BD7F7 /* NVActivityIndicatorAnimationCubeTransition.swift in Sources */, + B17492C72B87424E005D9601 /* PaywallViewModel.swift in Sources */, + 402D251929E517E100A5AB60 /* ConfiguredNavigationView.swift in Sources */, + 7C422EA525279724007F9C22 /* Align.swift in Sources */, A118F64120B33FED009A64E7 /* TransitionSubmitButton.swift in Sources */, + B1F11C802A49E35800A137A3 /* QuestionTitleView.swift in Sources */, 3DBD57A622FBCD7A00DE189F /* WhitelistViewController.swift in Sources */, + 402D252129E52D7600A5AB60 /* BottomMenu.swift in Sources */, + 40CC84A82A14C09600F9805E /* String+URL.swift in Sources */, A1EBEAD82097AE6E002B9087 /* M13CheckboxBounceController.swift in Sources */, 3DCA4F2E22F190720017740D /* Client.swift in Sources */, + B17492C92B8742B6005D9601 /* PaywallView.swift in Sources */, A101106D202B9D4300807612 /* BaseViewController.swift in Sources */, - 3DABD9FF22F7AD4D00480AAC /* FirewallUtilities.swift in Sources */, + 402D252B29E6335100A5AB60 /* SwitchBlockingView.swift in Sources */, + 402D251F29E52D6A00A5AB60 /* EditDomainsViewController.swift in Sources */, + 7C0D11122473EE2E00A26E04 /* DomainNameValidator.swift in Sources */, A1EBEADD2097AE6E002B9087 /* M13CheckboxStrokeController.swift in Sources */, A1EBEADA2097AE6E002B9087 /* M13CheckboxFillController.swift in Sources */, A1EBEAD42097AE6E002B9087 /* M13CheckboxPathGenerator.swift in Sources */, + 40CC817E2A14BB3600F9805E /* UIView+Corners.swift in Sources */, + F01CAB7C2C61106F009C19CF /* SUI+Extensions.swift in Sources */, + 5145A19A2CBE37C40074C562 /* FeedbackFlow.swift in Sources */, + B1062A2D2A447B2F00FA9E8B /* RadioSwitcher.swift in Sources */, A1EBEACB2097AE6E002B9087 /* M13CheckboxDisclosurePathGenerator.swift in Sources */, + 40960AEB2A03396F000F82EB /* ProductPurchasable.swift in Sources */, + 40CC84AA2A14C0A300F9805E /* String+Attributed.swift in Sources */, + 40CC817B2A14BAA600F9805E /* EmailValidatable.swift in Sources */, 3D47CDC622F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulse.swift in Sources */, + 402D254829EE112E00A5AB60 /* LDFirewallViewController.swift in Sources */, 3D47CDCB22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallGridBeat.swift in Sources */, 3D47CDCA22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationTriangleSkewSpin.swift in Sources */, + 40CC84C52A14C2B800F9805E /* FloatingTextInputTextField.swift in Sources */, + 7C422EB72527A2D1007F9C22 /* MainTabBarViewController.swift in Sources */, 3D47CDC722F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBlank.swift in Sources */, A1E7481A1F9108B6004B8021 /* SpeedTest.swift in Sources */, 3D970DAF22EC15D800F9CC93 /* BlockLogViewController.swift in Sources */, + 402D254A29EE1C6E00A5AB60 /* DescriptionLabel.swift in Sources */, + 402D24D429D87F4500A5AB60 /* CustomBlockedTableHeader.swift in Sources */, + 92CCC17B2BEE40C900C38E1C /* ProductButton.swift in Sources */, A1EBEADB2097AE6E002B9087 /* M13CheckboxAnimationGenerator.swift in Sources */, + 7CC8EFED254036050005054C /* FirewallRepair.swift in Sources */, 3D47CDD422F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScalePulseOutRapid.swift in Sources */, - A12186271FB8F691007058B3 /* SignupViewController.swift in Sources */, + F0B12AF42C60CA63008EF8AA /* PaywallRoundContainer.swift in Sources */, + 402BAD382A0CD3B0009B8820 /* ConnectionState.swift in Sources */, + B1F11C862A4B02EE00A137A3 /* SelectCountryViewModel.swift in Sources */, + 7CE91C712521D58C009D8269 /* Metrics.swift in Sources */, + 40960AE92A033514000F82EB /* AccessLevel.swift in Sources */, + 40098E2A29FDA6A800886474 /* BulletView.swift in Sources */, + 402D24B829D59B4400A5AB60 /* EmptyListsView.swift in Sources */, + 402D252329E5473E00A5AB60 /* EditDomainsCell.swift in Sources */, A1EBEAD12097AE6E002B9087 /* M13Checkbox+IB.swift in Sources */, + B1F11C8E2A4C27B800A137A3 /* BaseSelectCountryViewModel.swift in Sources */, + F01CAB7E2C61316C009C19CF /* OneTimePaywallModel.swift in Sources */, + 40CC84B92A14C0EA00F9805E /* Font+Ext.swift in Sources */, + 5E9BD5882D787D8500E8DE4F /* ReviewAlertManager.swift in Sources */, A1FCDA5F22C14EB800C928BC /* BlockListGroupCell.swift in Sources */, + B1F11C7E2A49AE4000A137A3 /* YesNoRadioSwitcherView.swift in Sources */, + 40098E3B29FF378F00886474 /* FirewallPaywallViewController.swift in Sources */, + 40CC84C72A14C2B800F9805E /* TextInputState.swift in Sources */, + 409B59E02A14CB7B0010242C /* SignUpViewController.swift in Sources */, + B1F11C8A2A4B050500A137A3 /* CountryView.swift in Sources */, A1EBEACC2097AE6E002B9087 /* M13CheckboxDotController.swift in Sources */, + 7C1AE075247FE1FB0000A7D3 /* PushNotificationsAuthorizationUI.swift in Sources */, + 40098E3C29FF378F00886474 /* AnnualPlanView.swift in Sources */, A1EBEAD92097AE6E002B9087 /* M13CheckboxGestureRecognizer.swift in Sources */, A1FCDA5D22C1301A00C928BC /* BlockListGroupViewController.swift in Sources */, + B1F11C842A4B029800A137A3 /* SelectCountryViewController.swift in Sources */, 3DBD57B022FC14CD00DE189F /* Shared.swift in Sources */, + 40960AE22A029A7D000F82EB /* UIApplication+Extension.swift in Sources */, + 40E04A572A29D7BC00000E8C /* CustomListsViewController.swift in Sources */, + B1F11C822A49E63400A137A3 /* NavigationLinkView.swift in Sources */, A154A07E215C78180010FFCC /* BlockListCell.swift in Sources */, + F0B12AF82C60D602008EF8AA /* OneTimePaywallView.swift in Sources */, + 510AA2F62CB8222100E53560 /* FeedbackPaywallViewModel.swift in Sources */, 3D47CDCE22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScaleRippleMultiple.swift in Sources */, + B1BA87012A4C4BC400D141A8 /* QuestionModel.swift in Sources */, + 40FC414329F74C7900BD7396 /* String+Extensions.swift in Sources */, + 40CC84B22A14C0D800F9805E /* UIAppearance+Ext.swift in Sources */, + B1F11C732A45DBF900A137A3 /* DomainListSaveable.swift in Sources */, + 40E7A3012A0E1C7A00E0231A /* SplashScreenViewController.swift in Sources */, 3D47CDCC22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScaleMultiple.swift in Sources */, A1EBEADC2097AE6E002B9087 /* DefaultValues.swift in Sources */, A1EBEAD02097AE6E002B9087 /* M13CheckboxController.swift in Sources */, @@ -1491,33 +3095,91 @@ 3D47CDC322F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallClipRotatePulse.swift in Sources */, A1EBEAD52097AE6E002B9087 /* M13CheckboxRadioPathGenerator.swift in Sources */, 3D47CDB122F3C3F3003BD7F7 /* NVActivityIndicatorViewable.swift in Sources */, + 402D253329E6588000A5AB60 /* ListDescriptionViewController.swift in Sources */, A154A080215C7A8C0010FFCC /* BlockListAddCell.swift in Sources */, + 40960AF22A033AF9000F82EB /* String+Localized.swift in Sources */, 3D47CDB822F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallClipRotateMultiple.swift in Sources */, + 40E04A552A29D7AB00000E8C /* CuratedListsViewController.swift in Sources */, 3DBD57A822FBD7A100DE189F /* WhitelistUtilities.swift in Sources */, + 40CC84A12A14BECF00F9805E /* EnableNotificationsViewController.swift in Sources */, + 40CC84BE2A14C15400F9805E /* LockdownGradient.swift in Sources */, + 40CC84C82A14C2B800F9805E /* TextBoxLabel.swift in Sources */, + 40960AED2A0339E2000F82EB /* UserService.swift in Sources */, + 3D9FC67923E521DE004122D3 /* ForgotPasswordViewController.swift in Sources */, + 5E9AF7302CF5D198001239A0 /* SpecialOfferPaywallView.swift in Sources */, + 92635DD62BF2764E0044673D /* Availability.swift in Sources */, 3D47CDC122F3C3F3003BD7F7 /* NVActivityIndicatorAnimationSemiCircleSpin.swift in Sources */, 3DBD57B422FCFF2500DE189F /* SetRegionViewController.swift in Sources */, + B1062A382A45BEBE00FA9E8B /* WhatProblemStepViewModel.swift in Sources */, + 51006F192CBD0E7400F5142C /* ImageBannerWithTitleView.swift in Sources */, + 402D253129E635CB00A5AB60 /* DomainsBlockedTableViewCell.swift in Sources */, + 7C4D9BBB252C8748004175EA /* AccountUI.swift in Sources */, 3D47CDBE22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScale.swift in Sources */, + 7C44081B2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */, + 7C1AE073247FD82A0000A7D3 /* PushNotificationsAuthorization.swift in Sources */, + 7C798A1A25409F8100A99695 /* Mailto.swift in Sources */, + B1A01CA92A432926004D43EE /* StepModel.swift in Sources */, + 40CC84B02A14C0D800F9805E /* CALayer+Ext.swift in Sources */, 3D47CDBC22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineSpinFadeLoader.swift in Sources */, 3D47CDBA22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallDoubleBounce.swift in Sources */, A1DBA18621B77C66008A9322 /* VPNSubscription.swift in Sources */, + 40CC816E2A14B25C00F9805E /* DeleteMyAccountViewController.swift in Sources */, + 402BAD362A0CD37C009B8820 /* ConnectivityService.swift in Sources */, 3D0711BB22FE7B5100391C6E /* TitleViewController.swift in Sources */, 3D47CDB222F3C3F3003BD7F7 /* NVActivityIndicatorShape.swift in Sources */, + 5117DE882CBD4A6600C4A61B /* SectionTitleView.swift in Sources */, + 7C422E97252796EE007F9C22 /* StaticTableView.swift in Sources */, + 7C3EFA0224867DEE00719D96 /* TrackerInfo.swift in Sources */, + 40960B152A034400000F82EB /* Keychainable.swift in Sources */, + 402D252D29E6346900A5AB60 /* ListBlockedTableViewCell.swift in Sources */, + 40CC84B82A14C0EA00F9805E /* UIViewController+Ext.swift in Sources */, A1EBEAD32097AE6E002B9087 /* M13CheckboxFlatController.swift in Sources */, + 40CC81702A14B29100F9805E /* EmailComposable.swift in Sources */, + 40960B0D2A034054000F82EB /* LockdownStorageIdentifier.swift in Sources */, 3D47CDC022F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallBeat.swift in Sources */, 3D47CDC222F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallScaleRipple.swift in Sources */, + 5E13011D2D5E0131003896BD /* OnboardingView.swift in Sources */, + 402D252F29E6357700A5AB60 /* ListDetailViewController.swift in Sources */, A1EBEAD62097AE6E002B9087 /* M13CheckboxSpiralController.swift in Sources */, 3D47CDAF22F3C3F3003BD7F7 /* NVActivityIndicatorAnimationDelegate.swift in Sources */, + 40098E3D29FF378F00886474 /* MonthlyPlanView.swift in Sources */, + 408E7A9729FA698C00B2F587 /* UIVIew+Extensions.swift in Sources */, + 40960B072A033F0B000F82EB /* WeakObject.swift in Sources */, + 40960AF02A033A41000F82EB /* LockdownUser.swift in Sources */, + 40CC84B12A14C0D800F9805E /* UIStackView+Ext.swift in Sources */, + 7C1AE080248028F40000A7D3 /* UIKit+Extensions.swift in Sources */, + 40960B0A2A03400E000F82EB /* UserDefault.swift in Sources */, 3DBD57AC22FBDFE300DE189F /* WhitelistCell.swift in Sources */, + 402D251629E514CF00A5AB60 /* MoveToListViewController.swift in Sources */, + 3DF2455423A2F8A400E46613 /* EmailSignUpViewController.swift in Sources */, 3D47CDB522F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallRotate.swift in Sources */, 3D47CDC922F3C3F3003BD7F7 /* NVActivityIndicatorAnimationAudioEqualizer.swift in Sources */, + B1062A332A459AC800FA9E8B /* TextViewWithPlaceholder.swift in Sources */, + 40098E2E29FDA6E500886474 /* PlanView.swift in Sources */, + 40E04A592A2A1B4C00000E8C /* CTAView.swift in Sources */, 3D47CDB722F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallClipRotate.swift in Sources */, 3D47CDD322F3C3F3003BD7F7 /* NVActivityIndicatorAnimationLineScaleParty.swift in Sources */, A1141A151F46230500F54698 /* AppDelegate.swift in Sources */, + B1062A312A449F5200FA9E8B /* TitleAndSubtitleView.swift in Sources */, 3DBD57AE22FBE04300DE189F /* WhitelistAddCell.swift in Sources */, + 4015B4F729EFD9AC004102E0 /* AccessLevelView.swift in Sources */, 3D47CDB922F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallTrianglePath.swift in Sources */, + 402D24CB29D87B5A00A5AB60 /* ListsSubmenuView.swift in Sources */, + 402BAD252A0B675B009B8820 /* LockedListsView.swift in Sources */, + 40CC84BA2A14C0EA00F9805E /* UICollectionView+Dequeue.swift in Sources */, + B1F11C7A2A498C5300A137A3 /* QuestionsStepViewModel.swift in Sources */, + 40E04A242A26758200000E8C /* WhatsNewDescriptionLabel.swift in Sources */, + 4015B4FD29F00DD8004102E0 /* LDVpnViewController.swift in Sources */, A1EBEACE2097AE6E002B9087 /* M13CheckboxExpandController.swift in Sources */, + 3D5F5A0C23109ABB004C3860 /* WhatIsVpnViewController.swift in Sources */, + 40CC84C62A14C2B800F9805E /* TextBox.swift in Sources */, A174CCAE22B15B1000F1B840 /* BlockListViewController.swift in Sources */, + 409B59E42A15CC900010242C /* WelcomeView.swift in Sources */, + 3DAF73522768572300D97BB0 /* FirewallUtilities.swift in Sources */, + 402D253B29E8F9A400A5AB60 /* JSONSerialization+Extensions.swift in Sources */, 3D47CDD022F3C3F3003BD7F7 /* NVActivityIndicatorAnimationBallPulseSync.swift in Sources */, + 40E04A532A29D79100000E8C /* BlockListContainerViewController.swift in Sources */, + 402D252729E5843300A5AB60 /* ImportBlockListViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1526,15 +3188,25 @@ buildActionMask = 2147483647; files = ( 3DBD57A922FBD7A100DE189F /* WhitelistUtilities.swift in Sources */, - 3D94AAF622FDEAD60012B0DE /* FirewallController.swift in Sources */, + 7C1AE076247FE2000000A7D3 /* PushNotificationsAuthorization.swift in Sources */, + 92635DD82BF2764E0044673D /* Availability.swift in Sources */, + 7C6619BD247810EE005E8BB1 /* BlockDayLog.swift in Sources */, + 54F0B1A0273200B0002F3630 /* FirewallController.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 */, 3DAF7C5622F4568C003C8F9C /* Client.swift in Sources */, + 7CE91C972521ED5E009D8269 /* VPNRegion.swift in Sources */, + 7CD52D82247EC18800D0530F /* PushNotifications.swift in Sources */, 3D94AB1322FE3BA10012B0DE /* Environment.swift in Sources */, - 3DABDA0022F7AD4D00480AAC /* FirewallUtilities.swift in Sources */, - A1DBA18E21B77C8E008A9322 /* VPNSubscription.swift in Sources */, + 7CE91C602521D564009D8269 /* UserDefaults.swift in Sources */, A12473F71FE44285008493B8 /* VPNTodayViewController.swift in Sources */, + 7CE91C7E2521D5B6009D8269 /* Metrics.swift in Sources */, + F0A8E0432C64E9A5001303C6 /* Defaults.swift in Sources */, + 3D40826A27F6A03F004C146B /* DNSCryptThread.swift in Sources */, + 7C44081C2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1543,6 +3215,9 @@ buildActionMask = 2147483647; files = ( A1931D0020791F5900E695EB /* ContentBlockerRequestHandler.swift in Sources */, + F0A8E0472C64E9AA001303C6 /* Defaults.swift in Sources */, + 40960B0E2A034054000F82EB /* LockdownStorageIdentifier.swift in Sources */, + 92635DD72BF2764E0044673D /* Availability.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1550,12 +3225,24 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3DABDA0122F7AD4D00480AAC /* FirewallUtilities.swift in Sources */, + 7CE91C852521D5B7009D8269 /* Metrics.swift in Sources */, + 7C3E8D22247D8057004B81D6 /* PushNotifications.swift in Sources */, 3D94AB1522FE3BA40012B0DE /* Environment.swift in Sources */, 3DBD57B222FC14CD00DE189F /* Shared.swift in Sources */, + 409481AE2A431D78001F11EB /* VPNController.swift in Sources */, A1FCDA4422C0651300C928BC /* PacketTunnelProvider.swift in Sources */, 3DBD57AA22FBD7A100DE189F /* WhitelistUtilities.swift in Sources */, + 7C1AE077247FE2010000A7D3 /* PushNotificationsAuthorization.swift in Sources */, + 3DAF73542768572300D97BB0 /* FirewallUtilities.swift in Sources */, + 7C44081D2539BCCE003FAD1E /* ProtectedFileAccess.swift in Sources */, + F0A8E0442C64E9A6001303C6 /* Defaults.swift in Sources */, + 7CE91C982521ED5E009D8269 /* VPNRegion.swift in Sources */, 3DABDA0222F7DD7700480AAC /* ClientModels.swift in Sources */, + 7CE91C672521D565009D8269 /* UserDefaults.swift in Sources */, + 7C6619BF247810EF005E8BB1 /* BlockDayLog.swift in Sources */, + 92635DDF2BF2780A0044673D /* Availability.swift in Sources */, + 7C1AE07C247FF87F0000A7D3 /* OneTimeActions.swift in Sources */, + 3D40826B27F6A03F004C146B /* DNSCryptThread.swift in Sources */, 3DABD9FD22F7961F00480AAC /* Client.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1568,6 +3255,16 @@ target = 3DBD57BA22FD727900DE189F /* Lockdown Firewall Widget */; targetProxy = 3DBD57C422FD727900DE189F /* PBXContainerItemProxy */; }; + 7C0D11202473FC7E00A26E04 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = A1141A101F46230500F54698 /* Lockdown */; + targetProxy = 7C0D111F2473FC7E00A26E04 /* PBXContainerItemProxy */; + }; + 7C9A9376251E1EC700DA5721 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7C9A9369251E1EC700DA5721 /* LockdownFirewallWidgetExtension */; + targetProxy = 7C9A9375251E1EC700DA5721 /* PBXContainerItemProxy */; + }; A118F63620B33F44009A64E7 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = A12473F21FE44284008493B8 /* Lockdown VPN Widget */; @@ -1586,10 +3283,25 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 3D3BF4D0233D5E9100D0C482 /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + 3D3BF4CF233D5E9100D0C482 /* en */, + 3D896110253527B2006D8C12 /* fr */, + 3DCBC8FF25425AB200446C98 /* ja */, + 3DCBC90A25425BC900446C98 /* es */, + ); + name = Localizable.strings; + sourceTree = ""; + }; 3D94AB0222FDEDEB0012B0DE /* MainInterface.storyboard */ = { isa = PBXVariantGroup; children = ( 3D94AB0322FDEDEB0012B0DE /* Base */, + 3DE443FA25353453006DF67D /* fr */, + 3DCBC90125425AB200446C98 /* ja */, + 3DCBC90B25425BC900446C98 /* es */, + 3DA14D3C255DF5CF00A3658E /* en */, ); name = MainInterface.storyboard; sourceTree = ""; @@ -1598,6 +3310,10 @@ isa = PBXVariantGroup; children = ( A1141A191F46230500F54698 /* Base */, + 3D89610D253527B1006D8C12 /* fr */, + 3DCBC90025425AB200446C98 /* ja */, + 3DCBC90925425BC900446C98 /* es */, + 3DA14D34255DF56E00A3658E /* en */, ); name = Main.storyboard; sourceTree = ""; @@ -1606,6 +3322,10 @@ isa = PBXVariantGroup; children = ( A12473F91FE44285008493B8 /* Base */, + 3DE443FE253534C7006DF67D /* fr */, + 3DCBC90225425AB200446C98 /* ja */, + 3DCBC90C25425BC900446C98 /* es */, + 3DA14D3E255DF5D400A3658E /* en */, ); name = MainInterface.storyboard; sourceTree = ""; @@ -1623,7 +3343,7 @@ /* Begin XCBuildConfiguration section */ 3DBD57C822FD727900DE189F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7CDB1F5AC85EB2D826BB00C2 /* Pods-Lockdown Firewall Widget.debug.xcconfig */; + baseConfigurationReference = 982620F84D937F22C4824017 /* Pods-Lockdown Firewall Widget.debug.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; @@ -1632,28 +3352,35 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Firewall Today/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Lockdown-Firewall-Today"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 3DBD57C922FD727900DE189F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FDC72127CE99C59603A65899 /* Pods-Lockdown Firewall Widget.release.xcconfig */; + baseConfigurationReference = 383AB71E3748C73C2FE45B7D /* Pods-Lockdown Firewall Widget.release.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; @@ -1662,21 +3389,160 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Firewall Today/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Lockdown-Firewall-Today"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7C0D11212473FC7E00A26E04 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA9718C85DF38D42AFFA32FA /* Pods-LockdownTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = V8J3Z26F6Z; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = Tests/LockdownTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 2.0.8; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.LockdownTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Lockdown.app/Lockdown"; + }; + name = Debug; + }; + 7C0D11222473FC7E00A26E04 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 66424506768B2196F870B04C /* Pods-LockdownTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = V8J3Z26F6Z; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = Tests/LockdownTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 2.0.8; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.LockdownTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Lockdown.app/Lockdown"; + }; + name = Release; + }; + 7C9A9378251E1EC700DA5721 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = LockdownFirewallWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = V8J3Z26F6Z; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", + ); + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = LockdownFirewallWidget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownFirewallWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7C9A9379251E1EC700DA5721 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = LockdownFirewallWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = V8J3Z26F6Z; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", + ); + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = LockdownFirewallWidget/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownFirewallWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1730,7 +3596,8 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 2.0.8; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -1785,11 +3652,13 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MARKETING_VERSION = 2.0.8; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; @@ -1798,7 +3667,7 @@ }; A1141A3A1F46230600F54698 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8827B5DAD4A819CDC5115562 /* Pods-Lockdown.debug.xcconfig */; + baseConfigurationReference = C06D645D3C224C044075C2B2 /* Pods-Lockdown.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1806,33 +3675,39 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = V8J3Z26F6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", ); GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", ); INFOPLIST_FILE = "$(SRCROOT)/LockdowniOS/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; - VALID_ARCHS = arm64; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; A1141A3B1F46230600F54698 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = C184E908CB776A3C52800606 /* Pods-Lockdown.release.xcconfig */; + baseConfigurationReference = 173D3911239ED434E2139981 /* Pods-Lockdown.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -1840,33 +3715,38 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = V8J3Z26F6Z; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", ); GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", ); INFOPLIST_FILE = "$(SRCROOT)/LockdowniOS/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; - VALID_ARCHS = arm64; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; A12474001FE44285008493B8 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A6822C9110BC5F2F96454261 /* Pods-Lockdown VPN Widget.debug.xcconfig */; + baseConfigurationReference = 71D50056A5E2E1F6486369F9 /* Pods-Lockdown VPN Widget.debug.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; @@ -1880,15 +3760,22 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Today/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Lockdown-VPN-Today"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -1896,14 +3783,13 @@ SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; - VALID_ARCHS = arm64; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; A12474011FE44285008493B8 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EE344B485CF03034ED1715B2 /* Pods-Lockdown VPN Widget.release.xcconfig */; + baseConfigurationReference = 2C5C889B3C3F01CB4B730A22 /* Pods-Lockdown VPN Widget.release.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; @@ -1917,15 +3803,21 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = Today/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Lockdown-VPN-Today"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; @@ -1933,8 +3825,7 @@ SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; - VALID_ARCHS = arm64; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -1947,19 +3838,25 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = V8J3Z26F6Z; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Blocker/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Confirmed-Blocker"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; - VALID_ARCHS = arm64; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -1972,25 +3869,30 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = V8J3Z26F6Z; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Blocker/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; PRODUCT_BUNDLE_IDENTIFIER = "com.confirmed.lockdown.Confirmed-Blocker"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; - VALID_ARCHS = arm64; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; A1FCDA4B22C0651300C928BC /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 953709B6B9D85B15EF763F5B /* Pods-LockdownTunnel.debug.xcconfig */; + baseConfigurationReference = E49CFBA25B9560416CF24373 /* Pods-LockdownTunnel.debug.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; @@ -1999,28 +3901,53 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Tunnel/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + "\"CoreTelephony\"", + "-framework", + "\"Foundation\"", + "-framework", + "\"KeychainAccess\"", + "-framework", + "\"PromiseKit\"", + "-framework", + "\"SwiftyStoreKit\"", + "-framework", + "\"SystemConfiguration\"", + "-framework", + "\"UIKit\"", + ); PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownTunnel; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "./Lockdown Tunnel/LockdownTunnelBridgingHeader.h"; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; A1FCDA4C22C0651300C928BC /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 726E8CFC747C13F896CA72B6 /* Pods-LockdownTunnel.release.xcconfig */; + baseConfigurationReference = AF8AFE376E3ABA6AFC1C0A48 /* Pods-LockdownTunnel.release.xcconfig */; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; @@ -2029,21 +3956,45 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = V8J3Z26F6Z; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Dnscryptproxy.xcframework/ios-arm64", ); GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "Lockdown Tunnel/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.3; MTL_FAST_MATH = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-framework", + "\"CoreTelephony\"", + "-framework", + "\"Foundation\"", + "-framework", + "\"KeychainAccess\"", + "-framework", + "\"PromiseKit\"", + "-framework", + "\"SwiftyStoreKit\"", + "-framework", + "\"SystemConfiguration\"", + "-framework", + "\"UIKit\"", + ); PRODUCT_BUNDLE_IDENTIFIER = com.confirmed.lockdown.LockdownTunnel; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "./Lockdown Tunnel/LockdownTunnelBridgingHeader.h"; SWIFT_VERSION = 4.2; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -2059,6 +4010,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7C0D11232473FC7E00A26E04 /* Build configuration list for PBXNativeTarget "LockdownTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C0D11212473FC7E00A26E04 /* Debug */, + 7C0D11222473FC7E00A26E04 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7C9A937A251E1EC700DA5721 /* Build configuration list for PBXNativeTarget "LockdownFirewallWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7C9A9378251E1EC700DA5721 /* Debug */, + 7C9A9379251E1EC700DA5721 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; A1141A0C1F46230500F54698 /* Build configuration list for PBXProject "LockdowniOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/LockdowniOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/LockdowniOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/LockdowniOS.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/LockdownTunnel.xcscheme b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Firewall Widget.xcscheme similarity index 82% rename from LockdowniOS.xcodeproj/xcshareddata/xcschemes/LockdownTunnel.xcscheme rename to LockdowniOS.xcodeproj/xcshareddata/xcschemes/Firewall Widget.xcscheme index 998bbf1..baa803b 100644 --- a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/LockdownTunnel.xcscheme +++ b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Firewall Widget.xcscheme @@ -1,6 +1,6 @@ @@ -43,24 +43,24 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + - - - - - - - - - - - - + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Tunnel.xcscheme b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Tunnel.xcscheme new file mode 100644 index 0000000..97c06d5 --- /dev/null +++ b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown Tunnel.xcscheme @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown.xcscheme b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown.xcscheme index 261cb34..b7a7d07 100644 --- a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown.xcscheme +++ b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Lockdown.xcscheme @@ -27,28 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - - - - - - - + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Today.xcscheme b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/VPN Widget.xcscheme similarity index 88% rename from LockdowniOS.xcodeproj/xcshareddata/xcschemes/Today.xcscheme rename to LockdowniOS.xcodeproj/xcshareddata/xcschemes/VPN Widget.xcscheme index cd2e35a..547ad99 100644 --- a/LockdowniOS.xcodeproj/xcshareddata/xcschemes/Today.xcscheme +++ b/LockdowniOS.xcodeproj/xcshareddata/xcschemes/VPN Widget.xcscheme @@ -42,8 +42,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + + + + + - - - - - - Bool { + if (textField == emailField) { + passwordField.becomeFirstResponder() + } + else if (textField == passwordField) { + textField.resignFirstResponder() + signIn() + } + return false + } + + @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") + return + } + + showLoadingView() + firstly { + try Client.signInWithEmail(email: email, password: password) + } + .done { (signin: 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.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 + try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + if (getUserWantsVPNEnabled() == true) { + VPNController.shared.restart() + } + } + .catch { error in + // it's okay for this to error out with "no subscription in receipt" + DDLogError("HomeViewController ConfirmEmail subscriptionevent error (ok for it to be \"no subscription in receipt\"): \(error)") + } + + }) + } + } + .catch { error in + self.hideLoadingView() + var errorMessage = error.localizedDescription + if let apiError = error as? ApiError { + errorMessage = apiError.message + } + self.showPopupDialog(title: "Error Signing In", message: errorMessage, acceptButton: "Okay") { + } + } + } + + @IBAction func cancelTapped(_ sender: Any) { + self.dismiss(animated: true, completion: nil) + } +} diff --git a/LockdowniOS/Account/EmailSignUpViewController.swift b/LockdowniOS/Account/EmailSignUpViewController.swift new file mode 100644 index 0000000..62094a3 --- /dev/null +++ b/LockdowniOS/Account/EmailSignUpViewController.swift @@ -0,0 +1,172 @@ +// +// EmailSignUpViewController.swift +// Lockdown +// +// Created by Johnny Lin on 12/12/19. +// Copyright © 2019 Confirmed Inc. All rights reserved. +// +// https://medium.com/jen-hamilton/swift-4-password-validation-helper-methods-f98a7ea5dcbb + +import Foundation +import UIKit +import PopupDialog +import PromiseKit + +class EmailSignUpViewController: BaseViewController, UITextFieldDelegate, Loadable { + + struct Delegate { + var showSignIn: () -> () = { } + } + + var delegate = Delegate() + + @IBOutlet weak var emailField: UITextField! + @IBOutlet weak var passwordField: UITextField! + @IBOutlet weak var lblPasswordValidation: UILabel! + var isPasswordValid = false + + override func viewDidLoad() { + super.viewDidLoad() + defaults.set(true, forKey: kHasSeenEmailSignup) + emailField.delegate = self + passwordField.delegate = self + setupToHideKeyboardOnTapOnView() + } + + func setupToHideKeyboardOnTapOnView() + { + let tap: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(self.dismissKeyboard)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + } + + @objc 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: ""), + attributes: [ + .font: UIFont(name: "Montserrat-Regular", size: 11) ?? UIFont.systemFont(ofSize: 11), + .foregroundColor: UIColor.lightGray + ]) + + 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: ""))) + attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.uppercaseLetters) != nil)), + range: findRange(in: attrStr.string, for: NSLocalizedString("one uppercase letter", comment: ""))) + attrStr.addAttributes(setupAttributeColor(if: (txt.rangeOfCharacter(from: CharacterSet.lowercaseLetters) != nil)), + range: findRange(in: attrStr.string, for: NSLocalizedString("one lowercase letter", comment: ""))) + 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: ""))) + } else { + isPasswordValid = false + } + + lblPasswordValidation.attributedText = attrStr + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if (textField == emailField) { + passwordField.becomeFirstResponder() + } + else if (textField == passwordField) { + textField.resignFirstResponder() + createAccount() + } + return false + } + + @IBAction 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.emailField.text ?? "", password: self.passwordField.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.emailField.text!, password: self.passwordField.text!) + setAPICredentialsConfirmed(confirmed: false) + 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: ""), + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + popup.addButtons([ + DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) { + self.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") + } + default: + _ = self.popupErrorAsApiError(error) + } + } + else { + self.showPopupDialog(title: NSLocalizedString("Error Creating Email Account", comment: ""), + message: "\(error)", + acceptButton: NSLocalizedString("Okay", comment: "")) + } + } + } + + @IBAction func signInClicked(_ sender: Any) { + dismiss(animated: true, completion: { + self.delegate.showSignIn() + }) + } + + func setupAttributeColor(if isValid: Bool) -> [NSAttributedString.Key: Any] { + if isValid { + return [NSAttributedString.Key.foregroundColor: UIColor.lightGray] + } else { + isPasswordValid = false + return [NSAttributedString.Key.foregroundColor: UIColor.red] + } + } + + 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 NSMakeRange(startIndex, length) + } else { + print("Range does not exist in the base string.") + return NSMakeRange(0, 0) + } + } + + @IBAction func cancelTapped(_ sender: Any) { + self.dismiss(animated: true, completion: nil) + } +} diff --git a/LockdowniOS/Account/ForgotPasswordViewController.swift b/LockdowniOS/Account/ForgotPasswordViewController.swift new file mode 100644 index 0000000..1143cd5 --- /dev/null +++ b/LockdowniOS/Account/ForgotPasswordViewController.swift @@ -0,0 +1,98 @@ +// +// ForgotPasswordViewController.swift +// Lockdown +// +// Created by Johnny Lin on 1/31/20. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation +import UIKit +import PopupDialog +import PromiseKit + +class ForgotPasswordViewController: BaseViewController, Loadable { + + @IBOutlet weak var submitButton: UIButton! + @IBOutlet weak var emailField: UITextField! + + override func viewDidLoad() { + super.viewDidLoad() + emailField.delegate = self + setupToHideKeyboardOnTapOnView() + updateSubmitButton(isEnabled: false) + } + + func setupToHideKeyboardOnTapOnView() + { + let tap: UITapGestureRecognizer = UITapGestureRecognizer( + target: self, + action: #selector(self.dismissKeyboard)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + } + + @objc func dismissKeyboard() + { + view.endEditing(true) + } + + private func updateSubmitButton(isEnabled: Bool) { + submitButton.isEnabled = isEnabled + submitButton.backgroundColor = isEnabled ? .tunnelsBlue : .borderGray + } + + @IBAction func submit() { + guard let email = emailField.text else { + showPopupDialog(title: "Check Fields", message: "Email must not be empty", acceptButton: "Okay") + return + } + showLoadingView() + firstly { + try Client.forgotPassword(email: email) + } + .done { (success: Bool) in + self.hideLoadingView() + guard success else { + self.showPopupDialog( + title: NSLocalizedString("Unknown error", comment: ""), + message: "", + acceptButton: "Okay" + ) + return + } + 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.dismiss(animated: true, completion: nil) + } + } + .catch { error in + self.hideLoadingView() + var errorMessage = error.localizedDescription + if let apiError = error as? ApiError { + errorMessage = apiError.message + } + self.showPopupDialog(title: "Error Sending Reset Password Email", message: errorMessage, acceptButton: "Okay") { + } + } + } + + @IBAction func cancelTapped(_ sender: Any) { + self.dismiss(animated: true, completion: nil) + } +} + +extension ForgotPasswordViewController: UITextFieldDelegate { + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let text = (textField.text as? NSString)?.replacingCharacters(in: range, with: string) ?? "" + updateSubmitButton(isEnabled: !text.isEmpty) + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if (textField == emailField) { + textField.resignFirstResponder() + submit() + } + return false + } +} diff --git a/LockdowniOS/AccountUI.swift b/LockdowniOS/AccountUI.swift new file mode 100644 index 0000000..83e148b --- /dev/null +++ b/LockdowniOS/AccountUI.swift @@ -0,0 +1,35 @@ +// +// AccountUI.swift +// Lockdown +// +// Created by Oleg Dreyman on 06.10.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import UIKit + +enum AccountUI { + + static let accountStateDidChange = Notification.Name("AccountUIAccountStateDidChangeNotification") + static let subscritionTypeChanged = + Notification.Name("AccountSubscritionTypeDidChanged") + + 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 new file mode 100644 index 0000000..37810ed --- /dev/null +++ b/LockdowniOS/AccountVC.swift @@ -0,0 +1,473 @@ +// +// AccountVC.swift +// Lockdown +// +// Created by Oleg Dreyman on 02.10.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import UIKit +import PopupDialog +import PromiseKit +import CocoaLumberjackSwift + +final class AccountViewController: BaseViewController, Loadable { + + // MARK: - Properties + let tableView = StaticTableView(frame: .zero, style: .grouped) + var activePlans: [Subscription.PlanType] = [] + var feedbackFlow: FeedbackFlow? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + do { + view.addSubview(tableView) + tableView.anchors.edges.pin() + tableView.separatorStyle = .singleLine + tableView.cellLayoutMarginsFollowReadableWidth = true + tableView.deselectsCellsAutomatically = true + tableView.tableFooterView = UIView() + + tableView.clear() + createTable() + } + + do { + NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange), name: AccountUI.accountStateDidChange, object: nil) + } + } + + @objc + func accountStateDidChange() { + assert(Thread.isMainThread) + self.reloadTable() + } + + func reloadTable() { + tableView.clear() + createTable() + tableView.reloadData() + } + + 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: "")) { + // AccountViewController will update itself by observing + // AccountUI.accountStateDidChange notification + AccountUI.presentCreateAccount(on: self) + } + firstButton.backgroundView = UIView() + firstButton.backgroundView?.backgroundColor = UIColor.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) + 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: "")) + }, + ]) + self.present(confirm, animated: true, completion: nil) + } + firstButton.backgroundView?.backgroundColor = UIColor.clear + firstButton.label.textColor = UIColor.systemRed + } + else { + title = "⚠️ Email Not Confirmed" + firstButton = DefaultCell(title: NSLocalizedString("Confirm Email", comment: "")) { + self.showLoadingView() + + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .done { (signin: SignIn) in + self.hideLoadingView() + // successfully signed in with no errors, show confirmation success + setAPICredentialsConfirmed(confirmed: true) + + // logged in and confirmed - update this email with the receipt and refresh VPN credentials + firstly { () -> Promise in + try Client.subscriptionEvent() + } + .then { (result: SubscriptionEvent) -> Promise in + try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + if (getUserWantsVPNEnabled() == true) { + VPNController.shared.restart() + } + } + .catch { error in + // it's okay for this to error out with "no subscription in receipt" + DDLogError("HomeViewController ConfirmEmail subscriptionevent error (ok for it to be \"no subscription in receipt\"): \(error)") + } + + let popup = PopupDialog(title: "Success! 🎉", + message: NSLocalizedString("Your account has been confirmed and you're now signed in. You'll get the latest block lists, access to Lockdown Mac, and get critical announcements.", comment: ""), + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + popup.addButtons([ + DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) { + self.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: ""))", + image: nil, + buttonAlignment: .vertical, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + popup.addButtons([ + DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {}, + DefaultButton(title: NSLocalizedString("Sign Out", comment: ""), dismissOnTap: true) { + URLCache.shared.removeAllCachedResponses() + Client.clearCookies() + clearAPICredentials() + setAPICredentialsConfirmed(confirmed: false) + self.reloadTable() + self.showPopupDialog(title: NSLocalizedString("Success", comment: ""), message: NSLocalizedString("Signed out successfully.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) + }, + DefaultButton(title: NSLocalizedString("Re-send", comment: ""), dismissOnTap: true) { + firstly { + try Client.resendConfirmCode(email: apiCredentials.email) + } + .done { (success: Bool) in + var message = NSLocalizedString("Successfully re-sent your email confirmation to ", comment: "") + apiCredentials.email + if (success == false) { + message = NSLocalizedString("Failed to re-send email confirmation.", comment: "") + } + self.showPopupDialog(title: "", message: message, acceptButton: NSLocalizedString("Okay", comment: "")) + } + .catch { error in + if (self.popupErrorAsNSURLError(error)) { + return + } + else if let apiError = error as? ApiError { + _ = self.popupErrorAsApiError(apiError) + } + else { + self.showPopupDialog(title: NSLocalizedString("Error Re-sending Email Confirmation", comment: ""), + message: "\(error)", + acceptButton: NSLocalizedString("Okay", comment: "")) + } + } + }, + ]) + self.present(popup, animated: true, completion: nil) + } + + } + } + } + + 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 { + }.done { subscriptions in + DDLogInfo("active-subs: \(subscriptions)") + self.activePlans = subscriptions.map({ $0.planType }) + } + .catch { error in + } + } + // not logged in via email, use receipt + else { + firstly { + try Client.signIn() + }.then { _ in + try Client.activeSubscriptions() + }.ensure { + }.done { subscriptions in + self.activePlans = subscriptions.map({ $0.planType }) + }.catch { error in + DDLogError("Error reloading subscription: \(error.localizedDescription)") + } + } + + let notificationsButton = DefaultCell(title: "", action: { }) + + let updateNotificationButtonTitle = { (cell: _DefaultCell) in + if PushNotifications.Authorization.getUserWantsNotificationsEnabled(forCategory: .weeklyUpdate) { + cell.label.text = NSLocalizedString("Notifications: On", comment: "") + } else { + cell.label.text = NSLocalizedString("Notifications: Off", comment: "") + } + } + + updateNotificationButtonTitle(notificationsButton) + + notificationsButton.onSelect { [unowned notificationsButton, unowned self] in + if PushNotifications.Authorization.getUserWantsNotificationsEnabled(forCategory: .weeklyUpdate) { + 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)") + } + } + } + + tableView.addRow { (contentView) in + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 8 + contentView.addSubview(stack) + stack.anchors.edges.marginsPin(insets: .init(top: 8, left: 0, bottom: 8, right: 0)) + + let titleLabel = UILabel() + titleLabel.text = title + titleLabel.font = fontBold18 + titleLabel.textAlignment = .center + titleLabel.numberOfLines = 0 + stack.addArrangedSubview(titleLabel) + + let messageLabel = UILabel() + messageLabel.text = message + messageLabel.font = fontMedium18 + messageLabel.textAlignment = .center + messageLabel.numberOfLines = 0 + stack.addArrangedSubview(messageLabel) + } + + let firstButtons: [SelectableTableViewCell] = [ + firstButton, + notificationsButton, + ] + + for cell in firstButtons { + tableView.addCell(cell) + } + + let otherCells = [ + // DefaultCell(title: NSLocalizedString("Tutorial", comment: "")) { [unowned self] in + // self.startTutorial() + // }, + MakeDefaultCell( + title: NSLocalizedString("What's New", comment: ""), + color: .paywallNew + ) { + self.showWhatsNewModal() + }, + DefaultCell(title: NSLocalizedString("Why Trust Lockdown", comment: "")) { + self.showWhyTrustPopup() + }, + DefaultCell(title: NSLocalizedString("Privacy Policy", comment: "")) { + self.showPrivacyPolicyModal() + }, + DefaultCell(title: NSLocalizedString("What is VPN?", comment: "")) { + self.performSegue(withIdentifier: "showWhatIsVPN", sender: self) + }, + DefaultCell(title: NSLocalizedString("Support | Feedback", comment: "")) { + self.showPopupDialog( + title: nil, + message: NSLocalizedString("Remember to check our FAQs first for answers to the most frequently asked questions.\n\nIf your question is not answered there, we're happy to provide support and receive feedback via email or through the bug reporting form.", comment: ""), + buttons: [ + .custom(title: NSLocalizedString("View FAQs", comment: ""), completion: { + self.showFAQsModal() + }), + .custom(title: NSLocalizedString("Email Us", comment: ""), completion: { + self.emailTeam() + }), + .custom(title: NSLocalizedString("Bug Reporting Form", comment: "")) { [weak self] in + self?.openQuestionnaire() + }, + .cancel() + ] + ) + }, + DefaultCell(title: NSLocalizedString("FAQs", comment: "")) { + self.showFAQsModal() + }, + DefaultCell(title: NSLocalizedString("Website", comment: "")) { + self.showWebsiteModal() + }, + ] + + for cell in otherCells { + tableView.addCell(cell) + } + + if let creds = getAPICredentials() { + tableView.addCell(MakeDefaultCell( + title: .localized("delete_account"), + color: .lockdownRed + ) { + self.deleteAccount(userEmail: creds.email) + }) + } + +#if DEBUG + let fixVPNConfig = DefaultCell(title: "_Fix Firewall Config", action: { + self.showFixFirewallConnectionDialog { + FirewallController.shared.deleteConfigurationAndAddAgain() + } + }) + tableView.addCell(fixVPNConfig) +#endif + + tableView.addRowCell { (cell) in + cell.textLabel?.text = Bundle.main.versionString + cell.textLabel?.font = fontSemiBold17 + cell.textLabel?.textColor = UIColor.systemGray + cell.textLabel?.textAlignment = .right + + // removing the bottom separator + cell.separatorInset = .init(top: 0, left: 0, bottom: 0, right: .greatestFiniteMagnitude) + cell.directionalLayoutMargins = .zero + } + } + + func startTutorial() { +// if let tabBarController = tabBarController as? MainTabBarController { +// tabBarController.selectedViewController = tabBarController.homeViewController +// tabBarController.homeViewController.startTutorial() +// } + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + switch segue.identifier { + case "showWhatIsVPN": + if let vc = segue.destination as? WhatIsVpnViewController { +// vc.parentVC = (tabBarController as? MainTabBarController)?.vpnViewController + } + default: + break + } + } + + private func openQuestionnaire() { + feedbackFlow?.startFlow() + } +} + +// MARK: - Helpers / Extensions + +class _DefaultButtonCell: SelectableTableViewCell { + let button = UIButton(type: .system) +} + +func DefaultButtonCell(title: String, action: @escaping () -> ()) -> _DefaultButtonCell { + let cell = _DefaultButtonCell() + cell.backgroundView = UIView() + cell.button.setTitle(title, for: .normal) + cell.button.isUserInteractionEnabled = false + cell.button.titleLabel?.font = fontSemiBold17 + cell.button.tintColor = .tunnelsBlue + cell.contentView.addSubview(cell.button) + cell.button.anchors.height.equal(21) + cell.button.anchors.edges.marginsPin(insets: .init(top: 8, left: 0, bottom: 8, right: 0)) + return cell.onSelect(callback: action) +} + +class _DefaultCell: SelectableTableViewCell { + let label = UILabel() +} + +func DefaultCell(title: String, action: @escaping () -> ()) -> _DefaultCell { + let cell = _DefaultCell() + cell.label.text = title + cell.label.font = fontSemiBold17 + cell.label.textColor = .tunnelsBlue + 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) +} + +func MakeDefaultCell(title: String, color: UIColor = .tunnelsBlue, action: @escaping () -> Void) -> _DefaultCell { + let cell = _DefaultCell() + cell.label.text = title + 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) +} + +extension AccountViewController: EmailComposable { + private func deleteAccount(userEmail: String) { + let deleteAccountViewController = DeleteMyAccountViewController(userEmail: userEmail) + deleteAccountViewController.modalPresentationStyle = .formSheet + present(deleteAccountViewController, animated: true) + } +} + +fileprivate extension _DefaultButtonCell { + func startActivityIndicator() { + let activity = UIActivityIndicatorView() + + if let label = button.titleLabel { + label.addSubview(activity) + activity.translatesAutoresizingMaskIntoConstraints = false + activity.centerYAnchor.constraint(equalTo: label.centerYAnchor).isActive = true + activity.leadingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1).isActive = true + activity.startAnimating() + } + } + + func stopActivityIndicator() { + if let label = button.titleLabel { + let indicators = label.subviews.compactMap { $0 as? UIActivityIndicatorView } + for indicator in indicators { + indicator.stopAnimating() + indicator.removeFromSuperview() + } + } + } +} diff --git a/LockdowniOS/Align.swift b/LockdowniOS/Align.swift new file mode 100644 index 0000000..f87e5bc --- /dev/null +++ b/LockdowniOS/Align.swift @@ -0,0 +1,462 @@ +// The MIT License (MIT) +// +// Copyright (c) 2017-2020 Alexander Grebenyuk (github.com/kean). + +#if os(iOS) || os(tvOS) +import UIKit + +internal protocol LayoutItem { // `UIView`, `UILayoutGuide` + var superview: UIView? { get } +} + +extension UIView: LayoutItem {} +extension UILayoutGuide: LayoutItem { + internal var superview: UIView? { owningView } +} +#elseif os(macOS) +import AppKit + +internal protocol LayoutItem { // `NSView`, `NSLayoutGuide` + var superview: NSView? { get } +} + +extension NSView: LayoutItem {} +extension NSLayoutGuide: LayoutItem { + internal var superview: NSView? { owningView } +} +#endif + +internal extension LayoutItem { // Align methods are available via `LayoutAnchors` + @nonobjc var anchors: LayoutAnchors { LayoutAnchors(base: self) } +} + +// MARK: - LayoutAnchors + +internal struct LayoutAnchors { + internal let base: Base +} + +internal extension LayoutAnchors where Base: LayoutItem { + + // MARK: Anchors + + var top: Anchor { Anchor(base, .top) } + var bottom: Anchor { Anchor(base, .bottom) } + var left: Anchor { Anchor(base, .left) } + var right: Anchor { Anchor(base, .right) } + var leading: Anchor { Anchor(base, .leading) } + var trailing: Anchor { Anchor(base, .trailing) } + + var centerX: Anchor { Anchor(base, .centerX) } + var centerY: Anchor { Anchor(base, .centerY) } + + var firstBaseline: Anchor { Anchor(base, .firstBaseline) } + var lastBaseline: Anchor { Anchor(base, .lastBaseline) } + + var width: Anchor { Anchor(base, .width) } + var height: Anchor { Anchor(base, .height) } + + // MARK: Anchor Collections + + var edges: AnchorCollectionEdges { AnchorCollectionEdges(item: base) } + var center: AnchorCollectionCenter { AnchorCollectionCenter(x: centerX, y: centerY) } + var size: AnchorCollectionSize { AnchorCollectionSize(width: width, height: height) } +} + +#if os(iOS) || os(tvOS) +internal extension LayoutAnchors where Base: UIView { + var margins: LayoutAnchors { base.layoutMarginsGuide.anchors } + var safeArea: LayoutAnchors { base.safeAreaLayoutGuide.anchors } +} +#endif + +// MARK: - Anchors + +// phantom types +internal enum AnchorAxis { + internal class Horizontal {} + internal class Vertical {} +} + +internal enum AnchorType { + internal class Dimension {} + internal class Alignment {} + internal class Center: Alignment {} + internal class Edge: Alignment {} + internal class Baseline: Alignment {} +} + +/// An anchor represents one of the view's layout attributes (e.g. `left`, +/// `centerX`, `width`, etc). Use the anchor’s methods to construct constraints. +internal struct Anchor { // type and axis are phantom types + fileprivate let item: LayoutItem + fileprivate let attribute: NSLayoutConstraint.Attribute + fileprivate let offset: CGFloat + fileprivate let multiplier: CGFloat + + fileprivate init(_ item: LayoutItem, _ attribute: NSLayoutConstraint.Attribute, offset: CGFloat = 0, multiplier: CGFloat = 1) { + self.item = item; self.attribute = attribute; self.offset = offset; self.multiplier = multiplier + } + + /// Returns a new anchor offset by a given amount. + /// + /// - note: Consider using a convenience operator instead: `view.anchors.top + 10`. + internal func offsetting(by offset: CGFloat) -> Anchor { + Anchor(item, attribute, offset: self.offset + offset, multiplier: self.multiplier) + } + + /// Returns a new anchor with a given multiplier. + /// + /// - note: Consider using a convenience operator instead: `view.anchors.height * 2`. + internal func multiplied(by multiplier: CGFloat) -> Anchor { + Anchor(item, attribute, offset: self.offset * multiplier, multiplier: self.multiplier * multiplier) + } +} + +internal func + (anchor: Anchor, offset: CGFloat) -> Anchor { + anchor.offsetting(by: offset) +} + +internal func - (anchor: Anchor, offset: CGFloat) -> Anchor { + anchor.offsetting(by: -offset) +} + +internal func * (anchor: Anchor, multiplier: CGFloat) -> Anchor { + anchor.multiplied(by: multiplier) +} + +// MARK: - Anchors (AnchorType.Alignment) + +internal extension Anchor where Type: AnchorType.Alignment { + @discardableResult func equal(_ anchor: Anchor, constant: CGFloat = 0) -> NSLayoutConstraint { + Constraints.constrain(self, anchor, constant: constant, relation: .equal) + } + + @discardableResult func greaterThanOrEqual(_ anchor: Anchor, constant: CGFloat = 0) -> NSLayoutConstraint { + Constraints.constrain(self, anchor, constant: constant, relation: .greaterThanOrEqual) + } + + @discardableResult func lessThanOrEqual(_ anchor: Anchor, constant: CGFloat = 0) -> NSLayoutConstraint { + Constraints.constrain(self, anchor, constant: constant, relation: .lessThanOrEqual) + } +} + +// MARK: - Anchors (AnchorType.Dimension) + +internal extension Anchor where Type: AnchorType.Dimension { + @discardableResult func equal(_ anchor: Anchor, constant: CGFloat = 0) -> NSLayoutConstraint { + Constraints.constrain(self, anchor, constant: constant, relation: .equal) + } + + @discardableResult func greaterThanOrEqual(_ anchor: Anchor, constant: CGFloat = 0) -> NSLayoutConstraint { + Constraints.constrain(self, anchor, constant: constant, relation: .greaterThanOrEqual) + } + + @discardableResult func lessThanOrEqual(_ anchor: Anchor, constant: CGFloat = 0) -> NSLayoutConstraint { + Constraints.constrain(self, anchor, constant: constant, relation: .lessThanOrEqual) + } +} + +// MARK: - Anchors (AnchorType.Dimension) + +extension Anchor where Type: AnchorType.Dimension { + @discardableResult internal func equal(_ constant: CGFloat) -> NSLayoutConstraint { + Constraints.constrain(item: item, attribute: attribute, relatedBy: .equal, constant: constant) + } + + @discardableResult internal func greaterThanOrEqual(_ constant: CGFloat) -> NSLayoutConstraint { + Constraints.constrain(item: item, attribute: attribute, relatedBy: .greaterThanOrEqual, constant: constant) + } + + @discardableResult internal func lessThanOrEqual(_ constant: CGFloat) -> NSLayoutConstraint { + Constraints.constrain(item: item, attribute: attribute, relatedBy: .lessThanOrEqual, constant: constant) + } +} + +// MARK: - Anchors (AnchorType.Edge) + +extension Anchor where Type: AnchorType.Edge { + /// Pins the edge to the respected edges of the given container. + @discardableResult internal func pin(to container: LayoutItem? = nil, inset: CGFloat = 0) -> NSLayoutConstraint { + let isInverted = [.trailing, .right, .bottom].contains(attribute) + return Constraints.constrain(self, toItem: container ?? item.superview!, attribute: attribute, constant: (isInverted ? -inset : inset)) + } + + /// Adds spacing between the current anchors. + @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) + return Constraints.constrain(self, anchor, constant: isInverted ? -spacing : spacing, relation: isInverted ? relation.inverted : relation) + } +} + +// MARK: - Anchors (AnchorType.Center) + +extension Anchor where Type: AnchorType.Center { + /// Aligns the axis with a superview axis. + @discardableResult internal func align(offset: CGFloat = 0) -> NSLayoutConstraint { + Constraints.constrain(self, toItem: item.superview!, attribute: attribute, constant: offset) + } +} + +// MARK: - AnchorCollectionEdges + +internal struct Alignmment { + internal enum Horizontal { + case fill, center, leading, trailing + } + internal enum Vertical { + case fill, center, top, bottom + } + + internal let horizontal: Horizontal + internal let vertical: Vertical + + internal init(horizontal: Horizontal, vertical: Vertical) { + (self.horizontal, self.vertical) = (horizontal, vertical) + } + + internal static let fill = Alignmment(horizontal: .fill, vertical: .fill) + internal static let center = Alignmment(horizontal: .center, vertical: .center) + internal static let topLeading = Alignmment(horizontal: .leading, vertical: .top) + internal static let leading = Alignmment(horizontal: .leading, vertical: .fill) + internal static let bottomLeading = Alignmment(horizontal: .leading, vertical: .bottom) + internal static let bottom = Alignmment(horizontal: .fill, vertical: .bottom) + internal static let bottomTrailing = Alignmment(horizontal: .trailing, vertical: .bottom) + internal static let trailing = Alignmment(horizontal: .trailing, vertical: .fill) + internal static let topTrailing = Alignmment(horizontal: .trailing, vertical: .top) + internal static let top = Alignmment(horizontal: .fill, vertical: .top) +} + +internal struct AnchorCollectionEdges { + fileprivate let item: LayoutItem + fileprivate var isAbsolute = false + + // By default, edges use locale-specific `.leading` and `.trailing` + internal func absolute() -> AnchorCollectionEdges { + AnchorCollectionEdges(item: item, isAbsolute: true) + } + + #if os(iOS) || os(tvOS) + internal typealias Axis = NSLayoutConstraint.Axis + #else + internal typealias Axis = NSLayoutConstraint.Orientation + #endif + + @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)) + } + + 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) + 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) + if alignment.vertical == .center { + constrain(attribute: .centerY, relation: .equal, constant: 0) + } + } + return constraints + } +} + +// MARK: - AnchorCollectionCenter + +internal struct AnchorCollectionCenter { + fileprivate let x: Anchor + fileprivate let y: Anchor + + /// Centers the view in the superview. + @discardableResult internal func align() -> [NSLayoutConstraint] { + [x.align(), y.align()] + } + + /// Makes the axis equal to the other collection of axis. + @discardableResult internal func align(with item: Item) -> [NSLayoutConstraint] { + [x.equal(item.anchors.centerX), y.equal(item.anchors.centerY)] + } +} + +// MARK: - AnchorCollectionSize + +internal struct AnchorCollectionSize { + fileprivate let width: Anchor + fileprivate let height: Anchor + + /// Set the size of item. + @discardableResult internal func equal(_ size: CGSize) -> [NSLayoutConstraint] { + [width.equal(size.width), height.equal(size.height)] + } + + /// Set the size of item. + @discardableResult internal func greaterThanOrEqul(_ size: CGSize) -> [NSLayoutConstraint] { + [width.greaterThanOrEqual(size.width), height.greaterThanOrEqual(size.height)] + } + + /// Set the size of item. + @discardableResult internal func lessThanOrEqual(_ size: CGSize) -> [NSLayoutConstraint] { + [width.lessThanOrEqual(size.width), height.lessThanOrEqual(size.height)] + } + + /// Makes the size of the item equal to the size of the other item. + @discardableResult internal func equal(_ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1) -> [NSLayoutConstraint] { + [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 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)] + } +} + +// MARK: - Constraints + +internal final class Constraints { + /// Returns all of the created constraints. + internal private(set) var constraints = [NSLayoutConstraint]() + + /// All of the constraints created in the given closure are automatically + /// activated at the same time. This is more efficient then installing them + /// one-be-one. More importantly, it allows to make changes to the constraints + /// before they are installed (e.g. change `priority`). + /// + /// - parameter activate: Set to `false` to disable automatic activation of + /// constraints. + @discardableResult internal init(activate: Bool = true, _ closure: () -> Void) { + Constraints._stack.append(self) + closure() // create constraints + Constraints._stack.removeLast() + if activate { NSLayoutConstraint.activate(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 { + 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) + _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) + } + + /// 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) + } + + private static var _stack = [Constraints]() // this is what enabled constraint auto-installing + + private static func _install(_ constraint: NSLayoutConstraint) { + if let group = _stack.last { + group.constraints.append(constraint) + } else { + constraint.isActive = true + } + } +} + +extension Constraints { + @discardableResult internal convenience init(for a: A, _ closure: (LayoutAnchors) -> Void) { + self.init { closure(a.anchors) } + } + + @discardableResult internal convenience init(for a: A, _ b: B, _ closure: (LayoutAnchors, LayoutAnchors) -> Void) { + self.init { closure(a.anchors, b.anchors) } + } + + @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) { + self.init { closure(a.anchors, b.anchors, c.anchors, d.anchors) } + } +} + +// MARK: - Misc + +#if os(iOS) || os(tvOS) +internal typealias EdgeInsets = UIEdgeInsets +#elseif os(macOS) +internal typealias EdgeInsets = NSEdgeInsets + +internal extension NSEdgeInsets { + static let zero = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) +} +#endif + +extension NSLayoutConstraint.Relation { + fileprivate var inverted: NSLayoutConstraint.Relation { + switch self { + case .greaterThanOrEqual: return .lessThanOrEqual + case .lessThanOrEqual: return .greaterThanOrEqual + case .equal: return self + @unknown default: return self + } + } +} + +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 + } + } +} + +// MARK: - Extensions + +extension Anchor where Type: AnchorType.Edge { + @discardableResult internal func safeAreaPin(inset: CGFloat = 0) -> NSLayoutConstraint { + pin(to: item.superview!.safeAreaLayoutGuide, inset: inset) + } + + @discardableResult internal func readableContentPin(inset: CGFloat = 0) -> NSLayoutConstraint { + pin(to: item.superview!.readableContentGuide, inset: inset) + } + + @discardableResult internal func marginsPin(inset: CGFloat = 0) -> NSLayoutConstraint { + pin(to: item.superview!.layoutMarginsGuide, inset: inset) + } +} + +extension AnchorCollectionEdges { + @discardableResult internal func safeAreaPin(insets: EdgeInsets = .zero, axis: Axis? = nil, alignment: Alignmment = .fill) -> [NSLayoutConstraint] { + 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] { + 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] { + pin(to: item.superview!.layoutMarginsGuide, insets: insets, axis: axis, alignment: alignment) + } +} diff --git a/LockdowniOS/AppDelegate.swift b/LockdowniOS/AppDelegate.swift index b760d03..4c0c8c3 100644 --- a/LockdowniOS/AppDelegate.swift +++ b/LockdowniOS/AppDelegate.swift @@ -5,17 +5,16 @@ // Copyright © 2018 Confirmed, Inc. All rights reserved. // -import UIKit -import NetworkExtension -import SwiftyStoreKit -import KeychainAccess -import SafariServices -import SwiftMessages -import StoreKit +import BackgroundTasks import CloudKit import CocoaLumberjackSwift +import NetworkExtension import PopupDialog +import SafariServices +import SwiftMessages +import SwiftyStoreKit import PromiseKit +import WidgetKit let fileLogger: DDFileLogger = DDFileLogger() @@ -25,62 +24,44 @@ let kHasShownTitlePage: String = "kHasShownTitlePage" class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? + private(set) var timeSinceLastStart: TimeInterval = 0 + + private let connectivityService = ConnectivityService() + private let paywallService = BasePaywallService.shared + private let userService = BaseUserService.shared + let noInternetMessageView = MessageView.viewFromNib(layout: .statusLine) func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Clear local data for testing +// try? keychain.removeAll() +// for d in defaults.dictionaryRepresentation() { +// defaults.removeObject(forKey: d.key) +// } +// for d in UserDefaults.standard.dictionaryRepresentation() { +// defaults.removeObject(forKey: d.key) +// } +// return true + // Set up basic logging setupLocalLogger() - // Set up PopupDialog - let dialogAppearance = PopupDialogDefaultView.appearance() - dialogAppearance.backgroundColor = .white - dialogAppearance.titleFont = UIFont(name: "Montserrat-Bold", size: 15)! - dialogAppearance.titleColor = .darkGray - dialogAppearance.titleTextAlignment = .center - dialogAppearance.messageFont = UIFont(name: "Montserrat-Medium", size: 15)! - dialogAppearance.messageColor = .darkGray - dialogAppearance.messageTextAlignment = .center - let buttonAppearance = DefaultButton.appearance() - buttonAppearance.titleFont = UIFont(name: "Montserrat-SemiBold", size: 17)! - buttonAppearance.titleColor = UIColor.tunnelsBlue - buttonAppearance.buttonColor = .clear - buttonAppearance.separatorColor = UIColor(white: 0.9, alpha: 1) - let cancelButtonAppearance = CancelButton.appearance() - cancelButtonAppearance.titleFont = UIFont(name: "Montserrat-SemiBold", size: 17)! - cancelButtonAppearance.titleColor = UIColor.lightGray - cancelButtonAppearance.buttonColor = .clear - cancelButtonAppearance.separatorColor = UIColor(white: 0.9, alpha: 1) - + DDLogInfo("Creating protectionAccess.check file...") + ProtectedFileAccess.createProtectionAccessCheckFile() + + UNUserNotificationCenter.current().delegate = self + // Lockdown default lists setupFirewallDefaultBlockLists() // 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: "No Internet Connection") - 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") - } + connectivityService.startObservingConnectivity() // Content Blocker - SFContentBlockerManager.reloadContentBlocker( withIdentifier: "com.confirmed.lockdown.Confirmed-Blocker") { (_ error: Error?) -> Void in + SFContentBlockerManager.reloadContentBlocker(withIdentifier: LockdownStorageIdentifier.contentBlockerId) { error in if error != nil { DDLogError("Error loading Content Blocker: \(String(describing: error))") } @@ -88,9 +69,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Prepare IAP VPNSubscription.cacheLocalizedPrices() + Task { + await VPNSubscription.shared.loadSubscriptions(type: .onboarding) + await VPNSubscription.shared.loadSubscriptions(type: .oneTime) + await VPNSubscription.shared.loadSubscriptions(type: .feedback) + await VPNSubscription.shared.loadSubscriptions(type: .specialOffer) + } 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)") @@ -100,70 +87,147 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - // Periodically check if the firewall is functioning correctly - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) + // 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.titleTextAlignment = .center + dialogAppearance.messageFont = fontMedium15 + 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.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.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.titleColor = UIColor.lightGray + + // Periodically check if the firewall is functioning correctly - every 2.5 hours + 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() + setupObservingSubscritionChanges() - // If not yet agreed to privacy policy, set initial view controller to TitleViewController - if (defaults.bool(forKey: kHasShownTitlePage) == false) { - // 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 { - DDLogInfo("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() return true } + func applicationDidEnterBackground(_ application: UIApplication) { + DDLogInfo("applicationDidEnterBackground") + FirewallRepair.reschedule() + } + + func applicationDidBecomeActive(_ application: UIApplication) { + let isCleanInstall = defaults.value(forKey: kOneTimeOfferShown) == nil + if isCleanInstall { + defaults.set(true, forKey: kSpecialOfferTimeDidReset) + } + let shouldResetOfferTime = !defaults.bool(forKey: kSpecialOfferTimeDidReset) + if shouldResetOfferTime && !isCleanInstall { + timeSinceLastStart = Date().timeIntervalSinceReferenceDate + defaults.set(true, forKey: kSpecialOfferTimeDidReset) + } else { + let lastStartup = defaults.double(forKey: kAppActivateTime) + timeSinceLastStart = lastStartup == 0.0 ? -1 : Date().timeIntervalSinceReferenceDate - lastStartup + } + defaults.set(Date().timeIntervalSinceReferenceDate, forKey: kAppActivateTime) + if timeSinceLastStart < 0 || timeSinceLastStart > 120 { + defaults.set(false, forKey: kOneTimeOfferShown) + } + + DDLogInfo("applicationDidBecomeActive") + PacketTunnelProviderLogs.flush() + flushBlockLog(log: { _ in }) + updateMetrics(.resetIfNeeded, rescheduleNotifications: .always) + + FirewallRepair.run(context: .homeScreenDidLoad) + + if UserDefaults.onboardingCompleted { + ReviewAlertManager.checkAndShowAlert() + } + } + + func applicationWillResignActive(_ application: UIApplication) { + DDLogInfo("applicationWillResignActive") + if #available(iOS 14.0, *) { + WidgetCenter.shared.reloadAllTimelines() + } + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + DDLogError("Successfully registered for remote notification: \(deviceToken)") + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + DDLogError("Error registering for remote notification: \(error)") + } + func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - if getUserWantsFirewallEnabled() && FirewallController.shared.status() == .connected { - DDLogInfo("user wants firewall enabled and connected, testing blocking with background fetch") - _ = Client.getBlockedDomainTest(connectionSuccessHandler: { - DDLogError("Background Fetch Test: Connected to \(testFirewallDomain) even though it's supposed to be blocked, restart the Firewall") - FirewallController.shared.restart(completion: { - error in - if error != nil { - DDLogError("Error restarting firewall on background fetch: \(error!)") - } - completionHandler(.newData) - }) - }, connectionFailedHandler: { - error in - if error != nil { - let nsError = error! as NSError - if nsError.domain == NSURLErrorDomain { - DDLogInfo("Background Fetch Test: Successful blocking of \(testFirewallDomain) with NSURLErrorDomain error: \(nsError)") - } - else { - DDLogInfo("Background Fetch Test: Successful blocking of \(testFirewallDomain), but seeing non-NSURLErrorDomain error: \(error!)") - } - } + + // Deprecated, uses BackgroundTasks after iOS 13+ + DDLogInfo("BGF called, running Repair") + FirewallRepair.run(context: .backgroundRefresh) { (result) in + switch result { + case .failed: + DDLogInfo("BGF: failed") + completionHandler(.failed) + case .repairAttempted: + DDLogInfo("BGF: attempted") completionHandler(.newData) - }) - } - else { - completionHandler(.newData) + case .noAction: + DDLogInfo("BGF: no action") + completionHandler(.noData) + } } } func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return .portrait } - + // MARK: - WIDGET TOGGLE WORKAROUND func setupWidgetToggleWorkaround() { DDLogInfo("Setting up CloudKit Workaround") @@ -172,72 +236,79 @@ class AppDelegate: UIResponder, UIApplicationDelegate { clearDatabaseForRecord(recordName: kRestartFirewallTunnelRecord) let privateDatabase = CKContainer(identifier: kICloudContainer).privateCloudDatabase privateDatabase.fetchAllSubscriptions(completionHandler: { subscriptions, error in - if error == nil, let subs = subscriptions { - var isSubscribedToOpen = false - var isSubscribedToClose = false - var isSubscribedToRestart = false - for subscriptionObject in subs { - if subscriptionObject.notificationInfo?.category == kCloseFirewallTunnelRecord { - isSubscribedToClose = true - } - if subscriptionObject.notificationInfo?.category == kOpenFirewallTunnelRecord { - isSubscribedToOpen = true - } - if subscriptionObject.notificationInfo?.category == kRestartFirewallTunnelRecord { - isSubscribedToRestart = true - } - } - if !isSubscribedToOpen { - self.setupCloudKitSubscription(categoryName: kOpenFirewallTunnelRecord) - } - if !isSubscribedToClose { - self.setupCloudKitSubscription(categoryName: kCloseFirewallTunnelRecord) - } - if !isSubscribedToRestart { - self.setupCloudKitSubscription(categoryName: kRestartFirewallTunnelRecord) - } - } - else { + // 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 +// var isSubscribedToOpen = false +// var isSubscribedToClose = false +// var isSubscribedToRestart = false +// for subscriptionObject in subs { +// if subscriptionObject.notificationInfo?.category == kCloseFirewallTunnelRecord { +// isSubscribedToClose = true +// } +// if subscriptionObject.notificationInfo?.category == kOpenFirewallTunnelRecord { +// isSubscribedToOpen = true +// } +// if subscriptionObject.notificationInfo?.category == kRestartFirewallTunnelRecord { +// isSubscribedToRestart = true +// } +// } +// if !isSubscribedToOpen { +// self.setupCloudKitSubscription(categoryName: kOpenFirewallTunnelRecord) +// } +// if !isSubscribedToClose { +// self.setupCloudKitSubscription(categoryName: kCloseFirewallTunnelRecord) +// } +// if !isSubscribedToRestart { +// self.setupCloudKitSubscription(categoryName: kRestartFirewallTunnelRecord) +// } +// } +// else { self.setupCloudKitSubscription(categoryName: kCloseFirewallTunnelRecord) self.setupCloudKitSubscription(categoryName: kOpenFirewallTunnelRecord) self.setupCloudKitSubscription(categoryName: kRestartFirewallTunnelRecord) - } +// } }) } - func setupCloudKitSubscription(categoryName : String) { + func setupCloudKitSubscription(categoryName: String) { let privateDatabase = CKContainer(identifier: kICloudContainer).privateCloudDatabase - let predicate = NSPredicate(value: true) let subscription = CKQuerySubscription(recordType: categoryName, - predicate: predicate, + predicate: NSPredicate(value: true), options: .firesOnRecordCreation) - let notificationInfo = CKSubscription.NotificationInfo() - notificationInfo.alertBody = "" + //notificationInfo.alertBody = "" // iOS 13 doesn't like this - fails to trigger notification notificationInfo.shouldSendContentAvailable = true notificationInfo.shouldBadge = false notificationInfo.category = categoryName - subscription.notificationInfo = notificationInfo - - privateDatabase.save(subscription, - completionHandler: ({returnRecord, error in - if let err = error { - DDLogInfo("Could not save CloudKit subscription (signed in?) \(err)") - } else { - DispatchQueue.main.async() { - DDLogInfo("Successfully saved CloudKit subscription") - } - } - })) + privateDatabase.save(subscription) { _, error in + if let err = error { + DDLogInfo("Could not save CloudKit subscription (not signed in?) \(err)") + } else { + DispatchQueue.main.async { + DDLogInfo("Successfully saved CloudKit subscription") + } + } + } } func clearDatabaseForRecord(recordName: String) { let privateDatabase = CKContainer(identifier: kICloudContainer).privateCloudDatabase - let predicate = NSPredicate.init(value: true) - let query = CKQuery.init(recordType: recordName, predicate: predicate) + let predicate = NSPredicate(value: true) + let query = CKQuery(recordType: recordName, predicate: predicate) privateDatabase.perform(query, inZoneWith: nil) { (record, error) in + if let err = error { + DDLogError("Error querying for CKRecordType: \(recordName) - \(err)") + } for aRecord in record! { privateDatabase.delete(withRecordID: aRecord.recordID, completionHandler: { (recordID, error) in DDLogInfo("Deleting record \(aRecord.recordID)") @@ -254,13 +325,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { 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!)") } @@ -275,6 +343,285 @@ class AppDelegate: UIResponder, UIApplicationDelegate { completionHandler(.newData) }) } + + 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 { + print("Invalid URL") + return false + } + + if (host == "resetsuccessful") { + let popup = PopupDialog(title: "Password Reset Successfully", + message: "Please Sign In With Your New Password", + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + popup.addButtons([ + DefaultButton(title: .localizedOkay, dismissOnTap: true) { + if let hvc = self.getCurrentViewController() as? HomeViewController { + AccountUI.presentSignInToAccount(on: hvc) + } + } + ]) + self.getCurrentViewController()?.present(popup, animated: true, completion: nil) + return true + } + + else if (host == "changeVPNregion") { + if let home = self.getCurrentViewController() as? HomeViewController { + home.showSetRegion(self) + } + } + + else if (host == "showMetrics") { + if let home = self.getCurrentViewController() as? HomeViewController { + home.showBlockLog(self) + } + } + + else if (host == "toggleFirewall") { + if let home = self.getCurrentViewController() as? HomeViewController { + home.toggleFirewall(self) + } + } + + else if (host == "toggleVPN") { + if let home = self.getCurrentViewController() as? HomeViewController { + home.toggleFirewall(self) + } + } + + 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) + popup.addButtons([ + DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {} + ]) + getCurrentViewController()?.present(popup, animated: true, completion: nil) + return true + } + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .done { (signin: 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 + try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + if (getUserWantsVPNEnabled() == true) { + VPNController.shared.restart() + } + } + .catch { error in + // it's okay for this to error out with "no subscription in receipt" + DDLogError("HomeViewController ConfirmEmail subscriptionevent error (ok for it to be \"no subscription in receipt\"): \(error)") + } + let popup = PopupDialog(title: "Success! 🎉", + message: NSLocalizedString("Your account has been confirmed and you're now signed in. You'll get the latest block lists, access to Lockdown Mac, and get critical announcements.", comment: ""), + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + popup.addButtons([ + DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {} + ]) + self.getCurrentViewController()?.present(popup, animated: true, completion: nil) + DispatchQueue.main.async { + NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) + } + } + .catch { error in + var errorMessage = error.localizedDescription + DDLogError("AppDelegate error: \(errorMessage)") + if let apiError = error as? ApiError { + errorMessage = apiError.message + } + + 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: ""))", + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + popup.addButtons([ + DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true) {} + ]) + self.getCurrentViewController()?.present(popup, animated: true, completion: nil) + } + } + + return true + } + + // MARK: - Utilities + // Returns the most recently presented UIViewController (visible) + func getCurrentViewController() -> UIViewController? { + return getCurrentViewController(in: UIApplication.shared.keyWindow?.rootViewController) + } + + private func getCurrentViewController(in root: UIViewController?) -> UIViewController? { + // If the root view is a navigation controller, we can just return the visible ViewController + if let navigationController = getNavigationController(in: root) { + return navigationController.visibleViewController + } + if let tabBarVC = root as? UITabBarController { + if let nvc = getNavigationController(in: tabBarVC.selectedViewController) { + return nvc.visibleViewController + } else if let selected = tabBarVC.selectedViewController { + return selected + } + } + // Otherwise, we must get the root UIViewController and iterate through presented views + if let rootController = root { + 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 ) { + currentController = currentController.presentedViewController + } + return currentController + } + return nil + } + + // Returns the navigation controller if it exists + func getNavigationController(in root: UIViewController?) -> UINavigationController? { + if let navigationController = root { + return navigationController as? UINavigationController + } + return nil + } } +extension AppDelegate: UNUserNotificationCenterDelegate { + 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) { + let identifier = PushNotifications.Identifier(rawValue: response.notification.request.identifier) + if identifier.isWeeklyUpdate { + showUpdateBlockListsFlow() + } else if identifier == .onboarding { + highlightBlockLogOnHomeVC() + } + completionHandler() + } + + private func highlightBlockLogOnHomeVC() { + if let hvc = self.getCurrentViewController() as? HomeViewController { + hvc.highlightBlockLog() + } + } + + private func showUpdateBlockListsFlow() { + // the actual update happens in `appHasJustBeenUpgradedOrIsNewInstall`, + // these are supporting visuals + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + self.showUpdatingBlockListsLoader() + DispatchQueue.main.asyncAfter(deadline: .now() + 2.25) { + self.hideUpdatingBlockListsLoader() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + self.showBlockListsUpdatedPopup() + } + } + } + } + + private func showUpdatingBlockListsLoader() { + let activity = ActivityData( + 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) + ) + NVActivityIndicatorPresenter.sharedInstance.startAnimating(activity, NVActivityIndicatorView.DEFAULT_FADE_IN_ANIMATION) + } + + private func hideUpdatingBlockListsLoader() { + NVActivityIndicatorPresenter.sharedInstance.stopAnimating(NVActivityIndicatorView.DEFAULT_FADE_OUT_ANIMATION) + } + + private func showBlockListsUpdatedPopup() { + let popup = PopupDialog( + title: NSLocalizedString("Update Success", comment: ""), + message: "You're now protected against the latest trackers. 🎉" + ) + popup.addButton(DefaultButton(title: .localizedOkay, dismissOnTap: true, action: nil)) + self.getCurrentViewController()?.present(popup, animated: true, completion: nil) + } + + // MARK: - subscription + + private func setupObservingSubscritionChanges() { + NotificationCenter.default.addObserver( + self, + selector: #selector(openSplashScreen), + name: AccountUI.subscritionTypeChanged, + object: nil + ) + } + + @objc + private func openSplashScreen() { + let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + let vc = SplashScreenViewController() + let navigation = UINavigationController(rootViewController: vc) + keyWindow?.rootViewController = navigation + } +} + +extension PacketTunnelProviderLogs { + static func flush() { + guard !PacketTunnelProviderLogs.allEntries.isEmpty else { + DDLogInfo("Packet Tunnel Provider Logs: EMPTY") + return + } + + DDLogInfo("Packet Tunnel Provider Logs: START") + for logEntry in PacketTunnelProviderLogs.allEntries { + DDLogError(logEntry) + } + DDLogInfo("Packet Tunnel Provider Logs: END") + PacketTunnelProviderLogs.clear() + } +} diff --git a/LockdowniOS/Availability.swift b/LockdowniOS/Availability.swift new file mode 100644 index 0000000..d461d17 --- /dev/null +++ b/LockdowniOS/Availability.swift @@ -0,0 +1,292 @@ + +// Copyright (c) 2014, Ashley Mills + +import SystemConfiguration +import Foundation + +public enum ReachabilityError: Error { + case FailedToCreateWithAddress(sockaddr_in) + case FailedToCreateWithHostname(String) + case UnableToSetCallback + case UnableToSetDispatchQueue + case UnableToGetInitialFlags +} + +@available(*, unavailable, renamed: "Notification.Name.availabilityChanged") +public let AvailabilityChangedNotification = NSNotification.Name("AvailabilityChangedNotification") + +public extension Notification.Name { + static let availabilityChanged = Notification.Name("availabilityChanged") +} + +public class Availability { + + public typealias NetworkReachable = (Availability) -> () + public typealias NetworkUnreachable = (Availability) -> () + + @available(*, unavailable, renamed: "Connection") + public enum NetworkStatus: CustomStringConvertible { + case notReachable, reachableViaWiFi, reachableViaWWAN + public var description: String { + switch self { + case .reachableViaWWAN: return "Cellular" + case .reachableViaWiFi: return "WiFi" + case .notReachable: return "No Connection" + } + } + } + + public enum Connection: CustomStringConvertible { + case none, wifi, cellular + public var description: String { + switch self { + case .cellular: return "Cellular" + case .wifi: return "WiFi" + case .none: return "No Connection" + } + } + } + + public var whenReachable: NetworkReachable? + public var whenUnreachable: NetworkUnreachable? + + @available(*, deprecated, renamed: "allowsCellularConnection") + public let reachableOnWWAN: Bool = true + + /// Set to `false` to force Reachability.connection to .none when on cellular connection (default value `true`) + public var allowsCellularConnection: Bool + + // The notification center on which "reachability changed" events are being posted + public var notificationCenter: NotificationCenter = NotificationCenter.default + + @available(*, deprecated, renamed: "connection.description") + public var currentReachabilityString: String { + return "\(connection)" + } + + @available(*, unavailable, renamed: "connection") + public var currentReachabilityStatus: Connection { + return connection + } + + public var connection: Connection { + if flags == nil { + try? setReachabilityFlags() + } + + switch flags?.connection { + case .none?, nil: return .none + case .cellular?: return allowsCellularConnection ? .cellular : .none + case .wifi?: return .wifi + } + } + + fileprivate var isRunningOnDevice: Bool = { + #if targetEnvironment(simulator) + return false + #else + return true + #endif + }() + + fileprivate var notifierRunning = false + fileprivate let ref: SCNetworkReachability + fileprivate let serialQueue: DispatchQueue + fileprivate(set) var flags: SCNetworkReachabilityFlags? { + didSet { + guard flags != oldValue else { return } + availabilityChanged() + } + } + + required public init(availabilityRef: SCNetworkReachability, queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { + self.allowsCellularConnection = true + self.ref = availabilityRef + self.serialQueue = DispatchQueue(label: "uk.co.ashleymills.availability", qos: queueQoS, target: targetQueue) + } + + public convenience init?(hostname: String, queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { + guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else { return nil } + self.init(availabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue) + } + + public convenience init?(queueQoS: DispatchQoS = .default, targetQueue: DispatchQueue? = nil) { + var zeroAddress = sockaddr() + zeroAddress.sa_len = UInt8(MemoryLayout.size) + zeroAddress.sa_family = sa_family_t(AF_INET) + + guard let ref = SCNetworkReachabilityCreateWithAddress(nil, &zeroAddress) else { return nil } + + self.init(availabilityRef: ref, queueQoS: queueQoS, targetQueue: targetQueue) + } + + deinit { + stopNotifier() + } +} + +public extension Availability { + + // MARK: - *** Notifier methods *** + func startNotifier() throws { + guard !notifierRunning else { return } + + let callback: SCNetworkReachabilityCallBack = { (reachability, flags, info) in + guard let info = info else { return } + + let reachability = Unmanaged.fromOpaque(info).takeUnretainedValue() + reachability.flags = flags + } + + var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil) + context.info = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + if !SCNetworkReachabilitySetCallback(ref, callback, &context) { + stopNotifier() + throw ReachabilityError.UnableToSetCallback + } + + if !SCNetworkReachabilitySetDispatchQueue(ref, serialQueue) { + stopNotifier() + throw ReachabilityError.UnableToSetDispatchQueue + } + + // Perform an initial check + try setReachabilityFlags() + + notifierRunning = true + } + + func stopNotifier() { + defer { notifierRunning = false } + + SCNetworkReachabilitySetCallback(ref, nil, nil) + SCNetworkReachabilitySetDispatchQueue(ref, nil) + } + + // MARK: - *** Connection test methods *** + @available(*, deprecated, message: "Please use `connection != .none`") + var isReachable: Bool { + return connection != .none + } + + @available(*, deprecated, message: "Please use `connection == .cellular`") + var isReachableViaWWAN: Bool { + // Check we're not on the simulator, we're REACHABLE and check we're on WWAN + return connection == .cellular + } + + @available(*, deprecated, message: "Please use `connection == .wifi`") + var isReachableViaWiFi: Bool { + return connection == .wifi + } + + var description: String { + guard let flags = flags else { return "unavailable flags" } + let W = isRunningOnDevice ? (flags.isOnWWANFlagSet ? "W" : "-") : "X" + let R = flags.isReachableFlagSet ? "R" : "-" + let c = flags.isConnectionRequiredFlagSet ? "c" : "-" + let t = flags.isTransientConnectionFlagSet ? "t" : "-" + let i = flags.isInterventionRequiredFlagSet ? "i" : "-" + let C = flags.isConnectionOnTrafficFlagSet ? "C" : "-" + let D = flags.isConnectionOnDemandFlagSet ? "D" : "-" + let l = flags.isLocalAddressFlagSet ? "l" : "-" + let d = flags.isDirectFlagSet ? "d" : "-" + + return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)" + } +} + +fileprivate extension Availability { + + func setReachabilityFlags() throws { + try serialQueue.sync { [unowned self] in + var flags = SCNetworkReachabilityFlags() + if !SCNetworkReachabilityGetFlags(self.ref, &flags) { + self.stopNotifier() + throw ReachabilityError.UnableToGetInitialFlags + } + + self.flags = flags + } + } + + func availabilityChanged() { + let block = connection != .none ? whenReachable : whenUnreachable + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + block?(self) + self.notificationCenter.post(name: .availabilityChanged, object: self) + } + } +} + +extension SCNetworkReachabilityFlags { + + typealias Connection = Availability.Connection + + var connection: Connection { + guard isReachableFlagSet else { return .none } + + // If we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi + #if targetEnvironment(simulator) + return .wifi + #else + var connection = Connection.none + + if !isConnectionRequiredFlagSet { + connection = .wifi + } + + if isConnectionOnTrafficOrDemandFlagSet { + if !isInterventionRequiredFlagSet { + connection = .wifi + } + } + + if isOnWWANFlagSet { + connection = .cellular + } + + return connection + #endif + } + + var isOnWWANFlagSet: Bool { + #if os(iOS) + return contains(.isWWAN) + #else + return false + #endif + } + var isReachableFlagSet: Bool { + return contains(.reachable) + } + var isConnectionRequiredFlagSet: Bool { + return contains(.connectionRequired) + } + var isInterventionRequiredFlagSet: Bool { + return contains(.interventionRequired) + } + var isConnectionOnTrafficFlagSet: Bool { + return contains(.connectionOnTraffic) + } + var isConnectionOnDemandFlagSet: Bool { + return contains(.connectionOnDemand) + } + var isConnectionOnTrafficOrDemandFlagSet: Bool { + return !intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty + } + var isTransientConnectionFlagSet: Bool { + return contains(.transientConnection) + } + var isLocalAddressFlagSet: Bool { + return contains(.isLocalAddress) + } + var isDirectFlagSet: Bool { + return contains(.isDirect) + } + var isConnectionRequiredAndTransientFlagSet: Bool { + return intersection([.connectionRequired, .transientConnection]) == [.connectionRequired, .transientConnection] + } +} diff --git a/LockdowniOS/Base.lproj/LaunchScreen.storyboard b/LockdowniOS/Base.lproj/LaunchScreen.storyboard index f651bd4..7059bb5 100644 --- a/LockdowniOS/Base.lproj/LaunchScreen.storyboard +++ b/LockdowniOS/Base.lproj/LaunchScreen.storyboard @@ -1,11 +1,11 @@ - - - - + + - + + + @@ -13,14 +13,31 @@ - - - - - + - + + + + + + + + + + + + + + + + + + + + + + @@ -28,4 +45,11 @@ + + + + + + + diff --git a/LockdowniOS/Base.lproj/Main.storyboard b/LockdowniOS/Base.lproj/Main.storyboard index 1574b53..50d9531 100644 --- a/LockdowniOS/Base.lproj/Main.storyboard +++ b/LockdowniOS/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - - - - + + - + + + @@ -21,38 +21,70 @@ Montserrat-SemiBold + + SFProRounded-Bold + + + SFProRounded-Medium + + + SFProRounded-Regular + - + - + + + + + + + + + + + - + - - + + - + + @@ -86,7 +119,7 @@ - + + + - + - - + + + + + + + + + + + + + - + - + - + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - + - + - + - + - + - - - + + - - + + - - + + - - - + + - - - + + - + @@ -525,11 +445,11 @@ - - + + @@ -557,10 +476,10 @@ - + @@ -588,12 +506,13 @@ + - - + + + + + + @@ -615,10 +570,11 @@ - + + - + @@ -643,31 +599,45 @@ - + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - @@ -684,18 +654,23 @@ - - + + + + - + @@ -705,7 +680,7 @@ - + @@ -716,15 +691,18 @@ - + + + + - - + + @@ -733,17 +711,17 @@ - + - - + + - + - - + - - - + + + - - + + + + + @@ -915,7 +898,7 @@ - + @@ -953,84 +936,69 @@ - + + + + + + + + + + + + - - - - - - - + + - - - - - - - - - - - - + + + - + - + - - - - - - - - - + + + + + + + - + @@ -1041,14 +1009,14 @@ - + - + - + @@ -1070,7 +1038,7 @@ - + @@ -1081,11 +1049,11 @@ - + - - + @@ -1146,7 +1122,11 @@ + + + + @@ -1154,75 +1134,108 @@ + - + - + - + - + - - + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + - - + + - + - + @@ -1242,8 +1255,8 @@ - - + - - + + - + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + - + @@ -1796,22 +1397,22 @@ - + - + - + - - + + - - + @@ -1953,17 +1558,17 @@ - + - + - - - + + - + - + - @@ -2050,21 +1658,20 @@ - + - + @@ -2092,11 +1699,11 @@ - + - - + - - - - + - - + + - - - - - - - + + + + + - - - - + @@ -2256,14 +1829,12 @@ - + - - @@ -2283,11 +1854,11 @@ - + - - + - - + + @@ -2358,8 +1928,8 @@ - - + @@ -2393,8 +1962,8 @@ - - - + - + @@ -2465,13 +2034,1193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/BaseViewController.swift b/LockdowniOS/BaseViewController.swift index ee7e953..21c9672 100644 --- a/LockdowniOS/BaseViewController.swift +++ b/LockdowniOS/BaseViewController.swift @@ -9,6 +9,8 @@ import UIKit import MessageUI import CocoaLumberjackSwift import PopupDialog +import PromiseKit +import StoreKit open class BaseViewController: UIViewController, MFMailComposeViewControllerDelegate { @@ -17,9 +19,14 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele override open func viewDidLoad() { super.viewDidLoad() - let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(emailTeam)) - longPressRecognizer.minimumPressDuration = 4 - self.view.addGestureRecognizer(longPressRecognizer) + // disable swipe down to dismiss + if #available(iOS 13.0, *) { + self.isModalInPresentation = true + } + +// let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(emailTeam)) +// longPressRecognizer.minimumPressDuration = 4 +// self.view.addGestureRecognizer(longPressRecognizer) // let doubleLongPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(signoutUser)) // doubleLongPressRecognizer.minimumPressDuration = 5 @@ -27,12 +34,21 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele // self.view.addGestureRecognizer(doubleLongPressRecognizer) } + // MARK: - AwesomeSpotlight Helper + + func getRectForView(_ v: UIView) -> CGRect { + if let sv = v.superview { + return sv.convert(v.frame, to: self.view) + } + return CGRect.zero; + } + // MARK: - Handle NSURLError and APIErrors func popupErrorAsNSURLError(_ error: Error) -> Bool { let nsError = error as NSError if nsError.domain == NSURLErrorDomain { - self.showPopupDialog(title: "Network Error", message: "Please check your internet connection. If this persists, please contact team@lockdownhq.com.\n\nError Description\n\(nsError.localizedDescription)", acceptButton: "Okay") + self.showPopupDialog(title: NSLocalizedString("Network Error", comment: ""), message: NSLocalizedString("Please check your internet connection. If this persists, please contact team@lockdownprivacy.com.\n\nError Description\n", comment: "") + nsError.localizedDescription, acceptButton: NSLocalizedString("Okay", comment: "")) return true } else { @@ -42,7 +58,7 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele func popupErrorAsApiError(_ error: Error) -> Bool { if let e = error as? ApiError { - self.showPopupDialog(title: "Error Code \(e.code)", message: "\(e.message)\n\n If this persists, please contact team@lockdownhq.com.", acceptButton: "Okay") + self.showPopupDialog(title: NSLocalizedString("Error Code ", comment: "") + "\(e.code)", message: "\(e.message)" + NSLocalizedString("\n\n If this persists, please contact team@lockdownprivacy.com.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) return true } else { @@ -52,8 +68,8 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele func showWhyTrustPopup() { let popup = PopupDialog( - title: "Why Trust Lockdown?", - message: "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.", + title: NSLocalizedString("Why Trust Lockdown?", comment: ""), + message: NSLocalizedString("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.", comment: ""), image: UIImage(named: "whyTrustImage")!, buttonAlignment: .vertical, transitionStyle: .bounceDown, @@ -63,74 +79,220 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele hideStatusBar: true, completion: nil) - let privacyPolicyButton = DefaultButton(title: "Privacy Policy", dismissOnTap: true) { + let privacyPolicyButton = DefaultButton(title: NSLocalizedString("Privacy Policy", comment: ""), dismissOnTap: true) { self.showPrivacyPolicyModal() } - let auditReportsButton = DefaultButton(title: "Audit Reports", dismissOnTap: true) { + let auditReportsButton = DefaultButton(title: NSLocalizedString("Audit Reports", comment: ""), dismissOnTap: true) { self.showAuditModal() } - let pressButton = DefaultButton(title: "Press & Media", dismissOnTap: true) { + let pressButton = DefaultButton(title: NSLocalizedString("Press & Media", comment: ""), dismissOnTap: true) { self.showWebsitePressModal() } - let okayButton = CancelButton(title: "Done", dismissOnTap: true) { } + let okayButton = CancelButton(title: NSLocalizedString("Done", comment: ""), dismissOnTap: true) { } popup.addButtons([privacyPolicyButton, auditReportsButton, pressButton, okayButton]) self.present(popup, animated: true, completion: nil) } func showVPNDetails() { - let popup = PopupDialog( - title: "About Lockdown VPN", - message: "Lockdown VPN is powered by Confirmed VPN, the open source, no-logs, and fully audited VPN.", - buttonAlignment: .vertical, - transitionStyle: .bounceDown, - preferredWidth: 300.0, - tapGestureDismissal: true, - panGestureDismissal: false, - hideStatusBar: true, - completion: nil) + self.showModalWebView(title: NSLocalizedString("Secure Tunnel VPN", comment: ""), urlString: "https://lockdownprivacy.com/secure-tunnel") +// let popup = PopupDialog( +// title: NSLocalizedString("About Lockdown VPN", comment: ""), +// message: NSLocalizedString("Lockdown VPN is powered by Confirmed VPN, the open source, no-logs, and fully audited VPN.", comment: ""), +// buttonAlignment: .vertical, +// transitionStyle: .bounceDown, +// preferredWidth: 300.0, +// tapGestureDismissal: true, +// panGestureDismissal: false, +// hideStatusBar: true, +// completion: nil) +// +// let whyUseVPNButton = DefaultButton(title: NSLocalizedString("Why Use VPN?", comment: ""), dismissOnTap: true) { +// self.showModalWebView(title: NSLocalizedString("Why Use VPN?", comment: ""), urlString: "https://confirmedvpn.com/why-vpn") +// } +// +// let auditReportsButton = DefaultButton(title: NSLocalizedString("Audit Reports", comment: ""), dismissOnTap: true) { +// self.showAuditModal() +// } +// +// let confirmedWebsiteButton = DefaultButton(title: NSLocalizedString("Confirmed Site", comment: ""), dismissOnTap: true) { +// self.showModalWebView(title: NSLocalizedString("Why Use VPN?", comment: ""), urlString: "https://confirmedvpn.com") +// } - let whyUseVPNButton = DefaultButton(title: "Why Use VPN?", dismissOnTap: true) { - self.showModalWebView(title: "Why VPN?", urlString: "https://confirmedvpn.com/why-vpn") - } +// let okayButton = CancelButton(title: NSLocalizedString("Done", comment: ""), dismissOnTap: true) { } +// popup.addButtons([whyUseVPNButton, auditReportsButton, confirmedWebsiteButton, okayButton]) +// +// self.present(popup, animated: true, completion: nil) + } + + func handlePurchaseSuccessful(placement: PurchasePlacement = .homeScreen, completion: (()->Void)? = nil) { + let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + let vc = SplashScreenViewController() + let navigation = UINavigationController(rootViewController: vc) + keyWindow?.rootViewController = navigation - let auditReportsButton = DefaultButton(title: "Audit Reports", dismissOnTap: true) { - self.showAuditModal() + // force refresh receipt, and sync with email if it exists + 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<[Subscription]> in + DDLogInfo("plan status: subscriptionevent result: \(result)") + return try Client.activeSubscriptions() + } + .done { subscriptions in + DDLogInfo("active-subs (start trial): \(subscriptions)") + NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) + + BaseUserService.shared.user.updateSubscription(to: subscriptions.first) + + if subscriptions.first != nil { + if placement == .onboarding { + UserDefaults.hasPurchasedFromOnboarding = true + } else if UserDefaults.hasPurchasedFromOnboarding { + UserDefaults.shouldShowMultipleSubscriptionAlert = true + NotificationCenter.default.post(name: .showMultipleSubscriptionsAlert, object: nil) + } + } + } + .ensure { + completion?() + } + .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() + }.then { _ in + try Client.activeSubscriptions() + }.done { subscriptions in + DDLogInfo("active-subs (start trial): \(subscriptions)") + NotificationCenter.default.post(name: AccountUI.accountStateDidChange, object: self) + + BaseUserService.shared.user.updateSubscription(to: subscriptions.first) + + if subscriptions.first != nil { + if placement == .onboarding { + UserDefaults.hasPurchasedFromOnboarding = true + } else if UserDefaults.hasPurchasedFromOnboarding { + UserDefaults.shouldShowMultipleSubscriptionAlert = true + NotificationCenter.default.post(name: .showMultipleSubscriptionsAlert, object: nil) + } + } + } + .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)") + } + } + } } - - let confirmedWebsiteButton = DefaultButton(title: "Confirmed Site", dismissOnTap: true) { - self.showModalWebView(title: "Why VPN?", urlString: "https://confirmedvpn.com") + } + + @objc func showMultipleSubscriptionsAlert() { + self.showPopupDialog(title: NSLocalizedString("multiple_subscriptions_title", comment: ""), + message: NSLocalizedString("multiple_subscriptions_message", comment: ""), + buttons: [.defaultAccept(completion: { + UserDefaults.didShowMultipleSubscriptionAlert = true + })]) + } + + func handlePurchaseFailed(error: 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 Making Purchase"), message: errorText, acceptButton: .localizedOkay) + } + else if self.popupErrorAsNSURLError(error) { + return + } + else if self.popupErrorAsApiError(error) { + return + } + else { + self.showPopupDialog( + title: .localized("Error Making Purchase"), + message: .localized("Please contact team@lockdownprivacy.com.\n\nError details:\n") + "\(error)", + acceptButton: .localizedOkay) } - - let okayButton = CancelButton(title: "Done", dismissOnTap: true) { } - popup.addButtons([whyUseVPNButton, auditReportsButton, confirmedWebsiteButton, okayButton]) - - self.present(popup, animated: true, completion: nil) } // MARK: - WebView + func showWhatsNewModal() { + let vc = WhatsNewViewController() + present(vc, animated: true) + } + func showPrivacyPolicyModal() { - self.showModalWebView(title: "Privacy Policy", urlString: "https://lockdownhq.com/privacy") + self.showModalWebView(title: NSLocalizedString("Privacy Policy", comment: ""), urlString: "https://lockdownprivacy.com/privacy") } func showTermsModal() { - self.showModalWebView(title: "Terms", urlString: "https://lockdownhq.com/terms") + self.showModalWebView(title: NSLocalizedString("Terms", comment: ""), urlString: "https://lockdownprivacy.com/terms") + } + + func showFAQsModal() { + self.showModalWebView(title: NSLocalizedString("FAQs", comment: ""), urlString: "https://lockdownprivacy.com/faq") } func showWebsiteModal() { - self.showModalWebView(title: "Website", urlString: "https://lockdownhq.com") + self.showModalWebView(title: NSLocalizedString("Website", comment: ""), urlString: "https://lockdownprivacy.com") } func showWebsitePressModal() { - self.showModalWebView(title: "Press & Media", urlString: "https://lockdownhq.com/#press") + self.showModalWebView(title: NSLocalizedString("Press & Media", comment: ""), urlString: "https://lockdownprivacy.com/#press") } func showAuditModal() { - self.showModalWebView(title: "Audit Reports", urlString: "https://openlyoperated.org/report/confirmedvpn") + self.showModalWebView(title: NSLocalizedString("Audit Reports", comment: ""), urlString: "https://openaudit.com/lockdownprivacy") } func showModalWebView(title: String, urlString: String) { @@ -171,12 +333,83 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele func showPopupDialog(title: String, message: String, acceptButton: String, completionHandler: @escaping () -> () = {}) { let popup = PopupDialog(title: title.uppercased(), message: message, image: nil, transitionStyle: .bounceDown, hideStatusBar: false) - let acceptButton = DefaultButton(title: "OK", dismissOnTap: true) { completionHandler() } + let acceptButton = DefaultButton(title: NSLocalizedString("OK", comment: ""), dismissOnTap: true) { completionHandler() } popup.addButtons([acceptButton]) + let topVC = presentedViewController ?? self + topVC.present(popup, animated: true, completion: nil) + } + + enum PopupButton { + case custom(PopupDialogButton) + case defaultAccept(completion: () -> ()) + + static func custom(title: String, titleColor: UIColor? = nil, completion: @escaping () -> ()) -> PopupButton { + let button = DefaultButton(title: title, dismissOnTap: true, action: completion) + if let color = titleColor { + button.titleColor = color + } + return .custom(button) + } + + static func destructive(title: String, completion: @escaping () -> ()) -> PopupButton { + return .custom(title: title, titleColor: UIColor.systemRed, completion: completion) + } + + static func cancel(completion: @escaping () -> () = { }) -> PopupButton { + return .custom(CancelButton(title: NSLocalizedString("Cancel", comment: ""), dismissOnTap: true, action: completion)) + } + + static func preferredCancel(completion: @escaping () -> () = { }) -> PopupButton { + return .custom(title: NSLocalizedString("Cancel", comment: ""), titleColor: nil, completion: completion) + } + + fileprivate func makeButton() -> PopupDialogButton { + switch self { + case .custom(let button): + return button + case .defaultAccept(completion: let completion): + let acceptButton = DefaultButton(title: NSLocalizedString("OK", comment: ""), dismissOnTap: true) { completion() } + return acceptButton + } + } + } + + func showPopupDialog(title: String?, message: String?, buttons: [PopupButton]) { + let popup = PopupDialog(title: title?.uppercased(), message: message, image: nil, transitionStyle: .bounceDown, tapGestureDismissal: false, panGestureDismissal: false, hideStatusBar: false) + + for action in buttons { + let button = action.makeButton() + popup.addButton(button) + } + self.present(popup, animated: true, completion: nil) } + func showFixFirewallConnectionDialog(completion: @escaping () -> ()) { + VPNController.shared.isConfigurationExisting { (exists) in + if exists { + // if VPN configuration exists, the system will not show an alert, + // so we do need to warn users about it + completion() + } else { + // if there is no existing VPN configuration, + // we need to show a dialog explaining the + // upcoming popup + self.showPopupDialog( + title: "Tap \"Allow\" on the Next Popup", + message: "Due to a recent iOS or Lockdown update, the Firewall needs to be refreshed to run properly.\n\nIf asked, tap \"Allow\" on the next dialog to automatically complete this process.", + buttons: [ + .cancel(), + .defaultAccept(completion: { + completion() + }) + ] + ) + } + } + } + // func showPopupDialogSubmitError(title : String = "Sorry, An Error Occurred", message : String, error: Error?) { // let popup = PopupDialog(title: title, message: message, image: nil, transitionStyle: .zoomIn, hideStatusBar: false) // let acceptButton = DefaultButton(title: "Don't Submit", dismissOnTap: true) { } @@ -187,13 +420,7 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele // self.present(popup, animated: true, completion: nil) // } - // MARK: - Email Team - - public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { - controller.dismiss(animated: true) - } - - @objc func emailTeam(messageBody: String = "Hey Lockdown team, \nI have a question, issue, or suggestion - ", messageErrorBody: String = "") { + @objc func emailTeam(messageBody: String = NSLocalizedString("Hi, my question or feedback for Lockdown is: ", comment: ""), messageErrorBody: String = "") { DDLogInfo("") DDLogInfo("UserId: \(keychain[kVPNCredentialsId] ?? "No User ID")") DDLogInfo("UserReceipt: \(keychain[kVPNCredentialsKeyBase64] ?? "No User Receipt")") @@ -202,26 +429,56 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele DDLogInfo("Has loaded cookie.") } DDLogInfo("") + PacketTunnelProviderLogs.flush() + DDLogInfo("") + var appendString = "" + if (getUserWantsVPNEnabled()) { + appendString = appendString + " - S" + } + let subject = "Lockdown Question or Feedback (iOS \(Bundle.main.versionString))" + appendString + + var message = messageBody + if messageErrorBody != "" { + message = messageBody + "\n\nError Details: " + messageErrorBody + } + message += "\n\n\n" + + sendMessage(message, subject: subject) + } + + func sendMessage(_ message: String, subject: String) { + let recipient = "team@lockdownprivacy.com" // TODO: change email if MFMailComposeViewController.canSendMail() { let composeVC = MFMailComposeViewController() composeVC.mailComposeDelegate = self - composeVC.setToRecipients(["team@lockdownhq.com"]) - composeVC.setSubject("Lockdown Feedback (iOS)") - var message = messageBody - if messageErrorBody != "" { - message = messageBody + "\n\nError Details: " + messageErrorBody - } + composeVC.setToRecipients([recipient]) + composeVC.setSubject(subject) composeVC.setMessageBody(message, isHTML: false) let attachmentData = NSMutableData() for logFileData in logFileDataArray { attachmentData.append(logFileData as Data) } - composeVC.addAttachmentData(attachmentData as Data, mimeType: "text/plain", fileName: "ConfirmedLogs.log") - self.present(composeVC, animated: true, completion: nil) + composeVC.addAttachmentData(attachmentData as Data, mimeType: "text/plain", fileName: "diagnostics.txt") + + let topVC = presentedViewController ?? self + topVC.present(composeVC, animated: true, completion: nil) } else { - showPopupDialog(title: "Couldn't Find Your Email Client", - message: "Please make sure you have added an e-mail account to your iOS device and try again.", acceptButton: "OK") + + guard let mailtoURL = Mailto.generateURL(recipient: recipient, subject: subject, body: message) else { + DDLogError("Failed to generate mailto url") + return + } + + UIApplication.shared.open(mailtoURL, options: [:]) { (success) in + if !success { + self.showPopupDialog( + title: NSLocalizedString("Couldn't Find Your Email Client", comment: ""), + message: NSLocalizedString("Please make sure you have added an e-mail account to your iOS device and try again.", comment: ""), + acceptButton: NSLocalizedString("OK", comment: "") + ) + } + } } } @@ -242,4 +499,22 @@ open class BaseViewController: UIViewController, MFMailComposeViewControllerDele // self.present(popup, animated: true, completion: nil) // } + // MARK: - Email Team + + public func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + controller.dismiss(animated: true) { [weak self] in + self?.actionUponEmailComposeClosure() + } + } + + func actionUponEmailComposeClosure() {} + +} + +extension UIStoryboard { + static let main = UIStoryboard(name: "Main", bundle: nil) +} + +extension Notification.Name { + static let showMultipleSubscriptionsAlert = Notification.Name("showMultipleSubscriptionsAlert") } diff --git a/LockdowniOS/BlockListAddCell.swift b/LockdowniOS/BlockListAddCell.swift index fdcf9fb..4f1b7f6 100644 --- a/LockdowniOS/BlockListAddCell.swift +++ b/LockdowniOS/BlockListAddCell.swift @@ -7,18 +7,56 @@ import UIKit -class BlockListAddCell: UITableViewCell { +class BlockListAddView: UIView { + + let textField = UITextField() + + init() { + super.init(frame: .zero) + didLoad() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func didLoad() { + textField.font = fontRegular17 + textField.placeholder = "domain-to-block.com" + textField.clearButtonMode = .whileEditing + textField.keyboardType = .URL + textField.textContentType = .URL + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + textField.smartDashesType = .no + textField.smartInsertDeleteType = .no + textField.smartQuotesType = .no + textField.spellCheckingType = .no + textField.returnKeyType = .done + + addSubview(textField) + textField.anchors.width.equal(280) + textField.anchors.centerX.align() + textField.anchors.bottom.marginsPin(inset: 8) + + let label = UILabel() + label.text = NSLocalizedString("Add a domain to block", comment: "") + label.font = fontRegular14 + addSubview(label) + label.anchors.top.marginsPin() + label.anchors.bottom.spacing(4, to: textField.anchors.top) + label.anchors.leading.pin(to: textField) + label.anchors.trailing.pin(to: textField) + } override func layoutSubviews() { super.layoutSubviews() // Add line to bottom of Add Domain Text Field let bottomLine = CALayer() - bottomLine.frame = CGRect(x: 0, y: addBlockListDomain.frame.height - 2, width: addBlockListDomain.frame.width, height: 2) + bottomLine.frame = CGRect(x: 0, y: textField.frame.height + 2, width: textField.frame.width, height: 2) bottomLine.backgroundColor = UIColor.tunnelsBlue.cgColor - addBlockListDomain.layer.addSublayer(bottomLine) + textField.layer.addSublayer(bottomLine) } - - @IBOutlet weak var addBlockListDomain: UITextField! - } diff --git a/LockdowniOS/BlockListCell.swift b/LockdowniOS/BlockListCell.swift index 404fdef..06410e9 100644 --- a/LockdowniOS/BlockListCell.swift +++ b/LockdowniOS/BlockListCell.swift @@ -7,10 +7,92 @@ import UIKit -class BlockListCell: UITableViewCell { +class BlockListView: UIView { + + struct Contents { + let image: UIImage? + let title: String? + let status: String? + + static func lockdownGroup(_ lockdownGroup: LockdownGroup) -> Contents { + let image = UIImage(named: lockdownGroup.iconURL) ?? UIImage(named: "website_icon.png") + let status = lockdownGroup.enabled ? + NSLocalizedString("Blocked", comment: "") : + NSLocalizedString("Not Blocked", comment: "") + return Contents(image: image, title: lockdownGroup.name, status: status) + } + + static func userBlocked(domain: String, isEnabled: Bool) -> Contents { + let image = UIImage(named: "website_icon.png") + let status = isEnabled ? + NSLocalizedString("Blocked", comment: "") : + NSLocalizedString("Not Blocked", comment: "") + return Contents(image: image, title: domain, status: status) + } + } + + var contents: Contents = Contents(image: nil, title: nil, status: nil) { + didSet { + imageView.image = contents.image + groupNameLabel.text = contents.title + statusLabel.text = contents.status + } + } + + private let imageView = UIImageView() + private let groupNameLabel = UILabel() + private let statusLabel = UILabel() + + init() { + super.init(frame: .zero) + self.didLoad() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func didLoad() { + addSubview(imageView) + do { + imageView.anchors.size.equal(.init(width: 24, height: 24)) + imageView.anchors.leading.marginsPin(inset: 8) + imageView.anchors.centerY.align() + } + + groupNameLabel.text = contents.title + groupNameLabel.font = fontRegular14 + groupNameLabel.numberOfLines = 0 + addSubview(groupNameLabel) + do { + groupNameLabel.anchors.leading.spacing(10, to: imageView.anchors.trailing) + groupNameLabel.anchors.top.marginsPin(inset: 8) + groupNameLabel.anchors.bottom.marginsPin(inset: 8) + groupNameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + } + + statusLabel.font = fontRegular14 + statusLabel.text = contents.status + statusLabel.textAlignment = .right + addSubview(statusLabel) + do { + statusLabel.anchors.trailing.marginsPin(inset: 4) + statusLabel.anchors.width.equal(110) + statusLabel.anchors.leading.spacing(0, to: groupNameLabel.anchors.trailing) + statusLabel.anchors.centerY.equal(groupNameLabel.anchors.centerY) + statusLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + } + } +} - @IBOutlet weak var blockListDomain: UILabel? - @IBOutlet weak var blockListStatus: UILabel? - @IBOutlet weak var blockListIcon: UIImageView? +extension BlockListView.Contents { + static func listsBlocked(_ userBlockListsGroup: UserBlockListsGroup) -> Self { + let image = UIImage(named: "icn_list_lock") + let status = userBlockListsGroup.enabled ? + NSLocalizedString("On", comment: "") : + NSLocalizedString("Off", comment: "") + return Self(image: image, title: userBlockListsGroup.name, status: status) + } } diff --git a/LockdowniOS/BlockListContainerViewController.swift b/LockdowniOS/BlockListContainerViewController.swift new file mode 100644 index 0000000..7d96956 --- /dev/null +++ b/LockdowniOS/BlockListContainerViewController.swift @@ -0,0 +1,135 @@ +// +// BlockListContainerViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 2.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class BlockListContainerViewController: UIViewController { + + // MARK: - Properties + var didMakeChange = true + + private lazy var customNavigationView: CustomNavigationView = { + let view = CustomNavigationView() + view.title = NSLocalizedString("Configure Blocking", comment: "") + view.buttonTitle = NSLocalizedString("CLOSE", comment: "") + view.onButtonPressed { [unowned self] in + self.close() + } + return view + }() + + private let paragraphLabel: UILabel = { + let view = UILabel() + view.font = fontRegular14 + view.numberOfLines = 0 + view.text = NSLocalizedString("Block all your apps from connecting to the domains and sites below. For your convenience, Lockdown also has pre-configured suggestions.", comment: "") + return view + }() + + private lazy var segmented: UISegmentedControl = { + let view = UISegmentedControl() + view.insertSegment(withTitle: "Curated", at: 0, animated: false) + view.insertSegment(withTitle: "Custom", at: 1, animated: false) + view.selectedSegmentIndex = 0 + view.setTitleTextAttributes([.font: fontMedium14], for: .normal) + view.selectedSegmentTintColor = .tunnelsBlue + view.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected) + view.addTarget(self, action: #selector(segmentedControlDidChangeValue(_:)), for: .valueChanged) + return view + }() + + // MARK: Child ViewControllers + private lazy var curatedListsViewController: CuratedListsViewController = { + let vc = CuratedListsViewController() + self.add(asChildViewController: vc) + return vc + }() + + private lazy var customListsViewController: CustomListsViewController = { + let vc = CustomListsViewController() + self.add(asChildViewController: vc) + return vc + }() + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + configureUI() + } + + private func configureUI() { + view.addSubview(customNavigationView) + customNavigationView.anchors.leading.pin() + customNavigationView.anchors.trailing.pin() + customNavigationView.anchors.top.safeAreaPin() + + view.addSubview(paragraphLabel) + paragraphLabel.anchors.top.spacing(0, to: customNavigationView.anchors.bottom) + paragraphLabel.anchors.leading.readableContentPin(inset: 3) + paragraphLabel.anchors.trailing.readableContentPin(inset: 3) + paragraphLabel.anchors.height.equal(60) + + view.addSubview(segmented) + segmented.anchors.top.spacing(12, to: paragraphLabel.anchors.bottom) + segmented.anchors.leading.readableContentPin() + segmented.anchors.trailing.readableContentPin() + segmented.anchors.height.equal(40) + + updateView() + } +} + +private extension BlockListContainerViewController { + func close() { + dismiss(animated: true, completion: { [weak self] in + guard let self else { return } + if (self.didMakeChange == true) { + if getIsCombinedBlockListEmpty() { + FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) + } else if (FirewallController.shared.status() == .connected) { + FirewallController.shared.restart() + } + } + }) + } + + @objc func segmentedControlDidChangeValue(_ sender: UISegmentedControl) { + updateView() + } + + func updateView() { + + if segmented.selectedSegmentIndex == 0 { + remove(asChildViewController: customListsViewController) + add(asChildViewController: curatedListsViewController) + } else { + remove(asChildViewController: curatedListsViewController) + add(asChildViewController: customListsViewController) + } + } + + func add(asChildViewController viewController: UIViewController) { + addChild(viewController) + + view.addSubview(viewController.view) + viewController.view.anchors.top.spacing(24, to: segmented.anchors.bottom) + viewController.view.anchors.leading.pin() + viewController.view.anchors.trailing.pin() + viewController.view.anchors.bottom.pin() + + viewController.didMove(toParent: self) + } + + func remove(asChildViewController viewController: UIViewController) { + viewController.willMove(toParent: nil) + viewController.view.removeFromSuperview() + viewController.removeFromParent() + } +} diff --git a/LockdowniOS/BlockListGroupViewController.swift b/LockdowniOS/BlockListGroupViewController.swift index dc32881..26c06fc 100644 --- a/LockdowniOS/BlockListGroupViewController.swift +++ b/LockdowniOS/BlockListGroupViewController.swift @@ -7,12 +7,17 @@ import UIKit -class BlockListGroupViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { +class BlockListGroupViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource { - var lockdownGroup : LockdownGroup? - @IBOutlet var lockdownEnabled : UISwitch? - @IBOutlet var groupTitle : UILabel? - var blockListVC : BlockListViewController? + var lockdownGroup: LockdownGroup? + + @IBOutlet var warningContainer: UIView! + @IBOutlet var warningLabel: UILabel! + @IBOutlet var lockdownEnabled: UISwitch! + @IBOutlet var groupTitle: UILabel! + +// weak var blockListVC: BlockListViewController? + weak var blockListVC: CuratedListsViewController? override func viewDidLoad() { super.viewDidLoad() @@ -20,37 +25,37 @@ class BlockListGroupViewController: UIViewController, UITableViewDelegate, UITab self.groupTitle?.text = lockdown.name self.lockdownEnabled?.isOn = lockdown.enabled } + + warningLabel.text = lockdownGroup?.warning + if lockdownGroup?.warning != nil { + warningContainer.isHidden = false + } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == 0 { return (lockdownGroup?.domains.count)! } - else { - return (lockdownGroup?.ipRanges.keys.count)! - } + return 0 } func numberOfSections(in tableView: UITableView) -> Int { - return 2 + return 1 } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 45 + return 32 } 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)) + let view = UIView(frame: CGRect.init(x: 0, y: 0, width: tableView.frame.size.width, height: 32)) view.backgroundColor = UIColor.groupTableViewBackground - let label = UILabel(frame: CGRect.init(x: 20, y: 20, width: tableView.frame.size.width, height: 24)) - label.font = UIFont.init(name: "Montserrat-Medium", size: 14) + let label = UILabel(frame: CGRect.init(x: 12, y: 6, width: tableView.frame.size.width, height: 24)) + label.font = fontMedium14 label.textColor = UIColor.darkGray if section == 0 { - label.text = "Domains".localized() - } - else { - label.text = "IP Ranges".localized() + label.text = NSLocalizedString("Blocked Domains", comment: "") } view.addSubview(label) @@ -67,30 +72,19 @@ class BlockListGroupViewController: UIViewController, UITableViewDelegate, UITab cell.cellTitle?.text = keys[indexPath.row] } } - else { - if let ipKeys = lockdownGroup?.ipRanges { - let keys = ipKeys.keys.sorted {$0 < $1} - if let bits = ipKeys[keys[indexPath.row]]?.subnetBits { - if bits == 0 { - cell.cellTitle?.text = "\(keys[indexPath.row])" - } - else { - cell.cellTitle?.text = "\(keys[indexPath.row]) / \(bits)" - } - } - else { - cell.cellTitle?.text = "\(keys[indexPath.row])" - } - } - } return cell } - @IBAction func toggleLockdown(sender : Any) { + @IBAction func toggleLockdown(sender: UISwitch) { + setIsLockdownEnabled(sender.isOn) + } + + private func setIsLockdownEnabled(_ isEnabled: Bool) { if let vc = self.blockListVC { vc.didMakeChange = true } - lockdownGroup?.enabled = self.lockdownEnabled!.isOn + + lockdownGroup?.enabled = isEnabled var ldDefaults = getLockdownBlockedDomains() ldDefaults.lockdownDefaults[(lockdownGroup?.internalID)!] = lockdownGroup @@ -98,11 +92,7 @@ class BlockListGroupViewController: UIViewController, UITableViewDelegate, UITab } @IBAction func dismiss() { - self.dismiss(animated: true, completion: { - if let vc = self.blockListVC { - vc.tableView.reloadData(); - } - }) + blockListVC?.reloadTableView() + self.navigationController?.popViewController(animated: true) } - } diff --git a/LockdowniOS/BlockListViewController.swift b/LockdowniOS/BlockListViewController.swift index acee6dd..2065a17 100644 --- a/LockdowniOS/BlockListViewController.swift +++ b/LockdowniOS/BlockListViewController.swift @@ -6,238 +6,633 @@ // import UIKit +import Foundation import CocoaLumberjackSwift -class BlockListViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate { - - var addDomainTextField: UITextField? - @IBOutlet weak var tableView: UITableView! +final class BlockListViewController: BaseViewController { + + // MARK: - Properties var didMakeChange = false + var chosenBlocking = 0 + + var lockdownBlockLists: [LockdownGroup] = [] + var customBlockedDomains: [(String, Bool)] = [] + + var customBlockedLists: [UserBlockListsGroup] = [] + + let curatedBlockedDomainsTableView = StaticTableView() + let customBlockedListsTableView = StaticTableView() + let customBlockedDomainsTableView = StaticTableView() + + private lazy var listsSubmenuView: ListsSubmenuView = { + let view = ListsSubmenuView() + view.topButton.addTarget(self, action: #selector(addList), for: .touchUpInside) + view.bottomButton.addTarget(self, action: #selector(importBlockList), for: .touchUpInside) + view.isHidden = true + return view + }() + + private lazy var customNavigationView: CustomNavigationView = { + let view = CustomNavigationView() + view.title = NSLocalizedString("Configure Blocking", comment: "") + view.buttonTitle = NSLocalizedString("CLOSE", comment: "") + view.onButtonPressed { [unowned self] in + self.close() + } + return view + }() + + enum Page: CaseIterable { + case curated + case custom + + var localizedTitle: String { + switch self { + case .curated: + return NSLocalizedString("Curated", comment: "") + case .custom: + return NSLocalizedString("Custom", comment: "") + } + } + } + + private lazy var segmented: UISegmentedControl = { + let view = UISegmentedControl(items: Page.allCases.map(\.localizedTitle)) + view.selectedSegmentIndex = chosenBlocking + view.setTitleTextAttributes([.font: fontMedium14], for: .normal) + view.selectedSegmentTintColor = .tunnelsBlue + view.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected) + view.addTarget(self, action: #selector(segmentedControlDidChangeValue), for: .valueChanged) + return view + }() + + private let paragraphLabel: UILabel = { + let view = UILabel() + view.font = fontRegular14 + view.numberOfLines = 0 + view.text = NSLocalizedString("Block all your apps from connecting to the domains and sites below. For your convenience, Lockdown also has pre-configured suggestions.", comment: "") + return view + }() + + private lazy var addNewListButton: UIButton = { + let button = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + button.tintColor = .tunnelsBlue + button.setImage(UIImage(systemName: "plus", withConfiguration: symbolConfig), for: .normal) + button.addTarget(self, action: #selector(showSubmenu), for: .touchUpInside) + button.isEnabled = false + return button + }() + + private lazy var listsLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Lists", comment: "") + label.textColor = .label + label.font = fontBold18 + return label + }() + + private lazy var emptyListsView: EmptyListsView = { + let view = EmptyListsView() + view.descriptionLabel.text = NSLocalizedString("No lists yet", comment: "") + view.addButton.setTitle(NSLocalizedString("Create a list", comment: ""), for: .normal) + view.addButton.addTarget(self, action: #selector(addList), for: .touchUpInside) + return view + }() + + private lazy var lockedListsView: LockedListsView = { + let view = LockedListsView() + return view + }() + + private lazy var domainsLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Domains", comment: "") + label.textColor = .label + label.font = fontBold18 + return label + }() + + private lazy var addNewDomainButton: UIButton = { + let button = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + button.tintColor = .tunnelsBlue + button.setImage(UIImage(systemName: "plus", withConfiguration: symbolConfig), for: .normal) + button.addTarget(self, action: #selector(addDomain), for: .touchUpInside) + return button + }() + + private lazy var editDomainButton: UIButton = { + let button = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + button.tintColor = .tunnelsBlue + button.setImage(UIImage(named: "icn_edit"), for: .normal) + button.addTarget(self, action: #selector(editDomains), for: .touchUpInside) +// button.isHidden = true + return button + }() + + private lazy var emptyDomainsView: EmptyListsView = { + let view = EmptyListsView() + view.descriptionLabel.text = NSLocalizedString("No custom domains yet", comment: "") + view.addButton.setTitle(NSLocalizedString("Add a domain", comment: ""), for: .normal) + view.addButton.addTarget(self, action: #selector(addDomain), for: .touchUpInside) + return view + }() + + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .secondarySystemBackground - let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissView)) tap.cancelsTouchesInView = false view.addGestureRecognizer(tap) - self.tableView.reloadData() + configure() + configureCuratedBlockedDomainsTableView() + configureCustomBlockedListsTableView() + configureCustomBlockedDomainsTableView() } - @objc func dismissKeyboard() { - view.endEditing(true) + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + reloadCustomBlockedLists() } - @IBAction func save() { - self.dismiss(animated: true, completion: { - if (self.didMakeChange == true) { - if (FirewallController.shared.status() == .connected) { - FirewallController.shared.restart() - } - } - }) + private func configure() { + view.addSubview(customNavigationView) + customNavigationView.anchors.leading.pin() + customNavigationView.anchors.trailing.pin() + customNavigationView.anchors.top.safeAreaPin() + + view.addSubview(paragraphLabel) + paragraphLabel.anchors.top.spacing(0, to: customNavigationView.anchors.bottom) + paragraphLabel.anchors.leading.readableContentPin(inset: 3) + paragraphLabel.anchors.trailing.readableContentPin(inset: 3) + paragraphLabel.anchors.height.equal(60) + + view.addSubview(segmented) + segmented.anchors.top.spacing(12, to: paragraphLabel.anchors.bottom) + segmented.anchors.leading.readableContentPin() + segmented.anchors.trailing.readableContentPin() + segmented.anchors.height.equal(40) } - func saveNewDomain() { - // TODO: Check it's a valid domain format - didMakeChange = true - if let text = addDomainTextField?.text { - if text.count > 0 { - DDLogInfo("Adding custom domain - \(text)") - addUserBlockedDomain(domain: text.lowercased()) - addDomainTextField!.text = "" - tableView.reloadData() - } - } + private func configureCuratedBlockedDomainsTableView() { + addTableView(curatedBlockedDomainsTableView, layout: { tableView in + tableView.anchors.top.spacing(8, to: segmented.anchors.bottom) + tableView.anchors.leading.pin() + tableView.anchors.trailing.pin() + tableView.anchors.bottom.pin() + }) + + reloadCuratedBlockDomains() + chosenBlocking == 0 ? transition(toPage: .curated) : transition(toPage: .custom) } - //MARK: - TABLE VIEW + private func configureCustomBlockedListsTableView() { + + view.addSubview(listsLabel) + listsLabel.anchors.top.spacing(24, to: segmented.anchors.bottom) + listsLabel.anchors.leading.marginsPin() + + view.addSubview(addNewListButton) + addNewListButton.anchors.centerY.equal(listsLabel.anchors.centerY) + addNewListButton.anchors.trailing.marginsPin() + + view.addSubview(domainsLabel) + domainsLabel.anchors.top.spacing(300, to: segmented.anchors.bottom) + domainsLabel.anchors.height.equal(30) + domainsLabel.anchors.leading.marginsPin() + + addTableView(customBlockedListsTableView, layout: { tableView in + tableView.anchors.top.spacing(8, to: listsLabel.anchors.bottom) + tableView.anchors.leading.pin() + tableView.anchors.trailing.pin() + tableView.anchors.bottom.spacing(8, to: domainsLabel.anchors.top) + }) + + view.addSubview(listsSubmenuView) + listsSubmenuView.anchors.trailing.marginsPin() + listsSubmenuView.anchors.top.spacing(60, to: paragraphLabel.anchors.bottom) + + customBlockedListsTableView.deselectsCellsAutomatically = true + } - func numberOfSections(in tableView: UITableView) -> Int { - return 2 + private func configureCustomBlockedDomainsTableView() { + + view.addSubview(addNewDomainButton) + addNewDomainButton.anchors.centerY.equal(domainsLabel.anchors.centerY) + addNewDomainButton.anchors.trailing.marginsPin() + +// view.addSubview(editDomainButton) +// editDomainButton.anchors.centerY.equal(domainsLabel.anchors.centerY) +// editDomainButton.anchors.trailing.spacing(16, to: addNewDomainButton.anchors.leading) + + addTableView(customBlockedDomainsTableView, layout: { tableView in + tableView.anchors.top.spacing(8, to: domainsLabel.anchors.bottom) + tableView.anchors.leading.pin() + tableView.anchors.trailing.pin() + tableView.anchors.bottom.safeAreaPin() + }) + + customBlockedDomainsTableView.deselectsCellsAutomatically = true + + reloadCustomBlockedDomains() } - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 45 + // Curated lists + func reloadCuratedBlockDomains() { + curatedBlockedDomainsTableView.clear() + lockdownBlockLists = { + let domains = getLockdownBlockedDomains().lockdownDefaults + let sorted = domains.sorted(by: { $0.key < $1.key }) + return Array(sorted.map(\.value)) + }() + createCuratedBlockedDomainsRows() + curatedBlockedDomainsTableView.reloadData() } - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return 0 + func reloadCustomBlockedLists() { + customBlockedListsTableView.clear() + customBlockedLists = { + let lists = getBlockedLists().userBlockListsDefaults + let sorted = lists.sorted(by: { $0.key < $1.key }) + return Array(sorted.map(\.value)) + }() + createCustomBlockedListsRows() + customBlockedListsTableView.reloadData() } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - if indexPath.section == 0 { - // Add Domain row - if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { - return 70 + func reloadCustomBlockedDomains() { + customBlockedDomainsTableView.clear() + customBlockedDomains = { + let domains = getUserBlockedDomains() + return domains.sorted(by: { $0.key < $1.key }).map { (key, value) -> (String, Bool) in + if let status = value as? NSNumber { + return (key, status.boolValue) + } else { + return (key, false) + } } - else { - return 50 + }() + createCustomBlockedDomainsRows() + customBlockedDomainsTableView.reloadData() + } + + // Curated Lists + func createCuratedBlockedDomainsRows() { + let tableView = curatedBlockedDomainsTableView + + for lockdownGroup in lockdownBlockLists { + + let cell = tableView.addRow { [unowned self] (contentView) in + let blockListView = BlockListView() + blockListView.contents = .lockdownGroup(lockdownGroup) + contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + }.onSelect { [unowned self] in + if UserDefaults.hasSeenAdvancedPaywall || UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { + let storyboard = UIStoryboard.main + let target = storyboard.instantiate(BlockListGroupViewController.self) + target.lockdownGroup = lockdownGroup +// target.blockListVC = self + self.navigationController?.pushViewController(target, animated: true) + } else { + if lockdownGroup.accessLevel == "advanced" { + let vc = VPNPaywallViewController() + present(vc, animated: true) + } else { + let storyboard = UIStoryboard.main + let target = storyboard.instantiate(BlockListGroupViewController.self) + target.lockdownGroup = lockdownGroup +// target.blockListVC = self + self.navigationController?.pushViewController(target, animated: true) + } + } } - } - else { - return 50 + + cell.accessoryType = .disclosureIndicator } } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0 { - return getUserBlockedDomains().count + 1 - } - else { - return getLockdownBlockedDomains().lockdownDefaults.keys.count - } + @objc + func segmentedControlDidChangeValue() { + let page = Page.allCases[segmented.selectedSegmentIndex] + transition(toPage: page) } - 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 = UIFont.init(name: "Montserrat-Medium", size: 14) - label.textColor = UIColor.darkGray + func transition(toPage page: Page) { - if section == 0 { - label.text = "Your settings".localized() - } - else { - label.text = "Pre-configured Suggestions".localized() + switch page { + case .curated: + customBlockedDomainsTableView.isHidden = true + customBlockedListsTableView.isHidden = true + curatedBlockedDomainsTableView.isHidden = false + listsLabel.isHidden = true + addNewListButton.isHidden = true + listsSubmenuView.isHidden = true + addNewDomainButton.isHidden = true + domainsLabel.isHidden = true + editDomainButton.isHidden = true + case .custom: + customBlockedListsTableView.isHidden = false + customBlockedDomainsTableView.isHidden = false + curatedBlockedDomainsTableView.isHidden = true + listsLabel.isHidden = false + addNewListButton.isHidden = false + addNewDomainButton.isHidden = false + domainsLabel.isHidden = false + editDomainButton.isHidden = false } + } + + func saveNewList(userEnteredListName: String) { + didMakeChange = true + DDLogInfo("Adding custom list - \(userEnteredListName)") + addBlockedList(listName: userEnteredListName) + reloadCustomBlockedLists() + } + + func saveNewDomain(userEnteredDomainName: String) { + let validation = DomainNameValidator.validate(userEnteredDomainName) - view.addSubview(label) - return view + switch validation { + case .valid: + didMakeChange = true + + DDLogInfo("Adding custom domain - \(userEnteredDomainName)") + addUserBlockedDomain(domain: userEnteredDomainName.lowercased()) + reloadCustomBlockedDomains() + case .notValid(let reason): + DDLogWarn("Custom domain is not valid - \(userEnteredDomainName), reason - \(reason)") + 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 block. For example, \"google.com\" without \"https://\"", comment: ""), + acceptButton: NSLocalizedString("Okay", comment: "") + ) + } } +} + +// MARK: - Functions +extension BlockListViewController { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) + func createCustomBlockedListsRows() { + let tableView = customBlockedListsTableView + let emptyList = emptyListsView + let lockedList = lockedListsView - if indexPath.section == 0 { - if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { - // Do nothing for add domain row - } - else { - didMakeChange = true - let domains = getUserBlockedDomains(); - 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 { - setUserBlockedDomain(domain: domainArray[indexPath.row].key, enabled: false) - } - else { - setUserBlockedDomain(domain: domainArray[indexPath.row].key, enabled: true) - } + if UserDefaults.hasSeenAdvancedPaywall || UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { + addNewListButton.isEnabled = true + if customBlockedLists.count == 0 { + tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(emptyList) + emptyListsView.anchors.edges.pin() + }.onSelect { [unowned self] in + self.addList() } - tableView.reloadData() + } + } else { + tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(lockedList) + lockedList.anchors.edges.pin() + }.onSelect { [unowned self] in + let vc = VPNPaywallViewController() + self.present(vc, animated: true) } } - else if indexPath.section == 1 { - let domains = getLockdownBlockedDomains().lockdownDefaults - let domainKeys = domains.keys.sorted {$0 < $1} - let lockdownGroup = domains[domainKeys[indexPath.row]] - self.performSegue(withIdentifier: "showBlockListGroup", sender: lockdownGroup) + + for list in customBlockedLists { + let blockListView = BlockListView() + blockListView.contents = .listsBlocked(list) + + let cell = tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + }.onSelect { [unowned self] in + self.didMakeChange = true + let vc = ListSettingsViewController() + vc.listName = list.name + vc.didMakeChange = list.enabled + vc.blockListVC = self + navigationController?.pushViewController(vc, animated: true) + }.onSwipeToDelete { [unowned self] in + self.didMakeChange = true + deleteList(list: list.name) + DDLogInfo("Deleting custom list - \(list.name)") + } + cell.accessoryType = .disclosureIndicator } } - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if (segue.identifier == "showBlockListGroup") { - if let target = segue.destination as? BlockListGroupViewController, - let lockdownGroup = sender as? LockdownGroup { - target.lockdownGroup = lockdownGroup; - target.blockListVC = self + // Custom Domains + func createCustomBlockedDomainsRows() { + let tableView = customBlockedDomainsTableView + let emptyDomains = emptyDomainsView + if customBlockedDomains.count == 0 { + tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(emptyDomains) + emptyDomains.anchors.edges.pin() + }.onSelect { [unowned self] in + self.addDomain() } } + + for (domain, isEnabled) in customBlockedDomains { + var currentEnabledStatus = isEnabled + let blockListView = BlockListView() + blockListView.contents = .userBlocked(domain: domain, isEnabled: isEnabled) + + tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + view.addSubview(editDomainButton) + editDomainButton.anchors.centerY.equal(domainsLabel.anchors.centerY) + editDomainButton.anchors.trailing.spacing(16, to: addNewDomainButton.anchors.leading) + }.onSelect { [unowned blockListView, unowned self] in + self.didMakeChange = true + currentEnabledStatus.toggle() + blockListView.contents = .userBlocked(domain: domain, isEnabled: currentEnabledStatus) + setUserBlockedDomain(domain: domain, enabled: currentEnabledStatus) + }.onSwipeToDelete { [unowned self] in + self.didMakeChange = true + deleteUserBlockedDomain(domain: domain) + DDLogInfo("Deleting custom domain - \(domain)") + } + } + } + + func close() { + dismiss(animated: true, completion: { [weak self] in + guard let self else { return } + if (self.didMakeChange == true) { + if getIsCombinedBlockListEmpty() { + FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) + } else if (FirewallController.shared.status() == .connected) { + FirewallController.shared.restart() + } + } + }) } - func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - if indexPath.section == 0 { - if indexPath.row < tableView.numberOfRows(inSection: indexPath.section) - 1 { - return true + @objc func addList() { + + let tableView = customBlockedListsTableView + + let alertController = UIAlertController(title: "Create New List", message: nil, preferredStyle: .alert) + + let saveAction = UIAlertAction(title: "Save", style: .default) { [weak self] (_) in + if let txtField = alertController.textFields?.first, let text = txtField.text { + guard let self else { return } + self.saveNewList(userEnteredListName: text) +// if !getBlockedLists().isEmpty { +// tableView.clear() +// } + self.reloadCustomBlockedLists() + self.listsSubmenuView.isHidden = true } } - return false + saveAction.isEnabled = false + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { [weak self] (_) in + guard let self else { return } + self.listsSubmenuView.isHidden = true + } + + alertController.addTextField { (textField) in + textField.placeholder = NSLocalizedString("List Name", comment: "") + } + + NotificationCenter.default.addObserver( + forName: UITextField.textDidChangeNotification, + object: alertController.textFields?.first, + queue: .main) { (notification) -> Void in + guard let textFieldText = alertController.textFields?.first?.text else { return } + saveAction.isEnabled = textFieldText.isValid(.listName) + } + + alertController.addAction(saveAction) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) } - func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - let cell = tableView.cellForRow(at: indexPath) as! BlockListCell - let domainLabel = cell.blockListDomain - - didMakeChange = true - deleteUserBlockedDomain(domain: (domainLabel?.text)!) - self.tableView.deleteRows(at: [indexPath], with: .fade) - } + func deleteList(list: String) { + + let alert = UIAlertController(title: NSLocalizedString("Delete List?", comment: ""), + message: NSLocalizedString("Are you sure you want to remove this list?", comment: ""), + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: NSLocalizedString("No, Return", comment: ""), + style: .default, + handler: { [weak self] (_) in + guard let self else { return } + self.reloadCustomBlockedLists() + })) + + alert.addAction(UIAlertAction(title: NSLocalizedString("Yes, Delete", comment: ""), + style: .destructive, + handler: { [weak self] (_) in + guard let self else { return } + deleteBlockedList(listName: list) + self.customBlockedListsTableView.clear() + self.reloadCustomBlockedLists() + })) + + present(alert, animated: true, completion: nil) + } + + @objc func showSubmenu() { + listsSubmenuView.isHidden = false } - func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { - return .delete + @objc func dismissView() { + listsSubmenuView.isHidden = true } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + @objc func importBlockList() { + listsSubmenuView.isHidden = true + let vc = ImportBlockListViewController() + vc.importCompletion = { [unowned self] in + self.reloadCustomBlockedLists() + self.showSuccessImportAlert() + } - if indexPath.section == 0 { - // Add Domain - if indexPath.row == tableView.numberOfRows(inSection: 0) - 1 { - let cell = tableView.dequeueReusableCell(withIdentifier: "blockListAddCell", for: indexPath) as! BlockListAddCell - let textfield = cell.addBlockListDomain - textfield?.addTarget(self, action: #selector(textFieldDidEndOnExit), for: .editingDidEndOnExit) - textfield?.addTarget(self, action: #selector(didSelectTextField), for: .editingDidBegin) - addDomainTextField = textfield - return cell - } - else { - let domains = getUserBlockedDomains() - let domainArray = domains.sorted {$0.key < $1.key} - if domainArray.count > indexPath.row { - let cell = tableView.dequeueReusableCell(withIdentifier: "blockListCell", for: indexPath) as! BlockListCell - cell.blockListDomain?.text = domainArray[indexPath.row].key - if let status = domainArray[indexPath.row].value as? NSNumber, status.boolValue == true { - cell.blockListStatus?.text = "Blocked".localized() - } - else { - cell.blockListStatus?.text = "Not Blocked".localized() - } - cell.blockListIcon?.image = UIImage(named: "website_icon.png") - - return cell + navigationController?.present(vc, animated: true) + } + + @objc func addDomain() { + let tableView = customBlockedDomainsTableView + + let alertController = UIAlertController(title: NSLocalizedString("Add a Domain to Block", comment: ""), + message: nil, + preferredStyle: .alert) + + let saveAction = UIAlertAction(title: "Save", style: .default) { [weak self] (_) in + if let txtField = alertController.textFields?.first, let text = txtField.text { + guard let self else { return } + self.saveNewDomain(userEnteredDomainName: text) + if !getUserBlockedDomains().isEmpty { + tableView.clear() } + + self.reloadCustomBlockedDomains() +// self.editDomainButton.isHidden = false } } - else if indexPath.section == 1 { - let cell = tableView.dequeueReusableCell(withIdentifier: "blockListCell", for: indexPath) as! BlockListCell - let domains = getLockdownBlockedDomains().lockdownDefaults - let domainKeys = domains.keys.sorted {$0 < $1} - if domainKeys.count > indexPath.row { - cell.blockListDomain?.text = domains[domainKeys[indexPath.row]]?.name - if domains[domainKeys[indexPath.row]]!.enabled { - cell.blockListStatus?.text = "Blocked".localized() - } - else { - cell.blockListStatus?.text = "Not Blocked".localized() - } - if let imageView = cell.blockListIcon, - let lockdownGroup = domains[domainKeys[indexPath.row]] { - if let icon = UIImage(named: lockdownGroup.iconURL) { - imageView.image = icon - } - else { - imageView.image = UIImage(named: "website_icon.png") - } - } - } - return cell + + saveAction.isEnabled = false + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (_) in } + + alertController.addTextField { (textField) in + textField.keyboardType = .URL + textField.placeholder = "domain-to-block" } - return UITableViewCell() + NotificationCenter.default.addObserver( + forName: UITextField.textDidChangeNotification, + object: alertController.textFields?.first, + queue: .main) { (notification) -> Void in + guard let textFieldText = alertController.textFields?.first?.text else { return } + saveAction.isEnabled = textFieldText.isValid(.domainName) && !textFieldText.isEmpty + } + + alertController.addAction(saveAction) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) } - @IBAction func textFieldDidEndOnExit(textField: UITextField) { - self.dismissKeyboard() - saveNewDomain() + @objc func editDomains() { + if !customBlockedDomains.isEmpty { + let vc = EditDomainsViewController() + vc.updateCompletion = { [weak self] in + self?.reloadCustomBlockedDomains() + } + navigationController?.pushViewController(vc, animated: true) + } } - @objc func didSelectTextField(textField: UITextField) { - let addDomainRow = tableView.numberOfRows(inSection: 0) - 1 - self.tableView.scrollToRow(at: IndexPath.init(row: addDomainRow, section: 0), at: .middle, animated: true) + func showSuccessImportAlert() { + let alert = UIAlertController(title: NSLocalizedString("Success!", comment: ""), + message: NSLocalizedString("The list has been imported successfully. You can start blocking the list's domains", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), + style: .default, + handler: nil)) + present(alert, animated: true, completion: nil) } + func showErrorAlert() { + let alert = UIAlertController(title: NSLocalizedString("Error", comment: ""), + message: NSLocalizedString("Unable to import the list. Please try again or contact support for assistance", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), + style: .default, + handler: nil)) + present(alert, animated: true, completion: nil) + } } diff --git a/LockdowniOS/BlockLogCell.swift b/LockdowniOS/BlockLogCell.swift index 6f5b0f6..01d1294 100644 --- a/LockdowniOS/BlockLogCell.swift +++ b/LockdowniOS/BlockLogCell.swift @@ -11,5 +11,4 @@ class BlockLogCell: UITableViewCell { @IBOutlet weak var logHost: UILabel? @IBOutlet weak var time: UILabel! - } diff --git a/LockdowniOS/BlockLogViewController.swift b/LockdowniOS/BlockLogViewController.swift index 903eeb2..046a296 100644 --- a/LockdowniOS/BlockLogViewController.swift +++ b/LockdowniOS/BlockLogViewController.swift @@ -7,7 +7,15 @@ import UIKit -class BlockLogViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { +class BlockLogViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource { + + @IBOutlet var blockDayCounterLabel: UILabel! + + // -- SUPPORTING LIVE UPDATES + var timer: Timer? + var kvoObservationToken: Any? + let debouncer = Debouncer(seconds: 0.3) + // func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dayLogTime.count; @@ -21,8 +29,14 @@ class BlockLogViewController: UIViewController, UITableViewDelegate, UITableView return 0 } + func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + return false + } + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "blockLogCell", for: indexPath) as! BlockLogCell + guard let cell = tableView.dequeueReusableCell(withIdentifier: "blockLogCell", for: indexPath) as? BlockLogCell else { + return UITableViewCell() + } cell.time.text = dayLogTime[indexPath.row]; cell.logHost?.text = dayLogHost[indexPath.row]; @@ -30,31 +44,84 @@ class BlockLogViewController: UIViewController, UITableViewDelegate, UITableView return cell } + func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { + let logHost = dayLogHost[indexPath.row] + let info = TrackerInfoRegistry.shared.info(forTrackerDomain: logHost) + + showPopupDialog(title: info.title, message: info.description, acceptButton: "Okay") + } + override func viewDidLoad() { super.viewDidLoad() + blockDayCounterLabel.text = getDayMetricsString(commas: true) tableView.refreshControl = refreshControl - refreshControl.addTarget(self, action: #selector(self.refreshData(_:)), for: .valueChanged); + refreshControl.addTarget(self, action: #selector(self.refreshData(_:)), for: .valueChanged) + + configureObservers() - refreshData(self); + refreshData(self) } - @objc func refreshData(_ sender: Any) { - let kDayLogs = "LockdownDayLogs"; - dayLogTime = []; - dayLogHost = []; - if var dayLogs = defaults.array(forKey: kDayLogs) as? [String] { - dayLogs = dayLogs.reversed(); - for log in dayLogs { - let sp = log.components(separatedBy: "_"); - if sp.count == 2 { - dayLogTime.append(sp[0]); - dayLogHost.append(sp[1]); + deinit { + timer?.invalidate() + timer = nil + } + + func configureObservers() { + kvoObservationToken = defaults.observe(\.LockdownDayLogs, options: [.new, .old]) { [weak self] (defaults, change) in + DispatchQueue.main.async { + self?.debouncer.debounce { + self?.refreshData(defaults) } } } - tableView.reloadData(); - DispatchQueue.main.async { - self.refreshControl.endRefreshing() + + // timer is used as a backup in case KVO fails for any reason + timer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: true) { [weak self] (timer) in + self?.refreshData(timer) + } + timer?.tolerance = 3.0 + } + + @objc func refreshData(_ sender: Any) { + flushBlockLog(log: { _ in }) + blockDayCounterLabel.text = getDayMetricsString(commas: true) + if BlockDayLog.shared.isEnabled { + tableView.isHidden = false + blockLogDisabledContainer.isHidden = true + + let oldDayLogTime = dayLogTime + + dayLogTime = [] + dayLogHost = [] + if let dayLogs = BlockDayLog.shared.strings?.reversed() { + for log in dayLogs { + let sp = log.components(separatedBy: "_"); + if sp.count == 2 { + dayLogTime.append(sp[0]); + dayLogHost.append(sp[1]); + } + } + } + + if dayLogTime.count > oldDayLogTime.count, oldDayLogTime != [] { + let diff = dayLogTime.count - oldDayLogTime.count + let indexPaths = (0 ..< diff).map({ IndexPath(row: $0, section: 0) }) + tableView.performBatchUpdates { + tableView.insertRows(at: indexPaths, with: .top) + } completion: { (finished) in + return + } + } else { + tableView.reloadData() + } + + DispatchQueue.main.async { + self.refreshControl.endRefreshing() + } + } else { + tableView.isHidden = true + blockLogDisabledContainer.isHidden = false } } @@ -62,9 +129,90 @@ class BlockLogViewController: UIViewController, UITableViewDelegate, UITableView self.dismiss(animated: true, completion: {}) } - var dayLogTime: [String] = []; - var dayLogHost: [String] = []; + @IBAction func showMenu() { + let isBlockEnabled = BlockDayLog.shared.isEnabled + + let message = """ +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. +""" + showPopupDialog( + title: .localized("Settings"), + message: .localized(message), + buttons: [ + .custom(title: isBlockEnabled ? .localized("Disable Block Log") : .localized("Enable Block Log")) { + if isBlockEnabled { + self.showDisableBlockLog() + } else { + self.enableBlockLog() + } + }, + .custom(title: .localized("Clear Block Log")) { + BlockDayLog.shared.clear() + defaults.set(0, forKey: kDayMetrics) + self.refreshData(self) + }, + .cancel() + ]) + } + + func showDisableBlockLog() { + showPopupDialog( + title: .localized("Disable Block Log?"), + message: .localized("You'll have to reenable it later here to start seeing blocked entries again."), + buttons: [ + .destructive(title: .localized("Disable")) { + BlockDayLog.shared.disable(shouldClear: true) + self.refreshData(self) + }, + .preferredCancel() + ]) + } + + @IBAction func enableBlockLog() { + BlockDayLog.shared.enable() + self.refreshData(self) + } + + var dayLogTime: [String] = [] + var dayLogHost: [String] = [] private let refreshControl = UIRefreshControl() - @IBOutlet weak var tableView: UITableView! + @IBOutlet var tableView: UITableView! + @IBOutlet var blockLogDisabledContainer: UIStackView! + +} + +fileprivate extension UserDefaults { + @objc + dynamic var LockdownDayLogs: [Any]? { + get { + return array(forKey: "LockdownDayLogs") + } + set { + set(newValue, forKey: "LockdownDayLogs") + } + } +} + +// https://stackoverflow.com/a/52338788 +// by Frédéric Adda +class Debouncer { + + // MARK: - Properties + private let queue = DispatchQueue.main + private var workItem = DispatchWorkItem(block: {}) + private var interval: TimeInterval + + // MARK: - Initializer + init(seconds: TimeInterval) { + self.interval = seconds + } + + // MARK: - Debouncing function + func debounce(action: @escaping (() -> Void)) { + workItem.cancel() + workItem = DispatchWorkItem(block: { action() }) + queue.asyncAfter(deadline: .now() + interval, execute: workItem) + } } diff --git a/LockdowniOS/BottomMenu.swift b/LockdowniOS/BottomMenu.swift new file mode 100644 index 0000000..5830f3c --- /dev/null +++ b/LockdowniOS/BottomMenu.swift @@ -0,0 +1,86 @@ +// +// BottomMenu.swift +// LockdownSandbox +// +// Created by Aliaksandr Dvoineu on 26.04.23. +// + +import UIKit + +final class BottomMenu: UIView { + + private(set) var buttonCallback: () -> () = { } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = 8 + } + + @discardableResult + func onButtonPressed(_ callback: @escaping () -> ()) -> Self { + buttonCallback = callback + return self + } + + lazy var leftButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .tunnelsBlue + button.setTitle(NSLocalizedString("Select All", comment: ""), for: .normal) + button.titleLabel?.font = fontMedium15 + return button + }() + + lazy var middleButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .tunnelsBlue + button.titleLabel?.font = fontMedium15 + button.setTitle(NSLocalizedString("Move to List", comment: ""), for: .normal) + return button + }() + + lazy var rightButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .red + button.titleLabel?.font = fontMedium15 + button.setTitle(NSLocalizedString("Delete", comment: ""), for: .normal) + return button + }() + + private lazy var backgroundView: UIView = { + let view = UIView() + view.backgroundColor = .secondarySystemBackground + return view + }() + + lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(leftButton) + stackView.addArrangedSubview(middleButton) + stackView.addArrangedSubview(rightButton) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.alignment = .leading + return stackView + }() + + private func configure() { + addSubview(backgroundView) + backgroundView.anchors.height.equal(60) + backgroundView.anchors.leading.pin() + backgroundView.anchors.trailing.pin() + backgroundView.anchors.bottom.pin() + + addSubview(stackView) + stackView.anchors.edges.pin() + } +} diff --git a/LockdowniOS/BulletView.swift b/LockdowniOS/BulletView.swift new file mode 100644 index 0000000..978258a --- /dev/null +++ b/LockdowniOS/BulletView.swift @@ -0,0 +1,78 @@ +// +// BulletView.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 27.04.2023. +// + +import UIKit + +struct BulletViewModel { + let image: UIImage + let title: String + let highlightedStrings: [String] + + init( + image: UIImage, + title: String, + highlightedStrings: [String] = [] + ) { + self.image = image + self.title = title + self.highlightedStrings = highlightedStrings + } +} + +final class BulletView: UIView { + + private lazy var bulletImage: UIImageView = { + let image = UIImageView() + image.contentMode = .left + image.anchors.width.equal(24) + return image + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = fontMedium15 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(bulletImage) + stackView.addArrangedSubview(titleLabel) + stackView.axis = .horizontal + stackView.distribution = .fill + stackView.alignment = .top + stackView.spacing = 8 + return stackView + }() + + //MARK: Initialization + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: Functions + private func configureUI() { + addSubview(stackView) + stackView.anchors.edges.pin() + } + + func configure(with model: BulletViewModel) { + bulletImage.image = model.image + titleLabel.text = model.title + model.highlightedStrings.forEach { + titleLabel.highlight($0, font: .boldLockdownFont(size: 15)) + } + } +} diff --git a/LockdowniOS/CALayer+Ext.swift b/LockdowniOS/CALayer+Ext.swift new file mode 100644 index 0000000..a758b27 --- /dev/null +++ b/LockdowniOS/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/CTAView.swift b/LockdowniOS/CTAView.swift new file mode 100644 index 0000000..5a9c68b --- /dev/null +++ b/LockdowniOS/CTAView.swift @@ -0,0 +1,155 @@ +// +// CTAView.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 2.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +protocol CTAViewDelegate { + func onRemove(_ view: CTAView) +} + +final class CTAView: UIView { + + let delegate: CTAViewDelegate + + // MARK: - Properties + +// private lazy var bkgView: UIView = { +// let view = UIView() +// view.backgroundColor = .lightGray +// view.layer.cornerRadius = 15 +// return view +// }() + + private lazy var closeButton: UIButton = { + let button = UIButton(type: .system) + button.setImage(UIImage(named: "icn_close_filled"), for: .normal) + button.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) + return button + }() + + private lazy var mainTitle: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Get Advanced protection", comment: "") + label.font = fontBold24 + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + private lazy var hStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(mainTitle) + stackView.addArrangedSubview(closeButton) + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + stackView.alignment = .top + stackView.spacing = 12 + return stackView + }() + + private lazy var descriptionLabel1: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Block as many trackers as you want", comment: ""))) + return label + }() + + private lazy var descriptionLabel2: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Import and export your own block lists", comment: ""))) + return label + }() + + private lazy var descriptionLabel3: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Access to new curated lists of trackers", comment: ""))) + return label + }() + + private lazy var upgradeButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setTitle(NSLocalizedString("Upgrade", comment: ""), for: .normal) + button.titleLabel?.font = fontBold18 + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 28 + button.anchors.height.equal(56) +// button.addTarget(self, action: #selector(upgrade), for: .touchUpInside) + return button + }() + +// private lazy var close: UIImageView = { +// let image = UIImageView() +// image.image = UIImage(named: "icn_close_filled") +// image.contentMode = .scaleAspectFit +// image.layer.masksToBounds = true +// return image +// }() + + + + private lazy var vStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(hStackView) + stackView.addArrangedSubview(descriptionLabel1) + stackView.addArrangedSubview(descriptionLabel2) + stackView.addArrangedSubview(descriptionLabel3) + stackView.addArrangedSubview(upgradeButton) + stackView.axis = .vertical + stackView.distribution = .fillProportionally + stackView.alignment = .fill + stackView.spacing = 4 + return stackView + }() + + // MARK: - Initializer + + init(delegate: CTAViewDelegate) { + self.delegate = delegate + super.init(frame: .zero) + backgroundColor = .extraLightGray + layer.cornerRadius = 15 + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Configure UI + + func configure() { + +// vStackView.layer.cornerRadius = 15 + + addSubview(vStackView) + vStackView.anchors.top.marginsPin() + vStackView.anchors.bottom.marginsPin() + vStackView.anchors.leading.marginsPin() + vStackView.anchors.trailing.marginsPin() + } + + @objc func closeButtonTapped() { +// UIView.animate(withDuration: 0.2) { +// self.imageView.isHidden = !self.imageView.isHidden +// } +// +// if self.imageView.isHidden { +// btnShow.setTitle("Show", for: .normal) +// } else { +// btnShow.setTitle("Hide", for: .normal) +// } + + self.delegate.onRemove(self) + } + +// @objc func upgrade() { +// let vc = VPNPaywallViewController() +// present(vc, animated: true) +// } + +} diff --git a/LockdowniOS/CodableUserDefaults.swift b/LockdowniOS/CodableUserDefaults.swift new file mode 100644 index 0000000..49a7f4c --- /dev/null +++ b/LockdowniOS/CodableUserDefaults.swift @@ -0,0 +1,63 @@ +// +// CodableUserDefaults.swift +// Lockdown +// +// Created by Pavel Vilbik on 6.07.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import Foundation + +@propertyWrapper +struct CodableUserDefaults { + private let key: String + private let defaultValue: T? + private let isLogged: Bool + private let userDefaults: UserDefaults + private let decoder: JSONDecoder + private let encoder: JSONEncoder + + init( + key: String, + defaultValue: T? = nil, + isLogged: Bool = false, + userDefaults: UserDefaults = UserDefaults( + suiteName: LockdownStorageIdentifier.userDefaultsId + )!, + decoder: JSONDecoder = .init(), + encoder: JSONEncoder = .init() + ) { + self.key = key + self.defaultValue = defaultValue + self.isLogged = isLogged + self.userDefaults = userDefaults + self.decoder = decoder + self.encoder = encoder + } + + var wrappedValue: T? { + get { + guard let data = userDefaults.object(forKey: key) as? Data, + let value = try? decoder.decode(T.self, from: data) else { + return defaultValue + } + if isLogged { + DDLogInfo("Reading UserDefaults.\(key) of value \(value).") + } + return value + } + set { + guard let newValue, + let data = try? encoder.encode(newValue) else { + userDefaults.removeObject(forKey: key) + return + } + + if isLogged { + DDLogInfo("Setting UserDefaults.\(key) value as \(newValue).") + } + userDefaults.set(data, forKey: key) + } + } +} diff --git a/LockdowniOS/ConfiguredNavigationView.swift b/LockdowniOS/ConfiguredNavigationView.swift new file mode 100644 index 0000000..c99a76f --- /dev/null +++ b/LockdowniOS/ConfiguredNavigationView.swift @@ -0,0 +1,88 @@ +// +// ConfiguredNavigationView.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 1.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class ConfiguredNavigationView: UIView { + + private(set) var buttonCallback: () -> () = { } + var accentColor = UIColor.white + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontBold17 + label.textColor = UIColor.white + label.textAlignment = .center + return label + }() + + lazy var leftNavButton: UIButton = { + let button = UIButton(type: .system) + button.titleLabel?.font = fontBold13 + button.tintColor = UIColor.white + button.addTarget(self, action: #selector(buttonDidPress), for: .touchUpInside) + return button + }() + + lazy var rightNavButton: UIButton = { + let button = UIButton(type: .system) + button.titleLabel?.font = fontBold13 + button.tintColor = UIColor.white + button.addTarget(self, action: #selector(buttonDidPress), for: .touchUpInside) + return button + }() + + init(accentColor: UIColor = .tunnelsBlue) { + super.init(frame: .zero) + self.accentColor = accentColor + configureUI() + } + + var titleView: UIView? { + didSet { + oldValue?.removeFromSuperview() + guard let titleView else { + titleLabel.isHidden = false + return + } + titleLabel.isHidden = true + + addSubview(titleView) + titleView.anchors.leading.marginsPin(inset: 67) + titleView.anchors.trailing.marginsPin(inset: 67) + titleView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configureUI() { + addSubview(titleLabel) + titleLabel.anchors.leading.marginsPin(inset: 20) + titleLabel.anchors.trailing.marginsPin(inset: 20) + titleLabel.anchors.top.pin(inset: 18) + + addSubview(leftNavButton) + leftNavButton.anchors.centerY.equal(titleLabel.anchors.centerY) + leftNavButton.anchors.leading.marginsPin(inset: 8) + leftNavButton.anchors.bottom.marginsPin() + + addSubview(rightNavButton) + rightNavButton.anchors.centerY.equal(titleLabel.anchors.centerY) + rightNavButton.anchors.trailing.marginsPin(inset: 8) + rightNavButton.anchors.bottom.marginsPin() + } + + @objc func buttonDidPress() { + buttonCallback() + } +} diff --git a/LockdowniOS/ConnectionState.swift b/LockdowniOS/ConnectionState.swift new file mode 100644 index 0000000..af82d4c --- /dev/null +++ b/LockdowniOS/ConnectionState.swift @@ -0,0 +1,31 @@ +// +// ConnectionState.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 11.05.23. +// Copyright © 2023 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/ConnectivityService.swift b/LockdowniOS/ConnectivityService.swift new file mode 100644 index 0000000..0febc2d --- /dev/null +++ b/LockdowniOS/ConnectivityService.swift @@ -0,0 +1,86 @@ +// +// ConnectivityService.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 11.05.23. +// Copyright © 2023 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/Controllers/FirewallPaywallViewController.swift b/LockdowniOS/Controllers/FirewallPaywallViewController.swift new file mode 100644 index 0000000..da30d54 --- /dev/null +++ b/LockdowniOS/Controllers/FirewallPaywallViewController.swift @@ -0,0 +1,512 @@ +// +// AdvancedPaywall.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 29.04.2023. +// + +import UIKit +import NetworkExtension +import PromiseKit +import CocoaLumberjackSwift +import StoreKit + +protocol PaywallViewControllerCloseDelegate: AnyObject { + func didClosePaywall() +} + +final class FirewallPaywallViewController: BaseViewController, Loadable { + + var parentVC: UIViewController? + + weak var firewallVC: LDFirewallViewController? + + var advancedPlanUpdated: (() -> ())? + + enum Mode { + case newSubscription + case upgrade(active: [Subscription.PlanType]) + } + + var mode = Mode.newSubscription + + //MARK: Properties + private var titleName = NSLocalizedString("Lockdown", comment: "") + + private lazy var navigationView: ConfiguredNavigationView = + { + let view = ConfiguredNavigationView() + view.rightNavButton.setTitle(NSLocalizedString("RESTORE", comment: ""), for: .normal) + view.rightNavButton.addTarget(self, action: #selector(restorePurchase), for: .touchUpInside) + view.titleLabel.text = NSLocalizedString(titleName, comment: "") + view.leftNavButton.setTitle(NSLocalizedString("CLOSE", comment: ""), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(closeButtonClicked), for: .touchUpInside) + return view + }() + + private lazy var annualPlan: AdvancedPlansViews = { + let view = AdvancedPlansViews() + view.title.text = "Annual" + view.detailTitle.text = "\(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedYearly))/year" + view.detailTitle2.text = "$2.99/month" + view.discountImageView.image = UIImage(named: "saveDiscount") + view.iconImageView.image = UIImage(named: "fill-1") + view.backgroundView.layer.borderColor = UIColor.white.cgColor + view.isUserInteractionEnabled = true + + view.setOnClickListener { [unowned self] in + + selectAdvancedYearly() + + annualView.isHidden = false + monthlyView.isHidden = true + + view.iconImageView.image = UIImage(named: "fill-1") + view.backgroundView.layer.borderColor = UIColor.white.cgColor + + monthlyPlan.iconImageView.image = UIImage(named: "grey-ellipse-1") + monthlyPlan.backgroundView.layer.borderColor = UIColor.borderGray.cgColor + + ftPriceLabel.text = "7-day free trial, then \(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedYearly)) yearly. Cancel anytime." + } + return view + }() + + private lazy var monthlyPlan: AdvancedPlansViews = { + let view = AdvancedPlansViews() + view.title.text = "Monthly" + view.detailTitle.text = "\(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedMonthly))/month" + view.detailTitle2.text = " " + view.isUserInteractionEnabled = true + + view.setOnClickListener { [unowned self] in + + selectAdvancedMonthly() + + monthlyView.isHidden = false + annualView.isHidden = true + + view.iconImageView.image = UIImage(named: "fill-1") + view.backgroundView.layer.borderColor = UIColor.white.cgColor + + annualPlan.iconImageView.image = UIImage(named: "grey-ellipse-1") + annualPlan.backgroundView.layer.borderColor = UIColor.borderGray.cgColor + + ftPriceLabel.text = "7-day free trial, then \(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedMonthly)) monthly. Cancel anytime." + } + return view + }() + + private lazy var plansStack: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.addArrangedSubview(annualPlan) + stack.addArrangedSubview(monthlyPlan) + stack.alignment = .leading + stack.distribution = .fillEqually + stack.spacing = 16 + return stack + }() + + lazy var ftPriceLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("7-day free trial, then \(VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedYearly)) yearly. Cancel anytime.", comment: "") + label.textColor = .smallGrey + label.font = fontMedium11 + label.textAlignment = .center + label.numberOfLines = 0 + return label + }() + + private lazy var freeTrialButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 28 + button.addTarget(self, action: #selector(startTrial), for: .touchUpInside) + let titleLabel = UILabel() + let title = NSLocalizedString("Try 7-day free trial", comment: "") + titleLabel.font = fontSemiBold17 + titleLabel.text = title + titleLabel.textColor = .white + titleLabel.textAlignment = .center + + button.addSubview(titleLabel) + titleLabel.anchors.top.pin(inset: 16) + titleLabel.anchors.bottom.pin(inset: 16) + titleLabel.anchors.leading.pin(inset: 24) + titleLabel.anchors.trailing.pin(inset: 24) + button.anchors.height.equal(56) + return button + }() + + private lazy var annualView: AnnualPlanView = { + let view = AnnualPlanView() + return view + }() + + private lazy var monthlyView: MonthlyPlanView = { + let view = MonthlyPlanView() + view.isHidden = true + return view + }() + + private lazy var privacyLabel: UILabel = { + let label = UILabel() + label.font = fontMedium11 + label.textAlignment = .center + label.numberOfLines = 0 + + let attributedText = NSMutableAttributedString(string: NSLocalizedString("By continuing you agree with our ", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.smallGrey]) + let termsRange = NSRange(location: attributedText.length, length: NSLocalizedString("Terms of Service", comment: "").count) + attributedText.append(NSAttributedString(string: NSLocalizedString("Terms of Service", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.white])) + attributedText.append(NSAttributedString(string: NSLocalizedString(" and ", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.smallGrey])) + let privacyRange = NSRange(location: attributedText.length, length: NSLocalizedString("Privacy Policy", comment: "").count) + attributedText.append(NSAttributedString(string: NSLocalizedString("Privacy Policy", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.white])) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + attributedText.addAttributes([NSAttributedString.Key.paragraphStyle: paragraphStyle], range: NSRange(location: 0, length: attributedText.length)) + label.attributedText = attributedText + label.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(labelTapped(sender:))) + label.addGestureRecognizer(tapGesture) + return label + }() + + //MARK: Lificycle + override func viewDidLoad() { + super.viewDidLoad() + let gradientLayer = CAGradientLayer() + gradientLayer.colors = [UIColor.purplePaywall.cgColor, UIColor.purplePaywall2.cgColor] + gradientLayer.frame = view.bounds + + view.layer.insertSublayer(gradientLayer, at: 0) + configureUI() + } + + //MARK: ConfigureUI + private func configureUI() { + view.addSubview(navigationView) + navigationView.anchors.top.safeAreaPin(inset: 18) + navigationView.anchors.leading.marginsPin() + navigationView.anchors.trailing.marginsPin() + + view.addSubview(privacyLabel) + privacyLabel.anchors.bottom.safeAreaPin() + privacyLabel.anchors.leading.marginsPin() + privacyLabel.anchors.trailing.marginsPin() + privacyLabel.anchors.height.equal(34) + + view.addSubview(freeTrialButton) + freeTrialButton.anchors.bottom.spacing(8, to: privacyLabel.anchors.top) + freeTrialButton.anchors.leading.marginsPin() + freeTrialButton.anchors.trailing.marginsPin() + + view.addSubview(ftPriceLabel) + ftPriceLabel.anchors.bottom.spacing(8, to: freeTrialButton.anchors.top) + ftPriceLabel.anchors.leading.marginsPin() + ftPriceLabel.anchors.trailing.marginsPin() + + view.addSubview(plansStack) + plansStack.anchors.bottom.spacing(20, to: ftPriceLabel.anchors.top) + plansStack.anchors.leading.pin(inset: 16) + plansStack.anchors.trailing.pin(inset: 16) + + view.addSubview(annualView) + annualView.anchors.top.spacing(24, to: navigationView.anchors.bottom) + annualView.anchors.leading.pin() + annualView.anchors.trailing.pin() + annualView.anchors.bottom.spacing(8, to: plansStack.anchors.top) + + view.addSubview(monthlyView) + monthlyView.anchors.top.spacing(24, to: navigationView.anchors.bottom) + monthlyView.anchors.leading.pin() + monthlyView.anchors.trailing.pin() + monthlyView.anchors.bottom.spacing(8, to: plansStack.anchors.top) + } + + //MARK: Functions + + @objc func closeButtonClicked() { + dismiss(animated: true) + } + + @objc private func labelTapped(sender: UITapGestureRecognizer) { + let termsRange = NSRange(location: privacyLabel.attributedText!.length - NSLocalizedString("Terms of Service", comment: "").count - 18, length: NSLocalizedString("Terms of Service", comment: "").count) + let privacyRange = NSRange(location: privacyLabel.attributedText!.length - NSLocalizedString("Privacy Policy", comment: "").count, length: NSLocalizedString("Privacy Policy", comment: "").count) + + if sender.didTapAttributedTextInLabel(label: privacyLabel, inRange: privacyRange), + let url = URL(string: "https://lockdownprivacy.com/privacy") { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } else if sender.didTapAttributedTextInLabel(label: privacyLabel, inRange: termsRange), + let url = URL(string: "https://lockdownprivacy.com/terms") { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } +} + +extension UITapGestureRecognizer { + func didTapAttributedTextInLabel(label: UILabel, inRange targetRange: NSRange) -> Bool { + guard let attributedText = label.attributedText else { return false } + + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: CGSize.zero) + let textStorage = NSTextStorage(attributedString: attributedText) + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = label.lineBreakMode + textContainer.maximumNumberOfLines = label.numberOfLines + textContainer.size = label.bounds.size + let locationOfTouchInLabel = self.location(in: label) + let textBoundingBox = layoutManager.usedRect(for: textContainer) + let textContainerOffset = CGPoint(x: (label.bounds.width - textBoundingBox.width) * 0.5 - textBoundingBox.minX, + y: (label.bounds.height - textBoundingBox.height) * 0.5 - textBoundingBox.minY) + let locationOfTouchInTextContainer = CGPoint(x: locationOfTouchInLabel.x - textContainerOffset.x, + y: locationOfTouchInLabel.y - textContainerOffset.y) + let index = layoutManager.characterIndex(for: locationOfTouchInTextContainer, + in: textContainer, + fractionOfDistanceBetweenInsertionPoints: nil) + return NSLocationInRange(index, targetRange) + } +} + +extension FirewallPaywallViewController: ProductPurchasable { + @objc private func restorePurchase() { + //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? LDFirewallViewController + self.dismiss(animated: true, completion: { + if presentingViewController != nil { + presentingViewController?.toggleFirewall() + } + 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: "")) + } + } + } + + func selectAdvancedYearly() { + VPNSubscription.selectedProductId = VPNSubscription.productIdAdvancedYearly + updatePricingSubtitle() + } + + func selectAdvancedMonthly() { + VPNSubscription.selectedProductId = VPNSubscription.productIdAdvancedMonthly + updatePricingSubtitle() + } + + func updatePricingSubtitle() { + let context: VPNSubscription.SubscriptionContext = { + switch mode { + case .newSubscription: + return .new + case .upgrade: + return .upgrade + } + }() + + if monthlyPlan.isSelected { + let priceLabel = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedMonthly, for: context) + ftPriceLabel.text = "7-day free trial, then \(priceLabel). Cancel anytime." + } else if annualPlan.isSelected { + let priceLabel = VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAdvancedYearly, for: context) + ftPriceLabel.text = "7-day free trial, then \(priceLabel). Cancel anytime." + } + } + + @objc func startTrial() { + showLoadingView() + VPNSubscription.purchase( + succeeded: { + self.dismiss(animated: true, completion: { [self] in + if let presentingViewController = self.parentVC as? LDFirewallViewController { + + // TODO: change view of LDFirewallViewController + } + + // 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 + DDLogError("Start Trial Failed: \(error)") + self.hideLoadingView() + 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 Making Purchase"), message: errorText, acceptButton: .localizedOkay) + } else if self.popupErrorAsNSURLError(error) { + return + } else if self.popupErrorAsApiError(error) { + return + } else { + self.showPopupDialog( + title: .localized("Error Making Purchase"), + message: .localized("Please contact team@lockdownprivacy.com.\n\nError details:\n") + "\(error)", + acceptButton: .localizedOkay) + } + }) + } + + 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 + } +} diff --git a/LockdowniOS/CountdownDisplayService.swift b/LockdowniOS/CountdownDisplayService.swift new file mode 100644 index 0000000..8b2d51c --- /dev/null +++ b/LockdowniOS/CountdownDisplayService.swift @@ -0,0 +1,156 @@ +// +// CountdownDisplayService.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 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/CuratedListsViewController.swift b/LockdowniOS/CuratedListsViewController.swift new file mode 100644 index 0000000..6fdf4a1 --- /dev/null +++ b/LockdowniOS/CuratedListsViewController.swift @@ -0,0 +1,184 @@ +// +// CuratedListsViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 2.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit +import CocoaLumberjackSwift + +final class CuratedListsViewController: UIViewController { + + // MARK: - Properties + + var didMakeChange = false { + didSet{ + + } + } + var lockdownBlockLists: [LockdownGroup] = [] + var basicLockdownBlockLists: [LockdownGroup] = [] + var advancedLockdownBlockLists: [LockdownGroup] = [] + + let curatedBlockedDomainsTableView = StaticTableView() + + private let curatedTableView = UITableView(frame: .zero, style: .insetGrouped) + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .secondarySystemBackground + + configureTableView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + reloadTableView() + } + + private func configureTableView() { + curatedTableView.delegate = self + curatedTableView.dataSource = self + + addTableView(curatedTableView) { tableview in + curatedTableView.anchors.top.pin() + curatedTableView.anchors.leading.pin() + curatedTableView.anchors.trailing.pin() + curatedTableView.anchors.bottom.pin() + } + + reloadTableView() + } + + func reloadTableView() { + + lockdownBlockLists = [] + + lockdownBlockLists = { + let domains = getLockdownBlockedDomains().lockdownDefaults + let sorted = domains.sorted(by: { $0.key < $1.key }) + return Array(sorted.map(\.value)) + }() + + basicLockdownBlockLists = lockdownBlockLists.filter{ $0.accessLevel == "basic"} + advancedLockdownBlockLists = lockdownBlockLists.filter{ $0.accessLevel == "advanced"} + + curatedTableView.reloadData() + } +} + +extension CuratedListsViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + + let view = UIView() + + let sectionName = UILabel() + sectionName.font = fontBold18 + + view.addSubview(sectionName) + sectionName.anchors.top.marginsPin() + sectionName.anchors.leading.marginsPin() + sectionName.anchors.bottom.marginsPin() + + switch section { + case 0: + sectionName.text = NSLocalizedString("Basic", comment: "") + case 1: + sectionName.text = NSLocalizedString("Premium", comment: "") + + let lockImage = UIImageView() + lockImage.image = UIImage(named: "icn_lock") + lockImage.contentMode = .center + + view.addSubview(lockImage) + lockImage.anchors.trailing.marginsPin() + lockImage.anchors.centerY.equal(sectionName.anchors.centerY) + + if UserDefaults.hasSeenAdvancedPaywall || UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { lockImage.isHidden = true } + + default: break + } + return view + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + 40 + } + + func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + + let numberOfBasicLists = basicLockdownBlockLists.count + let numberOfAdvancedLists = advancedLockdownBlockLists.count + + switch section { + case 0: return numberOfBasicLists + case 1: return numberOfAdvancedLists + default: return 0 + } + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 50 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + switch indexPath.section { + case 0: + let cell = UITableViewCell() + let blockListView = BlockListView() + blockListView.contents = .lockdownGroup(basicLockdownBlockLists[indexPath.row]) + cell.contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + cell.accessoryType = .disclosureIndicator + return cell + case 1: + let cell = UITableViewCell() + let blockListView = BlockListView() + blockListView.contents = .lockdownGroup(advancedLockdownBlockLists[indexPath.row]) + cell.contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + cell.accessoryType = .disclosureIndicator + return cell + default: + return UITableViewCell() + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + switch indexPath.section { + case 0: + let storyboard = UIStoryboard.main + let target = storyboard.instantiate(BlockListGroupViewController.self) + target.lockdownGroup = basicLockdownBlockLists[indexPath.row] + target.blockListVC = self + self.navigationController?.pushViewController(target, animated: true) + case 1: + if UserDefaults.hasSeenAdvancedPaywall || UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { + let storyboard = UIStoryboard.main + let target = storyboard.instantiate(BlockListGroupViewController.self) + target.lockdownGroup = advancedLockdownBlockLists[indexPath.row] + target.blockListVC = self + self.navigationController?.pushViewController(target, animated: true) + } else { + let vc = VPNPaywallViewController() + present(vc, animated: true) + } + + default: + break + } + } +} + +extension CuratedListsViewController: UITableViewDelegate {} diff --git a/LockdowniOS/CustomBlockedTableHeader.swift b/LockdowniOS/CustomBlockedTableHeader.swift new file mode 100644 index 0000000..3972b4c --- /dev/null +++ b/LockdowniOS/CustomBlockedTableHeader.swift @@ -0,0 +1,111 @@ +// +// CustomTableHeader.swift +// LockdownSandbox +// +// Created by Aliaksandr Dvoineu on 24.03.23. +// + +import UIKit + +enum Section: Int, CaseIterable, CustomStringConvertible { + + case lists + case domains + + var description: String { + switch self { + case .lists: return "Lists" + case .domains: return "Domains" + } + } +} + +class CustomBlockedTableHeader: UITableViewHeaderFooterView { + static let id = "CustomBlockedTableHeader" + + private(set) var addButtonCallback: () -> () = { } + private(set) var editButtonCallback: () -> () = { } + + lazy var listsTitleLabel: UILabel = { + let label = UILabel() + label.textColor = .label + label.textAlignment = .center + label.font = fontBold18 + return label + }() + + lazy var addButton: UIButton = { + let button = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + button.setImage(UIImage(systemName: "plus", withConfiguration: symbolConfig), for: .normal) + button.tintColor = .tunnelsBlue + button.addTarget(self, action: #selector(addButtonDidPress), for: .touchUpInside) + return button + }() + + lazy var editButton: UIButton = { + let button = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + button.setImage(UIImage(named: "icn_edit"), for: .normal) + button.tintColor = .tunnelsBlue + button.addTarget(self, action: #selector(editButtonDidPress), for: .touchUpInside) + button.isHidden = true + return button + }() + + var category: Section = .lists { + didSet { + switch category { + case .lists: + listsTitleLabel.text = category.description + + case .domains: + listsTitleLabel.text = category.description + } + } + } + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + configure() + } + + func configure() { + contentView.addSubview(listsTitleLabel) + contentView.addSubview(addButton) + contentView.addSubview(editButton) + + listsTitleLabel.anchors.leading.pin() + listsTitleLabel.anchors.bottom.marginsPin() + + addButton.anchors.trailing.pin() + addButton.anchors.bottom.marginsPin() + + editButton.anchors.trailing.spacing(12, to: addButton.anchors.leading) + editButton.anchors.bottom.marginsPin() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @discardableResult + func onAddButtonPressed(_ callback: @escaping () -> ()) -> Self { + addButtonCallback = callback + return self + } + + @discardableResult + func onEditButtonPressed(_ callback: @escaping () -> ()) -> Self { + editButtonCallback = callback + return self + } + + @objc func addButtonDidPress() { + addButtonCallback() + } + + @objc func editButtonDidPress() { + editButtonCallback() + } +} diff --git a/LockdowniOS/CustomListsViewController.swift b/LockdowniOS/CustomListsViewController.swift new file mode 100644 index 0000000..01922c7 --- /dev/null +++ b/LockdowniOS/CustomListsViewController.swift @@ -0,0 +1,437 @@ +// +// CustomListsViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 2.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit +import CocoaLumberjackSwift + +final class CustomListsViewController: UIViewController, DomainListSaveable { + + // MARK: - Properties + + var didMakeChange = false + var customBlockedDomains: [(String, Bool)] = [] + + var customBlockedLists: [UserBlockListsGroup] = [] + + let customBlockedListsTableView = StaticTableView() + let customBlockedDomainsTableView = StaticTableView() + + private lazy var listsSubmenuView: ListsSubmenuView = { + let view = ListsSubmenuView() + view.topButton.addTarget(self, action: #selector(addList), for: .touchUpInside) + view.bottomButton.addTarget(self, action: #selector(importBlockList), for: .touchUpInside) + view.isHidden = true + return view + }() + + private lazy var addNewListButton: UIButton = { + let button = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + button.tintColor = .tunnelsBlue + button.setImage(UIImage(systemName: "plus", withConfiguration: symbolConfig), for: .normal) + button.addTarget(self, action: #selector(showSubmenu), for: .touchUpInside) + button.isEnabled = false + return button + }() + + private lazy var listsLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Lists", comment: "") + label.textColor = .label + label.font = fontBold18 + return label + }() + + private lazy var emptyListsView: EmptyListsView = { + let view = EmptyListsView() + view.descriptionLabel.text = NSLocalizedString("No lists yet", comment: "") + view.addButton.setTitle(NSLocalizedString("Create a list", comment: ""), for: .normal) + view.addButton.addTarget(self, action: #selector(addList), for: .touchUpInside) + return view + }() + + private lazy var lockedListsView: LockedListsView = { + let view = LockedListsView() + return view + }() + + private lazy var domainsLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Domains", comment: "") + label.textColor = .label + label.font = fontBold18 + return label + }() + + private lazy var addNewDomainButton: UIButton = { + let button = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + button.tintColor = .tunnelsBlue + button.setImage(UIImage(systemName: "plus", withConfiguration: symbolConfig), for: .normal) + button.addTarget(self, action: #selector(addDomain), for: .touchUpInside) + return button + }() + + private lazy var editDomainButton: UIButton = { + let button = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + button.tintColor = .tunnelsBlue + button.setImage(UIImage(named: "icn_edit"), for: .normal) + button.addTarget(self, action: #selector(editDomains), for: .touchUpInside) + button.isHidden = true + return button + }() + + private lazy var emptyDomainsView: EmptyListsView = { + let view = EmptyListsView() + view.descriptionLabel.text = NSLocalizedString("No custom domains yet", comment: "") + view.addButton.setTitle(NSLocalizedString("Add a domain", comment: ""), for: .normal) + view.addButton.addTarget(self, action: #selector(addDomain), for: .touchUpInside) + return view + }() + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .secondarySystemBackground + + let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissView)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + + configureCustomBlockedListsTableView() + configureCustomBlockedDomainsTableView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + reloadCustomBlockedLists() + reloadCustomBlockedDomains() + } + + private func configureCustomBlockedListsTableView() { + + view.addSubview(listsLabel) + listsLabel.anchors.top.pin(inset: 12) + listsLabel.anchors.leading.marginsPin() + + view.addSubview(addNewListButton) + addNewListButton.anchors.centerY.equal(listsLabel.anchors.centerY) + addNewListButton.anchors.trailing.marginsPin() + + addTableView(customBlockedListsTableView, layout: { tableView in + tableView.anchors.top.spacing(0, to: listsLabel.anchors.bottom) + tableView.anchors.leading.pin() + tableView.anchors.trailing.pin() + tableView.anchors.height.equal(250) + }) + + view.addSubview(listsSubmenuView) + listsSubmenuView.anchors.trailing.marginsPin() + listsSubmenuView.anchors.top.marginsPin() + + customBlockedListsTableView.deselectsCellsAutomatically = true + } + + private func configureCustomBlockedDomainsTableView() { + + view.addSubview(domainsLabel) + domainsLabel.anchors.top.spacing(290, to: view.anchors.top) + domainsLabel.anchors.height.equal(30) + domainsLabel.anchors.leading.marginsPin() + + view.addSubview(addNewDomainButton) + addNewDomainButton.anchors.centerY.equal(domainsLabel.anchors.centerY) + addNewDomainButton.anchors.trailing.marginsPin() + + addTableView(customBlockedDomainsTableView, layout: { tableView in + tableView.anchors.top.spacing(0, to: domainsLabel.anchors.bottom) + tableView.anchors.leading.pin() + tableView.anchors.trailing.pin() + tableView.anchors.bottom.safeAreaPin() + }) + + customBlockedDomainsTableView.deselectsCellsAutomatically = true + + reloadCustomBlockedDomains() + } +} + +private extension CustomListsViewController { + + func reloadCustomBlockedLists() { + customBlockedListsTableView.clear() + customBlockedLists = { + let lists = getBlockedLists().userBlockListsDefaults + let sorted = lists.sorted(by: { $0.key < $1.key }) + return Array(sorted.map(\.value)) + }() + createCustomBlockedListsRows() + customBlockedListsTableView.reloadData() + } + + func reloadCustomBlockedDomains() { + customBlockedDomainsTableView.clear() + customBlockedDomains = { + let domains = getUserBlockedDomains() + return domains.sorted(by: { $0.key < $1.key }).map { (key, value) -> (String, Bool) in + if let status = value as? NSNumber { + return (key, status.boolValue) + } else { + return (key, false) + } + } + }() + createCustomBlockedDomainsRows() + customBlockedDomainsTableView.reloadData() + } + + func saveNewList(userEnteredListName: String) { + didMakeChange = true + DDLogInfo("Adding custom list - \(userEnteredListName)") + addBlockedList(listName: userEnteredListName) + reloadCustomBlockedLists() + } + + func saveNewDomain(userEnteredDomainName: String) { + let validation = DomainNameValidator.validate(userEnteredDomainName) + + switch validation { + case .valid: + didMakeChange = true + + DDLogInfo("Adding custom domain - \(userEnteredDomainName)") + addUserBlockedDomain(domain: userEnteredDomainName.lowercased()) + reloadCustomBlockedDomains() + case .notValid(let reason): + DDLogWarn("Custom domain is not valid - \(userEnteredDomainName), reason - \(reason)") +// 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 block. For example, \"google.com\" without \"https://\"", comment: ""), +// acceptButton: NSLocalizedString("Okay", comment: "") +// ) + } + } + + func createCustomBlockedListsRows() { + let tableView = customBlockedListsTableView + let emptyList = emptyListsView + let lockedList = lockedListsView + + if UserDefaults.hasSeenAdvancedPaywall || UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { + addNewListButton.isEnabled = true + if customBlockedLists.count == 0 { + tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(emptyList) + emptyListsView.anchors.edges.pin() + }.onSelect { [unowned self] in + self.addList() + } + } + } else { + tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(lockedList) + lockedList.anchors.edges.pin() + }.onSelect { [unowned self] in + let vc = VPNPaywallViewController() + self.present(vc, animated: true) + } + } + + for list in customBlockedLists { + let blockListView = BlockListView() + blockListView.contents = .listsBlocked(list) + + let cell = tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + }.onSelect { [unowned self] in + self.didMakeChange = true + let vc = ListSettingsViewController() + vc.listName = list.name + vc.didMakeChange = list.enabled +// vc.blockListVC = self + navigationController?.pushViewController(vc, animated: true) + }.onSwipeToDelete { [unowned self] in + self.didMakeChange = true + deleteList(list: list.name) + DDLogInfo("Deleting custom list - \(list.name)") + } + cell.accessoryType = .disclosureIndicator + } + } + + func createCustomBlockedDomainsRows() { + let tableView = customBlockedDomainsTableView + + let emptyDomains = emptyDomainsView + if customBlockedDomains.isEmpty { + editDomainButton.isHidden = true + tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(emptyDomains) + emptyDomains.anchors.edges.pin() + }.onSelect { [unowned self] in + self.addDomain() + } + } + + for (domain, isEnabled) in customBlockedDomains { + var currentEnabledStatus = isEnabled + let blockListView = BlockListView() + blockListView.contents = .userBlocked(domain: domain, isEnabled: isEnabled) + + tableView.addRow { [unowned self] (contentView) in + contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + self.view.addSubview(editDomainButton) + editDomainButton.anchors.centerY.equal(domainsLabel.anchors.centerY) + editDomainButton.anchors.trailing.spacing(16, to: addNewDomainButton.anchors.leading) + editDomainButton.isHidden = false + }.onSelect { [unowned blockListView, unowned self] in + self.didMakeChange = true + currentEnabledStatus.toggle() + blockListView.contents = .userBlocked(domain: domain, isEnabled: currentEnabledStatus) + setUserBlockedDomain(domain: domain, enabled: currentEnabledStatus) + }.onSwipeToDelete { [unowned self] in + self.didMakeChange = true + deleteUserBlockedDomain(domain: domain) + DDLogInfo("Deleting custom domain - \(domain)") + } + } + } + + @objc func addList() { + + let tableView = customBlockedListsTableView + + showCreateList( + initialListName: nil, + forDomainList: [] + ) { [weak self] _, name in + guard let self else { return } + self.saveNewList(userEnteredListName: name) + self.reloadCustomBlockedLists() + self.listsSubmenuView.isHidden = true + } + } + + func deleteList(list: String) { + + let alert = UIAlertController(title: NSLocalizedString("Delete List?", comment: ""), + message: NSLocalizedString("Are you sure you want to remove this list?", comment: ""), + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: NSLocalizedString("No, Return", comment: ""), + style: .default, + handler: { [weak self] (_) in + guard let self else { return } + self.reloadCustomBlockedLists() + })) + + alert.addAction(UIAlertAction(title: NSLocalizedString("Yes, Delete", comment: ""), + style: .destructive, + handler: { [weak self] (_) in + guard let self else { return } + deleteBlockedList(listName: list) + self.customBlockedListsTableView.clear() + self.reloadCustomBlockedLists() + })) + + present(alert, animated: true, completion: nil) + } + + @objc func showSubmenu() { + listsSubmenuView.isHidden = false + } + + @objc func dismissView() { + listsSubmenuView.isHidden = true + } + + @objc func importBlockList() { + listsSubmenuView.isHidden = true + let vc = ImportBlockListViewController() + vc.importCompletion = { [unowned self] in + self.reloadCustomBlockedLists() + self.showSuccessImportAlert() + } + + navigationController?.present(vc, animated: true) + } + + @objc func addDomain() { + let tableView = customBlockedDomainsTableView + + let alertController = UIAlertController(title: NSLocalizedString("Add a Domain to Block", comment: ""), + message: nil, + preferredStyle: .alert) + + let saveAction = UIAlertAction(title: "Save", style: .default) { [weak self] (_) in + if let txtField = alertController.textFields?.first, let text = txtField.text { + guard let self else { return } + self.saveNewDomain(userEnteredDomainName: text) + if !getUserBlockedDomains().isEmpty { + tableView.clear() + } + self.didMakeChange = true + self.reloadCustomBlockedDomains() + } + } + + saveAction.isEnabled = false + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (_) in } + + alertController.addTextField { (textField) in + textField.keyboardType = .URL + textField.placeholder = "domain-to-block" + } + + NotificationCenter.default.addObserver( + forName: UITextField.textDidChangeNotification, + object: alertController.textFields?.first, + queue: .main) { (notification) -> Void in + guard let textFieldText = alertController.textFields?.first?.text else { return } + saveAction.isEnabled = textFieldText.isValid(.domainName) && !textFieldText.isEmpty + } + + alertController.addAction(saveAction) + alertController.addAction(cancelAction) + present(alertController, animated: true, completion: nil) + } + + @objc func editDomains() { + if !customBlockedDomains.isEmpty { + let vc = EditDomainsViewController() + vc.updateCompletion = { [weak self] in + self?.reloadCustomBlockedDomains() + } + navigationController?.pushViewController(vc, animated: true) + } + } + + func showSuccessImportAlert() { + let alert = UIAlertController(title: NSLocalizedString("Success!", comment: ""), + message: NSLocalizedString("The list has been imported successfully. You can start blocking the list's domains", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), + style: .default, + handler: nil)) + present(alert, animated: true, completion: nil) + } + + func showErrorAlert() { + let alert = UIAlertController(title: NSLocalizedString("Error", comment: ""), + message: NSLocalizedString("Unable to import the list. Please try again or contact support for assistance", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), + style: .default, + handler: nil)) + present(alert, animated: true, completion: nil) + } +} diff --git a/LockdowniOS/CustomNavigationView.swift b/LockdowniOS/CustomNavigationView.swift new file mode 100644 index 0000000..7a87302 --- /dev/null +++ b/LockdowniOS/CustomNavigationView.swift @@ -0,0 +1,72 @@ +// +// CustomNavigationView.swift +// Lockdown +// +// Created by Oleg Dreyman on 23.10.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class CustomNavigationView: UIView { + + var title: String = "" { + didSet { + titleView.text = title + } + } + + var buttonTitle: String = NSLocalizedString("CLOSE", comment: "") { + didSet { + button.setTitle(buttonTitle, for: .normal) + } + } + + private(set) var buttonCallback: () -> () = { } + + @discardableResult + func onButtonPressed(_ callback: @escaping () -> ()) -> CustomNavigationView { + buttonCallback = callback + return self + } + + let titleView = UILabel() + private let button = UIButton(type: .system) + + init() { + super.init(frame: .zero) + didLoad() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func didLoad() { + addSubview(titleView) + titleView.textAlignment = .center + titleView.text = title + titleView.font = fontMedium17 + + titleView.anchors.centerX.align() + titleView.anchors.top.pin(inset: 18) + + addSubview(button) + button.titleLabel?.font = fontBold13 + button.contentHorizontalAlignment = .leading + button.tintColor = .confirmedBlue + button.setTitle(buttonTitle, for: .normal) + + button.anchors.centerY.equal(titleView.anchors.centerY) + button.anchors.leading.marginsPin(inset: 8) + button.anchors.bottom.marginsPin() + + button.addTarget(self, action: #selector(buttonDidPress), for: .touchUpInside) + } + + @objc + func buttonDidPress() { + buttonCallback() + } +} diff --git a/LockdowniOS/CustomTableView.swift b/LockdowniOS/CustomTableView.swift new file mode 100644 index 0000000..1c67e70 --- /dev/null +++ b/LockdowniOS/CustomTableView.swift @@ -0,0 +1,219 @@ +// +// CustomTableView.swift +// +// Created by Aliaksandr Dvoineu on 16.03.23. +// + +import UIKit + +class TableViewHeader: UIView { + + lazy var view: UIView = { + let view = UIView() + return view + }() + + override init(frame: CGRect) { + super.init(frame: .zero) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure() { + addSubview(view) + view.anchors.edges.pin() + } +} + +class CustomTableViewCell: UITableViewCell { + var selectionCallback: () -> () = { } + var deletionCallback: (() -> ())? + + enum Action { + case toggleCheckmark + } + + @discardableResult + func onSelect(callback: @escaping () -> ()) -> Self { + selectionStyle = .default + selectionCallback = callback + return self + } + + @discardableResult + func onSwipeToDelete(callback: @escaping () -> ()) -> Self { + deletionCallback = callback + return self + } + + @discardableResult + func onSelect(_ action: Action, callback: @escaping () -> () = { }) -> Self { + selectionCallback = { [unowned self] in + switch action { + case .toggleCheckmark: + if self.accessoryType == .checkmark { + self.accessoryType = .none + } else { + self.accessoryType = .checkmark + } + } + callback() + } + return self + } +} + +final class CustomTableView: UITableView { + + // Resizing UITableView to fit content + override var contentSize: CGSize { + didSet { + invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + layoutIfNeeded() + return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) + } + + var rows: [CustomTableViewCell] = [] + var deselectsCellsAutomatically: Bool = false + var headerView = TableViewHeader() + + override init(frame: CGRect, style: UITableView.Style) { + super.init(frame: frame, style: .grouped) + setup() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setup() { + dataSource = self + delegate = self + separatorStyle = .none + } + + enum Insert { + case last + case dontInsert + } + + @discardableResult + func addHeader(_ configure: (UIView) -> ()) -> TableViewHeader { + let header = headerView + configure(header.view) + return header + } + + @discardableResult + func addRow(insert: Insert = .last, _ configure: (UIView) -> ()) -> CustomTableViewCell { + let cell = CustomTableViewCell() + cell.selectionStyle = .none + cell.backgroundColor = nil + configure(cell.contentView) + self.insert(cell: cell, insert: insert) + return cell + } + + @discardableResult + func addRowCell(insert: Insert = .last, _ configure: (UITableViewCell) -> ()) -> CustomTableViewCell { + let cell = CustomTableViewCell() + cell.selectionStyle = .none + configure(cell) + self.insert(cell: cell, insert: insert) + return cell + } + + @discardableResult + func addCell(insert: Insert = .last, _ cell: CustomTableViewCell) -> CustomTableViewCell { + self.insert(cell: cell, insert: insert) + return cell + } + + @discardableResult + func addRow(insert: Insert = .last, view: UIView, insets: UIEdgeInsets = .zero) -> CustomTableViewCell { + return addRow(insert: insert) { (row) in + row.addSubview(view) + view.anchors.edges.pin(axis: .vertical) + view.anchors.edges.marginsPin(insets: insets, axis: .horizontal) + } + } + + private func insert(cell: CustomTableViewCell, insert: Insert) { + switch insert { + case .dontInsert: + break + case .last: + rows.append(cell) + } + } + + func clear() { + rows = [] + } + +} + +extension CustomTableView: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return rows.count + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let header = headerView + return header + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return rows[indexPath.row] + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + let cell = rows[indexPath.row] + return cell.deletionCallback != nil + } + + func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + return .delete + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + let cell = rows[indexPath.row] + guard cell.deletionCallback != nil else { + return + } + cell.deletionCallback?() + self.rows.removeAll(where: { $0 === cell }) + self.deleteRows(at: [indexPath], with: .fade) + } +} + +extension CustomTableView: UITableViewDelegate { + func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + if let cell = tableView.cellForRow(at: indexPath) as? CustomTableViewCell { + return cell.selectionStyle != .none + } else { + return false + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let cell = tableView.cellForRow(at: indexPath) as? CustomTableViewCell { + cell.selectionCallback() + if deselectsCellsAutomatically { + tableView.deselectRow(at: indexPath, animated: true) + } + } + } +} diff --git a/LockdowniOS/CustomUISwitch.swift b/LockdowniOS/CustomUISwitch.swift new file mode 100644 index 0000000..255ee92 --- /dev/null +++ b/LockdowniOS/CustomUISwitch.swift @@ -0,0 +1,60 @@ +// +// CustomUISwitch.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 26.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +import CoreMotion + +final class CustomUISwitch: UIButton { + + var status: Bool = false { + didSet { + self.update() + } + } + + var onImage: UIImage? + var offImage: UIImage? + + init(onImage: UIImage, offImage: UIImage) { + self.onImage = onImage + self.offImage = offImage + super.init(frame: CGRect.zero) + self.setStatus(false) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update() { + UIView.transition(with: self, duration: 0.10, options: .transitionCrossDissolve, animations: { + self.status ? self.setImage(self.onImage, for: .normal) : self.setImage(self.offImage, for: .normal) + }, completion: nil) + } + + func toggle() { + self.status ? self.setStatus(false) : self.setStatus(true) + } + + func setStatus(_ status: Bool) { + self.status = status + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + self.sendHapticFeedback() + self.toggle() + } + + func sendHapticFeedback() { + let impactFeedbackgenerator = UIImpactFeedbackGenerator(style: .heavy) + impactFeedbackgenerator.prepare() + impactFeedbackgenerator.impactOccurred() + } +} diff --git a/LockdowniOS/Date+Ext.swift b/LockdowniOS/Date+Ext.swift new file mode 100644 index 0000000..1f81302 --- /dev/null +++ b/LockdowniOS/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/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/DescriptionLabel.swift b/LockdowniOS/DescriptionLabel.swift new file mode 100644 index 0000000..6713824 --- /dev/null +++ b/LockdowniOS/DescriptionLabel.swift @@ -0,0 +1,87 @@ +// +// FirewallDescriptionLabel.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 18.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +struct DescriptionLabelViewModel { + let text: String +} + +final class DescriptionLabel: UIView { + + // MARK: - Properties + + lazy var lockImage: UIImageView = { + let image = UIImageView() + image.image = UIImage(named: "icn_lock") + image.setContentHuggingPriority(.required, for: .horizontal) + image.setContentCompressionResistancePriority(.required, for: .horizontal) + image.contentMode = .left + image.layer.masksToBounds = true + return image + }() + + lazy var checkmarkImage: UIImageView = { + let image = UIImageView() + image.image = UIImage(named: "icn_checkmark") + image.setContentHuggingPriority(.required, for: .horizontal) + image.setContentCompressionResistancePriority(.required, for: .horizontal) + image.contentMode = .left + image.layer.masksToBounds = true + image.isHidden = true + return image + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontMedium15 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(lockImage) + stackView.addArrangedSubview(checkmarkImage) + stackView.addArrangedSubview(descriptionLabel) + stackView.axis = .horizontal + stackView.distribution = .fill + stackView.alignment = .center + stackView.spacing = 12 + return stackView + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + private func configureUI() { + + addSubview(stackView) + stackView.anchors.top.pin() + stackView.anchors.bottom.pin() + stackView.anchors.leading.pin() + stackView.anchors.trailing.pin() + } + + func configure(with model: DescriptionLabelViewModel) { + descriptionLabel.text = model.text + } +} diff --git a/LockdowniOS/DomainListSaveable.swift b/LockdowniOS/DomainListSaveable.swift new file mode 100644 index 0000000..093a814 --- /dev/null +++ b/LockdowniOS/DomainListSaveable.swift @@ -0,0 +1,96 @@ +// +// DomainListSaveable.swift +// Lockdown +// +// Created by Pavel Vilbik on 23.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +protocol DomainListSaveable { + func showCreateList( + initialListName: String?, + forDomainList domains: Set, + completion: @escaping (Set, String) -> Void + ) +} + +extension DomainListSaveable where Self: UIViewController { + func showCreateList( + initialListName: String?, + forDomainList domains: Set, + completion: @escaping (Set, String) -> Void + ) { + let alertController = UIAlertController(title: "Create New List", message: nil, preferredStyle: .alert) + + let saveAction = UIAlertAction(title: "Save", style: .default) { [weak self] (_) in + if let txtField = alertController.textFields?.first, let text = txtField.text { + self?.validateListName(text, forDomainList: domains, completion: completion) + } + } + + saveAction.isEnabled = false + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { [weak self] (_) in + guard let self else { return } + self.dismiss(animated: true) + } + + alertController.addTextField { (textField) in + textField.text = initialListName + textField.placeholder = NSLocalizedString("List Name", comment: "") + } + + NotificationCenter.default.addObserver( + forName: UITextField.textDidChangeNotification, + object: alertController.textFields?.first, + queue: .main) { (notification) -> Void in + guard let textFieldText = alertController.textFields?.first?.text else { return } + saveAction.isEnabled = textFieldText.isValid(.listName) + } + + alertController.addAction(saveAction) + alertController.addAction(cancelAction) + self.present(alertController, animated: true, completion: nil) + } + + private func validateListName( + _ name: String, + forDomainList domains: Set, + completion: @escaping (Set, String) -> Void + ) { + guard !getBlockedLists().userBlockListsDefaults.keys.contains(name) else { + showAlertAboutExistingListName { [weak self] in + self?.showCreateList( + initialListName: name, + forDomainList: domains, + completion: completion + ) + } + return + } + + completion(domains, name) + } + + private func showAlertAboutExistingListName(completion: @escaping () -> Void) { + let alertController = UIAlertController( + title: NSLocalizedString("This list name is already exist!", comment: ""), + message: NSLocalizedString("Please choose another name.", comment: ""), + preferredStyle: .alert + ) + + alertController.addAction( + .init( + title: NSLocalizedString("Ok", comment: ""), + style: .default, + handler: { _ in + completion() + } + ) + ) + + present(alertController, animated: true) + } +} diff --git a/LockdowniOS/DomainsBlockedTableViewCell.swift b/LockdowniOS/DomainsBlockedTableViewCell.swift new file mode 100644 index 0000000..64de67a --- /dev/null +++ b/LockdowniOS/DomainsBlockedTableViewCell.swift @@ -0,0 +1,84 @@ +// +// DomainsBlockedTableViewCell.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 28.03.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class DomainsBlockedTableViewCell: UITableViewCell { + + // MARK: - Properties + static let identifier = "DomainsBlockedTableViewCell" + + var isBlocked = true + + private lazy var iconImageView: UIImageView = { + let view = UIImageView() + view.tintColor = .gray + view.contentMode = .scaleAspectFit + view.image = UIImage(systemName: "globe") + return view + }() + + lazy var label: UILabel = { + let label = UILabel() + label.font = fontRegular14 + label.textColor = .label + label.numberOfLines = 1 + return label + }() + + lazy var statusLabel: UILabel = { + let label = UILabel() + label.font = fontRegular14 + label.text = isBlocked ? "Blocked" : "Not Blocked" + label.textColor = .gray + label.textAlignment = .right + return label + }() + + // MARK: - Init + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + confugureUI() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + } + + override func prepareForReuse() { + super.prepareForReuse() +// iconImageView.image = nil + label.text = nil +// statusLabel.text = nil + } + + private func confugureUI() { + contentView.addSubview(iconImageView) + iconImageView.anchors.top.marginsPin() + iconImageView.anchors.bottom.marginsPin() + iconImageView.anchors.leading.pin(inset: 8) + + contentView.addSubview(label) + label.anchors.top.marginsPin() + label.anchors.leading.spacing(8, to: iconImageView.anchors.trailing) + label.anchors.bottom.marginsPin() + + contentView.addSubview(statusLabel) + statusLabel.anchors.top.marginsPin() + statusLabel.anchors.trailing.marginsPin() + statusLabel.anchors.bottom.marginsPin() + + contentView.clipsToBounds = true + accessoryType = .none + } +} diff --git a/LockdowniOS/EditDomainsCell.swift b/LockdowniOS/EditDomainsCell.swift new file mode 100644 index 0000000..acdfe34 --- /dev/null +++ b/LockdowniOS/EditDomainsCell.swift @@ -0,0 +1,85 @@ +// +// EditDomainsCell.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 05.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class EditDomainsCell: UIView { + + private let checkMarkView = UIImageView() + private let imageView = UIImageView() + private let groupNameLabel = UILabel() + private let statusLabel = UILabel() + + var contents: Contents = Contents(checkMark: nil, icon: nil, title: nil, status: nil) { + didSet { + checkMarkView.image = contents.checkMark + imageView.image = contents.icon + groupNameLabel.text = contents.title + statusLabel.text = contents.status + } + } + + init() { + super.init(frame: .zero) + configure() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + addSubview(checkMarkView) + checkMarkView.anchors.size.equal(.init(width: 24, height: 24)) + checkMarkView.anchors.leading.marginsPin(inset: 8) + checkMarkView.anchors.centerY.align() + + addSubview(imageView) + imageView.anchors.size.equal(.init(width: 24, height: 24)) + imageView.anchors.leading.spacing(10, to: checkMarkView.anchors.trailing) + imageView.anchors.centerY.align() + + groupNameLabel.text = contents.title + groupNameLabel.font = fontRegular14 + groupNameLabel.numberOfLines = 0 + + addSubview(groupNameLabel) + groupNameLabel.anchors.leading.spacing(10, to: imageView.anchors.trailing) + groupNameLabel.anchors.top.marginsPin(inset: 8) + groupNameLabel.anchors.bottom.marginsPin(inset: 8) + groupNameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + + statusLabel.font = fontRegular14 + statusLabel.text = contents.status + statusLabel.textAlignment = .right + + addSubview(statusLabel) + statusLabel.anchors.trailing.marginsPin(inset: 4) + statusLabel.anchors.width.equal(110) + statusLabel.anchors.leading.spacing(0, to: groupNameLabel.anchors.trailing) + statusLabel.anchors.centerY.equal(groupNameLabel.anchors.centerY) + statusLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + } +} + +extension EditDomainsCell { + struct Contents { + let checkMark: UIImage? + let icon: UIImage? + let title: String? + let status: String? + + static func userBlocked(domain: String, isSelected: Bool, isBlocked: Bool) -> Contents { + let checkMark = isSelected ? UIImage(systemName: "checkmark.circle.fill") : UIImage(systemName: "circle") + let image = UIImage(named: "website_icon.png") + let status = isBlocked ? NSLocalizedString("Blocked", comment: "") : NSLocalizedString("Not Blocked", comment: "") + return Contents(checkMark: checkMark, icon: image, title: domain, status: status) + } + } +} diff --git a/LockdowniOS/EditDomainsViewController.swift b/LockdowniOS/EditDomainsViewController.swift new file mode 100644 index 0000000..3cd024d --- /dev/null +++ b/LockdowniOS/EditDomainsViewController.swift @@ -0,0 +1,241 @@ +// +// EditDomainsViewController.swift +// LockdownSandbox +// +// Created by Aliaksandr Dvoineu on 4.04.23. +// + +import UIKit +import SwiftCSV + +final class EditDomainsViewController: UIViewController { + + // MARK: - Properties + var updateCompletion: (() -> ())? + + private var didMakeChange = false + + var customBlockedDomains: [(String, Bool)] = [] + + var selectedDomains: Dictionary = [:] { + didSet { + if selectedDomains.filter({ $0.value == true }).count == 0 { + bottomMenu.middleButton.isEnabled = false + bottomMenu.rightButton.isEnabled = false + } else { + bottomMenu.middleButton.isEnabled = true + bottomMenu.rightButton.isEnabled = true + } + } + } + + private var titleName = NSLocalizedString("Edit Domains", comment: "") + + private lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView() + view.titleLabel.text = NSLocalizedString(titleName, comment: "") + view.leftNavButton.setTitle(NSLocalizedString("CLOSE", comment: ""), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(closeButtonClicked), for: .touchUpInside) + return view + }() + + private lazy var domainsLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Domains", comment: "") + label.textColor = .label + label.font = fontBold18 + return label + }() + + private lazy var bottomMenu: BottomMenu = { + let view = BottomMenu() + view.leftButton.addTarget(self, action: #selector(selectAllddDomains), for: .touchUpInside) + view.middleButton.addTarget(self, action: #selector(moveToList), for: .touchUpInside) + view.middleButton.isHidden = true + view.middleButton.isEnabled = false + view.rightButton.addTarget(self, action: #selector(deleteDomains), for: .touchUpInside) + view.rightButton.isEnabled = false + return view + }() + + private let customBlockedDomainsTableView = CustomTableView() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .secondarySystemBackground + + if UserDefaults.hasSeenAdvancedPaywall || UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { + bottomMenu.middleButton.isHidden = false } + + configureDomainsTableView() + configureUI() + } + + // MARK: - Configure UI + private func configureUI() { + + view.addSubview(bottomMenu) + bottomMenu.anchors.bottom.pin() + bottomMenu.anchors.height.equal(60) + bottomMenu.anchors.leading.pin() + bottomMenu.anchors.trailing.pin() + } + + private func configureDomainsTableView() { + + view.addSubview(navigationView) + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + navigationView.anchors.top.safeAreaPin() + + addTableView(customBlockedDomainsTableView) { tableView in + tableView.anchors.top.spacing(24, to: navigationView.anchors.bottom) + tableView.anchors.leading.pin() + tableView.anchors.trailing.pin() + tableView.anchors.bottom.pin(inset: 60) + } + + reloadCustomBlockedDomains() + } +} + +// MARK: - Functions +private extension EditDomainsViewController { + + func reloadCustomBlockedDomains() { + customBlockedDomainsTableView.clear() + customBlockedDomains = { + let lists = getUserBlockedDomains() + return lists.sorted(by: { $0.key < $1.key }).map { (key, value) -> (String, Bool) in + if let status = value as? NSNumber { + return (key, status.boolValue) + } else { + return (key, false) + } + } + }() + + createUserBlockedDomainsRows() + customBlockedDomainsTableView.reloadData() + updateLeftButton() + } + + func createUserBlockedDomainsRows() { + let tableView = customBlockedDomainsTableView + tableView.separatorStyle = .singleLine + + let tableTitle = domainsLabel + + tableView.addHeader { view in + view.addSubview(tableTitle) + tableTitle.anchors.top.marginsPin() + tableTitle.anchors.leading.marginsPin() + tableTitle.anchors.bottom.marginsPin() + } + + for (domain, isBlocked) in customBlockedDomains { + let blockListView = EditDomainsCell() + + blockListView.contents = .userBlocked( + domain: domain, + isSelected: self.selectedDomains[domain] ?? false, + isBlocked: isBlocked + ) + + let cell = tableView.addRow { (contentView) in + contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + + }.onSelect { [unowned blockListView, unowned self] in + self.didMakeChange = true + + let isChecked = self.selectedDomains[domain] ?? false + blockListView.contents = .userBlocked( + domain: domain, + isSelected: !isChecked, + isBlocked: isBlocked + ) + + self.selectedDomains[domain] = !isChecked + self.updateLeftButton() + } + + cell.accessoryType = .none + } + } + + private func isAllDomainSelected() -> Bool { + guard !customBlockedDomains.isEmpty else { return false } + + for (domain, _) in customBlockedDomains { + if (selectedDomains[domain] ?? false) == false { + return false + } + } + return true + } + + @objc func closeButtonClicked() { + + updateCompletion?() + navigationController?.popViewController(animated: true) + } + + @objc func selectAllddDomains() { + let wasAllSelected = isAllDomainSelected() + for (domain, _) in customBlockedDomains { + selectedDomains[domain] = !wasAllSelected + } + reloadCustomBlockedDomains() + updateLeftButton() + } + + @objc func moveToList() { + let sortedDomains = selectedDomains.filter({ $0.value == true }) + let vc = MoveToListViewController() + vc.selectedDomains = sortedDomains + vc.moveToListCompletion = { [unowned self] in + self.refreshSelectedDomainsAndReloadCustomBlockedDomains() + } + + present(vc, animated: true) + } + + @objc func deleteDomains() { + let alert = UIAlertController(title: NSLocalizedString("Delete Entries?", comment: ""), + message: NSLocalizedString("Are you sure you want to remove these domains?", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("No, Return", comment: ""), + style: UIAlertAction.Style.default, + handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("Yes, Delete", comment: ""), + style: UIAlertAction.Style.destructive, + handler: { [weak self] (_) in + guard let self else { return } + let sortedDomains = self.selectedDomains.filter({ $0.value == true }) + + for domain in sortedDomains.keys { + deleteUserBlockedDomain(domain: domain) + } + + self.refreshSelectedDomainsAndReloadCustomBlockedDomains() + })) + self.present(alert, animated: true, completion: nil) + } + + private func refreshSelectedDomainsAndReloadCustomBlockedDomains() { + let sortedDomains = self.selectedDomains.filter { $0.value == false } + self.selectedDomains = sortedDomains + + self.reloadCustomBlockedDomains() + } + + private func updateLeftButton() { + bottomMenu.leftButton.setTitle( + isAllDomainSelected() ? NSLocalizedString("Deselect All", comment: "") : NSLocalizedString("Select All", comment: ""), + for: .normal + ) + bottomMenu.leftButton.isEnabled = !customBlockedDomains.isEmpty + } +} diff --git a/LockdowniOS/EmailAddress.swift b/LockdowniOS/EmailAddress.swift new file mode 100644 index 0000000..f029123 --- /dev/null +++ b/LockdowniOS/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/EmailComposable.swift b/LockdowniOS/EmailComposable.swift new file mode 100644 index 0000000..c25dcbf --- /dev/null +++ b/LockdowniOS/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/EmailValidatable.swift b/LockdowniOS/EmailValidatable.swift new file mode 100644 index 0000000..83cf92e --- /dev/null +++ b/LockdowniOS/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/EmptyListsView.swift b/LockdowniOS/EmptyListsView.swift new file mode 100644 index 0000000..5d27dc3 --- /dev/null +++ b/LockdowniOS/EmptyListsView.swift @@ -0,0 +1,94 @@ +// +// NothingBlockedView.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 21.03.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class EmptyListsView: UIView { + + var descriptionText: String = "" { + didSet { + descriptionLabel.text = descriptionText + } + } + + var buttonTitle: String = "" { + didSet { + addButton.setTitle(buttonTitle, for: .normal) + } + } + + private(set) var buttonCallback: () -> () = { } + + @discardableResult + func onButtonPressed(_ callback: @escaping () -> ()) -> Self { + buttonCallback = callback + return self + } + + // MARK: - Properties + + lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(descriptionLabel) + stackView.addArrangedSubview(addButton) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.alignment = .center + stackView.spacing = 4 + return stackView + }() + + lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.textColor = .lightGray + label.textAlignment = .center + label.font = fontBold13 + label.textAlignment = .center + return label + }() + + lazy var addButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .tunnelsBlue + button.backgroundColor = .tunnelsLightBlue + button.titleLabel?.font = fontBold13 + button.layer.cornerRadius = 8 + button.titleEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + return button + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Configure UI + + func configure() { + addSubview(addButton) + addButton.anchors.width.greaterThanOrEqual(120) + + addSubview(stackView) + stackView.anchors.top.marginsPin() + stackView.anchors.bottom.marginsPin() + stackView.anchors.leading.marginsPin() + stackView.anchors.trailing.marginsPin() + } + + // - MARK: Functions + + @objc func buttonDidPress() { + buttonCallback() + } +} 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/FeedbackPaywallViewController.swift b/LockdowniOS/FeedbackPaywallViewController.swift new file mode 100644 index 0000000..1189b8d --- /dev/null +++ b/LockdowniOS/FeedbackPaywallViewController.swift @@ -0,0 +1,410 @@ +// +// FeedbackPaywallViewController.swift +// Lockdown +// +// Created by Fabian Mistoiu on 10.10.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import UIKit +import Combine + +class FeedbackPaywallViewController: UIViewController { + + private let viewModel: FeedbackPaywallViewModel + private var subscriptions = Set() + + init(viewModel: FeedbackPaywallViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UI + private lazy var closeButton: UIButton = { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + button.tintColor = .feedbackText + button.setTitle(Copy.close, for: .normal) + button.titleLabel?.font = .close + button.addAction( + UIAction { [weak self] _ in + guard let self else { return } + self.viewModel.onCloseHandler?(self) + }, + for: .touchUpInside) + return button + }() + + private lazy var bannerView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "feedback-paywall-banner")) + imageView.translatesAutoresizingMaskIntoConstraints = false + + imageView.addSubview(bannerArrowImageView) + NSLayoutConstraint.activate([ + bannerArrowImageView.centerYAnchor.constraint(equalTo: imageView.bottomAnchor), + bannerArrowImageView.centerXAnchor.constraint(equalTo: imageView.rightAnchor, constant: -10), + bannerArrowImageView.widthAnchor.constraint(equalToConstant: 92), + bannerArrowImageView.heightAnchor.constraint(equalToConstant: 92), + imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: 1072 / 687), + imageView.widthAnchor.constraint(lessThanOrEqualToConstant: 420) + ]) + + return imageView + }() + + private lazy var bannerArrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "feedback-paywall-arrow")) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.textColor = .feedbackText + + let continueRange = (Copy.title as NSString).range(of: Copy.titleHighlight) + let attributedString = NSMutableAttributedString( + string: Copy.title, + attributes: [.font: UIFont.title as Any]) + attributedString.addAttribute(.foregroundColor, value: UIColor.feedbackBlue as Any, range: continueRange) + + label.attributedText = attributedString + return label + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.textColor = .feedbackText + label.font = .description + label.text = Copy.description + return label + }() + + private lazy var bulletPointContainer: UIView = { + let stackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 5 + + [Copy.bulletPoint1, Copy.bulletPoint2, Copy.bulletPoint3] + .map { createBulletPointView(copy: $0) } + .forEach { stackView.addArrangedSubview($0) } + return stackView + }() + + + private lazy var bottomContainer: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [continueButton, linksContainer]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .fill + stackView.distribution = .fill + stackView.spacing = 8 + stackView.setCustomSpacing(17, after: continueButton) + + return stackView + }() + + private lazy var continueButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setTitle(Copy.continue, for: .normal) + button.titleLabel?.font = .ctaButton + button.backgroundColor = .feedbackBlue + button.layer.cornerRadius = 29 + button.anchors.height.equal(58) + button.addAction( + UIAction { [weak self] _ in + guard let self else { return } + let pID = viewModel.paywallPlans[viewModel.selectedPlanIndex].id + viewModel.onPurchaseHandler?(self, pID) + }, + for: .touchUpInside) + return button + }() + + private lazy var linksContainer: UIView = { + let linkButtons = [ + createLinkButton(title: Copy.terms, url: URL(string: "https://lockdownprivacy.com/terms")!), + createLinkButton(title: Copy.privacy, url: URL(string: "https://lockdownprivacy.com/privacy")!) + ] + + let stackView = UIStackView(arrangedSubviews: linkButtons) + stackView.distribution = .fillEqually + return stackView + }() + + private var planButtons: [PlanContainer] = [] + + // MARK: - + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .background + + view.addSubview(closeButton) + NSLayoutConstraint.activate([ + closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 5), + closeButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 17) + ]) + + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(scrollView) + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 28 + 16), + scrollView.leftAnchor.constraint(equalTo: view.leftAnchor), + scrollView.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ]) + + let bannerImageContainer = UIView() + bannerImageContainer.translatesAutoresizingMaskIntoConstraints = false + bannerImageContainer.addSubview(bannerView) + NSLayoutConstraint.activate([ + bannerView.topAnchor.constraint(equalTo: bannerImageContainer.topAnchor), + bannerView.bottomAnchor.constraint(equalTo: bannerImageContainer.bottomAnchor), + bannerView.centerXAnchor.constraint(equalTo: bannerImageContainer.centerXAnchor) + ]) + let bannerWidthConstraint = bannerView.widthAnchor.constraint(equalTo: bannerImageContainer.widthAnchor, multiplier: 0.8) + bannerWidthConstraint.priority = .init(rawValue: 999) + bannerWidthConstraint.isActive = true + + let copyStackView = UIStackView(arrangedSubviews: [bannerImageContainer, titleLabel, descriptionLabel, bulletPointContainer]) + copyStackView.translatesAutoresizingMaskIntoConstraints = false + copyStackView.axis = .vertical + copyStackView.alignment = .fill + copyStackView.distribution = .fill + copyStackView.spacing = 0 + copyStackView.setCustomSpacing(22, after: bannerImageContainer) + copyStackView.setCustomSpacing(17, after: descriptionLabel) + scrollView.addSubview(copyStackView) + NSLayoutConstraint.activate([ + copyStackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 0), + copyStackView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor), + copyStackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 39), + copyStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor) + ]) + + view.addSubview(bottomContainer) + NSLayoutConstraint.activate([ + bottomContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 0), + bottomContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor), + bottomContainer.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 45), + bottomContainer.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 10) + ]) + + viewModel.$paywallPlans.sink(receiveValue: { [weak self] plans in + guard let self else { return } + planButtons.forEach { $0.removeFromSuperview() } + + planButtons = plans.map { self.createPlanButton(title: $0.name, price: $0.price, period: $0.pricePeriod, promo: $0.promo) } + planButtons.reversed().forEach { self.bottomContainer.insertArrangedSubview($0, at: 0) } + if let lastButton = planButtons.last { + bottomContainer.setCustomSpacing(30, after: lastButton) + } + + selectButton(at: viewModel.selectedPlanIndex) + }).store(in: &subscriptions) + + viewModel.$selectedPlanIndex.sink(receiveValue: { [weak self] selectedPlanIndex in + guard let self else { return } + selectButton(at: selectedPlanIndex) + }).store(in: &subscriptions) + } + + func selectButton(at selectedIndex: Int) { + for (index, button) in planButtons.map(\.button).enumerated() { + let selected = index == selectedIndex + + button.backgroundColor = selected ? .selectedPlanBackground : .clear + button.titleLabel?.font = selected ? .selectedPlanTitle: .unselectedPlanTitle + button.layer.borderColor = selected ? UIColor.feedbackBlue.cgColor : UIColor.smallGrey.cgColor + } + } + + // MARK: - UI helper + + private func createBulletPointView(copy: String) -> UIView { + let bulletPointImageView = UIImageView(image: UIImage(named: "feedback-checkmark")) + bulletPointImageView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + bulletPointImageView.widthAnchor.constraint(equalToConstant: 9), + bulletPointImageView.heightAnchor.constraint(equalToConstant: 6), + ]) + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.text = copy + label.textColor = .feedbackText + label.font = .bulletPoint + + let spacer = UIView() + spacer.setContentHuggingPriority(.required, for: .horizontal) + NSLayoutConstraint.activate([ + spacer.widthAnchor.constraint(equalToConstant: 5), + spacer.heightAnchor.constraint(equalToConstant: 5), + ]) + + let stackView = UIStackView(arrangedSubviews: [spacer, bulletPointImageView, label]) + stackView.spacing = 8 + stackView.alignment = .center + return stackView + } + + private func createPlanButton(title: String, price: String, period: String?, promo: String?) -> PlanContainer { + let button = UIButton(type: .system) + button.translatesAutoresizingMaskIntoConstraints = false + button.heightAnchor.constraint(equalToConstant: 54).isActive = true + button.layer.cornerRadius = 27 + button.layer.borderWidth = 1 + button.tintColor = .feedbackText + button.setTitle(title, for: .normal) + button.contentHorizontalAlignment = .left + button.contentVerticalAlignment = .center + button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 21, bottom: 0, right: 0) + button.addTarget(self, action: #selector(planSelected), for: .touchUpInside) + + let priceLabel = UILabel() + priceLabel.translatesAutoresizingMaskIntoConstraints = false + priceLabel.textColor = .feedbackText + priceLabel.font = .planPrice + priceLabel.text = price + + let pricePeriodLabel: UILabel? = if period != nil { UILabel() } else { nil } + pricePeriodLabel?.translatesAutoresizingMaskIntoConstraints = false + pricePeriodLabel?.textColor = .feedbackText + pricePeriodLabel?.font = .planPeriod + pricePeriodLabel?.text = period + + let stackView = UIStackView(arrangedSubviews: [priceLabel, pricePeriodLabel].compactMap { $0 }) + stackView.isUserInteractionEnabled = false + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .trailing + button.addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.centerYAnchor.constraint(equalTo: button.centerYAnchor), + stackView.rightAnchor.constraint(equalTo: button.rightAnchor, constant: -18) + ]) + + var promoView: UIView? + if let promo { + let gradientView = GradientView() + gradientView.translatesAutoresizingMaskIntoConstraints = false + gradientView.gradient = .custom([UIColor.promoGradientStart.cgColor, UIColor.promoGradientStart.cgColor], .horizontal) + gradientView.layer.cornerRadius = 11.5 + gradientView.clipsToBounds = true + + let promoLabel = UILabel() + promoLabel.translatesAutoresizingMaskIntoConstraints = false + promoLabel.textColor = .feedbackText + promoLabel.font = .selectedPlanTitle + promoLabel.text = promo + + gradientView.addSubview(promoLabel) + NSLayoutConstraint.activate([ + promoLabel.centerYAnchor.constraint(equalTo: gradientView.centerYAnchor), + promoLabel.centerXAnchor.constraint(equalTo: gradientView.centerXAnchor), + promoLabel.leftAnchor.constraint(equalTo: gradientView.leftAnchor, constant: 7), + gradientView.heightAnchor.constraint(equalToConstant: 23) + ]) + promoView = gradientView + } + + return PlanContainer(button: button, promoLabel: promoView) + } + + private func createLinkButton(title: String, url: URL) -> UIButton { + let button = UIButton(type: .system) + button.titleLabel?.font = fontMedium13 + button.setTitle(title, for: .normal) + button.tintColor = .feedbackText + button.addAction( + UIAction { _ in + UIApplication.shared.open(url, options: [:], completionHandler: nil) + }, + for: .touchUpInside) + return button + } + + @objc func planSelected(_ sender: UIButton) { + guard let index = planButtons.map(\.button).firstIndex(of: sender) else { return } + viewModel.selectPlan(at: index) + } +} + +private class PlanContainer: UIView { + let button: UIButton + let promoLabel: UIView? + + init(button: UIButton, promoLabel: UIView?) { + self.button = button + self.promoLabel = promoLabel + super.init(frame: .zero) + + addSubview(button) + NSLayoutConstraint.activate([ + button.topAnchor.constraint(equalTo: topAnchor), + button.bottomAnchor.constraint(equalTo: bottomAnchor), + button.leftAnchor.constraint(equalTo: leftAnchor), + button.rightAnchor.constraint(equalTo: rightAnchor) + ]) + if let promoLabel { + addSubview(promoLabel) + NSLayoutConstraint.activate([ + promoLabel.centerYAnchor.constraint(equalTo: button.topAnchor), + promoLabel.rightAnchor.constraint(equalTo: button.rightAnchor, constant: -18) + ]) + } + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +private enum Copy { + static var close: String = NSLocalizedString("CLOSE", comment: "") + static var title: String = NSLocalizedString("Tap Continue to Activate this ONE TIME Offer", comment: "") + static var titleHighlight: String = NSLocalizedString("Continue", comment: "") + static var `continue`: String = NSLocalizedString("Continue", comment: "") + static let description: String = NSLocalizedString("Private Browsing with Hidden IP and Global Region Switching", comment: "") + static var bulletPoint1: String = NSLocalizedString("Anonymised browsing", comment: "") + static var bulletPoint2: String = NSLocalizedString("Location and IP address hidden", comment: "") + static var bulletPoint3: String = NSLocalizedString("Unlimited bandwidth and data usage & more", comment: "") + static var terms: String = NSLocalizedString("Terms", comment: "") + static var privacy: String = NSLocalizedString("Privacy", comment: "") + +} + +private extension UIFont { + static let close = UIFont(name: "Montserrat-Bold", size: 13) + static let title = UIFont(name: "SFProRounded-Semibold", size: 28) + static let description = UIFont(name: "Montserrat-Regular", size: 14) + static let bulletPoint = UIFont(name: "Montserrat-SemiBold", size: 12) + static let ctaButton = UIFont(name: "Montserrat-SemiBold", size: 20) + static let selectedPlanTitle = UIFont(name: "Montserrat-Bold", size: 12) + static let unselectedPlanTitle = UIFont(name: "Montserrat-Medium", size: 12) + static let planPrice = UIFont(name: "Montserrat-SemiBold", size: 14) + static let planPeriod = UIFont(name: "Montserrat-Medium", size: 14) +} + +private extension UIColor { + static let background = UIColor.panelSecondaryBackground + static let feedbackText = UIColor.label + static let feedbackBlue = UIColor.fromHex("#00ADE7") + static let selectedPlanBackground = feedbackBlue.withAlphaComponent(0.1) + static let unselectedPlanBorder = UIColor.fromHex("#999999") + static let promoGradientStart = UIColor.fromHex("#FB923C") + static let promoGradientEnd = UIColor.fromHex("#EA580C") +} diff --git a/LockdowniOS/FeedbackPaywallViewModel.swift b/LockdowniOS/FeedbackPaywallViewModel.swift new file mode 100644 index 0000000..231bab6 --- /dev/null +++ b/LockdowniOS/FeedbackPaywallViewModel.swift @@ -0,0 +1,64 @@ +// +// FeedbackPaywallViewModel.swift +// Lockdown +// +// Created by Fabian Mistoiu on 10.10.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import Foundation +import StoreKit + +class FeedbackPaywallViewModel { + + struct PaywallPlan { + let id: String + let name: String + let price: String + let pricePeriod: String? + let promo: String? + } + + @Published var paywallPlans: [PaywallPlan] = [] + @Published var selectedPlanIndex: Int = 0 + + private let products: FeedbackProducts + private let subscriptionInfo: [InternalSubscription] + + var onCloseHandler: ((UIViewController) -> Void)? = nil + var onPurchaseHandler: ((UIViewController, String) -> Void)? = nil + + init(products: FeedbackProducts, subscriptionInfo: [InternalSubscription]) { + self.products = products + self.subscriptionInfo = subscriptionInfo + + createPaywallPlans() + } + + public func selectPlan(at index: Int) { + selectedPlanIndex = index + } + + private func createPaywallPlans() { + let currencyFormatter = NumberFormatter() + currencyFormatter.usesGroupingSeparator = true + currencyFormatter.numberStyle = .currency + currencyFormatter.locale = subscriptionInfo.first?.priceLocale + + guard let yearlyPlan = subscriptionInfo.first(where: { $0.productId == products.yearly}), + let weeklyPlan = subscriptionInfo.first(where: { $0.productId == products.weekly}), + let yearlyPrice = currencyFormatter.string(from: yearlyPlan.price), + let weeklyPrice = currencyFormatter.string(from: weeklyPlan.price) else { + return + } + + let yearlyPricePerWeek = yearlyPlan.price.dividing(by: 52) + let saving = 100 - Int(Double(truncating: yearlyPricePerWeek) / Double(truncating: weeklyPlan.price)*100) + + paywallPlans = [ + PaywallPlan(id: products.yearly, name: "Yearly Plan", price: yearlyPrice, pricePeriod: nil, promo: "SAVE \(saving)%"), + PaywallPlan(id: products.weekly, name: "Weekly Plan", price: weeklyPrice, pricePeriod: "per week", promo: nil) + ] + selectedPlanIndex = 0 + } +} diff --git a/LockdowniOS/FirewallRepair.swift b/LockdowniOS/FirewallRepair.swift new file mode 100644 index 0000000..b6aeb01 --- /dev/null +++ b/LockdowniOS/FirewallRepair.swift @@ -0,0 +1,144 @@ +// +// FirewallRepair.swift +// Lockdown +// +// Created by Oleg Dreyman on 21.10.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation +import CocoaLumberjackSwift +import BackgroundTasks + +enum FirewallRepair { + + static let identifier = "com.confirmed.lockdown.firewallscheduler" + + enum Result { + case repairAttempted + case failed(Swift.Error?) + case noAction + } + + enum Context: CustomDebugStringConvertible { + case backgroundRefresh + case homeScreenDidLoad + + var debugDescription: String { + switch self { + case .backgroundRefresh: + return "Background Check" + case .homeScreenDidLoad: + return "Home" + } + } + } + + @available(iOS 13.0, *) + static func handleAppRefresh(_ task: BGTask) { + DDLogInfo("BGTask: Handle App Refresh called") + + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + + let operation = BlockOperation { + DDLogInfo("BGTask: Operation started") + FirewallRepair.run(context: .backgroundRefresh) { (result) in + DDLogInfo("BGTask: Result: \(result)") + } + } + operation.completionBlock = { + DDLogInfo("BGTask: Operation completion block") + let success = !operation.isCancelled + DDLogInfo("BGTask: Operation completion success: \(success)") + task.setTaskCompleted(success: success) + } + + queue.addOperation(operation) + + task.expirationHandler = { + DDLogError("BGTask: Expiration Handler called") + queue.cancelAllOperations() + } + + FirewallRepair.reschedule() + } + + 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") + } + } + + static func run(context: Context, completion: @escaping (FirewallRepair.Result) -> Void = { _ in }) { + DDLogInfo("Repair \(context): Starting") + + // Check 2 conditions for firewall restart, but reload manager first to get non-stale one + FirewallController.shared.refreshManager(completion: { error in + if let e = error { + DDLogError("Repair \(context): Error refreshing Manager in \(context): \(e)") + completion(.failed(e)) + return + } + 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) { + DDLogInfo("Repair \(context): user wants enabled") + if (appHasJustBeenUpgradedOrIsNewInstall()) { + DDLogInfo("Repair \(context): APP UPGRADED, REFRESHING DEFAULT BLOCK LISTS, WHITELISTS, RESTARTING FIREWALL") + setupFirewallDefaultBlockLists() + setupLockdownWhitelistedDomains() + FirewallController.shared.restart(completion: { + error in + if error != nil { + DDLogError("Error restarting firewall on \(context): \(error!)") + } + completion(.repairAttempted) + }) + } + 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): Doing repair") + FirewallController.shared.restart(completion: { + error in + if error != nil { + DDLogError("Repair \(context): Error restarting firewall on \(context): \(error!)") + } + DDLogError("Repair \(context): Returned from restart, no error") + completion(.repairAttempted) + }) + }.catch { error in + let nsError = error as NSError + if nsError.domain == NSURLErrorDomain { + DDLogInfo("Repair \(context): Successful blocking of \(testFirewallDomain) with NSURLErrorDomain error: \(nsError)") + } + else { + DDLogInfo("Repair \(context): Successful blocking of \(testFirewallDomain), but seeing non-NSURLErrorDomain error: \(error)") + } + completion(.repairAttempted) + } + } + } + else { + completion(.noAction) + } + }) + } +} diff --git a/LockdowniOS/FloatingTextInputTextField.swift b/LockdowniOS/FloatingTextInputTextField.swift new file mode 100644 index 0000000..8343425 --- /dev/null +++ b/LockdowniOS/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/Font+Ext.swift b/LockdowniOS/Font+Ext.swift new file mode 100644 index 0000000..5cd1ff6 --- /dev/null +++ b/LockdowniOS/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/HomeViewController.swift b/LockdowniOS/HomeViewController.swift index e9f1302..8dadda6 100644 --- a/LockdowniOS/HomeViewController.swift +++ b/LockdowniOS/HomeViewController.swift @@ -12,34 +12,172 @@ import CocoaLumberjackSwift import UIKit import PromiseKit import StoreKit -import SwiftyStoreKit import PopupDialog import AwesomeSpotlightView +import SwiftUI class CircularView: UIView { override func layoutSubviews() { super.layoutSubviews() self.layer.cornerRadius = self.bounds.size.width * 0.50 } + + @IBInspectable var shadowUIColor: UIColor? { + didSet { + redrawShadowColor() + } + } + + func redrawShadowColor() { + self.layer.shadowColor = shadowUIColor?.cgColor + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + redrawShadowColor() + } } -class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { +let kHasSeenEmailSignup = "hasSeenEmailSignup" + +class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate, Loadable { let kHasViewedTutorial = "hasViewedTutorial" - let kVPNBodyViewVisible = "VPNBodyViewVisible" + let kHasSeenInitialFirewallConnectedDialog = "hasSeenInitialFirewallConnectedDialog11" + let kHasSeenShare = "hasSeenShareDialog4" let ratingCountKey = "ratingCount" + lastVersionToAskForRating let ratingTriggeredKey = "ratingTriggered" + lastVersionToAskForRating - @IBOutlet weak var menuButton: UIButton! + var feedbackFlow: FeedbackFlow? + + private lazy var scrollView: UIScrollView = { + let view = UIScrollView() + view.isScrollEnabled = true + return view + }() + + private lazy var contentView: UIView = { + let view = UIView() + return view + }() + + private lazy var yourCurrentPlanLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Your current plan is", comment: "") + label.font = fontRegular14 + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + private lazy var upgradeLabel: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setTitle(NSLocalizedString("See plans", comment: ""), for: .normal) + button.titleLabel?.font = fontBold13 + button.backgroundColor = .tunnelsBlue + button.anchors.height.equal(24) + button.layer.cornerRadius = 12 + button.anchors.width.equal(100) + button.addTarget(self, action: #selector(upgrade), for: .touchUpInside) + return button + }() + + private lazy var protectionPlanLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Basic Protection", comment: "") + label.font = fontBold22 + label.textColor = .label + return label + }() + + private lazy var mainTitle: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Get Anonymous protection", comment: "") + label.font = fontBold24 + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + private lazy var descriptionLabel1: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Block as many trackers as you want", comment: ""))) + return label + }() + + private lazy var descriptionLabel2: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Import and export your own block lists", comment: ""))) + return label + }() + + private lazy var descriptionLabel3: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Access to new curated lists of trackers", comment: ""))) + return label + }() + + private lazy var descriptionLabel4: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("The only fully open source VPN", comment: ""))) + return label + }() + + private lazy var descriptionLabel5: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Hide your identity around the world", comment: ""))) + return label + }() - @IBOutlet var stackEqualHeightConstraint: NSLayoutConstraint! + private lazy var descriptionLabel6: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Protect all Apple devices", comment: ""))) + return label + }() + + private lazy var upgradeButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setTitle(NSLocalizedString("Upgrade", comment: ""), for: .normal) + button.titleLabel?.font = fontBold18 + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 28 + button.anchors.height.equal(56) + button.addTarget(self, action: #selector(upgrade), for: .touchUpInside) + return button + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.distribution = .fill + stackView.spacing = 10 + stackView.layer.cornerRadius = 8 + stackView.backgroundColor = UIColor.dynamicColor(light: .extraLightGray, dark: .panelSecondaryBackground!) + stackView.layoutMargins = UIEdgeInsets(top: 17, left: 20, bottom: 17, right: 20) + stackView.isLayoutMarginsRelativeArrangement = true + return stackView + }() + + private lazy var closeButton: UIButton = { + let button = UIButton() + let config = UIImage.SymbolConfiguration(pointSize: 23) + button.setImage(UIImage(systemName: "xmark.circle.fill", withConfiguration: config), for: .normal) + button.tintColor = .secondaryLabel + button.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) + return button + }() + + @IBOutlet var mainStack: UIStackView! @IBOutlet weak var firewallTitleLabel: UILabel! @IBOutlet weak var firewallActive: UILabel! @IBOutlet weak var firewallToggleCircle: UIButton! @IBOutlet weak var firewallToggleAnimatedCircle: NVActivityIndicatorView! @IBOutlet weak var firewallButton: UIButton! + @IBOutlet weak var tapToActivateFirewallLabel: UILabel! var lastFirewallStatus: NEVPNStatus? @IBOutlet weak var metricsStack: UIStackView! @IBOutlet weak var dailyMetrics: UILabel? @@ -48,40 +186,40 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { var metricsTimer : Timer? @IBOutlet weak var firewallSettingsButton: UIButton! @IBOutlet weak var firewallViewLogButton: UIButton! + @IBOutlet weak var firewallShareButton: UIButton! - @IBOutlet var vpnViewHeightConstraint: NSLayoutConstraint! @IBOutlet weak var vpnHeaderView: UIView! - @IBOutlet weak var vpnHideButton: UIButton! - @IBOutlet weak var vpnBodyView: UIView! @IBOutlet weak var vpnActive: UILabel! - @IBOutlet var vpnActiveHeaderConstraint: NSLayoutConstraint! - @IBOutlet var vpnActiveTopBodyConstraint: NSLayoutConstraint! - @IBOutlet var vpnActiveVerticalBodyConstraint: NSLayoutConstraint! @IBOutlet weak var vpnToggleCircle: UIButton! @IBOutlet weak var vpnToggleAnimatedCircle: NVActivityIndicatorView! @IBOutlet weak var vpnButton: UIButton! -// @IBOutlet weak var vpnIP: UILabel! -// @IBOutlet weak var vpnSpeed: UILabel! var lastVPNStatus: NEVPNStatus? @IBOutlet weak var vpnSetRegionButton: UIButton! @IBOutlet weak var vpnRegionLabel: UILabel! @IBOutlet weak var vpnWhitelistButton: UIButton! + private let userService = BaseUserService.shared + + var activePlans: [Subscription.PlanType] = [] + override func viewDidLoad() { super.viewDidLoad() + view.addSubview(yourCurrentPlanLabel) + yourCurrentPlanLabel.anchors.leading.marginsPin() + yourCurrentPlanLabel.anchors.top.safeAreaPin(inset: 16) + + layoutUI() + updateFirewallButtonWithStatus(status: FirewallController.shared.status()) updateMetrics() - if metricsTimer == nil { - metricsTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(updateMetrics), userInfo: nil, repeats: true) - metricsTimer?.fire() - } + didBecomeActive() + firewallViewLogButton.layer.cornerRadius = 8 firewallViewLogButton.layer.maskedCorners = [.layerMinXMaxYCorner] firewallSettingsButton.layer.cornerRadius = 8 firewallSettingsButton.layer.maskedCorners = [.layerMaxXMaxYCorner] - vpnHeaderView.addGestureRecognizer( UITapGestureRecognizer(target: self, action: #selector (vpnHeaderTapped(_:))) ) updateVPNButtonWithStatus(status: VPNController.shared.status()) //updateIP() vpnWhitelistButton.layer.cornerRadius = 8 @@ -90,6 +228,8 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { vpnSetRegionButton.layer.maskedCorners = [.layerMaxXMaxYCorner] updateVPNRegionLabel() + updateStackViewAxis(basedOn: view.frame.size) + // Check Subscription - if VPN active but not subscribed, then disconnect and show dialog (don't do this if connection error) if (VPNController.shared.status() == .connected) { firstly { @@ -105,47 +245,334 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { else if let apiError = error as? ApiError { switch apiError.code { case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: - self.showPopupDialog(title: "VPN Subscription Expired", message: "Please renew your subscription to re-activate the VPN.", acceptButton: "Okay", completionHandler: { - self.performSegue(withIdentifier: "showSignup", sender: self) + 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: { + let vc = VPNPaywallViewController() + self.present(vc, animated: true) + // self.performSegue(withIdentifier: "showSignup", sender: self) }) default: _ = self.popupErrorAsApiError(error) } } else { - self.showPopupDialog(title: "Error Signing In To Verify Subscription", + self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""), message: "\(error)", - acceptButton: "Okay") + acceptButton: "Okay") } } } - NotificationCenter.default.addObserver(self, selector: #selector(tunnelStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(showMultipleSubscriptionsAlert), name: .showMultipleSubscriptionsAlert, object: nil) + } + + private func layoutUI() { + + if UserDefaults.hasSeenAnonymousPaywall { + mainTitle.text = "Get Universal\nprotection" + protectionPlanLabel.text = "Anonymous protection" + stackView.addArrangedSubview(mainTitle) + stackView.addArrangedSubview(descriptionLabel6) + stackView.addArrangedSubview(upgradeButton) + stackView.setCustomSpacing(14, after: mainTitle) + stackView.setCustomSpacing(14, after: descriptionLabel6) + } else if UserDefaults.hasSeenUniversalPaywall { + protectionPlanLabel.text = "Universal protection" + stackView.anchors.height.equal(0) + contentView.anchors.height.equal(UIScreen.main.bounds.height - 150) + closeButton.isHidden = true + upgradeLabel.isHidden = true + } else if UserDefaults.hasSeenAdvancedPaywall { + mainTitle.text = "Get Anonymous\nprotection" + protectionPlanLabel.text = "Advanced protection" + stackView.addArrangedSubview(mainTitle) + stackView.addArrangedSubview(descriptionLabel4) + stackView.addArrangedSubview(descriptionLabel5) + stackView.setCustomSpacing(14, after: mainTitle) + stackView.setCustomSpacing(14, after: descriptionLabel5) + stackView.addArrangedSubview(upgradeButton) + } else { + mainTitle.text = "Get Advanced\nprotection" + protectionPlanLabel.text = "Basic protection" + stackView.addArrangedSubview(mainTitle) + stackView.addArrangedSubview(descriptionLabel1) + stackView.addArrangedSubview(descriptionLabel2) + stackView.addArrangedSubview(descriptionLabel3) + stackView.addArrangedSubview(upgradeButton) + stackView.setCustomSpacing(14, after: mainTitle) + stackView.setCustomSpacing(14, after: descriptionLabel3) + } + + view.addSubview(upgradeLabel) + upgradeLabel.anchors.trailing.marginsPin() + upgradeLabel.anchors.centerY.equal(yourCurrentPlanLabel.anchors.centerY) + + view.addSubview(protectionPlanLabel) + protectionPlanLabel.anchors.top.spacing(8, to: yourCurrentPlanLabel.anchors.bottom) + protectionPlanLabel.anchors.leading.marginsPin() + + view.addSubview(scrollView) + scrollView.anchors.top.spacing(12, to: protectionPlanLabel.anchors.bottom) + scrollView.anchors.leading.pin() + scrollView.anchors.trailing.pin() + scrollView.anchors.bottom.pin(inset: 80) + + scrollView.addSubview(contentView) + contentView.anchors.top.pin() + contentView.anchors.leading.pin() + contentView.anchors.width.equal(scrollView.anchors.width) + contentView.anchors.bottom.pin() + contentView.anchors.trailing.pin() + + contentView.addSubview(stackView) + stackView.anchors.top.marginsPin() + stackView.anchors.leading.pin(inset: 22) + stackView.anchors.trailing.pin(inset: 22) + + contentView.addSubview(mainStack) + mainStack.anchors.top.spacing(8, to: stackView.anchors.bottom) + mainStack.anchors.leading.marginsPin() + mainStack.anchors.trailing.marginsPin() + mainStack.anchors.bottom.pin() + + contentView.addSubview(closeButton) + closeButton.anchors.trailing.spacing(-8, to: stackView.anchors.trailing) + closeButton.anchors.top.marginsPin(inset: 8) + closeButton.anchors.height.equal(40) + closeButton.anchors.width.equal(40) + } + + @objc func closeButtonTapped() { + stackView.anchors.height.equal(0) + upgradeButton.isHidden = true + closeButton.isHidden = true + } + + @objc func upgrade() { +// showSpecialOffer() + let vc = VPNPaywallViewController() + vc.purchaseSuccessful = { [weak self] in self?.handlePurchaseSuccessful() } + vc.purchaseFailed = { [weak self] err in self?.handlePurchaseFailed(error: err)} + present(vc, animated: true) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + let inset = firewallButton.frame.width * 0.175 + firewallButton.contentEdgeInsets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) + vpnButton.contentEdgeInsets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + tabBarController?.delegate = self + UIScrollView.appearance().bounces = true + UIScrollView.appearance().isScrollEnabled = true } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - toggleVPNBodyView(animate: false, show: defaults.bool(forKey: kVPNBodyViewVisible)) + OneTimeActions.performOnce(ifHasNotSeen: .welcomeScreen) { + let vc = WelcomeViewController() + vc.modalPresentationStyle = .overFullScreen + self.present(vc, animated: true) + } + // Used for debugging signup + //performSegue(withIdentifier: "showSignup", sender: nil) + if (defaults.bool(forKey: kHasViewedTutorial) == false) { - startTutorial() +// startTutorial() + } + else if (defaults.bool(forKey: kHasSeenEmailSignup) == false) { + AccountUI.presentCreateAccount(on: self) } + + if defaults.bool(forKey: kHasSeenInitialFirewallConnectedDialog) == false { + tapToActivateFirewallLabel.isHidden = false + } + + // If total blocked > 1000, and have not shown share dialog before, ask if user wants to share + if (getTotalMetrics() > 1000 && defaults.bool(forKey: kHasSeenShare) != true) { + defaults.set(true, forKey: kHasSeenShare) + let popup = PopupDialog(title: "You've blocked over 1000 trackers! 🎊", + message: NSLocalizedString("Share your anonymized metrics and show other people how to block invasive tracking.", comment: ""), + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + popup.addButtons([ + CancelButton(title: NSLocalizedString("Not Now", comment: ""), dismissOnTap: true) { + let s0 = AwesomeSpotlight(withRect: self.getRectForView(self.firewallShareButton).insetBy(dx: -13.0, dy: -13.0), shape: .roundRectangle, text: NSLocalizedString("You can tap this later if you feel like sharing.\n(Tap anywhere to dismiss)", comment: "")) + let spotlightView = AwesomeSpotlightView(frame: self.view.frame, + spotlight: [s0]) + spotlightView.cutoutRadius = 8 + spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); + spotlightView.enableArrowDown = true + spotlightView.textLabelFont = fontMedium16 + spotlightView.labelSpacing = 24; + spotlightView.delegate = self + self.view.addSubview(spotlightView) + spotlightView.start() + }, + DefaultButton(title: NSLocalizedString("Next", comment: ""), dismissOnTap: true) { + self.shareFirewallMetricsTapped("") + } + ]) + self.present(popup, animated: true, completion: nil) + } + + if UserDefaults.hasPurchasedFromOnboarding && UserDefaults.shouldShowMultipleSubscriptionAlert && !UserDefaults.didShowMultipleSubscriptionAlert { + self.showMultipleSubscriptionsAlert() + } + + ReviewAlertManager.checkAndShowAlert() + } + + @objc + private func didBecomeActive() { + NotificationCenter.default.addObserver(self, selector: #selector(tunnelStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil) + startTimer() + + let appDelegate = UIApplication.shared.delegate as! AppDelegate + if !defaults.bool(forKey: kOneTimeOfferShown) && appDelegate.timeSinceLastStart > 4 * 60 * 60 { + if BaseUserService.shared.user.currentSubscription == nil { + if let targetDate = Calendar.current.date(from: DateComponents(year: 2025, month: 2, day: 01, hour: 00, minute: 00, second: 00)), Date() < targetDate { + showSpecialOffer() + } else { + showOneTimeOffer() + } + } + } + } + + @objc + private func willResignActive() { + NotificationCenter.default.removeObserver(self, name: .NEVPNStatusDidChange, object: nil) + stopTimer() + } + + func updateStackViewAxis(basedOn size: CGSize) { + guard traitCollection.userInterfaceIdiom == .pad else { + // axis always vertical on iPhone + return + } + + if size.width > size.height { + mainStack.axis = .horizontal + } else { + mainStack.axis = .vertical + } + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + guard traitCollection.userInterfaceIdiom == .pad else { + return + } + + coordinator.animate { [unowned self] _ in + self.updateStackViewAxis(basedOn: size) + } completion: { (_) in + return + } + } + + private func showOneTimeOffer() { + Task { @MainActor in + try? await Task.sleep(nanoseconds: 1_000_000_000) + if let productInfos = await VPNSubscription.shared.loadSubscriptions(type: .oneTime) { + let model = OneTimePaywallModel(products: VPNSubscription.oneTimeProducts, infos: productInfos) + model.closeAction = { [weak self] in self?.dismiss(animated: true)} + model.continueAction = { [weak self] pid in + VPNSubscription.selectedProductId = pid + VPNSubscription.purchase { + self?.handlePurchaseSuccessful() + } errored: { err in + self?.handlePurchaseFailed(error: err) + } + } + let viewCtrl = UIHostingController(rootView: OneTimePaywallView(model: model)) + viewCtrl.modalPresentationStyle = .fullScreen + self.present(viewCtrl, animated: true) + defaults.set(true, forKey: kOneTimeOfferShown) + } + } + } + + private func showSpecialOffer() { + Task { @MainActor in + try? await Task.sleep(nanoseconds: 1_000_000_000) + if let productInfos = await VPNSubscription.shared.loadSubscriptions(type: .specialOffer) { + let model = SpecialOfferPaywallModel(products: VPNSubscription.specialOfferProducts, infos: productInfos) + model.closeAction = { [weak self] in self?.dismiss(animated: true)} + model.continueAction = { [weak self] pid in + VPNSubscription.selectedProductId = pid + VPNSubscription.purchase { + self?.handlePurchaseSuccessful() + } errored: { err in + model.showProgress = false + self?.handlePurchaseFailed(error: err) + } + } + let viewCtrl = UIHostingController(rootView: SpecialOfferPaywallView(model: model)) + viewCtrl.modalPresentationStyle = .fullScreen + self.present(viewCtrl, animated: true) + defaults.set(true, forKey: kOneTimeOfferShown) + } + } + } + + func showVPNSubscriptionDialog(title: String, message: String) { + let popup = PopupDialog( + title: title, + message: message, + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceUp, + preferredWidth: 300.0, + tapGestureDismissal: false, + panGestureDismissal: false, + hideStatusBar: true, + completion: nil) + + let getEnhancedPrivacyButton = DefaultButton(title: NSLocalizedString("1 Week Free", comment: ""), dismissOnTap: true) { [unowned self] in + let vc = VPNPaywallViewController() + present(vc, animated: true) + } + let laterButton = CancelButton(title: NSLocalizedString("Skip Trial", comment: ""), dismissOnTap: true) { } + + popup.addButtons([laterButton, getEnhancedPrivacyButton]) + + self.present(popup, animated: true, completion: nil) } // This notification is triggered for both Firewall and VPN @objc func tunnelStatusDidChange(_ notification: Notification) { // Firewall if let tunnelProviderSession = notification.object as? NETunnelProviderSession { - DDLogInfo("VPNStatusDidChange as NETunnelProviderSession with status: \(tunnelProviderSession.status.rawValue)"); - if (!getUserWantsFirewallEnabled()) { - updateFirewallButtonWithStatus(status: .disconnected) - } - else { - updateFirewallButtonWithStatus(status: tunnelProviderSession.status) + DDLogInfo("VPNStatusDidChange as NETunnelProviderSession with status: \(tunnelProviderSession.status.description)"); + updateFirewallButtonWithStatus(status: tunnelProviderSession.status) + + if getUserWantsFirewallEnabled() && + tunnelProviderSession.status == .connected && + defaults.bool(forKey: kHasSeenInitialFirewallConnectedDialog) == false { + defaults.set(true, forKey: kHasSeenInitialFirewallConnectedDialog) + self.tapToActivateFirewallLabel.isHidden = true + let hasSubscription = UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall || UserDefaults.hasSeenAdvancedPaywall + if (VPNController.shared.status() == .invalid && !hasSubscription) { + self.showVPNSubscriptionDialog(title: NSLocalizedString("🔥🧱 Firewall Activated 🎊🎉", comment: ""), message: NSLocalizedString("Trackers, ads, and other malicious scripts are now blocked in all your apps, even outside of Safari.\n\nGet maximum privacy with a Secure Tunnel that protects connections, anonymizes your browsing, and hides your location.", comment: "")) + } } } // VPN else if let neVPNConnection = notification.object as? NEVPNConnection { - DDLogInfo("VPNStatusDidChange as NEVPNConnection with status: \(neVPNConnection.status.rawValue)"); + 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 { @@ -159,77 +586,146 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { // MARK: - Top Buttons - @IBAction func menuTapped(_ sender: Any) { - let popup = PopupDialog(title: nil, - message: "For suggestions, questions, and issues, please tap Email Support.", - image: nil, - buttonAlignment: .vertical, - transitionStyle: .bounceDown, - preferredWidth: 270, - tapGestureDismissal: true, - panGestureDismissal: false, - hideStatusBar: false, - completion: nil) - popup.addButtons([ - DefaultButton(title: "Tutorial", dismissOnTap: true) { - self.startTutorial() - }, - DefaultButton(title: "Why Trust Lockdown", dismissOnTap: true) { - self.showWhyTrustPopup() - }, - DefaultButton(title: "Privacy Policy", dismissOnTap: true) { - self.showPrivacyPolicyModal() - }, - DefaultButton(title: "About The VPN", dismissOnTap: true) { - self.showVPNDetails() - }, - DefaultButton(title: "Email Support", dismissOnTap: true) { - self.emailTeam() - }, - DefaultButton(title: "Website", dismissOnTap: true) { - self.showWebsiteModal() - }, - CancelButton(title: "Cancel", dismissOnTap: true) {} - ]) - self.present(popup, animated: true, completion: nil) - } - 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: "Welcome to the Lockdown Tutorial.\n\nTap anywhere to continue.") - let s1 = AwesomeSpotlight(withRect: getRectForView(firewallTitleLabel).insetBy(dx: -13.0, dy: -13.0), shape: .roundRectangle, text: "Lockdown Firewall blocks bad and untrusted connections in all your apps - not just Safari.") - let s2 = AwesomeSpotlight(withRect: getRectForView(firewallToggleCircle).insetBy(dx: -10.0, dy: -10.0), shape: .circle, text: "Activate Firewall with this button.") - let s3 = AwesomeSpotlight(withRect: getRectForView(metricsStack).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: "See live metrics for how many bad connections Firewall has blocked.") - let s4 = AwesomeSpotlight(withRect: getRectForView(firewallViewLogButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: "\"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 s5 = AwesomeSpotlight(withRect: getRectForView(firewallSettingsButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: "\"Block List\" lets you choose what you want to block (e.g, Facebook, clickbait, etc). You can also set custom domains to block.") - let s6 = AwesomeSpotlight(withRect: getRectForView(vpnHeaderView).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: "If you want to protect your data and trusted connections when you're on insecure sites or public hotspots, you can try a 1-week free trial of Lockdown VPN.\n\n Lockdown's fast VPN is fully audited, open source, and has a no-logs policy.") - let s7 = AwesomeSpotlight(withRect: getRectForView(menuButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: "To see this Tutorial again, or for Privacy Policy, questions, and support, tap the menu button at the top left.") + 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: "")) + 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: "")) + spotlights.append(s7) + } let spotlightView = AwesomeSpotlightView(frame: view.frame, - spotlight: [s0, s1, s2, s3, s4, s5, s6, s7]) + spotlight: spotlights) + spotlightView.accessibilityIdentifier = "tutorial" spotlightView.cutoutRadius = 8 spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); spotlightView.enableArrowDown = true - spotlightView.textLabelFont = UIFont(name: "Montserrat-Medium", size: 16.0)! + spotlightView.textLabelFont = fontMedium16 spotlightView.labelSpacing = 24; spotlightView.delegate = self - view.addSubview(spotlightView) + tabBarController?.view.addSubview(spotlightView) spotlightView.start() } - func getRectForView(_ v: UIView) -> CGRect { - if let sv = v.superview { - return sv.convert(v.frame, to: self.view) + func spotlightViewDidCleanup(_ spotlightView: AwesomeSpotlightView) { + guard spotlightView.accessibilityIdentifier == "tutorial" else { + return + } + + defaults.set(true, forKey: kHasViewedTutorial) + if getAPICredentials() != nil { + // already has email signup pending or confirmed, don't show create account + } + else { + AccountUI.presentCreateAccount(on: self) } - return CGRect.zero; } - func spotlightViewDidCleanup(_ spotlightView: AwesomeSpotlightView) { - defaults.set(true, forKey: kHasViewedTutorial) + @IBAction func shareFirewallMetricsTapped(_ sender: Any) { + let thousandsFormatter = NumberFormatter() + thousandsFormatter.groupingSeparator = "," + thousandsFormatter.numberStyle = .decimal + + let imageSize = CGSize(width: 720, height: 420) + let renderer = UIGraphicsImageRenderer(size: imageSize) + let image = renderer.image { ctx in + let rectangle = CGRect(origin: CGPoint.zero, size: imageSize) + ctx.cgContext.setFillColor(UIColor.white.cgColor) + ctx.cgContext.addRect(rectangle) + ctx.cgContext.drawPath(using: .fill) + + UIImage(named: "share.png")!.draw(in: CGRect(origin: CGPoint.zero, size: imageSize)) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let sinceAttrs = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 30, weight: .semibold), NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.foregroundColor: UIColor(red: 176/255, green: 176/255, blue: 176/255, alpha: 0.59)] + let sinceY = 90 + + var date = "INSTALL" + let formatter = DateFormatter() + formatter.dateFormat = "MMM d YYYY" + if let appInstall = appInstallDate { + date = formatter.string(from: appInstall).uppercased() + } + + "SINCE \(date)".draw(with: CGRect(origin: CGPoint(x: 0, y: sinceY), size: CGSize(width: 720, height: 50)), options: .usesLineFragmentOrigin, attributes: sinceAttrs, context: nil) + + let attrs = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 46, weight: .bold), NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.foregroundColor: UIColor(red: 149/255, green: 149/255, blue: 149/255, alpha: 1.0)] + + let countSize = CGSize(width: 240, height: 50) + let countY = 216 + + thousandsFormatter.string(for: getDayMetrics())!.draw(with: CGRect(origin: CGPoint(x: 0, y: countY), size: countSize), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) + thousandsFormatter.string(for: getWeekMetrics())!.draw(with: CGRect(origin: CGPoint(x: 240, y: countY), size: countSize), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) + thousandsFormatter.string(for: getTotalMetrics())!.draw(with: CGRect(origin: CGPoint(x: 480, y: countY), size: countSize), options: .usesLineFragmentOrigin, attributes: attrs, context: nil) + + } + + let popup = PopupDialog( + title: NSLocalizedString("Share Your Stats", comment: ""), + message: NSLocalizedString("Show how invasive today's apps are, and help other people block trackers and badware, too.\n\nYour block log is not included - only the image above. Choose where to share in the next step.", comment: ""), + image: image, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 300.0, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: true, + completion: nil) + + let cancelButton = CancelButton(title: NSLocalizedString("Cancel", comment: ""), dismissOnTap: true) { } + + let shareButton = DefaultButton(title: NSLocalizedString("Next", comment: ""), dismissOnTap: true) { + let shareText = "\(NSLocalizedString("I blocked", 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 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.excludedActivityTypes = [ UIActivity.ActivityType.assignToContact, UIActivity.ActivityType.addToReadingList, UIActivity.ActivityType.openInIBooks, UIActivity.ActivityType.postToVimeo, UIActivity.ActivityType.print ] + + if let popoverPC = vc.popoverPresentationController { + popoverPC.sourceView = self.firewallShareButton + popoverPC.sourceRect = self.firewallShareButton.bounds + popoverPC.permittedArrowDirections = .up + } + self.present(vc, animated: true) + } + + popup.addButtons([cancelButton, shareButton]) + self.present(popup, animated: true, completion: nil) + } // MARK: - Firewall + private func startTimer() { + stopTimer() + metricsTimer = Timer.scheduledTimer( + timeInterval: 2.0, + target: self, + selector: #selector(updateMetrics), + userInfo: nil, + repeats: true + ) + metricsTimer?.fire() + } + + private func stopTimer() { + metricsTimer?.invalidate() + metricsTimer = nil + } + @objc func updateMetrics() { DispatchQueue.main.async { self.dailyMetrics?.text = getDayMetricsString() @@ -248,33 +744,86 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { return } + if getIsCombinedBlockListEmpty() { + FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) + self.showPopupDialog(title: NSLocalizedString("No Block Lists Enabled", comment: ""), message: NSLocalizedString("Please tap Block List and enable at least one block list to activate Firewall.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) + return + } + switch FirewallController.shared.status() { case .invalid: FirewallController.shared.setEnabled(true, isUserExplicitToggle: true) + case .disconnected: + ensureFirewallWorkingAfterEnabling(waitingSeconds: 3.0) updateFirewallButtonWithStatus(status: .connecting) FirewallController.shared.setEnabled(true, isUserExplicitToggle: true) - checkForAskRating() case .connected: updateFirewallButtonWithStatus(status: .disconnecting) FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) + case .connecting, .disconnecting, .reasserting: - break; + ensureFirewallWorkingAfterEnabling(waitingSeconds: 3.0) + } + } + + func ensureFirewallWorkingAfterEnabling(waitingSeconds: TimeInterval) { + FirewallController.shared.existingManagerCount { (count) in + if let count = count, count > 0 { + DispatchQueue.main.asyncAfter(deadline: .now() + waitingSeconds) { + DDLogInfo("\(waitingSeconds) seconds passed, checking if Firewall is enabled") + guard getUserWantsFirewallEnabled() else { + // firewall shouldn't be enabled, no need to act + DDLogInfo("User doesn't want Firewall enabled, no action") + return + } + + let status = FirewallController.shared.status() + switch status { + case .connecting, .disconnecting, .reasserting: + // check again in three seconds + DDLogInfo("Firewall is in transient state, will check again in 3 seconds") + self.ensureFirewallWorkingAfterEnabling(waitingSeconds: 3.0) + case .connected: + // all good + DDLogInfo("Firewall is connected, no action") + break + case .disconnected, .invalid: + // we suppose that the connection is somehow broken, trying to fix + DDLogInfo("Firewall is not connected even though it should be, attempting to fix") + self.showFixFirewallConnectionDialog { + FirewallController.shared.deleteConfigurationAndAddAgain() + } + } + } + } else { + DDLogInfo("No Firewall configurations in settings (likely fresh install): not checking") + return + } } } func updateFirewallButtonWithStatus(status: NEVPNStatus) { DDLogInfo("UpdateFirewallButton") + switch status { + case .connected: + LatestKnowledge.isFirewallEnabled = true + case .disconnected: + LatestKnowledge.isFirewallEnabled = false + default: + break + } updateToggleButtonWithStatus(lastStatus: lastFirewallStatus, newStatus: status, activeLabel: firewallActive, toggleCircle: firewallToggleCircle, toggleAnimatedCircle: firewallToggleAnimatedCircle, - button: firewallButton) + button: firewallButton, + prefixText: NSLocalizedString("Firewall", comment: "").uppercased()) } - func updateToggleButtonWithStatus(lastStatus: NEVPNStatus?, newStatus: NEVPNStatus, activeLabel: UILabel, toggleCircle: UIButton, toggleAnimatedCircle: NVActivityIndicatorView, button: UIButton) { + 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."); @@ -283,32 +832,32 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { DispatchQueue.main.async() { switch newStatus { case .connected: - activeLabel.text = "ACTIVE" + activeLabel.text = "\(prefixText)\(NSLocalizedString(" On", comment: "").uppercased())" activeLabel.backgroundColor = UIColor.tunnelsBlue toggleCircle.tintColor = .tunnelsBlue - toggleCircle.isHidden = false; - toggleAnimatedCircle.stopAnimating(); + toggleCircle.isHidden = false + toggleAnimatedCircle.stopAnimating() button.tintColor = .tunnelsBlue case .connecting: - activeLabel.text = "ACTIVATING" + activeLabel.text = NSLocalizedString("Activating", comment: "").uppercased() activeLabel.backgroundColor = .tunnelsBlue - toggleCircle.isHidden = true; + toggleCircle.isHidden = true toggleAnimatedCircle.color = .tunnelsBlue - toggleAnimatedCircle.startAnimating(); + toggleAnimatedCircle.startAnimating() button.tintColor = .tunnelsBlue case .disconnected, .invalid: - activeLabel.text = "NOT ACTIVE" - activeLabel.backgroundColor = .lightGray + activeLabel.text = "\(prefixText)\(NSLocalizedString(" Off", comment: "").uppercased())" + activeLabel.backgroundColor = .tunnelsWarning toggleCircle.tintColor = .lightGray - toggleCircle.isHidden = false; - toggleAnimatedCircle.stopAnimating(); + toggleCircle.isHidden = false + toggleAnimatedCircle.stopAnimating() button.tintColor = .lightGray case .disconnecting: - activeLabel.text = "DEACTIVATING" + activeLabel.text = NSLocalizedString("Deactivating", comment: "").uppercased() activeLabel.backgroundColor = .lightGray - toggleCircle.isHidden = true; + toggleCircle.isHidden = true toggleAnimatedCircle.color = .lightGray - toggleAnimatedCircle.startAnimating(); + toggleAnimatedCircle.startAnimating() button.tintColor = .lightGray case .reasserting: break; @@ -317,124 +866,179 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { } } - // MARK: - VPN - - @objc @IBAction func vpnHeaderTapped(_ sender: Any) { - toggleVPNBodyView(animate: true) + func highlightBlockLog() { + let blockLogSpotlight = AwesomeSpotlight(withRect: getRectForView(firewallViewLogButton).insetBy(dx: -10.0, dy: -10.0), shape: .roundRectangle, text: NSLocalizedString("Tap to see the blocked tracking attempts.", comment: "")) + + let spotlightView = AwesomeSpotlightView(frame: view.frame, spotlight: [blockLogSpotlight]) + spotlightView.accessibilityIdentifier = "highlightBlockLog" + spotlightView.cutoutRadius = 8 + spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); + spotlightView.enableArrowDown = true + spotlightView.textLabelFont = fontMedium16 + spotlightView.labelSpacing = 24; + view.addSubview(spotlightView) + spotlightView.start() } - func toggleVPNBodyView(animate: Bool, show: Bool? = nil) { - // If supplied a "show", use that. Otherwise, use the opposite of the current visible state (show -> hide, hide -> show) - var shouldShow = false - if show != nil { - shouldShow = show! - } - else { - shouldShow = !(defaults.bool(forKey: kVPNBodyViewVisible)) - } - - var animationTime = 0.0 - if (animate) { - animationTime = 0.2 - } - if (shouldShow) { - vpnBodyView.alpha = 0 - self.vpnBodyView.isHidden = false - UIView.animate(withDuration: animationTime, animations: { - self.vpnHideButton.setTitle("HIDE", for: .normal) - self.vpnBodyView.alpha = 1 - self.vpnActiveHeaderConstraint.isActive = false - self.vpnActiveTopBodyConstraint.isActive = true - self.vpnActiveVerticalBodyConstraint.isActive = true - self.stackEqualHeightConstraint.isActive = true - self.vpnViewHeightConstraint.isActive = false - self.view.layoutIfNeeded() - }, completion: { complete in - defaults.set(true, forKey: self.kVPNBodyViewVisible) - }) - } - else { - UIView.animate(withDuration: animationTime, animations: { - self.vpnHideButton.setTitle("SHOW", for: .normal) - self.vpnBodyView.alpha = 0 - self.vpnActiveHeaderConstraint.isActive = true - self.vpnActiveTopBodyConstraint.isActive = false - self.vpnActiveVerticalBodyConstraint.isActive = false - self.stackEqualHeightConstraint.isActive = false - self.vpnViewHeightConstraint.constant = self.vpnHeaderView.frame.height - self.vpnViewHeightConstraint.isActive = true - self.view.layoutIfNeeded() - }, completion: { complete in - self.vpnBodyView.isHidden = true - defaults.set(false, forKey: self.kVPNBodyViewVisible) - }) - } + // MARK: - VPN + + + @IBAction func vpnQuestionTapped(_ sender: Any) { + self.performSegue(withIdentifier: "showWhatIsVPN", sender: self) } func updateVPNButtonWithStatus(status: NEVPNStatus) { DDLogInfo("UpdateVPNButton") + switch status { + case .connected: + LatestKnowledge.isVPNEnabled = true + case .disconnected: + LatestKnowledge.isVPNEnabled = false + default: + break + } updateToggleButtonWithStatus(lastStatus: lastVPNStatus, newStatus: status, activeLabel: vpnActive, toggleCircle: vpnToggleCircle, toggleAnimatedCircle: vpnToggleAnimatedCircle, - button: vpnButton) + button: vpnButton, + prefixText: NSLocalizedString("Tunnel", comment: "").uppercased()) } - @IBAction func toggleVPN(_ sender: Any) { - if (defaults.bool(forKey: kHasAgreedToVPNPrivacyPolicy) == false) { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let viewController = storyboard.instantiateViewController(withIdentifier: "vpnPrivacyPolicyViewController") as! PrivacyPolicyViewController - viewController.privacyPolicyKey = kHasAgreedToVPNPrivacyPolicy - viewController.parentVC = self - self.present(viewController, animated: true, completion: nil) - return - } - - DDLogInfo("Toggle VPN") - switch VPNController.shared.status() { - case .connected, .connecting, .reasserting: - DDLogInfo("Toggle VPN: on currently, turning it off") - updateVPNButtonWithStatus(status: .disconnecting) - VPNController.shared.setEnabled(false) - case .disconnected, .disconnecting, .invalid: - DDLogInfo("Toggle VPN: off currently, turning it on") - updateVPNButtonWithStatus(status: .connecting) - firstly { - try Client.signIn() // this will fetch and set latest receipt, then submit to API to get cookie - } - .then { (signin: SignIn) -> Promise in - // TODO: don't always do this -- if we already have a key, then only do it once per day max - try Client.getKey() - } - .done { (getKey: GetKey) in - try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) - VPNController.shared.setEnabled(true) - } - .catch { error in + private func startVPNAfterSettingCredentials() { + VPNController.shared.setEnabled(true) { error in + if error != nil { self.updateVPNButtonWithStatus(status: .disconnected) - if (self.popupErrorAsNSURLError(error)) { - return - } - else if let apiError = error as? ApiError { - switch apiError.code { - case kApiCodeNoSubscriptionInReceipt: - self.performSegue(withIdentifier: "showSignup", sender: self) - case kApiCodeNoActiveSubscription: - self.showPopupDialog(title: "VPN Subscription Expired", message: "Please renew your subscription to activate the VPN.", acceptButton: "Okay", completionHandler: { - self.performSegue(withIdentifier: "showSignup", sender: self) - }) - default: - _ = self.popupErrorAsApiError(error) + } + } + } + + @IBAction func toggleVPN(_ sender: Any) { + if UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { + DDLogInfo("Toggle VPN") + switch VPNController.shared.status() { + case .connected, .connecting, .reasserting: + DDLogInfo("Toggle VPN: on currently, turning it off") + updateVPNButtonWithStatus(status: .disconnecting) + VPNController.shared.setEnabled(false) + case .disconnected, .disconnecting, .invalid: + DDLogInfo("Toggle VPN: off currently, turning it on") + updateVPNButtonWithStatus(status: .connecting) +// VPNController.shared.setEnabled(true) +// ensureFirewallWorkingAfterEnabling(waitingSeconds: 5.0) + // if there's a confirmed email, use that and sync the receipt with it + if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { + DDLogInfo("have confirmed API credentials, using them") + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .then { (signin: SignIn) -> Promise in + DDLogInfo("signin result: \(signin)") + return try Client.subscriptionEvent() + } + .then { (result: SubscriptionEvent) -> Promise in + DDLogInfo("subscriptionevent result: \(result)") + return try Client.getKey() + } + .done { [weak self] (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + DDLogInfo("setting VPN creds with ID: \(getKey.id)") + self?.startVPNAfterSettingCredentials() + } + .catch { error in + DDLogError("Error doing email-login -> subscription-event: \(error)") + self.updateVPNButtonWithStatus(status: .disconnected) + if (self.popupErrorAsNSURLError(error)) { + return + } + else if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeInvalidAuth, kApiCodeIncorrectLogin: + let confirm = PopupDialog(title: "Incorrect Login", + message: "Your saved login credentials are incorrect. Please sign out and try again.", + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + confirm.addButtons([ + DefaultButton(title: NSLocalizedString("Cancel", comment: ""), dismissOnTap: true) { + }, + DefaultButton(title: NSLocalizedString("Sign Out", comment: ""), dismissOnTap: true) { + URLCache.shared.removeAllCachedResponses() + Client.clearCookies() + clearAPICredentials() + setAPICredentialsConfirmed(confirmed: false) + self.showPopupDialog(title: "Success", message: "Signed out successfully.", acceptButton: NSLocalizedString("Okay", comment: "")) + }, + ]) + self.present(confirm, animated: true, completion: nil) + case kApiCodeNoSubscriptionInReceipt: + self.performSegue(withIdentifier: "showSignup", sender: self) + case kApiCodeNoActiveSubscription: + self.showPopupDialog(title: NSLocalizedString("Subscription Expired", comment: ""), message: NSLocalizedString("Please renew your subscription to activate the Secure Tunnel.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""), completionHandler: { + self.performSegue(withIdentifier: "showSignup", sender: self) + }) + default: + _ = self.popupErrorAsApiError(error) + } + } } } else { - self.showPopupDialog(title: "Error Signing In To Verify Subscription", - message: "\(error)", - acceptButton: "Okay") + firstly { + try Client.signIn() // this will fetch and set latest receipt, then submit to API to get cookie + } + .then { (signin: SignIn) -> Promise in + // TODO: don't always do this -- if we already have a key, then only do it once per day max + try Client.getKey() + } + .done { [weak self] (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + self?.startVPNAfterSettingCredentials() + } + .catch { error in + self.updateVPNButtonWithStatus(status: .disconnected) + if (self.popupErrorAsNSURLError(error)) { + return + } + else if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt: + self.performSegue(withIdentifier: "showSignup", sender: self) + case kApiCodeNoActiveSubscription: + self.showPopupDialog(title: NSLocalizedString("Subscription Expired", comment: ""), message: NSLocalizedString("Please renew your subscription to activate the Secure Tunnel.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""), completionHandler: { + self.performSegue(withIdentifier: "showSignup", sender: self) + }) + default: + if (apiError.code == kApiCodeNegativeError) { + if (getVPNCredentials() != nil) { + DDLogError("Unknown error -1 from API, but VPNCredentials exists, so activating anyway.") + self.updateVPNButtonWithStatus(status: .connecting) + self.startVPNAfterSettingCredentials() + } + 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 { + _ = self.popupErrorAsApiError(error) + } + } + } + else { + self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""), + message: "\(error)", + acceptButton: NSLocalizedString("Okay", comment: "")) + } + } } } - } + } else { upgrade() } } @IBAction func viewAuditReportTapped(_ sender: Any) { @@ -442,18 +1046,37 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { } @IBAction func showWhitelist(_ sender: Any) { - performSegue(withIdentifier: "showWhitelist", sender: nil) + if UserDefaults.hasSeenUniversalPaywall || UserDefaults.hasSeenAnonymousPaywall { + performSegue(withIdentifier: "showWhitelist", sender: nil) + } else { + upgrade() + } } @IBAction func showSetRegion(_ sender: Any) { - performSegue(withIdentifier: "showSetRegion", sender: nil) + if UserDefaults.hasSeenUniversalPaywall || UserDefaults.hasSeenAnonymousPaywall { + performSegue(withIdentifier: "showSetRegion", sender: nil) + } else { + upgrade() + } + } + + func showBlockLog(_ sender: Any) { + performSegue(withIdentifier: "showBlockLog", sender: nil) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if (segue.identifier == "showSetRegion") { + switch segue.identifier { + case "showSetRegion": if let vc = segue.destination as? SetRegionViewController { vc.homeVC = self } +// case "showWhatIsVPN": +// if let vc = segue.destination as? WhatIsVpnViewController { +// vc.parentVC = self +// } + default: + break } } @@ -495,48 +1118,170 @@ class HomeViewController: BaseViewController, AwesomeSpotlightViewDelegate { SKStoreReviewController.requestReview() } } +} + +// MARK: - Paywalling +extension HomeViewController: PaywallViewControllerCloseDelegate { + func didClosePaywall() { + + BaseUserService.shared.updateUserSubscription { [weak self] subscription in + self?.showLoadingView() + DispatchQueue.main.async { + + if subscription?.planType == .anonymousMonthly || subscription?.planType == .anonymousAnnual { + UserDefaults.hasSeenAdvancedPaywall = false + UserDefaults.hasSeenUniversalPaywall = false + UserDefaults.hasSeenAnonymousPaywall = true + } + else if subscription?.planType == .universalMonthly || subscription?.planType == .universalAnnual { + UserDefaults.hasSeenAnonymousPaywall = false + UserDefaults.hasSeenAdvancedPaywall = false + UserDefaults.hasSeenUniversalPaywall = true + } + else if subscription?.planType == .advancedMonthly || subscription?.planType == .advancedAnnual { + UserDefaults.hasSeenAnonymousPaywall = false + UserDefaults.hasSeenAdvancedPaywall = true + UserDefaults.hasSeenUniversalPaywall = false + } + else { + UserDefaults.hasSeenAnonymousPaywall = false + UserDefaults.hasSeenAdvancedPaywall = false + UserDefaults.hasSeenUniversalPaywall = false + } + } + self?.hideLoadingView() + } + } -// func updateIP() { -// DDLogInfo("Updating IP") -// self.vpnIP.text = "—" -// firstly { -// Client.getIP() -// } -// .done { (ip: IP) in -// DispatchQueue.main.async { -// self.vpnIP.text = ip.ip -// } -// } -// .catch { error in -// self.vpnIP.text = "error" -// DDLogError("Error getting IP: \(error)") -// } -// } - -// @IBAction func runSpeedTest() { -// DDLogInfo("Speed Test") -// vpnSpeed.text = "Testing..." -// vpnSpeed.alpha = 0.2 -// UIView.animate(withDuration: 0.65, delay: 0, options: [.curveEaseInOut, .autoreverse, .repeat], animations: { -// self.vpnSpeed.alpha = 1.0 -// }) -// firstly { -// SpeedTest().testDownloadSpeedWithTimeout(timeout: 10.0) -// } -// .done { (mbps: Double) in -// DispatchQueue.main.async { -// self.vpnSpeed.layer.removeAllAnimations() -// self.vpnSpeed.text = String(format: "%.1f", mbps) -// + " Mbps" -// UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseInOut], animations: { -// self.vpnSpeed.alpha = 1.0 -// }) -// } -// } -// .catch { error in -// self.vpnSpeed.text = "error" -// DDLogError("Error testing speed: \(error)") -// } -// } + 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 + } +} + +class LockdownCustomActivityItemProvider : UIActivityItemProvider { + + let shareText: String + + init(text: String) { + self.shareText = text + super.init(placeholderItem: text) + } + + override func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { + if let type = activityType { + switch type { + case UIActivity.ActivityType.postToTwitter: + return shareText + " @lockdown_hq" + default: + return shareText + } + } + else { + return shareText + } + } + +} + +fileprivate extension PopupDialogButton { + func startActivityIndicator() { + let activity = UIActivityIndicatorView() + + if let label = titleLabel { + label.addSubview(activity) + activity.translatesAutoresizingMaskIntoConstraints = false + activity.centerYAnchor.constraint(equalTo: label.centerYAnchor).isActive = true + activity.leadingAnchor.constraint(equalToSystemSpacingAfter: label.trailingAnchor, multiplier: 1).isActive = true + activity.startAnimating() + } + } + + func stopActivityIndicator() { + if let label = titleLabel { + let indicators = label.subviews.compactMap { $0 as? UIActivityIndicatorView } + for indicator in indicators { + indicator.stopAnimating() + indicator.removeFromSuperview() + } + } + } +} + +final class DynamicButton: PopupDialogButton { + var onTap: ((DynamicButton) -> ())? + + override var buttonAction: PopupDialogButton.PopupDialogButtonAction? { + get { + if let onTap = onTap { + return { [weak self] in if let value = self { return onTap(value) } } + } else { + return nil + } + } + } +} + +extension NEVPNStatus: CustomStringConvertible { + public var description: String { + switch self { + case .invalid: + return "invalid" + case .disconnected: + return "disconnected" + case .connecting: + return "connecting" + case .connected: + return "connected" + case .reasserting: + return "reasserting" + case .disconnecting: + return "disconnecting" + } + } +} + +extension HomeViewController: PurchaseHandler { + func purchase(productId: String) { + VPNSubscription.selectedProductId = productId + VPNSubscription.purchase { [weak self] in + self?.handlePurchaseSuccessful() + } errored: { [weak self] err in + self?.handlePurchaseFailed(error: err) + } + } +} + +extension HomeViewController: UITabBarControllerDelegate { + func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { + if viewController is UINavigationController || viewController is HomeViewController { + return true + } + + feedbackFlow?.startFlow() + return false + } + + func showFeedbackPaywall() async { + guard let productInfos = await VPNSubscription.shared.loadSubscriptions(type: .feedback) else { return } + + let viewModel = FeedbackPaywallViewModel(products: VPNSubscription.feedbackProducts, subscriptionInfo: productInfos) + viewModel.onCloseHandler = { vc in vc.dismiss(animated: true) } + viewModel.onPurchaseHandler = { [weak self] paywallVC, pid in + self?.purchase(productId: pid) + } + let paywalVC = FeedbackPaywallViewController(viewModel: viewModel) + present(paywalVC, animated: true) + } } diff --git a/LockdowniOS/ImportBlockListViewController.swift b/LockdowniOS/ImportBlockListViewController.swift new file mode 100644 index 0000000..d7529a4 --- /dev/null +++ b/LockdowniOS/ImportBlockListViewController.swift @@ -0,0 +1,309 @@ +// +// ImportBlockListViewController.swift +// LockdownSandbox +// +// Created by Aliaksandr Dvoineu on 3.04.23. +// + +import UIKit +import UniformTypeIdentifiers +import MobileCoreServices + +final class ImportBlockListViewController: UIViewController, UIDocumentPickerDelegate, DomainListSaveable { + + // MARK: - Properties + + var importCompletion: (() -> ())? + + var titleName = "Import Block List" + + private lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView() + view.rightNavButton.setTitle(NSLocalizedString("CANCEL", comment: ""), for: .normal) + view.titleLabel.text = titleName + view.rightNavButton.tintColor = .tunnelsBlue + view.rightNavButton.addTarget(self, action: #selector(cancel), for: .touchUpInside) + return view + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Import Domains from file", comment: "") + label.textColor = .label + label.font = fontBold17 + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + private lazy var descriptionParagraph1: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Take control of your browsing experience! Import your own custom block list and say goodbye to pesky trackers for good.", comment: "") + label.textColor = .label + label.font = fontRegular14 + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + private lazy var descriptionParagraph2: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontRegular14 + let highlightedText = NSLocalizedString("comma-separated values (.csv)*", comment: "a bold part of description") + label.text = NSLocalizedString("Simply select the ", comment: "") + + highlightedText + + NSLocalizedString(" file with the domains you want to block and import it. It's that easy!", comment: "") + label.highlight(highlightedText, font: UIFont.boldLockdownFont(size: 14)) + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + private lazy var selectFromFilesButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 28 + button.setTitle(NSLocalizedString("Select from Files", comment: ""), for: .normal) + button.setImage(UIImage(named: "icn_csv_file"), for: .normal) + button.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) + button.titleLabel?.font = fontBold17 + button.addTarget(self, action: #selector(selectFromFiles), for: .touchUpInside) + return button + }() + + private lazy var descriptionParagraph3: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("* The .csv file should contain a single column of domains and/or sub-domains. NO headers, NO additional columns, and NO URLs.", comment: "") + label.textColor = .label + label.font = fontRegular14 + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + private lazy var vStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(descriptionParagraph1) + stackView.addArrangedSubview(descriptionParagraph2) + stackView.addArrangedSubview(selectFromFilesButton) + stackView.addArrangedSubview(descriptionParagraph3) + stackView.axis = .vertical + stackView.alignment = .leading + stackView.distribution = .equalSpacing + stackView.spacing = 24 + return stackView + }() + + private lazy var pasteFromClipboardTitle: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Paste multiple domains from clipboard", comment: "") + label.textColor = .label + label.font = fontBold15 + label.textColor = .label + label.numberOfLines = 1 + label.textAlignment = .left + return label + }() + + private lazy var separator: UIView = { + let view = UIView() + view.backgroundColor = .gray + return view + }() + + private lazy var pasteFromClipboardText: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("You can copy existing data from a spreadsheet (like and Excel workbook or Google Sheet) and paste it in the field below.", comment: "") + label.textColor = .label + label.font = fontRegular14 + label.textColor = .label + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + private lazy var pasteFromClipboardTextfield: UITextField = { + let textFieled = UITextField() + textFieled.textColor = .label + textFieled.font = fontRegular14 + textFieled.textColor = .label + textFieled.textAlignment = .left + textFieled.contentVerticalAlignment = .top + textFieled.borderStyle = .line + return textFieled + }() + + private lazy var blockPastedDomainsButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.backgroundColor = .gray + button.layer.cornerRadius = 28 + button.setTitle(NSLocalizedString("Block Pasted Domains", comment: ""), for: .normal) + button.titleLabel?.font = fontBold15 + button.addTarget(self, action: #selector(blockPastedDomains), for: .touchUpInside) + return button + }() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .secondarySystemBackground + + configureUI() + } + + // MARK: - Configure UI + func configureUI() { + view.addSubview(navigationView) + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + navigationView.anchors.top.safeAreaPin() + + view.addSubview(vStackView) + vStackView.anchors.top.spacing(48, to: navigationView.anchors.bottom) + vStackView.anchors.leading.marginsPin() + vStackView.anchors.trailing.marginsPin() + + selectFromFilesButton.anchors.leading.marginsPin() + selectFromFilesButton.anchors.trailing.marginsPin() + selectFromFilesButton.anchors.height.equal(56) + } +} + + // MARK: - Private functions +extension ImportBlockListViewController { + + @objc func cancel() { + let viewController = BlockListViewController() + viewController.reloadCustomBlockedLists() + dismiss(animated: true) + } + + private func saveImportedDomains( + _ content: Set, + toNewListName newListName: String + ) { + if !content.isEmpty { + addBlockedList(listName: newListName) + var allData = getBlockedLists() + + let importedList = UserBlockListsGroup(name: newListName, domains: content) + + allData.userBlockListsDefaults[importedList.name] = importedList + + let encodedData = try? JSONEncoder().encode(allData) + defaults.set(encodedData, forKey: kUserBlockedLists) + + importCompletion?() + } + + closeScreen(withSuccess: !content.isEmpty) + } + + private func closeScreen(withSuccess success: Bool) { + dismiss(animated: true) { + let title = success + ? NSLocalizedString("Success!", comment: "") + : NSLocalizedString("Error", comment: "") + let message = success + ? NSLocalizedString("The list has been imported successfully. You can start blocking the list's domains", comment: "") + : NSLocalizedString("Your list of domains is empty or in the wrong format", comment: "") + let alert = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + alert.addAction( + UIAlertAction( + title: NSLocalizedString("Close", comment: ""), + style: .default, + handler: nil + ) + ) + UIApplication.getTopMostViewController()?.present(alert, animated: true, completion: nil) + } + } + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + + guard let url = urls.first else { + return + } + + do { + let content = csvProcessing(data: try String(contentsOf: url, encoding: .utf8)) + guard !content.isEmpty else { + closeScreen(withSuccess: false) + return + } + + showCreateList( + initialListName: nil, + forDomainList: content + ) { [weak self] in + self?.saveImportedDomains($0, toNewListName: $1) + } + } catch { + dismiss(animated: true) { + let alert = UIAlertController(title: NSLocalizedString("Error", comment: ""), + message: NSLocalizedString("Unable to import the list. Please try again or contact support for assistance", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), + style: .default, + handler: nil)) + UIApplication.getTopMostViewController()?.present(alert, animated: true, completion: nil) + } + } + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + print("view was cancelled") + + dismiss(animated: true, completion: nil) + } + + func csvProcessing(data: String) -> Set { + var domains = Set() + var arrayOfDomains = [String]() + + if data.contains("\r\n") { + arrayOfDomains = data.components(separatedBy: "\r\n") + } else if data.contains("\r") { + arrayOfDomains = data.components(separatedBy: "\r") + } else if data.contains("\n") { + arrayOfDomains = data.components(separatedBy: "\n") + } else if data.contains(",") { + arrayOfDomains = data.components(separatedBy: ",") + } + + for domain in arrayOfDomains { + if domain.isValid(.domainName) { + domains.insert(domain) + } + } + + return domains + } + + @objc func selectFromFiles() { + + if #available(iOS 14.0, *) { + let supportedFiles: [UTType] = [UTType.data] + + let controller = UIDocumentPickerViewController(forOpeningContentTypes: supportedFiles, asCopy: true) + + controller.delegate = self + controller.modalPresentationStyle = .formSheet + controller.allowsMultipleSelection = false + present(controller, animated: true) + } + } + + @objc func blockPastedDomains() { + + // TODO: future implementation according to the requirements + } +} diff --git a/LockdowniOS/Info.plist b/LockdowniOS/Info.plist index 71d942e..7db8e49 100644 --- a/LockdowniOS/Info.plist +++ b/LockdowniOS/Info.plist @@ -2,10 +2,14 @@ + BGTaskSchedulerPermittedIdentifiers + + com.confirmed.lockdown.firewallscheduler + CFBundleDevelopmentRegion en CFBundleDisplayName - Lockdown + Lockdown VPN CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -17,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.2.0 + $(MARKETING_VERSION) CFBundleURLTypes @@ -34,13 +38,17 @@ CFBundleVersion - 16 + $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSLocationAlwaysUsageDescription We use this to automatically select the best region for the VPN. NSLocationWhenInUseUsageDescription 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. UIAppFonts Montserrat-Bold.ttf @@ -49,6 +57,13 @@ Montserrat-Regular.ttf Montserrat-SemiBold.ttf Montserrat-Thin.ttf + SF-Pro-Rounded-Bold.otf + SF-Pro-Rounded-Medium.otf + SF-Pro-Rounded-Regular.otf + SF-Pro-Rounded-Semibold.otf + Juana-SemiBold.ttf + KumbhSans-Bold.ttf + KumbhSans-Regular.ttf UIBackgroundModes diff --git a/LockdowniOS/JSONSerialization+Extensions.swift b/LockdowniOS/JSONSerialization+Extensions.swift new file mode 100644 index 0000000..bf88688 --- /dev/null +++ b/LockdowniOS/JSONSerialization+Extensions.swift @@ -0,0 +1,39 @@ +// +// JSONSerialization+Extentions.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 06.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +extension JSONSerialization { + + static func loadJSON(withFilename filename: String) throws -> Any? { + let fm = FileManager.default + let urls = fm.urls(for: .documentDirectory, in: .userDomainMask) + if let url = urls.first { + var fileURL = url.appendingPathComponent(filename) + fileURL = fileURL.appendingPathExtension("json") + let data = try Data(contentsOf: fileURL) + let jsonObject = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers, .mutableLeaves]) + return jsonObject + } + return nil + } + + static func save(jsonObject: Any, toFilename filename: String) throws -> Bool{ + let fm = FileManager.default + let urls = fm.urls(for: .documentDirectory, in: .userDomainMask) + if let url = urls.first { + var fileURL = url.appendingPathComponent(filename) + fileURL = fileURL.appendingPathExtension("json") + let data = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted]) + try data.write(to: fileURL, options: [.atomicWrite]) + return true + } + + return false + } +} diff --git a/LockdowniOS/Keychainable.swift b/LockdowniOS/Keychainable.swift new file mode 100644 index 0000000..8372389 --- /dev/null +++ b/LockdowniOS/Keychainable.swift @@ -0,0 +1,40 @@ +// +// Keychainable.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 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: LockdownStorageIdentifier.keychainId).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/LDCardView.swift b/LockdowniOS/LDCardView.swift new file mode 100644 index 0000000..3514879 --- /dev/null +++ b/LockdowniOS/LDCardView.swift @@ -0,0 +1,90 @@ +// +// LDCardView.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 19.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class LDCardView: UIView { + + // MARK: - Properties + + var isSelected: Bool = false + + private lazy var backgroundView: UIView = { + let view = UIView() + view.isUserInteractionEnabled = true + view.layer.cornerRadius = 8 + view.layer.borderWidth = 2 + view.layer.borderColor = isSelected ? UIColor.gray.cgColor : UIColor.tunnelsBlue.cgColor + return view + }() + + lazy var iconImageView: UIImageView = { + let image = UIImageView() + image.contentMode = .scaleAspectFit + image.image = isSelected ? UIImage(named: "kksdlf") : UIImage(named: "dfgerte") + image.layer.masksToBounds = true + return image + }() + + lazy var title: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontBold15 + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + lazy var subTitle: UILabel = { + let label = UILabel() + label.text = "" + label.textColor = .label + label.font = fontBold15 + label.textColor = .lightGray + label.textAlignment = .center + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(iconImageView) + stackView.addArrangedSubview(title) + stackView.addArrangedSubview(subTitle) + stackView.axis = .vertical + stackView.distribution = .fillProportionally + stackView.alignment = .center + stackView.spacing = 8 + return stackView + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + private func configureUI() { + + addSubview(backgroundView) + backgroundView.anchors.edges.pin() + + backgroundView.addSubview(stackView) + stackView.anchors.centerX.align() + stackView.anchors.leading.marginsPin() + stackView.anchors.trailing.marginsPin() + stackView.anchors.centerY.align() + } +} diff --git a/LockdowniOS/LDConfigurationViewController.swift b/LockdowniOS/LDConfigurationViewController.swift new file mode 100644 index 0000000..472596e --- /dev/null +++ b/LockdowniOS/LDConfigurationViewController.swift @@ -0,0 +1,204 @@ +// +// LDConfigurationViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 20.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit +import CocoaLumberjackSwift + +final class LDConfigurationViewController: UIViewController { + + // MARK: - Properties + + private lazy var activityCard: LDCardView = { + let view = LDCardView() + view.title.text = "Activity" + view.iconImageView.image = UIImage(named: "icn_activity") + view.isUserInteractionEnabled = true + view.setOnClickListener { [weak self] in + guard let self else { return } + let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let vc = storyBoard.instantiateViewController(withIdentifier: "blockLogViewController") as! BlockLogViewController + self.present(vc, animated: true, completion: nil) + } + return view + }() + + private lazy var configureBlockingCard: LDCardView = { + let view = LDCardView() + view.title.text = "Configure blocking" + view.title.numberOfLines = 0 + view.iconImageView.image = UIImage(named: "icn_configure_blocking") + view.isUserInteractionEnabled = true + view.setOnClickListener { [weak self] in + guard let self else { return } + let navController = UINavigationController(rootViewController: BlockListViewController()) + navController.navigationBar.isHidden = true + self.present(navController, animated: true) + } + return view + }() + + private lazy var personalizedBlockingCard: LDCardView = { + let view = LDCardView() + view.title.text = "Personalized blocking" + view.title.numberOfLines = 0 + view.iconImageView.image = UIImage(named: "icn_personalized_blocking") + view.isUserInteractionEnabled = true + view.setOnClickListener { [weak self] in + guard let self else { return } + let vc = BlockListViewController() + vc.chosenBlocking = 1 + let navController = UINavigationController(rootViewController: vc) + navController.navigationBar.isHidden = true + self.present(navController, animated: true) + } + return view + }() + + private lazy var importListsCard: LDCardView = { + let view = LDCardView() + view.title.text = "Import custom block lists" + view.title.numberOfLines = 0 + view.iconImageView.image = UIImage(named: "icn_import") + view.isUserInteractionEnabled = true + view.setOnClickListener { [weak self] in + guard let self else { return } + if UserDefaults.hasSeenAdvancedPaywall || UserDefaults.hasSeenAnonymousPaywall || UserDefaults.hasSeenUniversalPaywall { + let vc = ImportBlockListViewController() + self.present(vc, animated: true) + } else { + let vc = VPNPaywallViewController() + self.present(vc, animated: true) + } + } + return view + }() + + private lazy var hStack1: UIStackView = { + let stack = UIStackView() + stack.addArrangedSubview(activityCard) + stack.addArrangedSubview(configureBlockingCard) + stack.alignment = .center + stack.axis = .horizontal + stack.distribution = .equalCentering + stack.spacing = 16 + return stack + }() + + private lazy var hStack2: UIStackView = { + let stack = UIStackView() + stack.addArrangedSubview(personalizedBlockingCard) + stack.addArrangedSubview(importListsCard) + stack.alignment = .center + stack.axis = .horizontal + stack.distribution = .equalCentering + stack.spacing = 16 + return stack + }() + + private lazy var vStack: UIStackView = { + let stack = UIStackView() + stack.addArrangedSubview(hStack1) + stack.addArrangedSubview(hStack2) + stack.alignment = .center + stack.axis = .vertical + stack.distribution = .equalCentering + stack.spacing = 16 + return stack + }() + + private lazy var viewAccountSettingsButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("View account settings", for: .normal) + button.setTitleColor(.tunnelsBlue, for: .normal) + button.titleLabel?.font = fontBold15 + button.addTarget(self, action: #selector(viewAccountSettings), for: .touchUpInside) + return button + }() + + private lazy var viewAuditReportButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("View audit report (Feb, 2023)", for: .normal) + button.setTitleColor(.tunnelsBlue, for: .normal) + button.titleLabel?.font = fontBold15 + button.addTarget(self, action: #selector(viewAuditReport), for: .touchUpInside) + return button + }() + + lazy var vStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(viewAccountSettingsButton) + stackView.addArrangedSubview(viewAuditReportButton) + stackView.axis = .vertical + stackView.alignment = .center + stackView.distribution = .fillEqually + stackView.spacing = 8 + return stackView + }() + + // MARK: - LifeCycle + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + view.addSubview(vStack) + vStack.anchors.centerY.align() + vStack.anchors.centerX.align() + + activityCard.anchors.width.equal(view.bounds.width / 2 - 20) + activityCard.anchors.height.equal(view.bounds.width / 2 - 20) + + configureBlockingCard.anchors.width.equal(view.bounds.width / 2 - 20) + configureBlockingCard.anchors.height.equal(view.bounds.width / 2 - 20) + + personalizedBlockingCard.anchors.width.equal(view.bounds.width / 2 - 20) + personalizedBlockingCard.anchors.height.equal(view.bounds.width / 2 - 20) + + importListsCard.anchors.width.equal(view.bounds.width / 2 - 20) + importListsCard.anchors.height.equal(view.bounds.width / 2 - 20) + + view.addSubview(vStackView) + vStackView.anchors.top.spacing(30, to: vStack.anchors.bottom) + vStackView.anchors.leading.marginsPin() + vStackView.anchors.trailing.marginsPin() + } +} + +// MARK: - Private functions + +private extension LDConfigurationViewController { + + @objc func viewAccountSettings() { + let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let vc = storyBoard.instantiateViewController(withIdentifier: "accountViewController") as! AccountViewController + vc.modalPresentationStyle = .overFullScreen + present(vc, animated: true, completion: nil) + } + + @objc func viewAuditReport() { + showModalWebView(title: NSLocalizedString("Audit Reports", comment: ""), urlString: "https://openaudit.com/lockdownprivacy") + } + + func showModalWebView(title: String, urlString: String) { + if let url = URL(string: urlString) { + let storyboardToUse = storyboard != nil ? storyboard! : UIStoryboard(name: "Main", bundle: nil) + if let webViewVC = storyboardToUse.instantiateViewController(withIdentifier: "webview") as? WebViewViewController { + webViewVC.titleLabelText = title + webViewVC.url = url + self.present(webViewVC, animated: true, completion: nil) + } + else { + DDLogError("Unable to instantiate webview VC") + } + } + else { + DDLogError("Invalid URL \(urlString)") + } + } +} diff --git a/LockdowniOS/LDFirewallViewController.swift b/LockdowniOS/LDFirewallViewController.swift new file mode 100644 index 0000000..354bdc1 --- /dev/null +++ b/LockdowniOS/LDFirewallViewController.swift @@ -0,0 +1,555 @@ +// +// LDFirewallViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 17.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit +import CocoaLumberjackSwift +import PromiseKit +import NetworkExtension +import PopupDialog + +final class LDFirewallViewController: BaseViewController { + + // MARK: Properties + let kHasViewedTutorial = "hasViewedTutorial" + let kHasSeenInitialFirewallConnectedDialog = "hasSeenInitialFirewallConnectedDialog11" + let kHasSeenShare = "hasSeenShareDialog4" + + let ratingCountKey = "ratingCount" + lastVersionToAskForRating + let ratingTriggeredKey = "ratingTriggered" + lastVersionToAskForRating + + var lastFirewallStatus: NEVPNStatus? + var activePlans: [Subscription.PlanType] = [] + let vc = FirewallPaywallViewController() + + enum Mode { + case newSubscription + case upgrade(active: [Subscription.PlanType]) + } + + var mode = Mode.newSubscription + + var metricsTimer : Timer? + + private lazy var scrollView: UIScrollView = { + let view = UIScrollView() + view.isScrollEnabled = true + return view + }() + + private lazy var contentView: UIView = { + let view = UIView() + view.anchors.height.equal(800) + return view + }() + + lazy var yourCurrentPlanLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Your current plan is", comment: "") + label.font = fontRegular14 + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + lazy var upgradeLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Upgrade?", comment: "") + label.font = fontBold13 + label.textColor = .tunnelsBlue + label.isUserInteractionEnabled = true + label.setOnClickListener { + let vc = VPNPaywallViewController() + self.present(vc, animated: true) + } + return label + }() + + lazy var protectionPlanLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Basic Protection", comment: "") + label.font = fontBold22 + label.textColor = .label + return label + }() + + lazy var firewallTitle: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Get complete protection", comment: "") + label.font = fontBold24 + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + private lazy var firewallDescriptionLabel1: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Block as many trackers as you want", comment: ""))) + return label + }() + + private lazy var firewallDescriptionLabel2: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Import and export your own block lists", comment: ""))) + return label + }() + + private lazy var firewallDescriptionLabel3: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Access to new curated lists of trackers", comment: ""))) + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(firewallTitle) + stackView.addArrangedSubview(firewallDescriptionLabel1) + stackView.addArrangedSubview(firewallDescriptionLabel2) + stackView.addArrangedSubview(firewallDescriptionLabel3) + stackView.axis = .vertical + stackView.distribution = .fillProportionally + stackView.spacing = 10 + return stackView + }() + + private lazy var cpTitle: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Don't let those trackers know your every move – Upgrade to Advanced now!", comment: "") + label.textColor = .black + label.font = fontBold15 + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + private lazy var cpTrackersGroupView1: TrackersGroupView = { + let view = TrackersGroupView() + view.placeNumber.text = "#1" + view.placeNumber.textColor = .black + view.titleLabel.textColor = .black + view.number.textColor = .black + view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_game_marketing")!, title: "Game Marketing", number: 4678)) + view.number.isHidden = true + return view + }() + + private lazy var cpTrackersGroupView2: TrackersGroupView = { + let view = TrackersGroupView() + view.placeNumber.text = "#2" + view.placeNumber.textColor = .black + view.titleLabel.textColor = .black + view.number.textColor = .black + view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_marketing_trackers")!, title: "Marketing Trackers", number: 3432)) + view.number.isHidden = true + return view + }() + + private lazy var cpTrackersGroupView3: TrackersGroupView = { + let view = TrackersGroupView() + view.placeNumber.text = "#3" + view.placeNumber.textColor = .black + view.titleLabel.textColor = .black + view.number.textColor = .black + view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_email_trackers")!, title: "Email Trackers", number: 2756)) + view.number.isHidden = true + return view + }() + + private lazy var cpStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(cpTitle) + stackView.addArrangedSubview(cpTrackersGroupView1) + stackView.addArrangedSubview(cpTrackersGroupView2) + stackView.addArrangedSubview(cpTrackersGroupView3) + stackView.layer.cornerRadius = 8 + stackView.layer.borderWidth = 2 + stackView.layer.borderColor = UIColor.gray.cgColor + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.spacing = 0 + stackView.backgroundColor = .extraLightGray + return stackView + }() + + private lazy var upgradeButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setTitle(NSLocalizedString("Upgrade", comment: ""), for: .normal) + button.titleLabel?.font = fontBold18 + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 28 + button.anchors.height.equal(56) + button.addTarget(self, action: #selector(upgrade), for: .touchUpInside) + return button + }() + + private lazy var maTitle: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Most active this week", comment: "") + label.textColor = .label + label.font = fontBold15 + label.textAlignment = .center + return label + }() + + private lazy var maTrackersGroupView1: TrackersGroupView = { + let view = TrackersGroupView() + view.placeNumber.text = "#1" + view.number.textColor = .label + view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_facebook_trackers")!, title: "Facebook Trackers", number: 89)) + view.lockImage.isHidden = true + return view + }() + + private lazy var maTrackersGroupView2: TrackersGroupView = { + let view = TrackersGroupView() + view.placeNumber.text = "#2" + view.number.textColor = .label + view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_data_trackers")!, title: "Data Trackers", number: 32)) + view.lockImage.isHidden = true + return view + }() + + private lazy var maTrackersGroupView3: TrackersGroupView = { + let view = TrackersGroupView() + view.placeNumber.text = "#3" + view.number.textColor = .label + view.configure(with: TrackersGroupViewModel(image: UIImage(named: "icn_clickbait_trackers")!, title: "Clickbait", number: 21)) + view.lockImage.isHidden = true + return view + }() + + private lazy var maStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(maTitle) + stackView.addArrangedSubview(maTrackersGroupView1) + stackView.addArrangedSubview(maTrackersGroupView2) + stackView.addArrangedSubview(maTrackersGroupView3) + stackView.layer.cornerRadius = 8 + stackView.layer.borderWidth = 1 + stackView.layer.borderColor = UIColor.lightGray.cgColor + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.spacing = 0 + return stackView + }() + + lazy var statisitcsView: OverallStatiscticView = { + let view = OverallStatiscticView() + return view + }() + + private lazy var firewallSwitchControl: CustomUISwitch = { + let uiSwitch = CustomUISwitch(onImage: UIImage(named: "firewall-on-image")!, offImage: UIImage(named: "firewall-off-image")!) + uiSwitch.setOnClickListener { + self.toggleFirewall() + } + return uiSwitch + }() + + override func viewDidLoad() { + super.viewDidLoad() + VPNSubscription.cacheLocalizedPrices() + + updateFirewallButtonWithStatus(status: FirewallController.shared.status()) + updateMetrics() + if metricsTimer == nil { + metricsTimer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(updateMetrics), userInfo: nil, repeats: true) + metricsTimer?.fire() + } + + view.backgroundColor = .systemBackground + + view.addSubview(firewallSwitchControl) + firewallSwitchControl.anchors.bottom.safeAreaPin() + firewallSwitchControl.anchors.leading.marginsPin() + firewallSwitchControl.anchors.trailing.marginsPin() + firewallSwitchControl.anchors.height.equal(56) + + view.addSubview(yourCurrentPlanLabel) + yourCurrentPlanLabel.anchors.leading.marginsPin() + yourCurrentPlanLabel.anchors.top.safeAreaPin() + + view.addSubview(upgradeLabel) + upgradeLabel.anchors.trailing.marginsPin() + upgradeLabel.anchors.centerY.equal(yourCurrentPlanLabel.anchors.centerY) + + view.addSubview(protectionPlanLabel) + protectionPlanLabel.anchors.top.spacing(8, to: yourCurrentPlanLabel.anchors.bottom) + protectionPlanLabel.anchors.leading.marginsPin() + + view.addSubview(scrollView) + scrollView.anchors.top.spacing(12, to: protectionPlanLabel.anchors.bottom) + scrollView.anchors.leading.pin() + scrollView.anchors.trailing.pin() + scrollView.anchors.bottom.spacing(8, to: firewallSwitchControl.anchors.top) + + scrollView.addSubview(contentView) + contentView.anchors.top.pin() + contentView.anchors.centerX.align() + contentView.anchors.width.equal(scrollView.anchors.width) + contentView.anchors.bottom.pin() + + contentView.addSubview(stackView) + stackView.anchors.top.marginsPin() + stackView.anchors.leading.marginsPin() + stackView.anchors.trailing.marginsPin() + + contentView.addSubview(upgradeButton) + upgradeButton.anchors.top.spacing(18, to: stackView.anchors.bottom) + upgradeButton.anchors.leading.marginsPin() + upgradeButton.anchors.trailing.marginsPin() + + contentView.addSubview(maStackView) + maStackView.anchors.top.spacing(18, to: upgradeButton.anchors.bottom) + maStackView.anchors.leading.marginsPin() + maStackView.anchors.trailing.marginsPin() + + contentView.addSubview(statisitcsView) + statisitcsView.anchors.top.spacing(18, to: maStackView.anchors.bottom) + statisitcsView.anchors.leading.marginsPin() + statisitcsView.anchors.trailing.marginsPin() + + updateProtectionPlanUI() + +// accountStateDidChange() + + NotificationCenter.default.addObserver(self, selector: #selector(tunnelStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updateMetrics() + } +} + +extension LDFirewallViewController: Loadable { + + @objc func accountStateDidChange() { + updateActiveSubscription() + } + + func updateProtectionPlanUI() { + if UserDefaults.hasSeenUniversalPaywall { + updateUI() + protectionPlanLabel.text = "Universal protection" + } else if UserDefaults.hasSeenAnonymousPaywall { + updateUI() + protectionPlanLabel.text = "Anonymous protection" + } else if UserDefaults.hasSeenAdvancedPaywall { + updateUI() + protectionPlanLabel.text = "Advanced protection" + } else { + protectionPlanLabel.text = "Basic protection" + } + } + + func updateUI() { + firewallTitle.isHidden = true + firewallDescriptionLabel1.isHidden = true + firewallDescriptionLabel2.isHidden = true + firewallDescriptionLabel3.isHidden = true + upgradeButton.isHidden = true + upgradeButton.anchors.height.equal(0) + cpTrackersGroupView1.lockImage.isHidden = true + cpTrackersGroupView1.number.isHidden = false + cpTrackersGroupView2.lockImage.isHidden = true + cpTrackersGroupView2.number.isHidden = false + cpTrackersGroupView3.lockImage.isHidden = true + cpTrackersGroupView3.number.isHidden = false + } + + func updateActiveSubscription() { + showLoadingView() + // not logged in via email, use receipt + firstly { + try Client.signIn() + }.then { _ in + try Client.activeSubscriptions() + }.ensure { + self.hideLoadingView() + }.done { [self] subscriptions in + self.activePlans = subscriptions.map({ $0.planType }) + if let active = subscriptions.first { + if active.planType == .universalAnnual || active.planType == .universalMonthly { + protectionPlanLabel.text = "Universal protection" + updateUI() + } else if active.planType == .anonymousMonthly || active.planType == .anonymousAnnual { + updateUI() + protectionPlanLabel.text = "Anonymous protection" + } else if active.planType == .advancedMonthly || active.planType == .advancedAnnual { + updateUI() + protectionPlanLabel.text = "Advanced protection" + } else { + firewallTitle.textColor = .red + } + } else { + firewallTitle.textColor = .red + } + }.catch { [self] error in + DDLogError("Error reloading subscription: \(error.localizedDescription)") + hideLoadingView() + if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: + UserDefaults.hasSeenAdvancedPaywall = false + case kApiCodeSandboxReceiptNotAllowed: + UserDefaults.hasSeenAdvancedPaywall = false + default: + DDLogError("Error loading plan: API error code - \(apiError.code)") + UserDefaults.hasSeenAdvancedPaywall = false + } + } else { + DDLogError("Error loading plan: Non-API Error - \(error.localizedDescription)") + UserDefaults.hasSeenAdvancedPaywall = false + } + } + } + + @objc func upgrade() { + let vc = FirewallPaywallViewController() + present(vc, animated: true) + } + + @objc func tunnelStatusDidChange(_ notification: Notification) { + // Firewall + if let tunnelProviderSession = notification.object as? NETunnelProviderSession { + DDLogInfo("VPNStatusDidChange as NETunnelProviderSession with status: \(tunnelProviderSession.status.description)"); + if (!getUserWantsFirewallEnabled()) { + updateFirewallButtonWithStatus(status: .disconnected) + } + else { + updateFirewallButtonWithStatus(status: tunnelProviderSession.status) + if (tunnelProviderSession.status == .connected && defaults.bool(forKey: kHasSeenInitialFirewallConnectedDialog) == false) { + defaults.set(true, forKey: kHasSeenInitialFirewallConnectedDialog) +// self.tapToActivateFirewallLabel.isHidden = true +// if (VPNController.shared.status() == .invalid) { +// self.showVPNSubscriptionDialog(title: NSLocalizedString("🔥🧱 Firewall Activated 🎊🎉", comment: ""), message: NSLocalizedString("Trackers, ads, and other malicious scripts are now blocked in all your apps, even outside of Safari.\n\nGet maximum privacy with a Secure Tunnel that protects connections, anonymizes your browsing, and hides your location.", comment: "")) +// } + } + } + } + } + + @objc func updateMetrics() { + DispatchQueue.main.async { [unowned self] in + self.statisitcsView.enabledBoxView.numberLabel.text = String(getTotalEnabled().count) + self.statisitcsView.disabledBoxView.numberLabel.text = String(getTotalDisabled().count) + self.statisitcsView.blockedBoxView.numberLabel.text = String(getAllBlockedDomains().count) + } + } + + func toggleFirewall() { + if (defaults.bool(forKey: kHasAgreedToFirewallPrivacyPolicy) == false) { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: "firewallPrivacyPolicyViewController") as! PrivacyPolicyViewController + viewController.privacyPolicyKey = kHasAgreedToFirewallPrivacyPolicy + viewController.parentVC1 = self + self.present(viewController, animated: true, completion: nil) + return + } + + if getIsCombinedBlockListEmpty() { + FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) + self.showPopupDialog(title: NSLocalizedString("No Block Lists Enabled", comment: ""), message: NSLocalizedString("Please tap Block List and enable at least one block list to activate Firewall.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: "")) + return + } + + switch FirewallController.shared.status() { + case .invalid: + FirewallController.shared.setEnabled(true, isUserExplicitToggle: true) + //ensureFirewallWorkingAfterEnabling(waitingSeconds: 5.0) + case .disconnected: + updateFirewallButtonWithStatus(status: .connecting) + FirewallController.shared.setEnabled(true, isUserExplicitToggle: true) + //ensureFirewallWorkingAfterEnabling(waitingSeconds: 5.0) + +// checkForAskRating() + case .connected: + updateFirewallButtonWithStatus(status: .disconnecting) + FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) + case .connecting, .disconnecting, .reasserting: + break; + } + } + + func ensureFirewallWorkingAfterEnabling(waitingSeconds: TimeInterval) { + FirewallController.shared.existingManagerCount { (count) in + if let count = count, count > 0 { + DispatchQueue.main.asyncAfter(deadline: .now() + waitingSeconds) { + DDLogInfo("\(waitingSeconds) seconds passed, checking if Firewall is enabled") + guard getUserWantsFirewallEnabled() else { + // firewall shouldn't be enabled, no need to act + DDLogInfo("User doesn't want Firewall enabled, no action") + return + } + + let status = FirewallController.shared.status() + switch status { + case .connecting, .disconnecting, .reasserting: + // check again in three seconds + DDLogInfo("Firewall is in transient state, will check again in 3 seconds") + self.ensureFirewallWorkingAfterEnabling(waitingSeconds: 3.0) + case .connected: + // all good + DDLogInfo("Firewall is connected, no action") + break + case .disconnected, .invalid: + // we suppose that the connection is somehow broken, trying to fix + DDLogInfo("Firewall is not connected even though it should be, attempting to fix") + self.showFixFirewallConnectionDialog { + FirewallController.shared.deleteConfigurationAndAddAgain() + } + } + } + } else { + DDLogInfo("No Firewall configurations in settings (likely fresh install): not checking") + return + } + } + } + + func updateFirewallButtonWithStatus(status: NEVPNStatus) { + DDLogInfo("UpdateFirewallButton") + switch status { + case .connected: + LatestKnowledge.isFirewallEnabled = true + case .disconnected: + LatestKnowledge.isFirewallEnabled = false + default: + break + } + updateToggleButtonWithStatus(lastStatus: lastFirewallStatus, + newStatus: status, + switchControl: firewallSwitchControl) + } + + func updateToggleButtonWithStatus(lastStatus: NEVPNStatus?, + newStatus: NEVPNStatus, + switchControl: CustomUISwitch) { + DDLogInfo("UpdateToggleButton") + if (newStatus == lastStatus) { + DDLogInfo("No status change from last time, ignoring."); + } + else { + DispatchQueue.main.async() { + switch newStatus { + case .connected: + switchControl.status = true + case .connecting: + switchControl.status = true + case .disconnected, .invalid: + switchControl.status = false + case .disconnecting: + switchControl.status = false + case .reasserting: + break; + } + } + } + } +} diff --git a/LockdowniOS/LDVpnViewController.swift b/LockdowniOS/LDVpnViewController.swift new file mode 100644 index 0000000..85451ce --- /dev/null +++ b/LockdowniOS/LDVpnViewController.swift @@ -0,0 +1,574 @@ +// +// LDVpnViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 19.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit +import CocoaLumberjackSwift +import PromiseKit +import NetworkExtension +import PopupDialog + +final class LDVpnViewController: BaseViewController { + + // MARK: - Properties + var activePlans: [Subscription.PlanType] = [] + + var lastVPNStatus: NEVPNStatus? + + private lazy var scrollView: UIScrollView = { + let view = UIScrollView() + view.isScrollEnabled = true + return view + }() + + private lazy var contentView: UIView = { + let view = UIView() + view.anchors.height.equal(640) + return view + }() + + private lazy var yourCurrentPlanLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Your current plan is", comment: "") + label.font = fontRegular14 + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + private lazy var upgradeLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Upgrade?", comment: "") + label.font = fontBold13 + label.textColor = .tunnelsBlue + label.isUserInteractionEnabled = true + label.setOnClickListener { + let vc = VPNPaywallViewController() + self.present(vc, animated: true) + } + return label + }() + + private lazy var protectionPlanLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Basic Protection", comment: "") + label.font = fontBold22 + label.textColor = .label + return label + }() + + private lazy var mainTitle: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Get Anonymous protection", comment: "") + label.font = fontBold24 + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + private lazy var descriptionLabel1: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Block as many trackers as you want", comment: ""))) + return label + }() + + private lazy var descriptionLabel2: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Import and export your own block lists", comment: ""))) + return label + }() + + private lazy var descriptionLabel3: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Access to new curated lists of trackers", comment: ""))) + return label + }() + + private lazy var descriptionLabel4: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("The only fully open source VPN", comment: ""))) + return label + }() + + private lazy var descriptionLabel5: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Hide your identity around the world", comment: ""))) + return label + }() + + private lazy var descriptionLabel6: DescriptionLabel = { + let label = DescriptionLabel() + label.configure(with: DescriptionLabelViewModel(text: NSLocalizedString("Protect all Apple devices", comment: ""))) + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(mainTitle) + stackView.addArrangedSubview(descriptionLabel1) + stackView.addArrangedSubview(descriptionLabel2) + stackView.addArrangedSubview(descriptionLabel3) + stackView.addArrangedSubview(descriptionLabel4) + stackView.addArrangedSubview(descriptionLabel5) + stackView.addArrangedSubview(descriptionLabel6) + stackView.axis = .vertical + stackView.distribution = .fillProportionally + stackView.spacing = 10 + return stackView + }() + + private lazy var upgradeButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.setTitle(NSLocalizedString("Upgrade", comment: ""), for: .normal) + button.titleLabel?.font = fontBold18 + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 28 + button.anchors.height.equal(56) + button.addTarget(self, action: #selector(upgrade), for: .touchUpInside) + return button + }() + + private lazy var whitelistCard: LDCardView = { + let view = LDCardView() + view.title.text = "Whitelist" + view.iconImageView.image = UIImage(named: "icn_whitelist") + view.isUserInteractionEnabled = true + view.setOnClickListener { [weak self] in + guard let self else { return } + let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let vc = storyBoard.instantiateViewController(withIdentifier: "WhitelistViewController") as! WhitelistViewController + self.present(vc, animated: true, completion: nil) + } + return view + }() + + private lazy var regionCard: LDCardView = { + let view = LDCardView() + view.title.text = "Region" + view.iconImageView.image = UIImage(named: "icn_globe") + view.isUserInteractionEnabled = true + view.setOnClickListener { [weak self] in + guard let self else { return } + let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + let vc = storyBoard.instantiateViewController(withIdentifier: "SetRegionViewController") as! SetRegionViewController + self.present(vc, animated: true, completion: nil) + } + return view + }() + + private lazy var hStack: UIStackView = { + let stack = UIStackView() + stack.addArrangedSubview(whitelistCard) + stack.addArrangedSubview(regionCard) + stack.alignment = .center + stack.distribution = .equalCentering + stack.spacing = 16 + return stack + }() + + private lazy var vpnSwitchControl: CustomUISwitch = { + let uiSwitch = CustomUISwitch(onImage: UIImage(named: "vpn-on-image")!, offImage: UIImage(named: "vpn-off-image")!) + uiSwitch.setOnClickListener { + self.toggleVPN() + } + return uiSwitch + }() + + // MARK: - LifeCycle + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + + view.addSubview(vpnSwitchControl) + vpnSwitchControl.anchors.bottom.safeAreaPin() + vpnSwitchControl.anchors.leading.marginsPin() + vpnSwitchControl.anchors.trailing.marginsPin() + vpnSwitchControl.anchors.height.equal(56) + + view.addSubview(yourCurrentPlanLabel) + yourCurrentPlanLabel.anchors.leading.marginsPin() + yourCurrentPlanLabel.anchors.top.safeAreaPin() + + view.addSubview(upgradeLabel) + upgradeLabel.anchors.trailing.marginsPin() + upgradeLabel.anchors.centerY.equal(yourCurrentPlanLabel.anchors.centerY) + + view.addSubview(protectionPlanLabel) + protectionPlanLabel.anchors.top.spacing(8, to: yourCurrentPlanLabel.anchors.bottom) + protectionPlanLabel.anchors.leading.marginsPin() + + view.addSubview(scrollView) + scrollView.anchors.top.spacing(12, to: protectionPlanLabel.anchors.bottom) + scrollView.anchors.leading.pin() + scrollView.anchors.trailing.pin() + scrollView.anchors.bottom.spacing(8, to: vpnSwitchControl.anchors.top) + + scrollView.addSubview(contentView) + contentView.anchors.top.pin() + contentView.anchors.centerX.align() + contentView.anchors.width.equal(scrollView.anchors.width) + contentView.anchors.bottom.pin() + + contentView.addSubview(stackView) + stackView.anchors.top.marginsPin() + stackView.anchors.leading.marginsPin() + stackView.anchors.trailing.marginsPin() + + contentView.addSubview(upgradeButton) + upgradeButton.anchors.top.spacing(18, to: stackView.anchors.bottom) + upgradeButton.anchors.leading.marginsPin() + upgradeButton.anchors.trailing.marginsPin() + + contentView.addSubview(hStack) + hStack.anchors.top.spacing(18, to: upgradeButton.anchors.bottom) + hStack.anchors.centerX.align() + + whitelistCard.anchors.width.equal(view.bounds.width / 2 - 20) + whitelistCard.anchors.height.equal(view.bounds.width / 2 - 20) + + regionCard.anchors.width.equal(view.bounds.width / 2 - 20) + regionCard.anchors.height.equal(view.bounds.width / 2 - 20) + + updateVPNButtonWithStatus(status: VPNController.shared.status()) + updateVPNRegionLabel() + + if (VPNController.shared.status() == .connected) { + firstly { + try Client.signIn() + } + .done { (signin: SignIn) in + // successfully signed in with no subscription errors, do nothing + } + .catch { error in + if (self.popupErrorAsNSURLError(error)) { + return + } + else if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: + self.showPopupDialog(title: NSLocalizedString("VPN Subscription Expired", comment: ""), message: NSLocalizedString("Please renew your subscription to re-activate the VPN.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""), completionHandler: { + self.present(VPNPaywallViewController(), animated: true) +// self.performSegue(withIdentifier: "showSignup", sender: self) + }) + default: +// _ = self.popupErrorAsApiError(error) + break + } + } + else { + self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""), + message: "\(error)", + acceptButton: "Okay") + } + } + } + + if UserDefaults.hasSeenAnonymousPaywall { + mainTitle.text = "Get Universal protection" + descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel4.lockImage.image = UIImage(named: "icn_checkmark") + protectionPlanLabel.text = "Anonymous protection" + } else if UserDefaults.hasSeenUniversalPaywall { + mainTitle.text = "You're fully protected" + descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel4.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel5.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel6.lockImage.image = UIImage(named: "icn_checkmark") + upgradeButton.isHidden = true + upgradeButton.anchors.height.equal(0) + protectionPlanLabel.text = "Universal protection" + } else if UserDefaults.hasSeenAdvancedPaywall { + descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark") + protectionPlanLabel.text = "Advanced protection" + } else { + protectionPlanLabel.text = "Basic protection" + } + +// accountStateDidChange() + + NotificationCenter.default.addObserver(self, selector: #selector(tunnelStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil) + } +} + +// MARK: - Private functions + +extension LDVpnViewController: Loadable { + + @objc func accountStateDidChange() { + updateActiveSubscription() + } + + func updateActiveSubscription() { + showLoadingView() + // not logged in via email, use receipt + firstly { + try Client.signIn() + }.then { _ in + try Client.activeSubscriptions() + }.ensure { + self.hideLoadingView() + }.done { [self] subscriptions in + self.activePlans = subscriptions.map({ $0.planType }) + if let active = subscriptions.first { + if active.planType == .advancedMonthly || active.planType == .advancedAnnual { + descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark") + UserDefaults.hasSeenAdvancedPaywall = true + } else if active.planType == .anonymousMonthly || active.planType == .anonymousAnnual { + mainTitle.text = "Get Universal protection" + descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel4.lockImage.image = UIImage(named: "icn_checkmark") + UserDefaults.hasSeenAdvancedPaywall = true + UserDefaults.hasSeenAnonymousPaywall = true + } else if active.planType == .universalAnnual || active.planType == .universalMonthly { + mainTitle.text = "You're fully protected" + descriptionLabel1.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel2.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel3.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel4.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel5.lockImage.image = UIImage(named: "icn_checkmark") + descriptionLabel6.lockImage.image = UIImage(named: "icn_checkmark") + upgradeButton.isHidden = true + upgradeButton.anchors.height.equal(0) + UserDefaults.hasSeenAdvancedPaywall = true + UserDefaults.hasSeenAnonymousPaywall = true + UserDefaults.hasSeenUniversalPaywall = true + } + } + }.catch { [self] error in + DDLogError("Error reloading subscription: \(error.localizedDescription)") + hideLoadingView() + if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt, kApiCodeNoActiveSubscription: + UserDefaults.hasSeenAdvancedPaywall = false + UserDefaults.hasSeenAnonymousPaywall = false + UserDefaults.hasSeenUniversalPaywall = false + case kApiCodeSandboxReceiptNotAllowed: + UserDefaults.hasSeenAdvancedPaywall = false + UserDefaults.hasSeenAnonymousPaywall = false + UserDefaults.hasSeenUniversalPaywall = false + default: + DDLogError("Error loading plan: API error code - \(apiError.code)") + UserDefaults.hasSeenAdvancedPaywall = false + UserDefaults.hasSeenAnonymousPaywall = false + UserDefaults.hasSeenUniversalPaywall = false + } + } else { + DDLogError("Error loading plan: Non-API Error - \(error.localizedDescription)") + UserDefaults.hasSeenAdvancedPaywall = false + UserDefaults.hasSeenAnonymousPaywall = false + UserDefaults.hasSeenUniversalPaywall = false + } + } + } + + @objc func upgrade() { + let vc = VPNPaywallViewController() + present(vc, animated: true) + } + + func toggleVPN() { + + DDLogInfo("Toggle VPN") + switch VPNController.shared.status() { + case .connected, .connecting, .reasserting: + DDLogInfo("Toggle VPN: on currently, turning it off") + updateVPNButtonWithStatus(status: .disconnecting) + VPNController.shared.setEnabled(false) + case .disconnected, .disconnecting, .invalid: + DDLogInfo("Toggle VPN: off currently, turning it on") + updateVPNButtonWithStatus(status: .connecting) + // if there's a confirmed email, use that and sync the receipt with it + if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { + DDLogInfo("have confirmed API credentials, using them") + firstly { + try Client.signInWithEmail(email: apiCredentials.email, password: apiCredentials.password) + } + .then { (signin: SignIn) -> Promise in + DDLogInfo("signin result: \(signin)") + return try Client.subscriptionEvent() + } + .then { (result: SubscriptionEvent) -> Promise in + DDLogInfo("subscriptionevent result: \(result)") + return try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + DDLogInfo("setting VPN creds with ID: \(getKey.id)") + VPNController.shared.setEnabled(true) + } + .catch { error in + DDLogError("Error doing email-login -> subscription-event: \(error)") + self.updateVPNButtonWithStatus(status: .disconnected) + if (self.popupErrorAsNSURLError(error)) { + return + } + else if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeInvalidAuth, kApiCodeIncorrectLogin: + let confirm = PopupDialog(title: "Incorrect Login", + message: "Your saved login credentials are incorrect. Please sign out and try again.", + image: nil, + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil) + confirm.addButtons([ + DefaultButton(title: NSLocalizedString("Cancel", comment: ""), dismissOnTap: true) { + }, + DefaultButton(title: NSLocalizedString("Sign Out", comment: ""), dismissOnTap: true) { + URLCache.shared.removeAllCachedResponses() + Client.clearCookies() + clearAPICredentials() + setAPICredentialsConfirmed(confirmed: false) + self.showPopupDialog(title: "Success", message: "Signed out successfully.", acceptButton: NSLocalizedString("Okay", comment: "")) + }, + ]) + self.present(confirm, animated: true, completion: nil) + case kApiCodeNoSubscriptionInReceipt: + self.present(VPNPaywallViewController(), animated: true) +// self.performSegue(withIdentifier: "showSignup", sender: self) + case kApiCodeNoActiveSubscription: + self.showPopupDialog(title: NSLocalizedString("Subscription Expired", comment: ""), message: NSLocalizedString("Please renew your subscription to activate the Secure Tunnel.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""), completionHandler: { + self.present(VPNPaywallViewController(), animated: true) +// self.performSegue(withIdentifier: "showSignup", sender: self) + }) + default: + _ = self.popupErrorAsApiError(error) + } + } + } + } + else { + firstly { + try Client.signIn() // this will fetch and set latest receipt, then submit to API to get cookie + } + .then { (signin: SignIn) -> Promise in + // TODO: don't always do this -- if we already have a key, then only do it once per day max + try Client.getKey() + } + .done { (getKey: GetKey) in + try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + VPNController.shared.setEnabled(true) + } + .catch { error in + self.updateVPNButtonWithStatus(status: .disconnected) + if (self.popupErrorAsNSURLError(error)) { + return + } + else if let apiError = error as? ApiError { + switch apiError.code { + case kApiCodeNoSubscriptionInReceipt: + self.present(VPNPaywallViewController(), animated: true) +// self.performSegue(withIdentifier: "showSignup", sender: self) + case kApiCodeNoActiveSubscription: + self.showPopupDialog(title: NSLocalizedString("Subscription Expired", comment: ""), message: NSLocalizedString("Please renew your subscription to activate the Secure Tunnel.", comment: ""), acceptButton: NSLocalizedString("Okay", comment: ""), completionHandler: { + self.present(VPNPaywallViewController(), animated: true) +// self.performSegue(withIdentifier: "showSignup", sender: self) + }) + default: + if (apiError.code == kApiCodeNegativeError) { + if (getVPNCredentials() != nil) { + DDLogError("Unknown error -1 from API, but VPNCredentials exists, so activating anyway.") + self.updateVPNButtonWithStatus(status: .connecting) + VPNController.shared.setEnabled(true) + } + else { + 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 { +// _ = self.popupErrorAsApiError(error) + self.updateVPNButtonWithStatus(status: .connecting) + VPNController.shared.setEnabled(true) + } + } + } + else { + self.showPopupDialog(title: NSLocalizedString("Error Signing In To Verify Subscription", comment: ""), + message: "\(error)", + acceptButton: NSLocalizedString("Okay", comment: "")) + } + } + } + } + } + + func updateVPNButtonWithStatus(status: NEVPNStatus) { + DDLogInfo("UpdateVPNButton") + switch status { + case .connected: + LatestKnowledge.isVPNEnabled = true + case .disconnected: + LatestKnowledge.isVPNEnabled = false + default: + break + } + updateToggleButtonWithStatus(lastStatus: lastVPNStatus, + newStatus: status, + switchControl: vpnSwitchControl) + } + + func updateToggleButtonWithStatus(lastStatus: NEVPNStatus?, + newStatus: NEVPNStatus, + switchControl: CustomUISwitch) { + DDLogInfo("UpdateToggleButton") + if (newStatus == lastStatus) { + DDLogInfo("No status change from last time, ignoring."); + } + else { + DispatchQueue.main.async() { + switch newStatus { + case .connected: + switchControl.status = true + case .connecting: + switchControl.status = true + case .disconnected, .invalid: + switchControl.status = false + case .disconnecting: + switchControl.status = false + case .reasserting: + break; + } + } + } + } + + func updateVPNRegionLabel() { + regionCard.subTitle.text = getSavedVPNRegion().regionDisplayNameShort + } + + @objc func tunnelStatusDidChange(_ notification: Notification) { + if let neVPNConnection = notification.object as? NEVPNConnection { + DDLogInfo("VPNStatusDidChange as NEVPNConnection with status: \(neVPNConnection.status.description)"); + updateVPNButtonWithStatus(status: neVPNConnection.status); + updateVPNRegionLabel() + if NEVPNManager.shared().connection.status == .connected || NEVPNManager.shared().connection.status == .disconnected { + //self.updateIP(); + } + } + else { + DDLogInfo("VPNStatusDidChange neither TunnelProviderSession nor NEVPNConnection"); + } + } +} diff --git a/LockdowniOS/ListBlockedTableViewCell.swift b/LockdowniOS/ListBlockedTableViewCell.swift new file mode 100644 index 0000000..ceb2c8f --- /dev/null +++ b/LockdowniOS/ListBlockedTableViewCell.swift @@ -0,0 +1,50 @@ +// +// ListBlockedTableViewCell.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 28.03.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class ListBlockedTableViewCell: UITableViewCell { + + // MARK: - Properties + static let identifier = "ListBlockedTableViewCell" + + lazy var label: UILabel = { + let label = UILabel() + label.font = fontRegular14 + label.textColor = .label + label.numberOfLines = 1 + return label + }() + + // MARK: - Init + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + confugureUI() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + label.text = nil + } + + private func confugureUI() { + + contentView.addSubview(label) + label.anchors.top.marginsPin() + label.anchors.leading.marginsPin() + label.anchors.bottom.marginsPin() + + contentView.clipsToBounds = true + accessoryType = .disclosureIndicator + } +} diff --git a/LockdowniOS/ListDescriptionViewController.swift b/LockdowniOS/ListDescriptionViewController.swift new file mode 100644 index 0000000..406d7dd --- /dev/null +++ b/LockdowniOS/ListDescriptionViewController.swift @@ -0,0 +1,132 @@ +// +// ListDescriptionViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 29.03.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +protocol ListDescriptionViewControllerDelegate { + func changeListDescription(description: String) +} + +final class ListDescriptionViewController: UIViewController { + + var listName = "" + + var delegate: ListDescriptionViewControllerDelegate? + + var domains = getBlockedLists().userBlockListsDefaults + + // MARK: - Properties + private lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView() + view.titleLabel.text = NSLocalizedString("Name", comment: "") + view.leftNavButton.setTitle("BACK", for: .normal) + view.leftNavButton.setImage(UIImage(systemName: "chevron.left"), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(returnBack), for: .touchUpInside) + view.rightNavButton.setTitle("DONE", for: .normal) + view.rightNavButton.addTarget(self, action: #selector(doneButtonClicked), for: .touchUpInside) + return view + }() + + private lazy var descriptionTextField: UITextField = { + let view = UITextField() + view.placeholder = "No Description" + view.text = domains[listName]?.description + view.font = fontMedium17 + view.textColor = .label + view.backgroundColor = .systemBackground + view.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: .zero)) + view.leftViewMode = .always + view.addTarget(self, action: #selector(handleTextChange), for: .editingChanged) + return view + }() + + private lazy var validationPrompt: UILabel = { + let label = UILabel() + label.textColor = .red + label.font = fontRegular14 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .secondarySystemBackground + let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + + configureUI() + } + + // MARK: - Configure UI + private func configureUI() { + view.addSubview(navigationView) + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + navigationView.anchors.top.safeAreaPin() + + view.addSubview(descriptionTextField) + descriptionTextField.anchors.top.spacing(12, to: navigationView.anchors.bottom) + descriptionTextField.anchors.leading.marginsPin() + descriptionTextField.anchors.trailing.marginsPin() + descriptionTextField.anchors.height.equal(40) + + view.addSubview(validationPrompt) + validationPrompt.anchors.top.spacing(8, to: descriptionTextField.anchors.bottom) + validationPrompt.anchors.leading.marginsPin() + validationPrompt.anchors.trailing.marginsPin() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + descriptionTextField.layer.cornerRadius = 8 + } +} + +// MARK: - Functions +private extension ListDescriptionViewController { + + @objc func handleTextChange() { + guard let text = descriptionTextField.text else { return } + if text.isValid(.listDescription) { + navigationView.rightNavButton.isEnabled = true + validationPrompt.text = "" + } else { + navigationView.rightNavButton.isEnabled = false + validationPrompt.text = "Invalid Description. Please use only letters and digits. The maximum number of symbols is 500." + } + } + + @objc func returnBack() { + navigationController?.popViewController(animated: true) + } + + @objc func doneButtonClicked() { + + guard let newDescription = descriptionTextField.text else { return } + delegate?.changeListDescription(description: newDescription) + + let domains = getBlockedLists().userBlockListsDefaults + var userList = domains[listName] + + userList?.description = descriptionTextField.text + + var data = getBlockedLists() + data.userBlockListsDefaults[listName] = userList + let encodedData = try? JSONEncoder().encode(data) + defaults.set(encodedData, forKey: kUserBlockedLists) + + navigationController?.popViewController(animated: true) + } + + @objc func dismissKeyboard() { + view.endEditing(true) + } +} diff --git a/LockdowniOS/ListDetailViewController.swift b/LockdowniOS/ListDetailViewController.swift new file mode 100644 index 0000000..d045f05 --- /dev/null +++ b/LockdowniOS/ListDetailViewController.swift @@ -0,0 +1,121 @@ +// +// ListDetailViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 29.03.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +protocol ListDetailViewControllerDelegate { + func changeListName(name: String) +} + +final class ListDetailViewController: UIViewController { + + var delegate: ListDetailViewControllerDelegate? + + var listName = "" + + // MARK: - Properties + private lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView() + view.titleLabel.text = NSLocalizedString("Name", comment: "") + view.leftNavButton.setTitle("BACK", for: .normal) + view.leftNavButton.setImage(UIImage(systemName: "chevron.left"), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(returnBack), for: .touchUpInside) + view.rightNavButton.setTitle("DONE", for: .normal) + view.rightNavButton.addTarget(self, action: #selector(doneButtonClicked), for: .touchUpInside) + return view + }() + + lazy var listNameTextField: UITextField = { + let view = UITextField() + view.text = listName + view.font = fontMedium17 + view.textColor = .label + view.backgroundColor = .systemBackground + view.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 15, height: .zero)) + view.leftViewMode = .always + view.addTarget(self, action: #selector(handleTextChange), for: .editingChanged) + return view + }() + + private lazy var validationPrompt: UILabel = { + let label = UILabel() + label.textColor = .red + label.font = fontRegular14 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .secondarySystemBackground + let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + configureUI() + } + + // MARK: - Configure UI + private func configureUI() { + view.addSubview(navigationView) + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + navigationView.anchors.top.safeAreaPin() + + view.addSubview(listNameTextField) + listNameTextField.anchors.top.spacing(12, to: navigationView.anchors.bottom) + listNameTextField.anchors.leading.marginsPin() + listNameTextField.anchors.trailing.marginsPin() + listNameTextField.anchors.height.equal(40) + + view.addSubview(validationPrompt) + validationPrompt.anchors.top.spacing(8, to: listNameTextField.anchors.bottom) + validationPrompt.anchors.leading.marginsPin() + validationPrompt.anchors.trailing.marginsPin() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + listNameTextField.layer.cornerRadius = 8 + } +} + +// MARK: - Functions +extension ListDetailViewController { + + @objc func handleTextChange() { + guard let text = listNameTextField.text else { return } + if text.isValid(.listName) { + navigationView.rightNavButton.isEnabled = true + validationPrompt.text = "" + } else { + navigationView.rightNavButton.isEnabled = false + validationPrompt.text = "Invalid name. Please use only letters and digits. The maximum number of symbols is 20." + } + } + + @objc func returnBack() { + navigationController?.popViewController(animated: true) + } + + @objc func doneButtonClicked() { + + guard let newListName = listNameTextField.text else { return } + + delegate?.changeListName(name: newListName) + if listName != newListName { + changeBlockedListName(from: listName, to: newListName) + } + navigationController?.popViewController(animated: true) + } + + @objc func dismissKeyboard() { + view.endEditing(true) + } +} diff --git a/LockdowniOS/ListSettingsViewController.swift b/LockdowniOS/ListSettingsViewController.swift new file mode 100644 index 0000000..c9f70de --- /dev/null +++ b/LockdowniOS/ListSettingsViewController.swift @@ -0,0 +1,379 @@ +// +// ListSettingsViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 28.03.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit +import CocoaLumberjackSwift + +final class ListSettingsViewController: UIViewController { + + // MARK: - Properties + private var blockedList: UserBlockListsGroup? + + var listName = "" + + weak var blockListVC: BlockListViewController? + + var didMakeChange = false + + lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView() + view.titleLabel.text = "List Settings" + view.leftNavButton.setTitle(NSLocalizedString("BACK", comment: ""), for: .normal) + view.leftNavButton.setImage(UIImage(systemName: "chevron.left"), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(backButtonClicked), for: .touchUpInside) + view.rightNavButton.setTitle("...", for: .normal) + view.rightNavButton.titleLabel?.font = fontBold18 + view.rightNavButton.addTarget(self, action: #selector(showSubmenu), for: .touchUpInside) + return view + }() + + private lazy var switchBlockingView: SwitchBlockingView = { + let view = SwitchBlockingView() + view.titleLabel.text = NSLocalizedString("Blocking", comment: "") + view.switchView.addTarget(self, action: #selector(toggleBlocking), for: .valueChanged) + return view + }() + + private lazy var subMenu: ListsSubmenuView = { + let view = ListsSubmenuView() + view.topButton.setTitle(NSLocalizedString("Export List...", comment: ""), for: .normal) + view.topButton.setImage(UIImage(named: "icn_export_folder"), for: .normal) + view.topButton.addTarget(self, action: #selector(exportList), for: .touchUpInside) + view.bottomButton.setTitle(NSLocalizedString("Delete List...", comment: ""), for: .normal) + view.bottomButton.setImage(UIImage(named: "icn_trash"), for: .normal) + view.bottomButton.addTarget(self, action: #selector(deleteList), for: .touchUpInside) + return view + }() + + private let tableView = UITableView(frame: .zero, style: .insetGrouped) + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .secondarySystemBackground + + + +// if let list = blockedList { +// list = getBlockedLists().userBlockListsDefaults[listName] +// switchBlockingView.switchView.isOn = list.enabled +// } + + configureUI() + configureTableView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + let userBlockedLists = getBlockedLists().userBlockListsDefaults + blockedList = userBlockedLists[listName] + + switchBlockingView.switchView.isOn = didMakeChange + } + + // MARK: - Configure UI + func configureUI() { + view.addSubview(navigationView) + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + navigationView.anchors.top.safeAreaPin() + + view.addSubview(switchBlockingView) + switchBlockingView.anchors.top.spacing(12, to: navigationView.anchors.bottom) + switchBlockingView.anchors.leading.marginsPin() + switchBlockingView.anchors.trailing.marginsPin() + switchBlockingView.anchors.height.equal(40) + + addTableView(tableView) { tableview in + tableView.anchors.top.spacing(20, to: switchBlockingView.anchors.bottom) + tableView.anchors.leading.pin() + tableView.anchors.trailing.pin() + tableView.anchors.bottom.pin() + } + + view.addSubview(subMenu) + subMenu.anchors.top.spacing(0, to: navigationView.anchors.bottom) + subMenu.anchors.trailing.marginsPin() + subMenu.isHidden = true + + let tap = UITapGestureRecognizer(target: self, action: #selector(hideSubmenu)) + tap.cancelsTouchesInView = false + view.addGestureRecognizer(tap) + } + + func configureTableView() { + + tableView.delegate = self + tableView.dataSource = self + tableView.rowHeight = 40 + + tableView.register(ListBlockedTableViewCell.self, forCellReuseIdentifier: ListBlockedTableViewCell.identifier) + tableView.register(DomainsBlockedTableViewCell.self, forCellReuseIdentifier: DomainsBlockedTableViewCell.identifier) + } + + private func removeDomain(at index: Int) { + guard let list = blockedList else { return } + let domain = Array(list.domains)[index] + blockedList = deleteDoman(domain: domain, inBlockedListName: listName) + } +} + +extension ListSettingsViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + + let view = UIView() + + let sectionName = UILabel() + sectionName.font = fontBold13 + + view.addSubview(sectionName) + sectionName.anchors.top.marginsPin() + sectionName.anchors.leading.marginsPin() + sectionName.anchors.bottom.marginsPin() + + switch section { + case 0: + sectionName.text = NSLocalizedString("NAME", comment: "") + case 1: + sectionName.text = NSLocalizedString("DESCRIPTION", comment: "") + case 2: + sectionName.text = NSLocalizedString("DOMAINS", comment: "") + let addButton = UIButton(type: .system) + let symbolConfig = UIImage.SymbolConfiguration(pointSize: 14, weight: .bold, scale: .large) + addButton.setImage(UIImage(systemName: "plus", withConfiguration: symbolConfig), for: .normal) + addButton.tintColor = .tunnelsBlue + addButton.addTarget(self, action: #selector(addDomain), for: .touchUpInside) + + view.addSubview(addButton) + addButton.anchors.top.marginsPin() + addButton.anchors.trailing.marginsPin() + addButton.anchors.bottom.marginsPin() + default: break + } + return view + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + 40 + } + + func numberOfSections(in tableView: UITableView) -> Int { + return 3 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let numberOfDomains = blockedList?.domains.count + + switch section { + case 0, 1: return 1 + case 2: return numberOfDomains ?? 0 + default: return 0 + } + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return 50 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + + switch indexPath.section { + case 0: + guard let cell = tableView.dequeueReusableCell(withIdentifier: ListBlockedTableViewCell.identifier, for: indexPath) as? ListBlockedTableViewCell else { + return UITableViewCell() + } + cell.label.text = listName + return cell + case 1: + guard let cell = tableView.dequeueReusableCell(withIdentifier: ListBlockedTableViewCell.identifier, for: indexPath) as? ListBlockedTableViewCell else { + return UITableViewCell() + } + cell.label.text = blockedList?.description ?? "Description" + return cell + case 2: + guard let cell = tableView.dequeueReusableCell(withIdentifier: DomainsBlockedTableViewCell.identifier, for: indexPath) as? DomainsBlockedTableViewCell else { + return UITableViewCell() + } + let domains: [String] = Array(blockedList!.domains) + cell.label.text = domains[indexPath.row] + return cell + default: + return UITableViewCell() + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + switch indexPath.section { + case 0: + let vc = ListDetailViewController() + vc.delegate = self + vc.listName = listName + navigationController?.pushViewController(vc, animated: true) + + case 1: + let vc = ListDescriptionViewController() + vc.delegate = self + vc.listName = listName + navigationController?.pushViewController(vc, animated: true) + default: + break + } + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + indexPath.section == 2 + } + + func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + .delete + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + guard indexPath.section == 2, + editingStyle == .delete else { + return + } + + removeDomain(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .automatic) + } +} + +extension ListSettingsViewController: UITableViewDelegate { + +} + +// MARK: - Functions +extension ListSettingsViewController { + + @objc func backButtonClicked() { + navigationController?.popViewController(animated: true) + } + + func saveNewDomain(userEnteredDomainName: String) { + didMakeChange = true + DDLogInfo("Adding custom domain - \(userEnteredDomainName)") + + addDomainToBlockedList(domain: userEnteredDomainName, for: listName) + blockedList = getBlockedLists().userBlockListsDefaults[listName] + + tableView.reloadData() + } + + @objc func addDomain() { + let alertController = UIAlertController(title: "Add a Domain to Block", message: nil, preferredStyle: .alert) + let saveAction = UIAlertAction(title: "Save", style: .default) { [weak self] (_) in + guard let self else { return } + if let txtField = alertController.textFields?.first, let text = txtField.text { + + self.saveNewDomain(userEnteredDomainName: text) + } + } + + saveAction.isEnabled = false + + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { (_) in } + alertController.addTextField { (textField) in + textField.keyboardType = .URL + textField.placeholder = "domain-to-block" + } + + NotificationCenter.default.addObserver( + forName: UITextField.textDidChangeNotification, + object: alertController.textFields?.first, + queue: .main) { (notification) -> Void in + guard let textFieldText = alertController.textFields?.first?.text else { return } + saveAction.isEnabled = textFieldText.isValid(.domainName) + } + + alertController.addAction(saveAction) + alertController.addAction(cancelAction) + self.present(alertController, animated: true, completion: nil) + } + + @objc func showSubmenu() { + subMenu.isHidden = false + } + + @objc func hideSubmenu() { + subMenu.isHidden = true + } + + @objc func deleteList() { + let alert = UIAlertController(title: NSLocalizedString("Delete List?", comment: ""), message: NSLocalizedString("Are you sure you want to remove this list?", comment: ""), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("No, Return", comment: ""), style: UIAlertAction.Style.default, handler: { _ in + print("Return") + })) + alert.addAction(UIAlertAction(title: NSLocalizedString("Yes, Delete", comment: ""), + style: UIAlertAction.Style.destructive, + handler: { [weak self] (_) in + guard let self else { return } + if let vc = self.blockListVC { + vc.didMakeChange = true + } + + if let list = self.blockedList { + deleteBlockedList(listName: list.name) + } + self.backButtonClicked() + })) + self.present(alert, animated: true, completion: nil) + } + + @objc func toggleBlocking(sender: UISwitch) { + let sender = switchBlockingView.switchView + setBlockingEnabled(sender.isOn) + } + + func setBlockingEnabled(_ isEnabled: Bool) { + + let domains = getBlockedLists().userBlockListsDefaults + var userList = domains[listName] + + userList?.enabled = isEnabled + + var data = getBlockedLists() + data.userBlockListsDefaults[listName] = userList + let encodedData = try? JSONEncoder().encode(data) + defaults.set(encodedData, forKey: kUserBlockedLists) + + if let vc = self.blockListVC { + vc.didMakeChange = true + } + } + + @objc func exportList(_ sender: UIButton) { + let userList = getBlockedLists().userBlockListsDefaults[listName] + + guard let url = userList?.exportToURL() else { return } + + let activity = UIActivityViewController( + activityItems: [url], + applicationActivities: nil + ) + activity.popoverPresentationController?.sourceView = sender + present(activity, animated: true, completion: nil) + } +} + +extension ListSettingsViewController: ListDetailViewControllerDelegate { + + func changeListName(name: String) { + listName = name + tableView.reloadData() + } +} + +extension ListSettingsViewController: ListDescriptionViewControllerDelegate { + + func changeListDescription(description: String) { + tableView.reloadData() + } +} diff --git a/LockdowniOS/ListsSubmenuView.swift b/LockdowniOS/ListsSubmenuView.swift new file mode 100644 index 0000000..290a80b --- /dev/null +++ b/LockdowniOS/ListsSubmenuView.swift @@ -0,0 +1,79 @@ +// +// ListsSubmenuView.swift +// LockdownSandbox +// +// Created by Aliaksandr Dvoineu on 23.03.23. +// + +import UIKit + +final class ListsSubmenuView: UIView { + + private(set) var buttonCallback: () -> () = { } + + @discardableResult + func onButtonPressed(_ callback: @escaping () -> ()) -> Self { + buttonCallback = callback + return self + } + + // MARK: - Properties + + lazy var topButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .tunnelsBlue + button.setTitle("Create New List...", for: .normal) + button.setImage(UIImage(named: "icn_create_list"), for: .normal) + button.setTitleColor(.label, for: .normal) + button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0) + return button + }() + + lazy var bottomButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .tunnelsBlue + button.setTitle("Import Block List...", for: .normal) + button.setImage(UIImage(named: "icn_import_list"), for: .normal) + button.setTitleColor(.label, for: .normal) + button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0) + return button + }() + + lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(topButton) + stackView.addArrangedSubview(bottomButton) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.alignment = .leading + stackView.spacing = 12 + return stackView + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + func configure() { + backgroundColor = .systemBackground + + addSubview(stackView) + stackView.anchors.top.marginsPin() + stackView.anchors.bottom.marginsPin() + stackView.anchors.leading.marginsPin(inset: 10) + stackView.anchors.trailing.marginsPin(inset: 16) + } + + @objc func buttonDidPress() { + buttonCallback() + } +} diff --git a/LockdowniOS/Loader.swift b/LockdowniOS/Loader.swift new file mode 100644 index 0000000..131bd11 --- /dev/null +++ b/LockdowniOS/Loader.swift @@ -0,0 +1,69 @@ +// +// Loader.swift +// Lockdown +// +// Created by Johnny Lin on 12/12/19. +// Copyright © 2019 Confirmed Inc. All rights reserved. +// +// https://david.y4ng.fr/simple-hud-with-swift-protocols/ + +import Foundation +import UIKit + +protocol Loadable { + func showLoadingView() + func hideLoadingView() +} + +final class LoadingView: UIView { + private let activityIndicatorView = UIActivityIndicatorView(style: .large) + + override func layoutSubviews() { + super.layoutSubviews() + + backgroundColor = UIColor.black.withAlphaComponent(0.6) + layer.cornerRadius = 5 + + if activityIndicatorView.superview == nil { + addSubview(activityIndicatorView) + + activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true + activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true + activityIndicatorView.startAnimating() + } + } + + public func animate() { + activityIndicatorView.startAnimating() + } +} + +fileprivate struct Constants { + fileprivate static let loadingViewTag = 63342 +} + +extension Loadable where Self: UIViewController { + + func showLoadingView() { + let loadingView = LoadingView() + view.addSubview(loadingView) + + loadingView.translatesAutoresizingMaskIntoConstraints = false + loadingView.widthAnchor.constraint(equalToConstant: 100).isActive = true + loadingView.heightAnchor.constraint(equalToConstant: 100).isActive = true + loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true + loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true + loadingView.animate() + + loadingView.tag = Constants.loadingViewTag + } + + func hideLoadingView() { + view.subviews.forEach { subview in + if subview.tag == Constants.loadingViewTag { + subview.removeFromSuperview() + } + } + } +} diff --git a/LockdowniOS/LocalLogger.swift b/LockdowniOS/LocalLogger.swift index c241db1..9fe59cd 100644 --- a/LockdowniOS/LocalLogger.swift +++ b/LockdowniOS/LocalLogger.swift @@ -24,26 +24,42 @@ var logFileDataArray: [NSData] { } func setupLocalLogger() { - DDLog.add(DDTTYLogger.sharedInstance) DDLog.add(DDOSLogger.sharedInstance) - DDTTYLogger.sharedInstance.logFormatter = LogFormatter() DDOSLogger.sharedInstance.logFormatter = LogFormatter() - + fileLogger.rollingFrequency = TimeInterval(60*60*24) // 24 hours fileLogger.logFileManager.maximumNumberOfLogFiles = 7 fileLogger.logFormatter = LogFormatter() DDLog.add(fileLogger) + writeCommonInfoToLog() +} + +func writeCommonInfoToLog() { let nsObject: String? = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as? String let systemVersion = UIDevice.current.systemVersion + let accessLevel = accessLevel() DDLogInfo("") DDLogInfo("") DDLogInfo("************************************************") DDLogInfo("Lockdown iOS: v" + nsObject!) DDLogInfo("iOS version: " + systemVersion) DDLogInfo("Device model: " + UIDevice.current.modelName) + DDLogInfo("Access level: " + accessLevel) DDLogInfo("************************************************") } +fileprivate func accessLevel() -> String { + if UserDefaults.hasSeenUniversalPaywall { + return "Universal" + } else if UserDefaults.hasSeenAnonymousPaywall { + return "Anonymous" + } else if UserDefaults.hasSeenAdvancedPaywall { + return "Advanced" + } else { + return "Basic" + } +} + class LogFormatter: DDDispatchQueueLogFormatter { let dateFormatter: DateFormatter diff --git a/LockdowniOS/LockdownGradient.swift b/LockdowniOS/LockdownGradient.swift new file mode 100644 index 0000000..df3f878 --- /dev/null +++ b/LockdowniOS/LockdownGradient.swift @@ -0,0 +1,50 @@ +// +// 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 welcomePurple + case custom([CGColor], NSLayoutConstraint.Axis = .vertical) + + 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 .welcomePurple: + return [UIColor.gradientPink1.cgColor, UIColor.gradientPink2.cgColor] + case .custom(let colors, _): + return colors + } + } + + var points: (start: CGPoint, end: CGPoint) { + switch self { + case .custom(_, .horizontal): + return (CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 0)) + default: + return (CGPoint(x: 0, y: 0), CGPoint(x: 0, y: 1)) + + } + } +} diff --git a/LockdowniOS/LockdownStorageIdentifier.swift b/LockdowniOS/LockdownStorageIdentifier.swift new file mode 100644 index 0000000..ef196c2 --- /dev/null +++ b/LockdowniOS/LockdownStorageIdentifier.swift @@ -0,0 +1,16 @@ +// +// LockdownStorageIdentifier.swift +// LockdowniOS +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +public struct LockdownStorageIdentifier { + + private init() {} + + static let keychainId = "com.confirmed.tunnels" + static let userDefaultsId = "group.com.confirmed" + static let contentBlockerId = "com.confirmed.lockdown.Confirmed-Blocker" +} diff --git a/LockdowniOS/LockdownUser.swift b/LockdowniOS/LockdownUser.swift new file mode 100644 index 0000000..7b32e55 --- /dev/null +++ b/LockdowniOS/LockdownUser.swift @@ -0,0 +1,45 @@ +// +// LockdownUser.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +final class LockdownUser { + private(set) var currentSubscription: Subscription? + + @CodableUserDefaults(key: "cahedUsedSubscription") + private var cachedUserSubscription: Subscription? + + func updateSubscription(to newSubscription: Subscription?) { + currentSubscription = newSubscription + updateCachedSubscription(to: newSubscription) + } + + private func updateCachedSubscription(to newSubscription: Subscription?) { + guard let newSubscription else { + return + } + + cachedUserSubscription = newSubscription + } + + func cachedSubscription() -> Subscription? { + guard let subscription = cachedUserSubscription else { + return nil + } + + guard subscription.expirationDate > Date() else { + cachedUserSubscription = nil + return nil + } + return subscription + } + + func resetCache() { + cachedUserSubscription = nil + } +} diff --git a/LockdowniOS/LockdowniOS.entitlements b/LockdowniOS/LockdowniOS.entitlements index 085c69e..2d938d7 100644 --- a/LockdowniOS/LockdowniOS.entitlements +++ b/LockdowniOS/LockdowniOS.entitlements @@ -4,6 +4,12 @@ aps-environment development + com.apple.developer.associated-domains + + webcredentials:confirmedvpn.com + webcredentials:lockdownprivacy.com + webcredentials:lockdownhq.com + com.apple.developer.icloud-container-identifiers iCloud.$(CFBundleIdentifier) @@ -11,6 +17,7 @@ com.apple.developer.icloud-services CloudKit + CloudDocuments com.apple.developer.networking.networkextension @@ -22,6 +29,10 @@ allow-vpn + com.apple.developer.ubiquity-container-identifiers + + iCloud.$(CFBundleIdentifier) + com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.application-groups diff --git a/LockdowniOS/LockedListsView.swift b/LockdowniOS/LockedListsView.swift new file mode 100644 index 0000000..aecb33c --- /dev/null +++ b/LockdowniOS/LockedListsView.swift @@ -0,0 +1,66 @@ +// +// LockedListsView.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 10.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class LockedListsView: UIView { + + // MARK: - Properties + + private lazy var image: UIImageView = { + let image = UIImageView() + image.image = UIImage(named: "icn_lock") + image.contentMode = .scaleAspectFit + image.layer.masksToBounds = true + return image + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.text = "Upgrade to unlock lists" + label.textColor = .lightGray + label.textAlignment = .center + label.font = fontBold13 + label.textAlignment = .center + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(image) + stackView.addArrangedSubview(descriptionLabel) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.alignment = .center + stackView.spacing = 4 + return stackView + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Configure UI + + func configure() { + + addSubview(stackView) + stackView.anchors.top.marginsPin() + stackView.anchors.bottom.marginsPin() + stackView.anchors.leading.marginsPin() + stackView.anchors.trailing.marginsPin() + } +} + diff --git a/LockdowniOS/Mailto.swift b/LockdowniOS/Mailto.swift new file mode 100644 index 0000000..98eb21b --- /dev/null +++ b/LockdowniOS/Mailto.swift @@ -0,0 +1,25 @@ +// +// Mailto.swift +// Lockdown +// +// Created by Oleg Dreyman on 21.10.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation +import CocoaLumberjackSwift + +enum Mailto { + + static func generateURL(recipient: String, subject: String, body: String) -> URL? { + let subjectEncoded = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let bodyEncoded = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let mailtoString = "mailto:\(recipient)?subject=\(subjectEncoded)&body=\(bodyEncoded)" + guard let mailtoURL = URL(string: mailtoString) else { + DDLogError("invalid url: \(mailtoString)") + return nil + } + + return mailtoURL + } +} diff --git a/LockdowniOS/MainTabBarViewController.swift b/LockdowniOS/MainTabBarViewController.swift new file mode 100644 index 0000000..41c5f58 --- /dev/null +++ b/LockdowniOS/MainTabBarViewController.swift @@ -0,0 +1,50 @@ +// +// MainTabBarViewController.swift +// Lockdown +// +// Created by Oleg Dreyman on 02.10.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class MainTabBarController: UITabBarController { + + var fireWallViewController: LDFirewallViewController? { viewControllers![0] as? LDFirewallViewController } + + var vpnViewController: LDVpnViewController? { viewControllers![1] as? LDVpnViewController } + + var configurationViewController: LDConfigurationViewController? { viewControllers![2] as? LDConfigurationViewController } + + var accountViewController: AccountViewController? { + for viewController in viewControllers ?? [] { + if let navigationController = viewController as? UINavigationController, + let accountViewController = navigationController.viewControllers.first as? AccountViewController { + return accountViewController + } + } + return nil + } + + var homeViewController: HomeViewController? { + if let homeVC = viewControllers?.first(where: { $0 is HomeViewController }) { + return homeVC as? HomeViewController + } + return nil + } + + override func viewDidLoad() { + super.viewDidLoad() + + guard let homeViewController else { return } + homeViewController.feedbackFlow = FeedbackFlow(presentingViewController: homeViewController, purchaseHandler: homeViewController) + + guard let accountViewController else { return } + accountViewController.feedbackFlow = FeedbackFlow(presentingViewController: accountViewController, purchaseHandler: homeViewController) + } + + var accountTabBarButton: UIView? { + // this assumes that "Account" is the last tab. Change the code if this is no longer true + return tabBar.subviews.last(where: { String(describing: type(of: $0)) == "UITabBarButton" }) + } +} diff --git a/LockdowniOS/MoveToListViewController.swift b/LockdowniOS/MoveToListViewController.swift new file mode 100644 index 0000000..e804ec1 --- /dev/null +++ b/LockdowniOS/MoveToListViewController.swift @@ -0,0 +1,277 @@ +// +// MoveToLsisViewController.swift +// LockdownSandbox +// +// Created by Aliaksandr Dvoineu on 28.04.23. +// + +import UIKit +import CocoaLumberjackSwift + +final class MoveToListViewController: UIViewController { + + // MARK: - Properties + + var moveToListCompletion: (() -> ())? + + private var didMakeChange = false + + var successMessage = "" + + var selectedDomains: Dictionary = [:] { + didSet { + if selectedDomains.count == 1 { + numberOfdomains.text = "\(selectedDomains.count) " + NSLocalizedString("domain", comment: "") + successMessage = "\(selectedDomains.count) domain has been moved to list successfully." + } else { + numberOfdomains.text = "\(selectedDomains.count) " + NSLocalizedString("domains", comment: "") + successMessage = "\(selectedDomains.count) domains have been moved to list successfully." + } + domainsList.text = selectedDomains.map(\.0).joined(separator: ", ") + } + } + + private var customBlockedLists: [UserBlockListsGroup] = [] + + private let customBlockedListsTableView = CustomTableView() + + private lazy var descriptionText: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Move selected Domains to an existing or a new list", comment: "") + label.textColor = .label + label.font = fontRegular14 + label.textColor = .gray + label.numberOfLines = 0 + label.textAlignment = .center + return label + }() + + private lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView() + view.titleLabel.text = NSLocalizedString("Move to list", comment: "") + view.leftNavButton.setTitle(NSLocalizedString("BACK", comment: ""), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(backButtonClicked), for: .touchUpInside) + view.rightNavButton.setTitle(NSLocalizedString("CANCEL", comment: ""), for: .normal) + view.rightNavButton.addTarget(self, action: #selector(cancelButtonClicked), for: .touchUpInside) + return view + }() + + private lazy var domainImage: UIImageView = { + let image = UIImageView() + image.image = UIImage(systemName: "globe") + image.contentMode = .scaleAspectFit + image.tintColor = .gray + return image + }() + + private lazy var domainsList: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontRegular14 + label.textAlignment = .natural + label.numberOfLines = 0 + return label + }() + + private lazy var numberOfdomains: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontBold13 + label.textAlignment = .natural + return label + }() + + private lazy var vstackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(domainsList) + stackView.addArrangedSubview(numberOfdomains) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.spacing = 3 + return stackView + }() + + private lazy var hstackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(domainImage) + stackView.addArrangedSubview(vstackView) + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + stackView.spacing = 8 + stackView.alignment = .top + return stackView + }() + + private lazy var addNewListButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .tunnelsBlue + button.setTitle(NSLocalizedString("Add a new List", comment: ""), for: .normal) + button.setImage(UIImage(systemName: "plus.circle.fill"), for: .normal) + button.setTitleColor(.label, for: .normal) + button.imageEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: 0, right: 0) + button.addTarget(self, action: #selector(addNewList), for: .touchUpInside) + return button + }() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .secondarySystemBackground + configureUI() + configureListsTableView() + } + + // MARK: - Configure UI + private func configureUI() { + view.addSubview(descriptionText) + descriptionText.anchors.leading.readableContentPin(inset: 12) + descriptionText.anchors.trailing.readableContentPin(inset: 12) + descriptionText.anchors.top.safeAreaPin() + + view.addSubview(navigationView) + navigationView.anchors.top.spacing(8, to: descriptionText.anchors.bottom) + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + + view.addSubview(hstackView) + hstackView.anchors.top.spacing(12, to: navigationView.anchors.bottom) + hstackView.anchors.leading.marginsPin() + hstackView.anchors.trailing.marginsPin() + } + + private func configureListsTableView() { + addTableView(customBlockedListsTableView) { tableView in + tableView.anchors.top.spacing(16, to: hstackView.anchors.bottom) + tableView.anchors.leading.pin() + tableView.anchors.trailing.pin() + } + + reloadCustomBlockedLists() + } +} + +// MARK: - Private functions +private extension MoveToListViewController { + + func reloadCustomBlockedLists() { + customBlockedListsTableView.clear() + customBlockedLists = { + let lists = getBlockedLists().userBlockListsDefaults + let sorted = lists.sorted(by: { $0.key < $1.key }) + return Array(sorted.map(\.value)) + }() + + createUserBlockedListsRows() + customBlockedListsTableView.reloadData() + } + + func createUserBlockedListsRows() { + let userBlockedLists = getBlockedLists().userBlockListsDefaults + + + let tableView = customBlockedListsTableView + tableView.separatorStyle = .singleLine + + let plusButton = UIButton(type: .system) + plusButton.setImage(UIImage(systemName: "plus"), for: .normal) + plusButton.tintColor = .tunnelsBlue + + tableView.addHeader { view in + plusButton.addTarget(self, action: #selector(addNewList), for: .touchUpInside) + view.addSubview(plusButton) + plusButton.anchors.top.marginsPin() + plusButton.anchors.trailing.marginsPin() + plusButton.anchors.bottom.marginsPin() + } + + for list in customBlockedLists { + let blockListView = BlockListView() + blockListView.contents = .listsBlocked(list) + + let cell = tableView.addRow { (contentView) in + contentView.addSubview(blockListView) + blockListView.anchors.edges.pin() + }.onSelect { [unowned self] in + self.didMakeChange = true + + let blockedList = userBlockedLists[list.name] + + if let blockedList = blockedList { + for domain in self.selectedDomains.keys { + addDomainToBlockedList(domain: domain, for: blockedList.name) + } + } + moveToList() + } + + cell.accessoryType = .none + } + } + + func saveNewList(userEnteredListName: String) { + DDLogInfo("Adding custom list - \(userEnteredListName)") + addBlockedList(listName: userEnteredListName) + reloadCustomBlockedLists() + } + + func close() { + dismiss(animated: true, completion: { [weak self] in + guard let self else { return } + if (self.didMakeChange == true) { + if getIsCombinedBlockListEmpty() { + FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) + } else if (FirewallController.shared.status() == .connected) { + FirewallController.shared.restart() + } + } + }) + } + + @objc func backButtonClicked() { + dismiss(animated: true) + } + + @objc func cancelButtonClicked() { + dismiss(animated: true) + } + + @objc func addNewList() { + let tableView = customBlockedListsTableView + let alertController = UIAlertController(title: "Create New List", message: nil, preferredStyle: .alert) + let saveAction = UIAlertAction(title: "Save", style: .default) { [weak self] (_) in + if let txtField = alertController.textFields?.first, let text = txtField.text { + guard let self else { return } + self.saveNewList(userEnteredListName: text) +// if !getBlockedLists().isEmpty { +// tableView.clear() +// } + self.reloadCustomBlockedLists() + } + } + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) + alertController.addTextField { (textField) in + textField.placeholder = NSLocalizedString("List Name", comment: "") + } + alertController.addAction(saveAction) + alertController.addAction(cancelAction) + self.present(alertController, animated: true, completion: nil) + } + + func moveToList() { + for domain in selectedDomains.keys { + deleteUserBlockedDomain(domain: domain) + } + + moveToListCompletion?() + + let alert = UIAlertController(title: NSLocalizedString("Success!", comment: ""), + message: NSLocalizedString("\(successMessage)", comment: ""), + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: ""), + style: .default, + handler: { _ in + self.dismiss(animated: true) + })) + present(alert, animated: true, completion: nil) + } +} diff --git a/LockdowniOS/NibLoadable.swift b/LockdowniOS/NibLoadable.swift new file mode 100644 index 0000000..fcccd80 --- /dev/null +++ b/LockdowniOS/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/Onboarding/OnboardingView.swift b/LockdowniOS/Onboarding/OnboardingView.swift new file mode 100644 index 0000000..368b2a8 --- /dev/null +++ b/LockdowniOS/Onboarding/OnboardingView.swift @@ -0,0 +1,217 @@ +// +// OnboardingView.swift +// LockdowniOS +// +// Created by George Apostu on 13/2/25. +// Copyright © 2025 Confirmed Inc. All rights reserved. +// + +import SwiftUI + +struct OnboardingView: View { + + @StateObject var paywallModel: OneTimePaywallModel + + @State var selectedTab: Int = { + if #available(iOS 17, *) { + 2 + } else { + 0 + } + }() + + func selectNextTab() { + guard let step = OnboardingStep(rawValue: selectedTab) else { return } + if step == .first { + selectedTab = OnboardingStep.second.rawValue + ReviewAlertManager.showOnboardingAlert() + } else if step == .second { + selectedTab = OnboardingStep.paywall.rawValue + } + } + + let steps: [OnboardingStep] = [.first, .second] + + var body: some View { + TabView(selection: $selectedTab) { + Group { + ForEach(steps) { step in + OnboardingStepView(step: step) { + withAnimation { + selectNextTab() + } + } + .tag(step.rawValue) + } + OneTimePaywallView(model: paywallModel) + .tag(OnboardingStep.paywall.rawValue) + } + .onAppear { + selectedTab = 1 + selectedTab = 0 + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .animation(.easeInOut, value: selectedTab) + .onAppear { + UIScrollView.appearance().bounces = false + UIScrollView.appearance().isScrollEnabled = false + } + .ignoresSafeArea() + } +} + +#Preview { + OnboardingView(paywallModel: OneTimePaywallModel(products: VPNSubscription.oneTimeProducts, infos: [.mockWeekly, .mockWeeklyTrial, .mockYearly, .mockWeeklyTrial])) +} + +struct OnboardingStepView: View { + + let step: OnboardingStep + + @State private var arrowOffset: CGFloat = -3.125 + + var continueAction: () -> Void = { } + + var body: some View { + ZStack(alignment: .bottom) { + Image(step.backgroundImageName) + .resizable() + .scaledToFill() + .ignoresSafeArea() + .frame(maxWidth: UIScreen.main.bounds.width, maxHeight: UIScreen.main.bounds.height) + + VStack(alignment: .leading, spacing: 25.0) { + Text(step.title) + .foregroundColor(.white) + .font(.system(size: 28, weight: .semibold)) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) + + Text(step.subtitle) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .multilineTextAlignment(.center) + .font(.custom("SFPro", size: 20)) + .padding(.horizontal, 25) + .padding(.bottom, 25) + + VStack(alignment: .leading, spacing: 25) { + ForEach(step.items, id: \.self) { item in + HStack(spacing: 12) { + Image("onboardingCheckmark") + Text(item) + .lineLimit(nil) + .foregroundColor(.white) + } + } + } + .font(.custom("SFPro", size: 14)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 10) + + + Button(action: { + continueAction() + }, label: { + ZStack(alignment: .trailing) { + Text("Onboarding.Continue") + .font(.custom("Montserrat-SemiBold", size: 20)) + .foregroundColor(.white) + .padding() + .padding(.vertical, 3) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 50) + .fill(Color("Confirmed Blue")) + ) + Image(systemName: "arrow.right") + .foregroundColor(.white) + .padding(20) + .offset(x: arrowOffset) + .animation(Animation.easeInOut(duration: 0.39).repeatForever(autoreverses: true), value: arrowOffset) + .onAppear { + arrowOffset = 3.125 + } + } + }) + .padding(.bottom, 30) + } + .padding(40) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + .ignoresSafeArea() + } +} + +struct OnboardingPage: Identifiable { + let id = UUID() + let image: String + let title: String + let subtitle: String + var isFirst: Bool = false + var isLast: Bool = false +} + +enum OnboardingStep: Int { + + case first = 0 + case second = 1 + case paywall = 2 + + var backgroundImageName: String { + // step1 & step2 are reversed by design + switch self { + case .first: + return "onboardingStep2" + case .second: + return "onboardingStep1" + case .paywall: + return "onboardingPaywall" + } + } + + var title: String { + // step1 & step2 are reversed by design + switch self { + case .first: + return NSLocalizedString("Onboarding.Step2.Title", comment: "") + case .second: + return NSLocalizedString("Onboarding.Step1.Title", comment: "") + case .paywall: + return "" + } + } + + var subtitle: String { + // step1 & step2 are reversed by design + switch self { + case .first: + return NSLocalizedString("Onboarding.Step2.Subtitle", comment: "") + case .second: + return NSLocalizedString("Onboarding.Step1.Subtitle", comment: "") + case .paywall: + return "" + } + } + + var items: [String] { + // step1 & step2 are reversed by design + switch self { + case .first: + return [NSLocalizedString("Onboarding.Step2.Item1", comment: ""), + NSLocalizedString("Onboarding.Step2.Item2", comment: ""), + NSLocalizedString("Onboarding.Step2.Item3", comment: "")] + case .second: + return [NSLocalizedString("Onboarding.Step1.Item1", comment: ""), + NSLocalizedString("Onboarding.Step1.Item2", comment: ""), + NSLocalizedString("Onboarding.Step1.Item3", comment: "")] + case .paywall: + return [] + } + } +} + +extension OnboardingStep: Identifiable { + var id: RawValue { rawValue } +} diff --git a/LockdowniOS/OneTimePaywallModel.swift b/LockdowniOS/OneTimePaywallModel.swift new file mode 100644 index 0000000..8e3b3ae --- /dev/null +++ b/LockdowniOS/OneTimePaywallModel.swift @@ -0,0 +1,86 @@ +// +// OneTimePaywallModel.swift +// Lockdown +// +// Created by Radu Lazar on 05.08.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import Foundation +import SwiftyStoreKit + +class OneTimePaywallModel: ObservableObject { + + enum ActivePlan { + case weekly + case yearly + } + + let products: OneTimeProducts + + var closeAction: (()->Void)? = nil + var restoreAction: (()->Void)? = nil + var continueAction: ((String)->Void)? = nil + + @Published var trialEnabled = true + @Published var activePlan: ActivePlan = .yearly + + @Published var yearlyPrice: String + @Published var offerPrice: String + @Published var weeklyPrice: String + @Published var trialWeeklyPrice: String + @Published var saving: Int + @Published var showProgress = false + + init(products: OneTimeProducts, infos: [InternalSubscription]) { + self.products = products + let currencyFormatter = NumberFormatter() + currencyFormatter.usesGroupingSeparator = true + currencyFormatter.numberStyle = .currency + currencyFormatter.locale = infos.first?.priceLocale + + let yp = infos.first(where: { $0.productId == products.yearly}).flatMap { $0.price } ?? 11.11 + let wp = yp.dividing(by: 52) + let twp = infos.first(where: { $0.productId == products.weeklyTrial}).flatMap { $0.price } ?? 0.11 + let op = infos.first(where: { $0.productId == products.yearly}).flatMap { $0.offer } ?? 0.11 + + yearlyPrice = currencyFormatter.string(from: yp) ?? "__" + weeklyPrice = currencyFormatter.string(from: wp) ?? "__" + trialWeeklyPrice = currencyFormatter.string(from: twp) ?? "__" + offerPrice = currencyFormatter.string(from: op) ?? "__" + + trialWeeklyPrice = infos.first(where: { $0.productId == products.weeklyTrial}).flatMap { + currencyFormatter.locale = $0.priceLocale + return currencyFormatter.string(from: $0.price) + } ?? "__" + + saving = 100 - Int(Double(truncating: wp) / Double(truncating: twp)*100) + } + + func purchase() { + showProgress = true + switch activePlan { + case .weekly: + continueAction?(trialEnabled ? products.weeklyTrial : products.weekly) + case .yearly: + continueAction?(trialEnabled ? products.yearlyTrial : products.yearly) + } + } + + func restore() { + showProgress = true + restoreAction?() + } + + func openTermsOfService() { + if let url = URL(string: "https://lockdownprivacy.com/terms") { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + func openPrivaciyPolicy() { + if let url = URL(string: "https://lockdownprivacy.com/privacy") { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } +} diff --git a/LockdowniOS/OneTimePaywallView.swift b/LockdowniOS/OneTimePaywallView.swift new file mode 100644 index 0000000..9a0ebba --- /dev/null +++ b/LockdowniOS/OneTimePaywallView.swift @@ -0,0 +1,341 @@ +// +// OneTimePaywallView.swift +// Lockdown +// +// Created by Radu Lazar on 05.08.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import SwiftUI + +struct OneTimePaywallView: View { + @StateObject var model: OneTimePaywallModel + @State private var arrowOffset: CGFloat = -3.125 + + @State private var screenSize = UIScreen.main.bounds.size + + private var isRunningOnIpad: Bool { + return UIDevice.current.userInterfaceIdiom == .pad + } + + var body: some View { + GeometryReader { geometry in + ZStack(alignment: .bottom) { + Image(isRunningOnIpad ? "bg_paywall_onetime_ss" : "bg_paywall_onetime") + .resizable() + .scaledToFill() + .frame(width: screenSize.width, height: screenSize.height) + + LinearGradient(stops: + [Gradient.Stop(color: Color.black.opacity(0.0), location: 0.0), + Gradient.Stop(color: Color.black.opacity(0.0), location: 0.2), + Gradient.Stop(color: Color.black.opacity(0.6), location: 0.5), + Gradient.Stop(color: Color.black.opacity(0.6), location: 1.0), + ], startPoint: .top, endPoint: .bottom) + + VStack(alignment: .leading, spacing: 8) { + title + subtitle + detailItems + + trialToggle + + yearlyProduct + weeklyProduct + + purchaseButton + + noPaymentFooter + .opacity(model.trialEnabled ? 1 : 0) + + footerLinks + } + .padding(40) + + VStack(alignment: .leading) { + closeButton + .padding(.top, geometry.safeAreaInsets.top + (isRunningOnIpad ? 30 : 0)) + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + .allowsHitTesting(model.showProgress ? false : true) + + ProgressView() + .offset(y: -70) + .scaleEffect(3) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .opacity(model.showProgress ? 1 : 0) + } + .allowsHitTesting(model.showProgress ? false : true) + + .frame(width: screenSize.width, height: screenSize.height) + .ignoresSafeArea() + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + screenSize = UIScreen.main.bounds.size + } + } + } + + private var purchaseButton: some View { + Button(action: { + model.purchase() + }, label: { + ZStack(alignment: .trailing) { + Text("Paywall.Onetime.Continue") + .font(.custom("Montserrat-SemiBold", size: 20)) + .foregroundColor(.white) + .padding() + .padding(.vertical, 2) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 50) + .fill(Color("Confirmed Blue")) + ) + Image(systemName: "arrow.right") + .foregroundColor(.white) + .padding(20) + .offset(x: arrowOffset) + .animation(Animation.easeInOut(duration: 0.39).repeatForever(autoreverses: true), value: arrowOffset) + .onAppear { + arrowOffset = 3.125 + } + } + }) + } + + private var title: some View { + Group { + Text("Tap") + .foregroundColor(.white) + + Text("Paywall.Onetime.Continue") + .foregroundColor(Color("Confirmed Blue")) + + Text("Paywall.Onetime.ToActivate") + .foregroundColor(.white) + } + .font(.system(size: 28, weight: .semibold)) + .frame(maxWidth: .infinity) + .minimumScaleFactor(0.75) + } + + private var subtitle: some View { + Text("Paywall.Onetime.PrivateBrowse") + .foregroundColor(.white) + .font(.custom("Montserrat-Regular", size: 14)) + } + + private var detailItems: some View { + VStack(alignment: .leading, spacing: 2) { + HStack { + Image(systemName: "checkmark") + .foregroundColor(Color("Confirmed Blue")) + Text("Paywall.Onetime.List1") + .foregroundColor(.white) + } + HStack { + Image(systemName: "checkmark") + .foregroundColor(Color("Confirmed Blue")) + Text("Paywall.Onetime.List2") + .foregroundColor(.white) + } + HStack { + Image(systemName: "checkmark") + .foregroundColor(Color("Confirmed Blue")) + Text("Paywall.Onetime.List3") + .foregroundColor(.white) + } + } + .font(.custom("Montserrat-Semibold", size: 12)) + .padding(.vertical, 5) + .minimumScaleFactor(0.75) + } + + private var trialToggle: some View { + HStack { + Text("Paywall.Onetime.FreeTrialE") + .lineLimit(1) + .font(.custom("Montserrat-SemiBold", size: 16)) + .foregroundColor(.white) + Spacer() + Toggle(isOn: $model.trialEnabled, label: {}) + .toggleStyle(SwitchToggleStyle(tint: Color("Confirmed Blue"))) + .frame(maxWidth: 60) + } + .modifier(BubbleBg()) + } + + private var yearlyProduct: some View { + ZStack { + HStack { + VStack(alignment: .leading) { + Text("Paywall.Onetime.YearlyPlan") + .font(.custom("Montserrat-Medium", size: 12)) + Text("Paywall.Onetime.Just \(model.yearlyPrice)") + .font(.custom("Montserrat-SemiBold", size: 12)) + } + .font(.custom("Montserrat-SemiBold", size: 16)) + .foregroundColor(.white) + + Spacer() + + VStack(alignment: .trailing) { + Text("\(model.weeklyPrice)") + .font(.custom("Montserrat-SemiBold", size: 14)) + Text("Paywall.Onetime.PerWeek") + .font(.custom("Montserrat-Medium", size: 14)) + } + .foregroundColor(.white) + } + .modifier(BubbleBg(lineColor: model.activePlan == .yearly ? Color("Confirmed Blue") : .gray)) + + HStack { + Text("Paywall.Onetime.Save \(String(model.saving))") + Text("%") + } + .foregroundColor(.white) + .font(.custom("Montserrat-Bold", size: 12)) + .padding(4) + .background( + RoundedRectangle(cornerRadius: 20) + .fill( + LinearGradient(stops: + [Gradient.Stop(color: Color(hex: 0xFB923C, alpha: 1), location: 0.0), + Gradient.Stop(color: Color(hex: 0xEA580C, alpha: 1), location: 1.0), + ], startPoint: .leading, endPoint: .trailing) + ) + + ) + .offset(x: 100, y: -30) + } + .padding(.top, 20) + .contentShape(Rectangle()) + .onTapGesture { + model.activePlan = .yearly + } + } + + private var weeklyProduct: some View { + HStack { + Text("Paywall.Onetime.3DayFT") + .font(.custom("Montserrat-SemiBold", size: 16)) + .foregroundColor(.white) + Spacer() + VStack(alignment: .trailing) { + Text("Paywall.Onetime.Then \(model.trialWeeklyPrice)") + .font(.custom("Montserrat-SemiBold", size: 14)) + Text("Paywall.Onetime.PerWeek") + .font(.custom("Montserrat-Medium", size: 14)) + } + .foregroundColor(.white) + + } + .modifier(BubbleBg(lineColor: model.activePlan == .weekly ? Color("Confirmed Blue") : .gray)) + .contentShape(Rectangle()) + .onTapGesture { + model.activePlan = .weekly + } + } + + private var noPaymentFooter: some View { + HStack { + Spacer() + Image("shield_checkmark") + Text("No payment now") + Spacer() + } + .font(.custom("Montserrat-Bold", size: 12)) + .foregroundColor(.white) + } + + private var closeButton: some View { + Button { + model.closeAction?() + } label: { + Image(systemName: "xmark") + .font(.system(size: isRunningOnIpad ? 17 : 14, weight: .bold)) + .foregroundColor(.white) + } + .padding(isRunningOnIpad ? 16 : 10) + .background( + ZStack { + if #available(iOS 15.0, *) { + Circle() + .fill(.ultraThinMaterial) + } else { + Circle() + .fill(Color.gray.opacity(0.2)) + .overlay(Color.white.opacity(0.3)) + } + } + ) + .clipShape(Circle()) + .padding(.leading, isRunningOnIpad ? 50 : 20) + } + + private var footerLinks: some View { + HStack(alignment: .center) { + Button { + model.openTermsOfService() + } label: { + Text("Terms of Use") + } + + Text("|") + + Button { + model.openPrivaciyPolicy() + } label: { + Text("Privacy Policy") + } + + Text("|") + + Button { + model.restoreAction?() + } label: { + Text("Restore") + } + } + .font(.system(size: 12)) + .foregroundColor(.white.opacity(0.8)) + .padding(.bottom, -30) + .padding(.top, -4) + .frame(maxWidth: .infinity) + } +} + +struct BubbleBg: ViewModifier { + let lineColor: Color + init (lineColor: Color = Color("Confirmed Blue")) { + self.lineColor = lineColor + } + func body(content: Content) -> some View { + content + .padding(.vertical, 12) + .padding(.horizontal, 20) + .background( + RoundedRectangle(cornerRadius: 52) + .stroke(lineColor, lineWidth: 2) + .background( + BlurView(style: .dark) + .opacity(0.9) + ) + ) + .clipShape(RoundedRectangle(cornerRadius: 52)) + } +} + +struct BlurView: UIViewRepresentable { + let style: UIBlurEffect.Style + init(style: UIBlurEffect.Style) { + self.style = style + } + func makeUIView(context: Context) -> UIVisualEffectView { + let blurEffect = UIBlurEffect(style: style) + let blurView = UIVisualEffectView(effect: blurEffect) + return blurView + } + func updateUIView(_ uiView: UIVisualEffectView, context: Context) {} +} + +#Preview { + OneTimePaywallView(model: OneTimePaywallModel(products: VPNSubscription.oneTimeProducts, infos: [.mockWeekly, .mockWeeklyTrial, .mockYearly, .mockWeeklyTrial])) +} diff --git a/LockdowniOS/OverallStatiscticView.swift b/LockdowniOS/OverallStatiscticView.swift new file mode 100644 index 0000000..b881710 --- /dev/null +++ b/LockdowniOS/OverallStatiscticView.swift @@ -0,0 +1,144 @@ +// +// OverallStatiscticView.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 18.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +struct OverallStatiscticViewModel { + let enabled: Int + let disabled: Int + let requests: Int + let blocked: Int +} + +final class BoxLabelView: UIView { + + // MARK: - Properties + + lazy var boxView: UIView = { + let view = UIView() + view.layer.cornerRadius = 4 + view.layer.borderColor = UIColor.lightGray.cgColor + view.layer.borderWidth = 1 + view.anchors.height.equal(85) + return view + }() + + lazy var numberLabel: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontBold24 + label.textAlignment = .center + return label + }() + + lazy var boxTitle: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontRegular14 + label.textAlignment = .center + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(boxView) + stackView.addArrangedSubview(boxTitle) + stackView.axis = .vertical + stackView.distribution = .fillProportionally + stackView.spacing = 8 + return stackView + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + private func configureUI() { + addSubview(stackView) + stackView.anchors.edges.pin() + + boxView.addSubview(numberLabel) + numberLabel.anchors.centerX.align() + numberLabel.anchors.centerY.align() + } +} + +final class OverallStatiscticView: UIView { + + // MARK: - Properties + + lazy var enabledBoxView: BoxLabelView = { + let box = BoxLabelView() + box.numberLabel.text = "7" + box.boxTitle.text = "Enabled" + return box + }() + + lazy var disabledBoxView: BoxLabelView = { + let box = BoxLabelView() + box.numberLabel.text = "3" + box.boxTitle.text = "Disabled" + return box + }() + + lazy var blockedBoxView: BoxLabelView = { + let box = BoxLabelView() + box.numberLabel.text = "0.2K" + box.boxTitle.text = "Blocked" + return box + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(enabledBoxView) + stackView.addArrangedSubview(disabledBoxView) + stackView.addArrangedSubview(blockedBoxView) + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.alignment = .center + stackView.spacing = 8 + return stackView + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + private func configureUI() { + + addSubview(stackView) + stackView.anchors.top.marginsPin() + stackView.anchors.bottom.marginsPin() + stackView.anchors.leading.pin() + stackView.anchors.trailing.pin() + } + + func configure(with model: OverallStatiscticViewModel) { + + } +} diff --git a/LockdowniOS/PaywallDescriptionLabel.swift b/LockdowniOS/PaywallDescriptionLabel.swift new file mode 100644 index 0000000..5d4ac67 --- /dev/null +++ b/LockdowniOS/PaywallDescriptionLabel.swift @@ -0,0 +1,60 @@ +// +// AdvancedDescriptionLabel.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 27.04.2023. +// + +import UIKit + +final class PaywallDescriptionLabel: UIView { + + //MARK: Properties + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = fontBold26 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = fontSemiBold15 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(subtitleLabel) + stackView.axis = .vertical + stackView.distribution = .fillProportionally + stackView.alignment = .leading + stackView.spacing = 8 + return stackView + }() + + //MARK: Initialization + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: Functions + private func configureUI() { + addSubview(stackView) + stackView.anchors.top.pin() + stackView.anchors.bottom.marginsPin() + stackView.anchors.leading.pin() + stackView.anchors.trailing.pin() + } +} diff --git a/LockdowniOS/PaywallRoundContainer.swift b/LockdowniOS/PaywallRoundContainer.swift new file mode 100644 index 0000000..5d0d67e --- /dev/null +++ b/LockdowniOS/PaywallRoundContainer.swift @@ -0,0 +1,14 @@ +// +// PaywallRoundContainer.swift +// Lockdown +// +// Created by Radu Lazar on 05.08.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import UIKit + +@IBDesignable +class PaywallRoundContainer: UIView { + +} diff --git a/LockdowniOS/PaywallService.swift b/LockdowniOS/PaywallService.swift new file mode 100644 index 0000000..2440858 --- /dev/null +++ b/LockdowniOS/PaywallService.swift @@ -0,0 +1,77 @@ +// +// PaywallService.swift +// LockdowniOS +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 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() + } +// let paywall = TablePaywallViewController() +// paywall.modalPresentationStyle = .formSheet + +// 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 +} + diff --git a/LockdowniOS/PaywallView.swift b/LockdowniOS/PaywallView.swift new file mode 100644 index 0000000..cdfaf62 --- /dev/null +++ b/LockdowniOS/PaywallView.swift @@ -0,0 +1,171 @@ +// +// AdvancedWallView.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 28.04.2023. +// + +import UIKit +import Foundation + +final class PaywallView: UIView { + + //MARK: Properties + private let model: PaywallViewModel + var isSelected: Bool = false + + lazy var scrollView: UIScrollView = { + let view = UIScrollView() + view.isScrollEnabled = true + view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 17, right: 0) + return view + }() + + lazy var contentView: UIView = { + let view = UIView() + view.anchors.height.equal(400) + return view + }() + + lazy var headlineLabel: PaywallDescriptionLabel = { + let label = PaywallDescriptionLabel() + label.titleLabel.text = model.title + label.subtitleLabel.text = model.subtitle + return label + }() + + private lazy var bulletsStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(headlineLabel) + model.bulletPoints.forEach { + stackView.addArrangedSubview(bulletView(forTitle: $0)) + } + stackView.axis = .vertical + stackView.spacing = 8 + return stackView + }() + + lazy var trialDescriptionLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = fontRegular12 + label.textColor = .white + label.textAlignment = .center + let anualPrice = VPNSubscription.getProductIdPrice(productId: model.annualProductId) + let monthlyPrice = VPNSubscription.getProductIdPriceMonthly(productId: model.annualProductId) + let trialDuation = VPNSubscription.trialDuration(productId: model.annualProductId) ?? "" + let title = trialDuation + " " + NSLocalizedString("free trial", comment: "") + "," + " then \(anualPrice) (\(monthlyPrice)/mo)" + label.text = title + return label + }() + + lazy var bottomProduct: ProductButton = { + let descriptionLabelPrice1 = VPNSubscription.getProductIdPrice(productId: model.mounthProductId) + let trialDuation = VPNSubscription.trialDuration(productId: model.annualProductId) ?? "" + let title = trialDuation + " " + NSLocalizedString("trial", comment: "") + + var descriptionTitle = trialDuation.isEmpty ? "" : NSLocalizedString("then", comment: "") + " " + descriptionTitle += descriptionLabelPrice1 + NSLocalizedString("/year", comment: "") + + let button = ProductButton(title: "Monthly", subtitle: descriptionLabelPrice1, toHighlight: descriptionLabelPrice1) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + lazy var topProduct: ProductButton = { + let annyalPrice = VPNSubscription.getProductIdPrice(productId: model.annualProductId) + let monthlyPrice = VPNSubscription.getProductIdPriceMonthly(productId: model.annualProductId) + var descriptionTitle = "\(annyalPrice)" + " (\(monthlyPrice)/mo)" + let button = ProductButton(title: "Yearly", subtitle: descriptionTitle, toHighlight: annyalPrice, isSelected: true) + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + + func updateCTATitle(for productID: String) { + let hasTrial = VPNSubscription.trialDuration(productId: productID) != nil + let title = hasTrial ? "Start for Free" : "Continue" + actionButton.setTitle(title, for: .normal) + } + + lazy var actionButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 28 + let title = "Start for Free" + button.titleLabel?.font = fontSemiBold17 + button.setTitle(title, for: .normal) + return button + }() + + //MARK: Initialization + + init(model: PaywallViewModel) { + self.model = model + super.init(frame: .zero) + configureUI() + } + + override init(frame: CGRect) { + model = .empty() + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: ConfigureUI + private func configureUI() { + + addSubview(actionButton) + actionButton.anchors.bottom.pin() + actionButton.anchors.leading.marginsPin() + actionButton.anchors.trailing.marginsPin() + actionButton.anchors.height.equal(58) + + addSubview(trialDescriptionLabel) + trialDescriptionLabel.anchors.leading.marginsPin() + trialDescriptionLabel.anchors.trailing.marginsPin() + trialDescriptionLabel.anchors.bottom.spacing(10, to: actionButton.anchors.top) + + addSubview(bottomProduct) + bottomProduct.anchors.bottom.spacing(35, to: actionButton.anchors.top) + bottomProduct.anchors.leading.marginsPin() + bottomProduct.anchors.trailing.marginsPin() + bottomProduct.anchors.height.equal(60) + + addSubview(topProduct) + topProduct.anchors.bottom.spacing(16, to: bottomProduct.anchors.top) + topProduct.anchors.leading.marginsPin() + topProduct.anchors.trailing.marginsPin() + topProduct.anchors.height.equal(60) + + addSubview(scrollView) + scrollView.anchors.top.pin() + scrollView.anchors.leading.pin(inset: 16) + scrollView.anchors.trailing.pin() + scrollView.showsVerticalScrollIndicator = false + scrollView.anchors.bottom.spacing(8, to: topProduct.anchors.top) + + scrollView.addSubview(contentView) + contentView.anchors.top.pin() + contentView.anchors.centerX.align() + contentView.anchors.width.equal(scrollView.anchors.width) + contentView.anchors.bottom.pin() + + contentView.addSubview(bulletsStackView) + bulletsStackView.anchors.top.marginsPin() + bulletsStackView.anchors.leading.marginsPin() + bulletsStackView.anchors.trailing.marginsPin() + } + + private func bulletView(forTitle title: String) -> BulletView { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: title)) + return view + } +} diff --git a/LockdowniOS/PaywallViewModel.swift b/LockdowniOS/PaywallViewModel.swift new file mode 100644 index 0000000..7e32d47 --- /dev/null +++ b/LockdowniOS/PaywallViewModel.swift @@ -0,0 +1,81 @@ +// +// PaywallViewModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 22.02.24. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import Foundation + +struct PaywallViewModel { + let title: String + let subtitle: String + let bulletPoints: [String] + let mounthProductId: String + let annualProductId: String + + static func empty() -> Self { + .init( + title: "", + subtitle: "", + bulletPoints: [], + mounthProductId: "", + annualProductId: "" + ) + } + + static func advancedDetails() -> Self { + .init( + title: NSLocalizedString("Advanced Level \nProtection", comment:""), + subtitle: NSLocalizedString("Used by 100,000+ Privacy-Conscious People", comment: ""), + bulletPoints: [ + "Custom block lists", + "Advanced malware & ads blocking", + "Unlimited blocking", + "App-specific block lists", + "Advanced encryption protocols", + "Import/Export block lists for more tailored protection" + ], + mounthProductId: VPNSubscription.productIdAdvancedMonthly, + annualProductId: VPNSubscription.productIdAdvancedYearly + ) + } + + static func anonymousDetails() -> Self { + .init( + title: NSLocalizedString("Secure Tunnel VPN + Advanced Firewall", comment:""), + subtitle: NSLocalizedString("Private Browsing with Hidden IP and Global Region Switching", comment: ""), + bulletPoints: [ + "Anonymized browsing", + "Change your IP address to another region", + "Maximum security with VPN and firewall", + "Location and IP address hidden", + "Custom block lists to block specific websites or domains", + "Advanced malware and ads blocking", + "Unlimited bandwidth and data usage", + "Import/Export block lists for more tailored protection" + ], + mounthProductId: VPNSubscription.productIdMonthly, + annualProductId: VPNSubscription.productIdAnnual + ) + } + + static func universalDetails() -> Self { + .init( + title: NSLocalizedString("Unlimited Universal Protection", comment:""), + subtitle: NSLocalizedString("Achieve Maximum Security for All Your Apple Devices", comment: ""), + bulletPoints: [ + "Comprehensive protection across all Apple devices (iPhone, iPad, and Mac)", + "Activation/Deactivation of MacOS protection", + "Device-specific block lists for tailored protection", + "Anonymized browsing and IP address change across all devices", + "Maximum security with VPN and firewall", + "Hidden location and IP address with advanced malware", + "Unlimited bandwidth and data usage for all devices" + ], + mounthProductId: VPNSubscription.productIdMonthlyPro, + annualProductId: VPNSubscription.productIdAnnualPro + ) + } +} diff --git a/LockdowniOS/PlanView.swift b/LockdowniOS/PlanView.swift new file mode 100644 index 0000000..09eae0c --- /dev/null +++ b/LockdowniOS/PlanView.swift @@ -0,0 +1,78 @@ +// +// PlansView.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 26.04.2023. +// + +import UIKit + +final class PlanView: UIView { + + //MARK: Properties + + lazy var backgroundView: UIView = { + let view = UIView() + view.isUserInteractionEnabled = true + view.layer.cornerRadius = 8 + view.layer.borderWidth = 2 + view.layer.borderColor = UIColor.borderGray.cgColor + return view + }() + + lazy var iconImageView: UIImageView = { + let image = UIImageView() + image.contentMode = .scaleAspectFit + image.image = UIImage(named: "grey-ellipse-1") + image.layer.masksToBounds = true + return image + }() + + lazy var title: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = fontSemiBold17 + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(title) + stackView.addArrangedSubview(iconImageView) + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + stackView.alignment = .center + stackView.spacing = 0 + stackView.anchors.width.equal(130) + return stackView + }() + + //MARK: Initialization + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: Functions + + private func configureUI() { + + addSubview(backgroundView) + backgroundView.anchors.edges.pin() + + backgroundView.addSubview(stackView) + + stackView.anchors.top.marginsPin(inset: 8) + stackView.anchors.bottom.marginsPin(inset: 8) + stackView.anchors.leading.marginsPin(inset: 16) + stackView.anchors.trailing.marginsPin(inset: 8) + } +} diff --git a/LockdowniOS/PrivacyPolicyViewController.swift b/LockdowniOS/PrivacyPolicyViewController.swift index e7177d5..3ef750d 100644 --- a/LockdowniOS/PrivacyPolicyViewController.swift +++ b/LockdowniOS/PrivacyPolicyViewController.swift @@ -20,43 +20,29 @@ class PrivacyPolicyViewController: BaseViewController { @IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var getStartedButton: UIButton! @IBOutlet weak var privacyPolicyWrap: UIView! - @IBOutlet weak var privacyPolicyCheckbox: M13Checkbox! - @IBOutlet weak var checkbox: M13Checkbox! @IBOutlet weak var whyTrustButton: UIButton! var parentVC: HomeViewController? = nil + var parentVC1: LDFirewallViewController? = nil + var privacyPolicyKey = kHasAgreedToFirewallPrivacyPolicy override func viewDidLoad() { super.viewDidLoad() - self.getStartedButton.backgroundColor = .gray + self.getStartedButton.backgroundColor = .tunnelsBlue } @IBAction func getStartedTapped(_ sender: Any) { - if (checkbox.checkState == .unchecked) { - showPopupDialog(title: "Privacy Policy", message: "Please tap the checkbox circle to agree to the Privacy Policy in order to continue.", acceptButton: "Okay") - } - else { - defaults.set(true, forKey: privacyPolicyKey) - if (privacyPolicyKey == kHasAgreedToFirewallPrivacyPolicy) { - self.dismiss(animated: true, completion: { - self.parentVC?.toggleFirewall(self) - }) - } - else { - self.dismiss(animated: true, completion: { - self.parentVC?.toggleVPN(self) - }) - } - } - } - - @IBAction func checkboxTapped(_ sender: Any) { - if checkbox.checkState == .checked { - self.getStartedButton.backgroundColor = .tunnelsBlue + defaults.set(true, forKey: privacyPolicyKey) + if (privacyPolicyKey == kHasAgreedToFirewallPrivacyPolicy) { + self.dismiss(animated: true, completion: { + self.parentVC?.toggleFirewall(self) + }) } else { - self.getStartedButton.backgroundColor = .gray + self.dismiss(animated: true, completion: { + self.parentVC?.toggleVPN(self) + }) } } diff --git a/LockdowniOS/ProductButton.swift b/LockdowniOS/ProductButton.swift new file mode 100644 index 0000000..903a06e --- /dev/null +++ b/LockdowniOS/ProductButton.swift @@ -0,0 +1,105 @@ +// +// ProductButton.swift +// Lockdown +// +// Created by Denis Aleshyn on 10/05/2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import Foundation +import UIKit + +final class ProductButton: UIButton { + lazy var iconImageView: UIImageView = { + let image = UIImageView() + image.translatesAutoresizingMaskIntoConstraints = false + image.contentMode = .scaleAspectFit + image.image = UIImage(named: "grey-ellipse-1") + image.layer.masksToBounds = true + image.widthAnchor.constraint(equalToConstant: 16).isActive = true + image.heightAnchor.constraint(equalToConstant: 16).isActive = true + return image + }() + + lazy var containerStack: UIStackView = { + let stack = UIStackView(frame: .zero) + stack.translatesAutoresizingMaskIntoConstraints = false + stack.axis = .horizontal + stack.alignment = .center + stack.distribution = .fillProportionally + stack.spacing = 0 + return stack + }() + + init(title: String, subtitle: String, toHighlight: String?, isSelected: Bool = false) { + super.init(frame: .zero) + self.tintColor = .white + self.backgroundColor = .clear + self.layer.cornerRadius = 8 + + self.layer.borderWidth = isSelected ? 3 : 1 + self.layer.borderColor = isSelected ? UIColor.white.cgColor : UIColor.gray.cgColor + self.iconImageView.image = isSelected ? UIImage(named: "fill-2") : UIImage(named: "grey-ellipse-1") + + let planPeriodLabel = UILabel() + planPeriodLabel.translatesAutoresizingMaskIntoConstraints = false + planPeriodLabel.font = fontSemiBold15 + planPeriodLabel.textColor = .white + planPeriodLabel.textAlignment = .left + planPeriodLabel.text = title + + let planPriceLabel = UILabel() + planPriceLabel.translatesAutoresizingMaskIntoConstraints = false + planPriceLabel.font = fontRegular15 + planPriceLabel.textColor = .white + planPriceLabel.textAlignment = .left + planPriceLabel.text = subtitle + + if let toHighlight { + planPriceLabel.highlight(toHighlight, font: UIFont.boldLockdownFont(size: 16)) + } + + let subscriptionPlanStack = UIStackView() + subscriptionPlanStack.addArrangedSubview(planPeriodLabel) + subscriptionPlanStack.addArrangedSubview(planPriceLabel) + subscriptionPlanStack.translatesAutoresizingMaskIntoConstraints = false + subscriptionPlanStack.axis = .vertical + subscriptionPlanStack.alignment = .leading + subscriptionPlanStack.distribution = .fill + subscriptionPlanStack.spacing = 7 + + let imageContainerStack = UIStackView(arrangedSubviews: [buffer(), iconImageView, buffer()]) + imageContainerStack.axis = .vertical + imageContainerStack.distribution = .fillEqually + + addSubview(containerStack) + + containerStack.anchors.top.pin() + containerStack.anchors.bottom.pin() + containerStack.anchors.leading.pin(inset: 10) + containerStack.anchors.trailing.pin(inset: 10) + containerStack.addArrangedSubview(subscriptionPlanStack) + containerStack.addArrangedSubview(buffer()) + containerStack.addArrangedSubview(imageContainerStack) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func buffer(_ color: UIColor = .clear) -> UIView { + let buf = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 10)) + buf.translatesAutoresizingMaskIntoConstraints = false + buf.backgroundColor = color + buf.widthAnchor.constraint(greaterThanOrEqualToConstant: 10).isActive = true + buf.heightAnchor.constraint(greaterThanOrEqualToConstant: 40).isActive = true + return buf + } + + func setSelected(_ isSelected: Bool) { + self.layer.borderWidth = isSelected ? 3 : 1 + self.layer.borderColor = isSelected ? UIColor.white.cgColor : UIColor.gray.cgColor + self.iconImageView.image = isSelected ? UIImage(named: "fill-2") : UIImage(named: "grey-ellipse-1") + } + +} diff --git a/LockdowniOS/ProductPurchasable.swift b/LockdowniOS/ProductPurchasable.swift new file mode 100644 index 0000000..99c65d3 --- /dev/null +++ b/LockdowniOS/ProductPurchasable.swift @@ -0,0 +1,239 @@ +// +// ProductPurchasable.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 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 Making Purchase"), message: errorText, acceptButton: .localizedOkay) + } else if self.popupErrorAsNSURLError(error) { + return + } else if self.popupErrorAsApiError(error) { + return + } else { + self.showPopupDialog( + title: .localized("Error Making Purchase"), + message: .localized("Please contact team@lockdownprivacy.com.\n\nError details:\n") + "\(error)", + acceptButton: .localizedOkay) + } + }) + } + + func restorePurchases() { + showLoadingView() + + firstly { + try Client.signIn(forceRefresh: true) + } + .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 + self.dismiss(animated: true, completion: { + self.hideLoadingView() + let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + let vc = SplashScreenViewController() + let navigation = UINavigationController(rootViewController: vc) + keyWindow?.rootViewController = navigation + }) + } + .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/SUI+Extensions.swift b/LockdowniOS/SUI+Extensions.swift new file mode 100644 index 0000000..578c1c6 --- /dev/null +++ b/LockdowniOS/SUI+Extensions.swift @@ -0,0 +1,22 @@ +// +// SUI+Extensions.swift +// Lockdown +// +// Created by Radu Lazar on 05.08.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import Foundation +import SwiftUI + +extension Color { + init(hex: UInt, alpha: Double = 1) { + self.init( + .sRGB, + red: Double((hex >> 16) & 0xff) / 255, + green: Double((hex >> 08) & 0xff) / 255, + blue: Double((hex >> 00) & 0xff) / 255, + opacity: alpha + ) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Controllers/SelectCountryViewController.swift b/LockdowniOS/Scenes/Questionnaire/Controllers/SelectCountryViewController.swift new file mode 100644 index 0000000..2081096 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Controllers/SelectCountryViewController.swift @@ -0,0 +1,102 @@ +// +// SelectCountryViewController.swift +// Lockdown +// +// Created by Pavel Vilbik on 27.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +protocol SelectCountryViewModelProtocol { + var countries: [Country] { get } + var selectedCountry: Country? { get set } + func bind(_ view: SelectCountryViewController) + func donePressed() + var title: String { get } +} + +class SelectCountryViewController: UIViewController { + + // MARK: - models + + private let staticTableView = StaticTableView() + var viewModel: SelectCountryViewModelProtocol? + + // MARK: - views + + private lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView(accentColor: .darkText) + view.titleLabel.textColor = .label + view.leftNavButton.setTitle(NSLocalizedString("DONE", comment: ""), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(doneClicked), for: .touchUpInside) + view.leftNavButton.tintColor = .tunnelsBlue + view.rightNavButton.setTitle(NSLocalizedString("CANCEL", comment: ""), for: .normal) + view.rightNavButton.addTarget(self, action: #selector(cancelClicked), for: .touchUpInside) + view.rightNavButton.tintColor = .tunnelsBlue + return view + }() + + override func viewDidLoad() { + super.viewDidLoad() + + configureUI() + updateView() + viewModel?.bind(self) + } + + // MARK: - Configure UI + private func configureUI() { + view.backgroundColor = .panelSecondaryBackground + + view.addSubview(navigationView) + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + navigationView.anchors.top.safeAreaPin() + + addTableView(staticTableView) { tableView in + staticTableView.anchors.top.spacing(0, to: navigationView.anchors.bottom) + staticTableView.anchors.leading.pin() + staticTableView.anchors.trailing.pin() + staticTableView.anchors.bottom.pin() + } + + staticTableView.backgroundColor = .clear + staticTableView.deselectsCellsAutomatically = true + staticTableView.separatorStyle = .none + navigationView.titleLabel.text = viewModel?.title + } + + func updateView() { + staticTableView.clear() + viewModel?.countries.forEach { country in + staticTableView.addRowCell { cell in + let view = CountryView() + view.titleLabel.text = country.title + view.emojiLabel.text = country.emojiSymbol + view.checkMark.isHidden = country != viewModel?.selectedCountry + view.didSelect = { [weak self] in + self?.viewModel?.selectedCountry = country + } + cell.backgroundColor = .clear + cell.backgroundView?.backgroundColor = .clear + cell.contentView.backgroundColor = .clear + cell.addSubview(view) + view.anchors.edges.pin(insets: .init(top: 5, left: 2, bottom: 5, right: 2)) + } + } + + staticTableView.reloadData() + } + + + // MARK: - actions + + @objc private func doneClicked() { + viewModel?.donePressed() + } + + @objc func cancelClicked() { + dismiss(animated: true) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Controllers/StepsViewController.swift b/LockdowniOS/Scenes/Questionnaire/Controllers/StepsViewController.swift new file mode 100644 index 0000000..4682de9 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Controllers/StepsViewController.swift @@ -0,0 +1,127 @@ +// +// StepsViewController.swift +// Lockdown +// +// Created by Pavel Vilbik on 21.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class StepsViewController: UIViewController, StepsViewProtocol { + + // MARK: - models + var viewModel: StepsViewModel! + + // MARK: - views + + private lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView(accentColor: .darkText) + view.leftNavButton.setImage(UIImage(systemName: "chevron.left"), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(backButtonClicked), for: .touchUpInside) + view.leftNavButton.tintColor = .label + view.titleView = stepsView + return view + }() + + private lazy var stepsView: StepsView = { + let view = StepsView() + view.steps = viewModel.stepsCount + return view + }() + + private lazy var actionButton: UIButton = { + let button = UIButton() + button.anchors.height.equal(56) + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 29 + button.titleLabel?.font = .semiboldLockdownFont(size: 17) + button.addTarget(self, action: #selector(actionClicked), for: .touchUpInside) + button.setTitle(viewModel.actionTitle, for: .normal) + return button + }() + + private var contentView: UIView? + + // MARK: - life cycle + override func viewDidLoad() { + super.viewDidLoad() + + configureUI() + viewModel.bind(self) + } + + // MARK: - Configure UI + private func configureUI() { + view.backgroundColor = .panelSecondaryBackground + + view.addSubview(navigationView) + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + navigationView.anchors.top.safeAreaPin() + + view.addSubview(actionButton) + actionButton.anchors.leading.pin(inset: 24) + actionButton.anchors.trailing.pin(inset: 24) + actionButton.anchors.bottom.safeAreaPin(inset: 14) + + view.addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(tapped)) + ) + } + + func changeContent() { + contentView?.removeFromSuperview() + + let staticTableView = viewModel.stepViewModel.contentView() + addTableView(staticTableView) { tableView in + staticTableView.anchors.top.spacing(0, to: stepsView.anchors.bottom) + staticTableView.anchors.leading.pin() + staticTableView.anchors.trailing.pin() + staticTableView.anchors.bottom.spacing(18, to: actionButton.anchors.top) + } + contentView = staticTableView + stepsView.currentStep = viewModel.currentStepIndex + updateNextButton() + } + + func close(completion: (() -> Void)?) { + dismiss(animated: true, completion: completion) + } + + func showSelectCountry(with viewModel: SelectCountryViewModelProtocol) { + let viewController = SelectCountryViewController() + viewController.viewModel = viewModel + present(viewController, animated: true) + } + + func showAlert(_ title: String?, message: String?) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + + alert.addAction(.init( + title: NSLocalizedString("Ok", comment: ""), style: .default) + ) + + present(alert, animated: true) + } + + func updateNextButton() { + actionButton.setTitle(viewModel.actionTitle, for: .normal) + actionButton.isEnabled = viewModel.isStepReady + actionButton.backgroundColor = viewModel.isStepReady ? .tunnelsBlue : .disabledGray + } + + // MARK: - actions + + @objc private func backButtonClicked() { + viewModel.backPressed() + } + + @objc private func actionClicked() { + viewModel.performStepAction() + } + + @objc private func tapped() { + view.endEditing(true) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/FeedbackFlow.swift b/LockdowniOS/Scenes/Questionnaire/FeedbackFlow.swift new file mode 100644 index 0000000..b2b3313 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/FeedbackFlow.swift @@ -0,0 +1,60 @@ +// +// FeedbackFlow.swift +// Lockdown +// +// Created by Fabian Mistoiu on 15.10.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import Foundation + +protocol PurchaseHandler: AnyObject { + func purchase(productId: String) +} + +class FeedbackFlow { + + weak var presentingViewController: BaseViewController? + weak var purchaseHandler: PurchaseHandler? + + init(presentingViewController: BaseViewController, purchaseHandler: PurchaseHandler) { + self.presentingViewController = presentingViewController + self.purchaseHandler = purchaseHandler + } + + func startFlow() { + let isPremiumUser = BaseUserService.shared.user.currentSubscription != nil + let viewModel = StepsViewModel(isUserPremium: isPremiumUser) { [weak self] message in + Task { @MainActor [weak self] in + if !isPremiumUser { + await self?.showFeedbackPaywall() + } + self?.presentingViewController?.sendMessage( + message, + subject: "Lockdown Error Reporting Form (iOS \(Bundle.main.versionString))" + ) + } + } + let stepsViewController = StepsViewController() + stepsViewController.viewModel = viewModel + stepsViewController.modalPresentationStyle = .fullScreen + presentingViewController?.present(stepsViewController, animated: true) + } + + @MainActor + private func showFeedbackPaywall() async { + guard let presentingViewController, + let productInfos = await VPNSubscription.shared.loadSubscriptions(type: .feedback) else { + return + } + + let viewModel = FeedbackPaywallViewModel(products: VPNSubscription.feedbackProducts, subscriptionInfo: productInfos) + viewModel.onCloseHandler = { vc in vc.dismiss(animated: true) } + viewModel.onPurchaseHandler = { [weak self] _, pid in + guard let purchaseHandler = self?.purchaseHandler else { return } + purchaseHandler.purchase(productId: pid) + } + let paywallVC = FeedbackPaywallViewController(viewModel: viewModel) + presentingViewController.present(paywallVC, animated: true) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Models/Country.swift b/LockdowniOS/Scenes/Questionnaire/Models/Country.swift new file mode 100644 index 0000000..2c11571 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Models/Country.swift @@ -0,0 +1,14 @@ +// +// Country.swift +// Lockdown +// +// Created by Pavel Vilbik on 27.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +struct Country: Equatable { + let title: String + let emojiSymbol: String? +} diff --git a/LockdowniOS/Scenes/Questionnaire/Models/QuestionModel.swift b/LockdowniOS/Scenes/Questionnaire/Models/QuestionModel.swift new file mode 100644 index 0000000..a768a5b --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Models/QuestionModel.swift @@ -0,0 +1,135 @@ +// +// QuestionModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 28.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +struct QuestionModel { + var isFirewallOn: Bool? + var isVPNOn: Bool? + var vpnRegion: Country? + var fromCountry: Country? + var isHappeningWifiIssue: Bool? + var isHappenningCellularIssue: Bool? + var haveOtherFirewall: Bool? + var haveOtherVPN: Bool? + + func generateMessage( + firewallInput: String?, + vpnInput: String?, + otherDetailsInput: String? + ) -> String? { + var result = "" + result.append( + stringForQuestion( + NSLocalizedString("1. Is the firewall on?", comment: ""), + answer: isFirewallOn, + input: firewallInput + ) + ) + result.append( + stringForQuestion( + NSLocalizedString("2. Is the VPN on?", comment: ""), + answer: isVPNOn, + input: vpnInput + ) + ) + result.append( + stringForCountry( + vpnRegion, + title: NSLocalizedString("Which region is the VPN set to?", comment: "") + ) + ) + result.append( + stringForCountry( + fromCountry, + title: NSLocalizedString("3. Where are you contacting us from?", comment: "") + ) + ) + result.append( + stringForQuestion( + NSLocalizedString("4. Is the issue happening on WiFi?", comment: ""), + answer: isHappeningWifiIssue, + input: nil + ) + ) + result.append( + stringForQuestion( + NSLocalizedString("5. Is the issue happening on cellular data?", comment: ""), + answer: isHappenningCellularIssue, + input: nil + ) + ) + result.append( + stringForQuestion( + NSLocalizedString("6. Do you have other firewall apps installed?", comment: ""), + answer: haveOtherFirewall, + input: nil + ) + ) + result.append( + stringForQuestion( + NSLocalizedString("7. Do you have other VPN apps installed?", comment: ""), + answer: haveOtherVPN, + input: nil + ) + ) + if let otherDetailsInput, + !otherDetailsInput.isEmpty { + result.append(NSLocalizedString("8. Additional details.", comment: "")) + result.append(" " + otherDetailsInput) + } + + guard !result.isEmpty else { return nil } + + return result + } + + private func stringForQuestion( + _ question: String, + answer: Bool?, + input: String? + ) -> String { + var result = "" + if let answer { + result.append(question) + result.append(" " + stringFor(answer)) + if let input, + !input.isEmpty { + result.append("\n") + result.append(input) + } + result.append("\n") + } + return result + } + + private func stringForCountry( + _ country: Country?, + title: String + ) -> String { + var result = "" + if let country { + result.append(title) + result.append(" " + country.title) + result.append("\n") + } + return result + } + + private func stringFor(_ boolValue: Bool) -> String { + boolValue ? NSLocalizedString("Yes", comment: "") : NSLocalizedString("No", comment: "") + } + + var isAllRequiredQuestionsAnswered: Bool { + fromCountry != nil + && isHappeningWifiIssue != nil + && isHappenningCellularIssue != nil + && haveOtherFirewall != nil + && haveOtherVPN != nil + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Models/StepModel.swift b/LockdowniOS/Scenes/Questionnaire/Models/StepModel.swift new file mode 100644 index 0000000..4bbf88a --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Models/StepModel.swift @@ -0,0 +1,28 @@ +// +// StepModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 21.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +enum Steps { + case whatsProblem + case questions + + var actionTitle: String { + switch self { + case .whatsProblem: return NSLocalizedString("Next", comment: "") + case .questions: return NSLocalizedString("Submit Feedback", comment: "") + } + } + + var showSkipButton: Bool { + switch self { + case .whatsProblem: return true + case .questions: return false + } + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseSelectCountryViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseSelectCountryViewModel.swift new file mode 100644 index 0000000..1d0decf --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseSelectCountryViewModel.swift @@ -0,0 +1,45 @@ +// +// BaseSelectCountryViewModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 28.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +class BaseSelectCountryViewModel { + var countries = [Country]() + var selectedCountry: Country? { + didSet { + DispatchQueue.main.async { + self.view?.updateView() + } + } + } + + private var didSelectCountry: ((Country?) -> Void)? + + private var view: SelectCountryViewController? + + + init( + selectedCountry: Country?, + didSelectCountry: ((Country?) -> Void)? + ) { + self.didSelectCountry = didSelectCountry + countries = generateCountryList() + self.selectedCountry = selectedCountry + } + + func bind(_ view: SelectCountryViewController) { + self.view = view + } + + func donePressed() { + didSelectCountry?(selectedCountry) + view?.cancelClicked() + } + + func generateCountryList() -> [Country] { [] } +} diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseStepViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseStepViewModel.swift new file mode 100644 index 0000000..7de60a7 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/BaseStepViewModel.swift @@ -0,0 +1,70 @@ +// +// BaseStepViewModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 26.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class BaseStepViewModel { + var staticTableView: StaticTableView? + + func contentView() -> UITableView { + let staticTableView = StaticTableView() + self.staticTableView = staticTableView + + staticTableView.backgroundColor = .clear + staticTableView.deselectsCellsAutomatically = true + staticTableView.separatorStyle = .none + + updateRows() + return staticTableView + } + + func updateRows() { } + + func addTitleRow( + _ title: String?, + subtitle: String?, + bottomSpacing: CGFloat = 29 + ) { + staticTableView?.addRowCell { cell in + let titleView = TitleAndSubtitleView() + titleView.titleLabel.text = title + titleView.subtitleLabel.text = subtitle + self.setupClear(cell) + cell.addSubview(titleView) + titleView.anchors.edges.pin(insets: .init(top: 0, left: 2, bottom: bottomSpacing, right: 2)) + } + } + + func addTextViewRow( + text: String?, + placeholder: String, + didChangeText: @escaping (String) -> Void + ) { + staticTableView?.addRowCell { cell in + let view = TextViewWithPlaceholder() + view.textView.text = text + view.placeholderLabel.text = placeholder + view.placeholderLabel.isHidden = !(text?.isEmpty ?? true) + self.setupClear(cell) + cell.addSubview(view) + view.anchors.edges.pin(insets: .init(top: 0, left: 0, bottom: 0, right: 0)) + view.textDidChanged = { [weak self] text in + didChangeText(text) + self?.staticTableView?.beginUpdates() + self?.staticTableView?.invalidateIntrinsicContentSize() + self?.staticTableView?.endUpdates() + } + } + } + + func setupClear(_ cell: UITableViewCell) { + cell.backgroundColor = .clear + cell.backgroundView?.backgroundColor = .clear + cell.contentView.backgroundColor = .clear + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/QuestionsStepViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/QuestionsStepViewModel.swift new file mode 100644 index 0000000..ec7aa15 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/QuestionsStepViewModel.swift @@ -0,0 +1,178 @@ +// +// QuestionsStepViewModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 26.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class QuestionsStepViewModel: BaseStepViewModel, StepViewModelProtocol { + var step: Steps = .questions + var message: String? { + model.generateMessage( + firewallInput: firewallInput, + vpnInput: vpnInput, + otherDetailsInput: otherDetailsInput + ) + } + + var isFilled = true + + private var model = QuestionModel() { + didSet { + updateRows() + } + } + + var firewallInput: String? + var vpnInput: String? + var otherDetailsInput: String? + + var selectCountry: ((SelectCountryViewModelProtocol) -> Void)? + + override func updateRows() { + staticTableView?.clear() + addTitleRow( + NSLocalizedString("Questions", comment: ""), + subtitle: NSLocalizedString("Please answer the questions and share as much information as possible", comment: ""), + bottomSpacing: 2 + ) + + addYesNoRow( + title: NSLocalizedString("1. Is the firewall on?", comment: ""), + initialValue: model.isFirewallOn, + didSelect: { [weak self] in self?.model.isFirewallOn = $0 } + ) + + addTextViewRow( + text: firewallInput, + placeholder: NSLocalizedString("Write more information...", comment: ""), + didChangeText: { [weak self] in self?.firewallInput = $0 } + ) + + addYesNoRow( + title: NSLocalizedString("2. Is the VPN on?", comment: ""), + initialValue: model.isVPNOn, + didSelect: { [weak self] in self?.model.isVPNOn = $0 } + ) + + addTextViewRow( + text: vpnInput, + placeholder: NSLocalizedString("Write more information...", comment: ""), + didChangeText: { [weak self] in self?.vpnInput = $0 } + ) + + if model.isVPNOn ?? false { + addQuestionTitleRow( + NSLocalizedString("Which region is the VPN set to?", comment: "") + ) + addNavigationLinkRow( + placeholder: NSLocalizedString("Select region", comment: ""), + country: model.vpnRegion + ) { [weak self] in + self?.selectCountry?( + SelectRegionViewModel( + selectedCountry: self?.model.vpnRegion, + didSelectCountry: { self?.model.vpnRegion = $0 } + ) + ) + } + } + + addQuestionTitleRow( + NSLocalizedString("3. Where are you contacting us from?", comment: "") + ) + addNavigationLinkRow( + placeholder: NSLocalizedString("Select country", comment: ""), + country: model.fromCountry + ) { [weak self] in + self?.selectCountry?( + SelectCountryViewModel( + selectedCountry: self?.model.fromCountry, + didSelectCountry: { self?.model.fromCountry = $0 } + ) + ) + } + + addYesNoRow( + title: NSLocalizedString("4. Is the issue happening on WiFi?", comment: ""), + initialValue: model.isHappeningWifiIssue, + didSelect: { [weak self] in self?.model.isHappeningWifiIssue = $0 } + ) + addYesNoRow( + title: NSLocalizedString("5. Is the issue happening on cellular data?", comment: ""), + initialValue: model.isHappenningCellularIssue, + didSelect: { [weak self] in self?.model.isHappenningCellularIssue = $0 } + ) + addYesNoRow( + title: NSLocalizedString("6. Do you have other firewall apps installed?", comment: ""), + initialValue: model.haveOtherFirewall, + didSelect: { [weak self] in self?.model.haveOtherFirewall = $0 } + ) + addYesNoRow( + title: NSLocalizedString("7. Do you have other VPN apps installed?", comment: ""), + initialValue: model.haveOtherVPN, + didSelect: { [weak self] in self?.model.haveOtherVPN = $0 } + ) + + addQuestionTitleRow( + NSLocalizedString("8. Additional details. (optional)", comment: "") + ) + addTextViewRow( + text: otherDetailsInput, + placeholder: NSLocalizedString("Write additional details here...", comment: ""), + didChangeText: { [weak self] in self?.otherDetailsInput = $0 } + ) + + staticTableView?.reloadData() + } + + private func addYesNoRow( + title: String, + initialValue: Bool?, + didSelect: ((Bool?) -> Void)? + ) { + staticTableView?.addRowCell { [unowned self] cell in + let switcher = YesNoRadioSwitcherView() + switcher.titleLabel.text = title + switcher.isSelected = initialValue + switcher.didSelect = didSelect + self.setupClear(cell) + cell.addSubview(switcher) + switcher.anchors.edges.pin(insets: .init(top: 18, left: 0, bottom: 3, right: 0)) + } + } + + private func addQuestionTitleRow(_ title: String) { + staticTableView?.addRowCell { [unowned self] cell in + let view = QuestionTitleView() + view.titleLabel.text = title + self.setupClear(cell) + cell.addSubview(view) + view.anchors.edges.pin(insets: .init(top: 18, left: 0, bottom: 3, right: 0)) + } + } + + private func addNavigationLinkRow( + placeholder: String, + country: Country?, + perform: (() -> Void)? + ) { + staticTableView?.addRowCell { [unowned self] cell in + let view = NavigationLinkView() + let isEmpty = country == nil + view.placeholderLabel.text = placeholder + view.placeholderLabel.isHidden = !isEmpty + view.titleLabel.text = country?.title + view.titleLabel.isHidden = isEmpty + view.emojiLabel.text = country?.emojiSymbol + view.emojiLabel.isHidden = isEmpty + view.didSelect = perform + self.setupClear(cell) + cell.addSubview(view) + view.anchors.edges.pin(insets: .init(top: 10, left: 23, bottom: 0, right: 23)) + } + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/SelectCountryViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/SelectCountryViewModel.swift new file mode 100644 index 0000000..a243f38 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/SelectCountryViewModel.swift @@ -0,0 +1,54 @@ +// +// SelectCountryViewModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 27.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +class SelectCountryViewModel: BaseSelectCountryViewModel, SelectCountryViewModelProtocol { + var title: String { + NSLocalizedString("Select country", comment: "") + } + + override func generateCountryList() -> [Country] { + if #available(iOS 16, *) { + return Locale.Region.isoRegions.filter { $0.subRegions.isEmpty } .map { region in + Country( + title: Locale.current.localizedString(forRegionCode: region.identifier) ?? "", + emojiSymbol: emojiFlag(for: region.identifier) + ) + }.sorted { $0.title < $1.title } + } else { + return Locale.isoRegionCodes.map { identifier in + Country( + title: Locale.current.localizedString(forRegionCode: identifier) ?? "", + emojiSymbol: emojiFlag(for: identifier) + ) + }.sorted { $0.title < $1.title } + } + } + + private func emojiFlag(for countryCode: String) -> String! { + func isLowercaseASCIIScalar(_ scalar: Unicode.Scalar) -> Bool { + return scalar.value >= 0x61 && scalar.value <= 0x7A + } + + func regionalIndicatorSymbol(for scalar: Unicode.Scalar) -> Unicode.Scalar { + precondition(isLowercaseASCIIScalar(scalar)) + + // 0x1F1E6 marks the start of the Regional Indicator Symbol range and corresponds to 'A' + // 0x61 marks the start of the lowercase ASCII alphabet: 'a' + return Unicode.Scalar(scalar.value + (0x1F1E6 - 0x61))! + } + + let lowercasedCode = countryCode.lowercased() + guard lowercasedCode.count == 2 else { return nil } + guard lowercasedCode.unicodeScalars.reduce(true, { accum, scalar in accum && isLowercaseASCIIScalar(scalar) }) else { return nil } + + let indicatorSymbols = lowercasedCode.unicodeScalars.map({ regionalIndicatorSymbol(for: $0) }) + return String(indicatorSymbols.map({ Character($0) })) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/SelectRegionViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/SelectRegionViewModel.swift new file mode 100644 index 0000000..25bd9eb --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/SelectRegionViewModel.swift @@ -0,0 +1,24 @@ +// +// SelectRegionViewModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 28.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +class SelectRegionViewModel: BaseSelectCountryViewModel, SelectCountryViewModelProtocol { + var title: String { + NSLocalizedString("Select Region", comment: "") + } + + override func generateCountryList() -> [Country] { + vpnRegions.map { + Country( + title: $0.regionDisplayName, + emojiSymbol: $0.regionFlagEmoji + ) + } + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/StepsViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/StepsViewModel.swift new file mode 100644 index 0000000..76c2757 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/StepsViewModel.swift @@ -0,0 +1,131 @@ +// +// StepsViewModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 23.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +protocol StepsViewProtocol: AnyObject { + func changeContent() + func updateNextButton() + func close(completion: (() -> Void)?) + func showSelectCountry(with viewModel: SelectCountryViewModelProtocol) + func showAlert(_ title: String?, message: String?) +} + +protocol StepViewModelProtocol { + func contentView() -> UITableView + var step: Steps { get } + var message: String? { get } + var isFilled: Bool { get } +} + +class StepsViewModel { + private let isUserPremium: Bool + + private lazy var steps: [StepViewModelProtocol] = [ + whatProblemStep, + questionsStep + ] + + private lazy var whatProblemStep: WhatProblemStepViewModel = { + let viewModel = WhatProblemStepViewModel(isUserPremium: isUserPremium) { [weak self] _ in + self?.view?.updateNextButton() + } + return viewModel + }() + + private lazy var questionsStep: QuestionsStepViewModel = { + let viewModel = QuestionsStepViewModel() + viewModel.selectCountry = { [weak self] in self?.selectCountry(viewModel: $0) } + return viewModel + }() + + private weak var view: StepsViewProtocol? + + var stepsCount: Int { + steps.count + } + + var actionTitle: String { + stepViewModel.step.actionTitle + } + + var currentStepIndex = 0 + + var stepViewModel: StepViewModelProtocol { + steps[currentStepIndex] + } + + var isStepReady: Bool { + steps[currentStepIndex].isFilled + } + + private var sendMessage: ((String) -> Void)? + private var isReadyToSend: Bool { + steps.reduce(true) { $0 && $1.isFilled } + } + + init(isUserPremium: Bool, sendMessage: ((String) -> Void)?) { + self.isUserPremium = isUserPremium + self.sendMessage = sendMessage + } + + func bind(_ view: StepsViewProtocol) { + self.view = view + view.changeContent() + } + + func performStepAction() { + guard currentStepIndex != steps.count - 1 else { + finishFlow() + return + } + + currentStepIndex += 1 + view?.changeContent() + } + + func backPressed() { + guard currentStepIndex > 0 else { + view?.close(completion: nil) + return + } + + currentStepIndex -= 1 + view?.changeContent() + } + + func selectCountry(viewModel: SelectCountryViewModelProtocol) { + view?.showSelectCountry(with: viewModel) + } + + private func finishFlow() { + guard isReadyToSend else { + view?.showAlert( + NSLocalizedString("Empty answers!", comment: ""), + message: NSLocalizedString("Could you answer all questions?", comment: "") + ) + return + } + let sendMessage = sendMessage + let message = message() + view?.close { + if let message { + sendMessage?(message) + } + } + } + + private func message() -> String? { + steps + .compactMap { $0.message } + .reduce("") { partialResult, message in + partialResult + "\n" + message + } + + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/ViewModel/WhatProblemStepViewModel.swift b/LockdowniOS/Scenes/Questionnaire/ViewModel/WhatProblemStepViewModel.swift new file mode 100644 index 0000000..7f69671 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/ViewModel/WhatProblemStepViewModel.swift @@ -0,0 +1,140 @@ +// +// WhatProblemStepViewModel.swift +// Lockdown +// +// Created by Pavel Vilbik on 23.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class WhatProblemStepViewModel: BaseStepViewModel, StepViewModelProtocol { + private let isUserPremium: Bool + let step: Steps = .whatsProblem + + private let problemList = [ + NSLocalizedString("Internet connection is blocked", comment: ""), + NSLocalizedString("VPN not connecting", comment: ""), + NSLocalizedString("Blocking not working", comment: ""), + NSLocalizedString("Battery Drain", comment: ""), + NSLocalizedString("Other", comment: "") + ] + + private var selectedProblemIndex = -1 + private var otherInput: String? { + didSet { + didChangeReady?(isFilled) + } + } + + private var selectedProblem: String? { + if (0..= 0 else { return nil } + var result = "" + result.append(problemList[selectedProblemIndex]) + if isSelectedOther(), + let otherInput { + result.append("\n") + result.append(otherInput) + } + result.append("\n") + return result + } + + var isFilled: Bool { + guard selectedProblemIndex >= 0 else { + return false + } + if isSelectedOther() { + return !(otherInput?.isEmpty ?? true) + } + return true + } + + var didChangeReady: ((Bool) -> Void)? + + init(isUserPremium: Bool, didChangeReady: ((Bool) -> Void)?) { + self.isUserPremium = isUserPremium + self.didChangeReady = didChangeReady + } + + override func updateRows() { + staticTableView?.clear() + + staticTableView?.addRowCell { cell in + let titleView = ImageBannerWithTitleView() + titleView.imageView.image = isUserPremium ? UIImage(named: "feedback") : UIImage(named: "feedback-promo") + titleView.titleLabel.text = isUserPremium ? NSLocalizedString("How can we assist you?", comment: "") : NSLocalizedString("Get a promo Discount", comment: "") + titleView.subtitleLabel.text = isUserPremium ? + NSLocalizedString("Your feedback is valuable to us. By selecting the issue you're facing, we can guide you through troubleshooting or escalate the problem to our support team.", comment: "") : + NSLocalizedString("Let us know your opinion, and as a thank you for your feedback, we’ll have a special offer waiting for you at the end!", comment: "") + titleView.subtitleLabel.textAlignment = isUserPremium ? .left : .center + self.setupClear(cell) + cell.addSubview(titleView) + titleView.anchors.edges.pin(insets: .init(top: 0, left: 0, bottom: 30, right: 0)) + } + staticTableView?.addRowCell { cell in + let titleView = SectionTitleView() + titleView.titleLabel.text = NSLocalizedString("Select your problem", comment: "") + self.setupClear(cell) + cell.addSubview(titleView) + titleView.anchors.edges.pin(insets: .init(top: 0, left: 0, bottom: 5, right: 0)) + + } + + for index in 0.. Bool { + selectedProblemIndex == problemList.count - 1 + } + + private func updateForSelect(problemIndex: Int, isSelected: Bool) { + if isSelected { + selectedProblemIndex = problemIndex + } else { + selectedProblemIndex = -1 + } + if !isSelectedOther() { + otherInput = nil + } + updateRows() + didChangeReady?(isFilled) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/CountryView.swift b/LockdowniOS/Scenes/Questionnaire/Views/CountryView.swift new file mode 100644 index 0000000..4777c44 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/CountryView.swift @@ -0,0 +1,82 @@ +// +// CountryView.swift +// Lockdown +// +// Created by Pavel Vilbik on 27.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class CountryView: UIView { + + var didSelect: (() -> Void)? + + var emojiLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLockdownFont(size: 36) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + var titleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLockdownFont(size: 16) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + var checkMark: UIImageView = { + let configuration = UIImage.SymbolConfiguration(pointSize: 16, weight: .medium) + let imageView = UIImageView( + image: .init( + systemName: "checkmark", + withConfiguration: configuration + ) + ) + imageView.tintColor = .tunnelsBlue + imageView.contentMode = .center + return imageView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + backgroundColor = .tableCellBackground + layer.cornerRadius = 8 + layer.borderWidth = 0 + + addSubview(emojiLabel) + emojiLabel.anchors.leading.pin(inset: 20) + emojiLabel.anchors.centerY.equal(anchors.centerY) + emojiLabel.anchors.size.equal(.init(width: 36, height: 24)) + + addSubview(checkMark) + checkMark.anchors.trailing.pin(inset: 22) + checkMark.anchors.centerY.equal(anchors.centerY) + checkMark.anchors.size.equal(.init(width: 15, height: 10)) + + addSubview(titleLabel) + titleLabel.anchors.top.pin(inset: 18) + titleLabel.anchors.bottom.pin(inset: 18) + titleLabel.anchors.leading.spacing(28, to: emojiLabel.anchors.trailing) + checkMark.anchors.leading.greaterThanOrEqual(titleLabel.anchors.trailing, constant: 8) + + addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(tapped)) + ) + } + + @objc private func tapped() { + didSelect?() + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/ImageBannerWithTitleView.swift b/LockdowniOS/Scenes/Questionnaire/Views/ImageBannerWithTitleView.swift new file mode 100644 index 0000000..f45e4aa --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/ImageBannerWithTitleView.swift @@ -0,0 +1,71 @@ +// +// ImageBannerWithTitleView.swift +// Lockdown +// +// Created by Fabian Mistoiu on 14.10.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import UIKit + +class ImageBannerWithTitleView: UIView { + var imageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .boldLockdownFont(size: 32) + label.textAlignment = .center + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + var subtitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .mediumLockdownFont(size: 14) + label.textAlignment = .center + label.numberOfLines = 0 + label.textColor = .secondaryLabel + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + backgroundColor = .clear + + addSubview(imageView) + addSubview(titleLabel) + addSubview(subtitleLabel) + + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 207), + imageView.heightAnchor.constraint(equalToConstant: 178), + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + imageView.topAnchor.constraint(equalTo: topAnchor, constant: -25), + + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: -4), + + subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/NavigationLinkView.swift b/LockdowniOS/Scenes/Questionnaire/Views/NavigationLinkView.swift new file mode 100644 index 0000000..2150ccb --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/NavigationLinkView.swift @@ -0,0 +1,92 @@ +// +// NavigationLinkView.swift +// Lockdown +// +// Created by Pavel Vilbik on 26.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class NavigationLinkView: UIView { + + var emojiLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLockdownFont(size: 36) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + var titleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLockdownFont(size: 12) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + var placeholderLabel: UILabel = { + let label = UILabel() + label.font = .regularLockdownFont(size: 12) + label.numberOfLines = 0 + label.textColor = .secondaryLabel + return label + }() + + private lazy var chevron: UIImageView = { + let configuration = UIImage.SymbolConfiguration(pointSize: 15, weight: .medium) + let view = UIImageView(image: .init(systemName: "chevron.right", withConfiguration: configuration)) + view.contentMode = .center + view.tintColor = .secondaryLabel + return view + }() + + var didSelect: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + backgroundColor = .tableCellBackground + layer.cornerRadius = 8 + layer.borderWidth = 1 + layer.borderColor = UIColor.secondaryLabel.cgColor + + addSubview(emojiLabel) + emojiLabel.anchors.leading.pin(inset: 20) + emojiLabel.anchors.centerY.equal(anchors.centerY) + emojiLabel.anchors.size.equal(.init(width: 36, height: 24)) + + addSubview(chevron) + chevron.anchors.trailing.pin(inset: 18) + chevron.anchors.centerY.equal(anchors.centerY) + chevron.anchors.size.equal(.init(width: 16, height: 16)) + + addSubview(titleLabel) + titleLabel.anchors.top.pin(inset: 12) + titleLabel.anchors.bottom.pin(inset: 12) + titleLabel.anchors.leading.spacing(28, to: emojiLabel.anchors.trailing) + chevron.anchors.leading.greaterThanOrEqual(titleLabel.anchors.trailing, constant: 8) + + addSubview(placeholderLabel) + placeholderLabel.anchors.top.greaterThanOrEqual(anchors.top, constant: 12) + placeholderLabel.anchors.leading.pin(inset: 18) + anchors.bottom.greaterThanOrEqual(placeholderLabel.anchors.bottom, constant: 12) + chevron.anchors.leading.greaterThanOrEqual(placeholderLabel.anchors.trailing, constant: 18) + + addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(tapped)) + ) + } + + @objc private func tapped() { + didSelect?() + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/QuestionTitleView.swift b/LockdowniOS/Scenes/Questionnaire/Views/QuestionTitleView.swift new file mode 100644 index 0000000..7e4b472 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/QuestionTitleView.swift @@ -0,0 +1,40 @@ +// +// QuestionTitleView.swift +// Lockdown +// +// Created by Pavel Vilbik on 26.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class QuestionTitleView: UIView { + + var titleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLockdownFont(size: 14) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + backgroundColor = .clear + + addSubview(titleLabel) + titleLabel.anchors.top.pin() + titleLabel.anchors.trailing.pin() + titleLabel.anchors.leading.pin() + titleLabel.anchors.bottom.pin() + } + +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/RadioSwitcher.swift b/LockdowniOS/Scenes/Questionnaire/Views/RadioSwitcher.swift new file mode 100644 index 0000000..308eeb7 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/RadioSwitcher.swift @@ -0,0 +1,60 @@ +// +// RadioSwitcher.swift +// Lockdown +// +// Created by Pavel Vilbik on 22.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class RadioSwitcher: UIView { + + var isSelected = false { + didSet { + updateImageView() + } + } + var didSelect: ((Bool) -> Void)? + + var selectedImage = UIImage(named: "selectedRadioSwitcher") + var unselectedImage = UIImage(named: "unselectedRadioSwitcher") + + private lazy var imageView: UIImageView = { + let view = UIImageView(image: unselectedImage) + view.contentMode = .scaleAspectFit + return view + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func toggle() { + tapped() + } + + private func configure() { + backgroundColor = .clear + + addSubview(imageView) + imageView.anchors.edges.pin() + addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(tapped)) + ) + } + + private func updateImageView() { + imageView.image = isSelected ? selectedImage : unselectedImage + } + + @objc private func tapped() { + isSelected.toggle() + didSelect?(isSelected) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/SectionTitleView.swift b/LockdowniOS/Scenes/Questionnaire/Views/SectionTitleView.swift new file mode 100644 index 0000000..473beb1 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/SectionTitleView.swift @@ -0,0 +1,43 @@ +// +// SectionTitleView.swift +// Lockdown +// +// Created by Fabian Mistoiu on 14.10.2024. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import UIKit + +class SectionTitleView: UIView { + + var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .semiboldLockdownFont(size: 14) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + backgroundColor = .clear + + addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8), + titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/SelectableRadioSwitcherWithTitle.swift b/LockdowniOS/Scenes/Questionnaire/Views/SelectableRadioSwitcherWithTitle.swift new file mode 100644 index 0000000..65d15cf --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/SelectableRadioSwitcherWithTitle.swift @@ -0,0 +1,72 @@ +// +// SelectableRadioSwitcherWithTitle.swift +// Lockdown +// +// Created by Pavel Vilbik on 22.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class SelectableRadioSwitcherWithTitle: UIView { + + var isSelected = false { + didSet { + updateView() + } + } + var didSelect: ((Bool) -> Void)? + + var unselectedBackgroundColor = UIColor.tableCellBackground + var selectedBackgroundColor = UIColor.tableCellSelectedBackground + var selectedBorderColor = UIColor.tunnelsBlue + + private lazy var switcher: RadioSwitcher = { + let view = RadioSwitcher() + view.didSelect = { [weak self] _ in self?.tapped() } + return view + }() + + var titleLabel: UILabel = { + let label = UILabel() + label.font = .regularLockdownFont(size: 14) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + addSubview(switcher) + switcher.anchors.leading.pin(inset: 26) + switcher.anchors.size.equal(.init(width: 13, height: 13)) + + addSubview(titleLabel) + switcher.anchors.centerY.equal(titleLabel.anchors.centerY) + titleLabel.anchors.leading.spacing(9, to: switcher.anchors.trailing) + titleLabel.anchors.top.pin(inset: 8) + titleLabel.anchors.bottom.pin(inset: 8) + titleLabel.anchors.trailing.pin(inset: 18) + + addGestureRecognizer( + UITapGestureRecognizer(target: self, action: #selector(tapped)) + ) + } + + private func updateView() { + switcher.isSelected = isSelected + } + + @objc private func tapped() { + didSelect?(!isSelected) + } + +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/StepsView.swift b/LockdowniOS/Scenes/Questionnaire/Views/StepsView.swift new file mode 100644 index 0000000..984a16b --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/StepsView.swift @@ -0,0 +1,80 @@ +// +// StepsView.swift +// Lockdown +// +// Created by Pavel Vilbik on 21.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class StepsView: UIView { + + var steps = 0 { + didSet { + if oldValue != steps { + resetupStepsView() + } + } + } + var currentStep = 0 { + didSet { + updateCurrentStep() + } + } + + var filledColor = UIColor.tunnelsBlue + var unfilledColor = UIColor.tableCellBackground + + private var views = [UIView]() + + private var stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.spacing = 6 + stackView.distribution = .fillEqually + return stackView + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + backgroundColor = .clear + + addSubview(stackView) + stackView.anchors.top.pin() + stackView.anchors.trailing.pin() + stackView.anchors.bottom.pin() + stackView.anchors.leading.pin() + + resetupStepsView() + } + + private func resetupStepsView() { + views.forEach { stackView.removeArrangedSubview($0) } + views = (0.. UIView { + let view = UIView() + view.backgroundColor = unfilledColor + view.anchors.height.equal(4) + view.layer.cornerRadius = 2 + return view + } + + private func updateCurrentStep() { + for index in 0.. Void)? + + private(set) lazy var textView: UITextView = { + let textView = UITextView() + textView.delegate = self + textView.font = .regularLockdownFont(size: 12) + textView.backgroundColor = .clear + textView.textColor = .label + textView.isScrollEnabled = false + return textView + }() + + private(set) lazy var placeholderLabel: UILabel = { + let label = UILabel() + label.font = .regularLockdownFont(size: 12) + label.textColor = .secondaryLabel + label.numberOfLines = 0 + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + let backgroundView = UIView() + backgroundView.backgroundColor = .tableCellBackground + backgroundView.layer.cornerRadius = 8 + backgroundView.layer.borderWidth = 1 + backgroundView.layer.borderColor = UIColor.secondaryLabel.cgColor + addSubview(backgroundView) + backgroundView.anchors.edges.pin(insets: .init(top: 10, left: 23, bottom: 0, right: 23)) + + addSubview(textView) + textView.anchors.edges.pin(insets: .init(top: 15, left: 32, bottom: 0, right: 23)) + textView.anchors.height.greaterThanOrEqual(95) + + addSubview(placeholderLabel) + placeholderLabel.anchors.leading.pin(inset: 36) + placeholderLabel.anchors.top.pin(inset: 18 + 5) + placeholderLabel.anchors.trailing.pin(inset: 36) + } +} + +extension TextViewWithPlaceholder: UITextViewDelegate { + func textViewDidChange(_ textView: UITextView) { + placeholderLabel.isHidden = !textView.text.isEmpty + textDidChanged?(textView.text) + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/TitleAndSubtitleView.swift b/LockdowniOS/Scenes/Questionnaire/Views/TitleAndSubtitleView.swift new file mode 100644 index 0000000..d90a658 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/TitleAndSubtitleView.swift @@ -0,0 +1,54 @@ +// +// TitleAndSubtitleView.swift +// Lockdown +// +// Created by Pavel Vilbik on 22.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class TitleAndSubtitleView: UIView { + + var titleLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.font = .boldLockdownFont(size: 20) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + var subtitleLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.font = .mediumLockdownFont(size: 14) + label.numberOfLines = 0 + label.textColor = .secondaryLabel + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + backgroundColor = .clear + + addSubview(titleLabel) + titleLabel.anchors.leading.pin() + titleLabel.anchors.top.pin() + titleLabel.anchors.trailing.pin() + + addSubview(subtitleLabel) + subtitleLabel.anchors.leading.pin() + subtitleLabel.anchors.top.spacing(8, to: titleLabel.anchors.bottom) + subtitleLabel.anchors.trailing.pin() + subtitleLabel.anchors.bottom.pin() + } +} diff --git a/LockdowniOS/Scenes/Questionnaire/Views/YesNoRadioSwitcherView.swift b/LockdowniOS/Scenes/Questionnaire/Views/YesNoRadioSwitcherView.swift new file mode 100644 index 0000000..30c5a90 --- /dev/null +++ b/LockdowniOS/Scenes/Questionnaire/Views/YesNoRadioSwitcherView.swift @@ -0,0 +1,126 @@ +// +// YesNoRadioSwitcherView.swift +// Lockdown +// +// Created by Pavel Vilbik on 26.06.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class YesNoRadioSwitcherView: UIView { + + var isSelected: Bool? { + didSet { + updateView() + didSelect?(isSelected) + } + } + var didSelect: ((Bool?) -> Void)? + + var titleLabel: UILabel = { + let label = UILabel() + label.font = .semiboldLockdownFont(size: 14) + label.numberOfLines = 0 + label.textColor = .label + return label + }() + + private lazy var yesSwitcher: RadioSwitcher = { + let view = RadioSwitcher() + view.didSelect = { [weak self] in self?.isSelected = $0 ? true : nil } + return view + }() + + private lazy var yesLabel: UILabel = { + let label = UILabel() + label.font = .regularLockdownFont(size: 14) + label.textColor = .label + label.text = NSLocalizedString("Yes", comment: "") + label.isUserInteractionEnabled = true + return label + }() + + private lazy var noSwitcher: RadioSwitcher = { + let view = RadioSwitcher() + view.didSelect = { [weak self] in self?.isSelected = $0 ? false : nil } + return view + }() + + private lazy var noLabel: UILabel = { + let label = UILabel() + label.font = .regularLockdownFont(size: 14) + label.textColor = .label + label.text = NSLocalizedString("No", comment: "") + label.isUserInteractionEnabled = true + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func configure() { + backgroundColor = .clear + + addSubview(titleLabel) + titleLabel.anchors.top.pin() + titleLabel.anchors.trailing.pin() + titleLabel.anchors.leading.pin() + + let stackView = UIStackView() + stackView.axis = .horizontal + stackView.distribution = .fillEqually + stackView.alignment = .fill + addSubview(stackView) + stackView.anchors.top.spacing(12, to: titleLabel.anchors.bottom) + stackView.anchors.leading.pin(inset: 23) + stackView.anchors.trailing.pin() + stackView.anchors.bottom.pin() + stackView.anchors.height.equal(23) + + let yesView = view(for: yesSwitcher, andLabel: yesLabel) + yesView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tappedYes))) + stackView.addArrangedSubview(yesView) + + let noView = view(for: noSwitcher, andLabel: noLabel) + noView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tappedNo))) + stackView.addArrangedSubview(noView) + } + + private func view(for switcher: RadioSwitcher, andLabel label: UILabel) -> UIView { + let view = UIView() + view.backgroundColor = .clear + + view.addSubview(switcher) + switcher.anchors.leading.pin() + switcher.anchors.centerY.equal(view.anchors.centerY) + switcher.anchors.size.equal(.init(width: 13, height: 13)) + + view.addSubview(label) + label.anchors.top.pin() + label.anchors.leading.spacing(10, to: switcher.anchors.trailing) + label.anchors.trailing.pin() + label.anchors.bottom.pin() + + return view + } + + private func updateView() { + yesSwitcher.isSelected = isSelected ?? false + noSwitcher.isSelected = !(isSelected ?? true) + } + + @objc private func tappedYes() { + yesSwitcher.toggle() + } + + @objc private func tappedNo() { + noSwitcher.toggle() + } +} diff --git a/LockdowniOS/SetRegionCell.swift b/LockdowniOS/SetRegionCell.swift index 151e14a..c079888 100644 --- a/LockdowniOS/SetRegionCell.swift +++ b/LockdowniOS/SetRegionCell.swift @@ -12,5 +12,4 @@ class SetRegionCell: UITableViewCell { @IBOutlet weak var regionFlag: UILabel! @IBOutlet weak var regionName: UILabel! @IBOutlet weak var regionSelected: UIButton! - } diff --git a/LockdowniOS/SetRegionViewController.swift b/LockdowniOS/SetRegionViewController.swift index e8af1b1..e486ffb 100644 --- a/LockdowniOS/SetRegionViewController.swift +++ b/LockdowniOS/SetRegionViewController.swift @@ -7,6 +7,7 @@ import UIKit import CocoaLumberjackSwift +import WidgetKit class SetRegionViewController: BaseViewController, UITableViewDataSource, UITableViewDelegate { @@ -14,6 +15,10 @@ class SetRegionViewController: BaseViewController, UITableViewDataSource, UITabl var homeVC: HomeViewController? + var vpnVC: LDVpnViewController? + + var firewall: LDFirewallViewController? + override func viewDidLoad() { super.viewDidLoad() self.tableView.reloadData() @@ -50,6 +55,10 @@ class SetRegionViewController: BaseViewController, UITableViewDataSource, UITabl let tappedVpnRegion = vpnRegions[indexPath.row] setSavedVPNRegion(vpnRegion: tappedVpnRegion) + if #available(iOSApplicationExtension 14.0, iOS 14.0, *) { + WidgetCenter.shared.reloadAllTimelines() + } + if homeVC != nil { homeVC!.updateVPNRegionLabel() } @@ -76,5 +85,4 @@ class SetRegionViewController: BaseViewController, UITableViewDataSource, UITabl return cell } - } 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 be79c4a..c083b90 100644 --- a/LockdowniOS/SignupViewController.swift +++ b/LockdowniOS/SignupViewController.swift @@ -1,196 +1,21 @@ // -// SignupViewController.swift -// Tunnels +// SignUpViewController.swift +// Lockdown // -// Copyright © 2019 Confirmed Inc. All rights reserved. +// Created by Aliaksandr Dvoineu on 17.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. // -import UIKit -import SwiftyStoreKit -import NetworkExtension -import PromiseKit import CocoaLumberjackSwift +import PopupDialog +import PromiseKit +import UIKit -class SignupViewController: BaseViewController { - - //MARK: - VARIABLES - - @IBOutlet var monthlyPlanCheckbox: M13Checkbox! - @IBOutlet var monthlyTitle: UILabel! - @IBOutlet var monthlyDescription: UILabel! - - @IBOutlet var annualPlanCheckbox: M13Checkbox! - @IBOutlet var annualTitle: UILabel! - @IBOutlet var annualDescription: UILabel! +final class SignUpViewController: BaseViewController, Loadable { - @IBOutlet var startTrialButton: TKTransitionSubmitButton! - @IBOutlet var pricingSubtitle: UILabel! +} - @IBOutlet var restorePurchasesButton: TKTransitionSubmitButton! - - override func viewDidLoad() { - super.viewDidLoad() - selectMonthly() - } - - @IBAction func detailsTapped(_ sender: Any) { - showVPNDetails() - } - - @objc func selectMonthly() { - annualPlanCheckbox.setCheckState(.unchecked, animated: true) - monthlyPlanCheckbox.setCheckState(.checked, animated: true) - VPNSubscription.selectedProductId = VPNSubscription.productIdMonthly - updatePricingSubtitle() - } - - @IBAction func monthlyTapped(_ sender: Any) { - selectMonthly() - } - - @objc func selectAnnual() { - monthlyPlanCheckbox.setCheckState(.unchecked, animated: true) - annualPlanCheckbox.setCheckState(.checked, animated: true) - VPNSubscription.selectedProductId = VPNSubscription.productIdAnnual - updatePricingSubtitle() - } - - @IBAction func annualTapped(_ sender: Any) { - selectAnnual() - } - - @objc func updatePricingSubtitle() { - if monthlyPlanCheckbox.checkState == .checked { - pricingSubtitle.text = String(format: NSLocalizedString("%@ per month after", comment: ""), VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdMonthly)) - } - else if annualPlanCheckbox.checkState == .checked { - pricingSubtitle.text = String(format: NSLocalizedString("%@ per year after", comment: ""), VPNSubscription.getProductIdPrice(productId: VPNSubscription.productIdAnnual)) - } - } - - @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: { - // TODO: show onboarding, and THEN activate VPN - VPNController.shared.setEnabled(true) - }) - }, - errored: { error in - self.toggleStartTrialButton(true) - DDLogError("Start Trial Failed: \(error)") - - if (self.popupErrorAsNSURLError(error)) { - return - } - else if (self.popupErrorAsApiError(error)) { - return - } - else { - self.showPopupDialog(title: "Error Starting Trial", - message: "Please contact team@lockdownhq.com.\n\nError details:\n\(error)", - acceptButton: "Okay") - } - }) - } - - 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 { (signin: SignIn) -> Promise in - try Client.getKey() - } - .done { (getKey: GetKey) in - try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) - self.dismiss(animated: true, completion: { - 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: - self.showPopupDialog(title: "No Active Subscription", - message: "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@lockdownhq.com", - acceptButton: "OK") - default: - self.showPopupDialog(title: "Error Restoring Subscription", - message: "Please email team@lockdownhq.com with the following error Code \(apiError.code): \(apiError.message)", - acceptButton: "OK") - } - } - else { - self.showPopupDialog(title: "Error Restoring Subscription", - message: "Please make sure your Internet connection is active. If this error persists, email team@lockdownhq.com with the following error message: \(error)", - acceptButton: "OK") - } - } - } - - @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) - } - +enum AuthenticationViewControllerMode { + case login, signUp } + diff --git a/LockdowniOS/SpecialOfferPaywallModel.swift b/LockdowniOS/SpecialOfferPaywallModel.swift new file mode 100644 index 0000000..76e9d17 --- /dev/null +++ b/LockdowniOS/SpecialOfferPaywallModel.swift @@ -0,0 +1,44 @@ +// +// SpecialOfferPaywallModel.swift +// LockdowniOS +// +// Created by George Apostu on 26/11/24. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import Foundation +import SwiftyStoreKit + +class SpecialOfferPaywallModel: ObservableObject { + + let products: SpecialOfferProducts + + var closeAction: (()->Void)? = nil + var continueAction: ((String)->Void)? = nil + + @Published var yearlyPrice: String + @Published var offerPrice: String + @Published var showProgress = false + @Published var isSmallScreen: Bool = UIScreen.main.bounds.width <= 375 || UIScreen.main.bounds.height <= 667 + + init(products: SpecialOfferProducts, infos: [InternalSubscription], closeAction: (()->Void)? = nil) { + self.products = products + + let offerPrice = infos.first(where: { $0.productId == products.yearly}).flatMap { $0.offer } ?? 29.99 + let yearlyPrice = offerPrice.dividing(by: 0.2999).subtracting(0.01) + + let currencyFormatter = NumberFormatter() + currencyFormatter.usesGroupingSeparator = true + currencyFormatter.numberStyle = .currency + currencyFormatter.locale = infos.first?.priceLocale + + self.yearlyPrice = currencyFormatter.string(from: yearlyPrice) ?? "__" + self.offerPrice = currencyFormatter.string(from: offerPrice) ?? "__" + self.closeAction = closeAction + } + + func purchase() { + showProgress = true + continueAction?(products.yearly) + } +} diff --git a/LockdowniOS/SpecialOfferPaywallView.swift b/LockdowniOS/SpecialOfferPaywallView.swift new file mode 100644 index 0000000..d7ab96f --- /dev/null +++ b/LockdowniOS/SpecialOfferPaywallView.swift @@ -0,0 +1,182 @@ +// +// SpecialOfferPaywallView.swift +// LockdowniOS +// +// Created by George Apostu on 26/11/24. +// Copyright © 2024 Confirmed Inc. All rights reserved. +// + +import SwiftUI + +struct SpecialOfferPaywallView: View { + @StateObject var model: SpecialOfferPaywallModel + let screenWidth = UIScreen.main.bounds.width + let screenHeight = UIScreen.main.bounds.height + var imgName = UIScreen.main.bounds.height > 700 ? "bg_paywall_onetime" : "bg_paywall_onetime_ss" + let txtColor = Color.white + + var body: some View { + ZStack(alignment: .center) { + Image(imgName) + .resizable() + .scaledToFill() + .edgesIgnoringSafeArea(.all) + LinearGradient(stops: + [Gradient.Stop(color: Color.black.opacity(0.0), location: 0.0), + Gradient.Stop(color: Color.black.opacity(0.0), location: 0.2), + Gradient.Stop(color: Color.black.opacity(0.6), location: 0.5), + Gradient.Stop(color: Color.black.opacity(0.6), location: 1.0), + ], startPoint: .top, endPoint: .bottom) + VStack(spacing: 4) { + VStack(spacing: 0) { + ZStack { + Image("special_offer_stars") + .resizable() + .frame(height: screenHeight * 0.3) + .edgesIgnoringSafeArea(.top) + Image("special_offer_2025") + .resizable() + .scaledToFit() + .padding(.horizontal, screenWidth * 0.12) + .padding(.vertical, screenWidth * 0.07) + } + Image("banner_70_percent") + .resizable() + .scaledToFit() + .frame(maxHeight:120) + .padding(.top, -20) + } + + if !model.isSmallScreen { + Spacer() + .frame(maxHeight: 30) + .layoutPriority(-1) + } + + Text("Paywall.Onetime.DecemberSale") + .multilineTextAlignment(.center) + .font(.custom("Juana-SemiBold", size: 36)) + .foregroundColor(txtColor) + .minimumScaleFactor(0.5) + + VStack(alignment: .center, spacing: 4) { + Text("Paywall.Onetime.perYear \(model.yearlyPrice)") + .strikethrough() + .foregroundColor(txtColor) + .font(.custom("KumbhSans-Regular", size: 18)) + + Text("Paywall.Onetime.perYear \(model.offerPrice)") + .foregroundColor(Color(hex: 0xFF004B)) + .font(.custom("KumbhSans-Bold", size: 28)) + .fontWeight(.bold) + } + + if !model.isSmallScreen { + Spacer() + .frame(maxHeight: 30) + .layoutPriority(-1) + } + + VStack(alignment: .leading) { + Group { + Text("Tap") + .foregroundColor(.black) + + Text("Paywall.Onetime.Continue") + .foregroundColor(Color("Confirmed Blue")) + + Text("Paywall.Onetime.ToActivate") + .foregroundColor(txtColor) + } + .font(.custom("SF Pro Rounded Semibold", size: 28)) + .lineLimit(2) + .frame(maxWidth: .infinity, alignment: .leading) + + Text("Paywall.Onetime.PrivateBrowse") + .foregroundColor(txtColor) + .font(.custom("Montserrat-Regular", size: 14)) + + + VStack(alignment: .leading, spacing: 2) { + HStack { + Image(systemName: "checkmark") + .foregroundColor(Color("Confirmed Blue")) + Text("Paywall.Onetime.List1") + .foregroundColor(txtColor) + } + HStack { + Image(systemName: "checkmark") + .foregroundColor(Color("Confirmed Blue")) + Text("Paywall.Onetime.List2") + .foregroundColor(txtColor) + } + HStack { + Image(systemName: "checkmark") + .foregroundColor(Color("Confirmed Blue")) + Text("Paywall.Onetime.List3") + .foregroundColor(txtColor) + } + } + .font(.custom("Montserrat-Semibold", size: 12)) + .padding(.top, 5) + .padding(.leading, 10) + } + .padding(.horizontal, 10) + + Spacer() + + Button(action: { + model.purchase() + }, label: { + Text("Paywall.Onetime.Continue") + .font(.custom("Montserrat-SemiBold", size: 20)) + .foregroundColor(.white) + .padding() + .frame(maxWidth:.infinity) + .background( + RoundedRectangle(cornerRadius: 55) + .fill(Color("Confirmed Blue")) + ) + }) + .frame(height: 58) + .padding(.horizontal, 20) + + Spacer() + } + .edgesIgnoringSafeArea(.top) + .frame(maxHeight: UIScreen.main.bounds.size.height) + .padding(.horizontal, 20) + + VStack(alignment: .leading) { + HStack { + Button { + model.closeAction?() + } label: { + Image(systemName: "xmark") + .font(.system(size: 14, weight: .bold)) + .foregroundColor(txtColor) + } + .padding() + .contentShape(Rectangle()) + Spacer() + } + Spacer() + } + .padding(.top, screenHeight * 0.02) + + ProgressView() + .offset(y: 60) + .scaleEffect(3) + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .opacity(model.showProgress ? 1 : 0) + } + .allowsHitTesting(model.showProgress ? false : true) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) + } +} + +#Preview { + var model = SpecialOfferPaywallModel(products: VPNSubscription.specialOfferProducts, infos: [.mockYearlyBF], + closeAction: { + print("") + }) + SpecialOfferPaywallView(model: model) +} diff --git a/LockdowniOS/SplashScreenViewController.swift b/LockdowniOS/SplashScreenViewController.swift new file mode 100644 index 0000000..291bd6c --- /dev/null +++ b/LockdowniOS/SplashScreenViewController.swift @@ -0,0 +1,209 @@ +// +// SplashScreenViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 12.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import AppTrackingTransparency +import UIKit +import SwiftUI +import PromiseKit +import CocoaLumberjackSwift + +final class SplashScreenViewController: BaseViewController { + + override func viewDidLoad() { + super.viewDidLoad() + if let cached = BaseUserService.shared.user.cachedSubscription() { + BaseUserService.shared.user.updateSubscription(to: cached) + finishFlow(with: cached) + + BaseUserService.shared.updateUserSubscription { subscription in + guard let subscription, + !cached.isSameType(subscription) else { + return + } + NotificationCenter.default.post(name: AccountUI.subscritionTypeChanged, object: nil) + } + } else { + BaseUserService.shared.updateUserSubscription { [weak self] subscription in + self?.finishFlow(with: subscription) + writeCommonInfoToLog() + } + } + } + + private func finishFlow(with subscription: Subscription?) { + updateUserDafauls(with: subscription) + + DispatchQueue.main.async { + self.dismiss() + } + } + + private func updateUserDafauls(with subscription: Subscription?) { + UserDefaults.hasSeenAdvancedPaywall = subscription?.planType.isAdvanced ?? false + UserDefaults.hasSeenUniversalPaywall = subscription?.planType.isUniversal ?? false + UserDefaults.hasSeenAnonymousPaywall = subscription?.planType.isAnonymous ?? false + } + + private func dismiss() { + dismiss(animated: false) { [weak self] in + if UserDefaults.onboardingCompleted { + self?.showMainTabView() + } else { + let isPremiumUser = BaseUserService.shared.user.currentSubscription != nil + if isPremiumUser { + self?.showMainTabView() + } else { + self?.showOnboardingFlow() + } + } + } + } + + private func showOnboardingFlow() { + Task { @MainActor in + if let productInfos = await VPNSubscription.shared.loadSubscriptions(type: .onboarding) { + let paywallModel = OneTimePaywallModel(products: VPNSubscription.onboardingProducts, infos: productInfos) + paywallModel.closeAction = { [weak self] in + UserDefaults.onboardingCompleted = true + self?.showMainTabView() + } + paywallModel.continueAction = { [weak self] pid in + VPNSubscription.selectedProductId = pid + VPNSubscription.purchase { + UserDefaults.onboardingCompleted = true + self?.handlePurchaseSuccessful(placement: .onboarding) + } errored: { err in + paywallModel.showProgress = false + self?.handlePurchaseFailed(error: err) + } + } + paywallModel.restoreAction = { [weak self] in + self?.restorePurchase(completion: { + UserDefaults.onboardingCompleted = true + paywallModel.showProgress = false + self?.handlePurchaseSuccessful() + }) + } + let onboardingController = UIHostingController(rootView: OnboardingView(paywallModel: paywallModel)) + onboardingController.modalPresentationStyle = .fullScreen + onboardingController.modalTransitionStyle = .crossDissolve + present(onboardingController, animated: true) + } else { + showMainTabView() + } + } + } + + private func showMainTabView() { + let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) + 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() + }) + } + + private func restorePurchase(completion: @escaping () -> Void) { + //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 + completion() + +// 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) + completion() +// DDLogInfo("restore: setting VPN creds with ID and Dismissing: \(getKey.id)") +// let presentingViewController = self.presentingViewController as? LDFirewallViewController +// self.dismiss(animated: true, completion: { +// if presentingViewController != nil { +// presentingViewController?.toggleFirewall() +// } +// 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: "")) + } + } + } +} diff --git a/LockdowniOS/SplashScreenViewController.xib b/LockdowniOS/SplashScreenViewController.xib new file mode 100644 index 0000000..183d2f3 --- /dev/null +++ b/LockdowniOS/SplashScreenViewController.xib @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/StaticTableView.swift b/LockdowniOS/StaticTableView.swift new file mode 100644 index 0000000..758da6b --- /dev/null +++ b/LockdowniOS/StaticTableView.swift @@ -0,0 +1,207 @@ +// +// StaticTableView.swift +// Private Analytics +// +// Created by Oleg Dreyman on 21.08.2020. +// Copyright © 2020 Confirmed, Inc. All rights reserved. +// + +import UIKit + +final class StaticTableView: UITableView { + + // Resizing UITableView to fit content + override var contentSize: CGSize { + didSet { + invalidateIntrinsicContentSize() + } + } + + override var intrinsicContentSize: CGSize { + layoutIfNeeded() + return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) + } + + var rows: [SelectableTableViewCell] = [] + var deselectsCellsAutomatically: Bool = false + + override init(frame: CGRect, style: UITableView.Style) { + super.init(frame: frame, style: .insetGrouped) + setup() + } + + func setup() { + dataSource = self + delegate = self +// separatorStyle = .none + } + + enum Insert { + case last + case dontInsert + } + + @discardableResult + func addRow(insert: Insert = .last, _ configure: (UIView) -> ()) -> SelectableTableViewCell { + let cell = SelectableTableViewCell() + cell.selectionStyle = .none + cell.backgroundColor = nil + configure(cell.contentView) + self.insert(cell: cell, insert: insert) + return cell + } + + @discardableResult + func addRowCell(insert: Insert = .last, _ configure: (UITableViewCell) -> ()) -> SelectableTableViewCell { + let cell = SelectableTableViewCell() + cell.selectionStyle = .none + configure(cell) + self.insert(cell: cell, insert: insert) + return cell + } + + @discardableResult + func addCell(insert: Insert = .last, _ cell: SelectableTableViewCell) -> SelectableTableViewCell { + self.insert(cell: cell, insert: insert) + return cell + } + + @discardableResult + func addRow(insert: Insert = .last, view: UIView, insets: UIEdgeInsets = .zero) -> SelectableTableViewCell { + return addRow(insert: insert) { (row) in + row.addSubview(view) + view.anchors.edges.pin(axis: .vertical) + view.anchors.edges.marginsPin(insets: insets, axis: .horizontal) + } + } + + private func insert(cell: SelectableTableViewCell, insert: Insert) { + switch insert { + case .dontInsert: + break + case .last: + rows.append(cell) + } + } + + func clear() { + rows = [] + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class SelectableTableViewCell: UITableViewCell { + var selectionCallback: () -> () = { } + var deletionCallback: (() -> ())? + + enum Action { + case toggleCheckmark + } + + @discardableResult + func onSelect(callback: @escaping () -> ()) -> Self { + selectionStyle = .default + selectionCallback = callback + return self + } + + @discardableResult + func onSwipeToDelete(callback: @escaping () -> ()) -> Self { + deletionCallback = callback + return self + } + + @discardableResult + func onSelect(_ action: Action, callback: @escaping () -> () = { }) -> Self { + selectionCallback = { [unowned self] in + switch action { + case .toggleCheckmark: + if self.accessoryType == .checkmark { + self.accessoryType = .none + } else { + self.accessoryType = .checkmark + } + } + callback() + } + return self + } +} + +extension StaticTableView: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + return 0 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return rows.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return rows[indexPath.row] + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + let cell = rows[indexPath.row] + return cell.deletionCallback != nil + } + + func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + return .delete + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + let cell = rows[indexPath.row] + guard cell.deletionCallback != nil else { + return + } + cell.deletionCallback?() + self.rows.removeAll(where: { $0 === cell }) + self.deleteRows(at: [indexPath], with: .fade) + } +} + +extension StaticTableView: UITableViewDelegate { + func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { + if let cell = tableView.cellForRow(at: indexPath) as? SelectableTableViewCell { + return cell.selectionStyle != .none + } else { + return false + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let cell = tableView.cellForRow(at: indexPath) as? SelectableTableViewCell { + cell.selectionCallback() + if deselectsCellsAutomatically { + tableView.deselectRow(at: indexPath, animated: true) + } + } + } +} + +extension UIViewController { + func addTableView(_ tableView: UITableView, layout: (UIView) -> ()) { + // adding UITableView as UITableViewController will enable + // UIKit's own "scroll to text field when keyboard appears" + let tableViewController = StaticTableViewController() + tableViewController.tableView = tableView + view.addSubview(tableViewController.view) + layout(tableView) + addChild(tableViewController) + tableViewController.didMove(toParent: self) + } +} + +final class StaticTableViewController: UITableViewController { + +} diff --git a/LockdowniOS/String+Attributed.swift b/LockdowniOS/String+Attributed.swift new file mode 100644 index 0000000..4b3bc15 --- /dev/null +++ b/LockdowniOS/String+Attributed.swift @@ -0,0 +1,47 @@ +// +// String+Attributed.swift +// Lockdown +// +// Created by Alexander Parshakov on 12/29/22 +// Copyright © 2022 Confirmed Inc. All rights reserved. +// + +import UIKit + +extension UILabel { + + func highlight(_ strings: String..., + with color: UIColor? = nil, + font: UIFont? = nil, + lineSpacing: CGFloat? = nil, + characterSpacing: UInt? = nil) { + guard let text else { return } + let attributedString = NSMutableAttributedString(string: text) + + for string in strings { + let range = (text as NSString).range(of: string) + if let color { + attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: color, range: range) + } + if let font { + attributedString.addAttribute(NSAttributedString.Key.font, value: font, range: range) + } + } + + if let lineSpacing { + let paragraphStyle = NSMutableParagraphStyle() + + paragraphStyle.lineSpacing = lineSpacing + paragraphStyle.alignment = textAlignment + + attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedString.length)) + } + + attributedText = attributedString + + guard let characterSpacing else { return } + + attributedString.addAttribute(NSAttributedString.Key.kern, value: characterSpacing, range: NSRange(location: 0, length: attributedString.length)) + attributedText = attributedString + } +} diff --git a/LockdowniOS/String+Extensions.swift b/LockdowniOS/String+Extensions.swift new file mode 100644 index 0000000..b73f7d9 --- /dev/null +++ b/LockdowniOS/String+Extensions.swift @@ -0,0 +1,40 @@ +// +// String+Extentions.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 25.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import Foundation + +extension String { + + enum ValidityType { + case listName + case domainName + case listDescription + } + + enum Regex: String { + case listName = "^[a-zA-Z0-9\\s]{1,30}$" + case domainName = "^(?:\\*\\.)?[a-z0-9]+(?:[\\-_.][a-z0-9]+)*\\.[a-z]{2,6}$" + case listDescription = "^[a-zA-Z0-9\\s]{1,500}$" + } + + func isValid(_ validityType: ValidityType) -> Bool { + let format = "SELF MATCHES %@" + var regex = "" + + switch validityType { + case .listName: + regex = Regex.listName.rawValue + case .domainName: + regex = Regex.domainName.rawValue + case .listDescription: + regex = Regex.listDescription.rawValue + } + + return NSPredicate(format: format, regex).evaluate(with: self) + } +} diff --git a/LockdowniOS/String+Localized.swift b/LockdowniOS/String+Localized.swift new file mode 100644 index 0000000..67d824b --- /dev/null +++ b/LockdowniOS/String+Localized.swift @@ -0,0 +1,26 @@ +// +// String+Localized.swift +// LockdowniOS +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 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/String+URL.swift b/LockdowniOS/String+URL.swift new file mode 100644 index 0000000..246a597 --- /dev/null +++ b/LockdowniOS/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/SwitchBlockingView.swift b/LockdowniOS/SwitchBlockingView.swift new file mode 100644 index 0000000..6f87ee0 --- /dev/null +++ b/LockdowniOS/SwitchBlockingView.swift @@ -0,0 +1,60 @@ +// +// SwitchBlockingView.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 28.03.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +final class SwitchBlockingView: UIView { + + private(set) var buttonCallback: () -> () = { } + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = .label + label.textAlignment = .left + label.font = fontMedium14 + return label + }() + + lazy var switchView: UISwitch = { + let view = UISwitch() + view.onTintColor = .tunnelsBlue +// view.isOn = true + return view + }() + + func configure() { + backgroundColor = .systemBackground + + addSubview(titleLabel) + titleLabel.anchors.leading.marginsPin() + titleLabel.anchors.top.marginsPin() + titleLabel.anchors.bottom.marginsPin() + + addSubview(switchView) + switchView.anchors.trailing.marginsPin() + switchView.anchors.centerY.equal(titleLabel.anchors.centerY) + } + + override func layoutSubviews() { + super.layoutSubviews() + layer.cornerRadius = 8 + } + + @objc func buttonDidPress() { + buttonCallback() + } +} diff --git a/LockdowniOS/TextBox.swift b/LockdowniOS/TextBox.swift new file mode 100644 index 0000000..8f91f20 --- /dev/null +++ b/LockdowniOS/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/TextBoxLabel.swift b/LockdowniOS/TextBoxLabel.swift new file mode 100644 index 0000000..85d9566 --- /dev/null +++ b/LockdowniOS/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/TextInputState.swift b/LockdowniOS/TextInputState.swift new file mode 100644 index 0000000..498b245 --- /dev/null +++ b/LockdowniOS/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/TitleViewController.swift b/LockdowniOS/TitleViewController.swift index e2b7eb1..bd4c79b 100644 --- a/LockdowniOS/TitleViewController.swift +++ b/LockdowniOS/TitleViewController.swift @@ -10,6 +10,7 @@ import Foundation import UIKit import RQShineLabel import PopupDialog +import CocoaLumberjackSwift class TitleViewController: BaseViewController { @@ -17,18 +18,34 @@ class TitleViewController: BaseViewController { @IBOutlet weak var descriptionLabel: UILabel! @IBOutlet weak var getStartedButton: UIButton! @IBOutlet weak var whyTrustButton: UIButton! + @IBOutlet weak var overOneBillionLabel: UILabel! + + var isAnimatingOnAppear = true override func viewDidLoad() { super.viewDidLoad() - self.titleLabel.textColor = .clear - self.descriptionLabel.alpha = 0 - self.getStartedButton.alpha = 0 - self.whyTrustButton.alpha = 0 + if isAnimatingOnAppear { + self.titleLabel.textColor = .clear + self.descriptionLabel.alpha = 0 + self.getStartedButton.alpha = 0 + self.whyTrustButton.alpha = 0 + self.overOneBillionLabel.alpha = 0 + } else { + self.titleLabel.textColor = .tunnelsBlue + self.descriptionLabel.alpha = 1 + self.getStartedButton.alpha = 1 + self.whyTrustButton.alpha = 1 + self.overOneBillionLabel.alpha = 1 + } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + guard isAnimatingOnAppear else { + return + } + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: { self.titleLabel.shine(animated: true, completion: { UIView.animate(withDuration: 1.5, animations: { @@ -37,6 +54,7 @@ class TitleViewController: BaseViewController { UIView.animate(withDuration: 1.5, animations: { self.getStartedButton.alpha = 1 self.whyTrustButton.alpha = 1 + self.overOneBillionLabel.alpha = 1 }) }) }) @@ -47,11 +65,17 @@ class TitleViewController: BaseViewController { @IBAction func getStartedTapped(_ sender: Any) { defaults.set(true, forKey: kHasShownTitlePage) - self.performSegue(withIdentifier: "getStartedTapped", sender: self) + + PushNotifications.Authorization.requestWeeklyUpdateAuthorization(presentingDialogOn: self).done { _ in + OneTimeActions.markAsSeen(.notificationAuthorizationRequestPopup) + self.performSegue(withIdentifier: "getStartedTapped", sender: self) + }.catch { error in + DDLogWarn(error.localizedDescription) + self.performSegue(withIdentifier: "getStartedTapped", sender: self) + } } @IBAction func whyTrustTapped(_ sender: Any) { showWhyTrustPopup() } - } diff --git a/LockdowniOS/TrackerInfo.swift b/LockdowniOS/TrackerInfo.swift new file mode 100644 index 0000000..17b12d9 --- /dev/null +++ b/LockdowniOS/TrackerInfo.swift @@ -0,0 +1,132 @@ +// +// TrackerInfo.swift +// Lockdown +// +// Created by Oleg Dreyman on 02.06.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation +import CocoaLumberjackSwift + +struct TrackerInfo: Decodable { + private var trackerIds: [String : String] + private var descriptions: [String : TrackerDescription] + + func description(forDomain domain: String) -> TrackerDescription? { + if let trackerId = trackerIds[domain] { + return descriptions[trackerId] + } else { + if let topDomain = trackerIds.first(where: { key, _ in domain.hasSuffix(key) }) { + return descriptions[topDomain.value] + } + } + return nil + } +} + +struct TrackerDescription: Decodable { + var title: String + var description: String +} + +final class TrackerInfoRegistry { + + static let shared = TrackerInfoRegistry() + + private var loaded: TrackerInfo? + + func info(forTrackerDomain domain: String) -> TrackerDescription { + do { + if let value = try getTrackerInfoDoc().description(forDomain: domain) { + return value + } else { + return inferInfo(forTrackerDomain: domain) + } + } catch { + DDLogError(error.localizedDescription) + return inferInfo(forTrackerDomain: domain) + } + } + + private func inferInfo(forTrackerDomain domain: String) -> TrackerDescription { + + let userBlocked = getUserBlockedDomains() + if userBlocked.keys.contains(domain) { + return TrackerDescription(title: domain, description: NSLocalizedString("You blocked this domain in your custom blocked domains.", comment: "")) + } 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 blocked = getLockdownBlockedDomains().lockdownDefaults + + let groups = blocked.values + .filter { $0.domains.keys.contains(where: { domain.hasSuffix($0) }) } + .map { $0.name } + .sorted() + + 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"))") + } + + return TrackerDescription(title: NSLocalizedString("No Info Found", comment: ""), description: NSLocalizedString("No additional information on this blocked domain found.", comment: "")) + } + + static private func formatList(strings: [String]) -> String { + var strings = strings + .map { "\"\($0)\"" } + + guard strings.count > 1 else { + return strings.first ?? "" + } + + let last = strings.removeLast() + return strings + .joined(separator: ", ") + NSLocalizedString(" and ", comment: "Used in: tracker1.com 'and' tracker2.com 'and' tracker3.com") + last + } + + private func getTrackerInfoDoc() throws -> TrackerInfo { + if let loaded = loaded { + return loaded + } else { + let fromDisk = try loadFromDisk() + self.loaded = fromDisk + return fromDisk + } + } + + enum Error: Swift.Error { + case noFileOnDisk + } + + private func loadFromDisk() throws -> TrackerInfo { + guard let url = Bundle.main.url(forResource: "tracker_info", withExtension: "json") else { + throw Error.noFileOnDisk + } + + do { + let content = try Data(contentsOf: url) + let info = try JSONDecoder().decode(TrackerInfo.self, from: content) + return info + } catch { + throw error + } + } + +} + +extension TrackerInfo { + #if DEBUG + // this is used for tracker-info.json validation; see TrackerInfoTests.swift + var test_trackerIds: [String: String] { + return trackerIds + } + + var test_descriptions: [String: TrackerDescription] { + return descriptions + } + #endif +} diff --git a/LockdowniOS/TrackersGroupView.swift b/LockdowniOS/TrackersGroupView.swift new file mode 100644 index 0000000..920fc99 --- /dev/null +++ b/LockdowniOS/TrackersGroupView.swift @@ -0,0 +1,121 @@ +// +// TrackersGroupView.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 18.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +struct TrackersGroupViewModel { + let image: UIImage + let title: String + let number: Int +} + +final class TrackersGroupView: UIView { + + // MARK: - Properties + + private lazy var separator: UIView = { + let view = UIView() + view.backgroundColor = .lightGray + view.anchors.height.equal(1) + return view + }() + + lazy var placeNumber: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontMedium15 + label.textAlignment = .left + return label + }() + + private lazy var trackersImage: UIImageView = { + let image = UIImageView() + image.contentMode = .left + image.layer.masksToBounds = true + return image + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontMedium15 + label.text = "Game Marketing" + label.textAlignment = .left + return label + }() + + lazy var number: UILabel = { + let label = UILabel() + label.font = fontBold18 + label.textColor = .label + label.textAlignment = .right + return label + }() + + lazy var lockImage: UIImageView = { + let image = UIImageView() + image.contentMode = .right + image.layer.masksToBounds = true + image.image = UIImage(named: "icn_lock") + return image + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(placeNumber) + stackView.addArrangedSubview(trackersImage) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(number) + stackView.addArrangedSubview(lockImage) + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + stackView.alignment = .leading + stackView.spacing = 8 + stackView.anchors.height.equal(40) + return stackView + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + private func configureUI() { + + addSubview(separator) + separator.anchors.leading.pin() + separator.anchors.trailing.pin() + separator.anchors.top.pin() + + addSubview(stackView) + stackView.anchors.top.spacing(12, to: separator.anchors.bottom) + stackView.anchors.bottom.pin() + stackView.anchors.leading.marginsPin() + stackView.anchors.trailing.marginsPin() + + trackersImage.anchors.leading.pin(inset: 35) + trackersImage.anchors.centerY.equal(placeNumber.anchors.centerY) + + titleLabel.anchors.leading.pin(inset: 70) + } + + func configure(with model: TrackersGroupViewModel) { + trackersImage.image = model.image + titleLabel.text = model.title + number.text = String(model.number) + } +} diff --git a/LockdowniOS/UIAppearance+Ext.swift b/LockdowniOS/UIAppearance+Ext.swift new file mode 100644 index 0000000..4906106 --- /dev/null +++ b/LockdowniOS/UIAppearance+Ext.swift @@ -0,0 +1,62 @@ +// +// UIAppearance+Ext.swift +// Lockdown +// +// Created by Alexander Parshakov on 1/13/23 +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import PopupDialog +import UIKit + +final class LockdownAppearance { + + static func configure() { + configurePopupDialog() + configureTabBar() + } + + private static func configurePopupDialog() { + let dialogAppearance = PopupDialogDefaultView.appearance() + dialogAppearance.backgroundColor = .systemBackground + dialogAppearance.titleColor = .label + dialogAppearance.messageColor = .label + dialogAppearance.titleFont = .boldLockdownFont(size: 15) + dialogAppearance.titleTextAlignment = .center + dialogAppearance.messageFont = .mediumLockdownFont(size: 15) + dialogAppearance.messageTextAlignment = .center + + [DefaultButton.appearance(), + DynamicButton.appearance(), + CancelButton.appearance()].forEach { appearance in + appearance.buttonColor = .systemBackground + appearance.separatorColor = UIColor(white: 0.2, alpha: 1) + appearance.titleFont = .semiboldLockdownFont(size: 17) + appearance.titleColor = .tunnelsBlue + } + + CancelButton.appearance().titleColor = .lightGray + } + + private static func configureTabBar() { + let textAttributes = [NSAttributedString.Key.font: UIFont.semiboldLockdownFont(size: 11)] + + UITabBarItem.appearance().setTitleTextAttributes(textAttributes, for: .normal) + UITabBarItem.appearance().setTitleTextAttributes(textAttributes, for: .selected) + + let tabBarItemAppearance = UITabBarItemAppearance() + tabBarItemAppearance.normal.titleTextAttributes = textAttributes + tabBarItemAppearance.selected.titleTextAttributes = textAttributes + + let tabBarAppearance = UITabBarAppearance() + tabBarAppearance.inlineLayoutAppearance = tabBarItemAppearance + tabBarAppearance.stackedLayoutAppearance = tabBarItemAppearance + tabBarAppearance.compactInlineLayoutAppearance = tabBarItemAppearance + tabBarAppearance.configureWithOpaqueBackground() + tabBarAppearance.backgroundColor = .systemBackground + tabBarAppearance.shadowImage = nil + tabBarAppearance.shadowColor = nil + + UITabBar.appearance().standardAppearance = tabBarAppearance + } +} diff --git a/LockdowniOS/UIApplication+Extension.swift b/LockdowniOS/UIApplication+Extension.swift new file mode 100644 index 0000000..f6591b1 --- /dev/null +++ b/LockdowniOS/UIApplication+Extension.swift @@ -0,0 +1,24 @@ +// +// UIApplication+Extension.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 3.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +extension UIApplication { + + class func getTopMostViewController() -> UIViewController? { + let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first + if var topController = keyWindow?.rootViewController { + while let presentedViewController = topController.presentedViewController { + topController = presentedViewController + } + return topController + } else { + return nil + } + } +} diff --git a/LockdowniOS/UICollectionView+Dequeue.swift b/LockdowniOS/UICollectionView+Dequeue.swift new file mode 100644 index 0000000..2243c73 --- /dev/null +++ b/LockdowniOS/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/UIKit+Extensions.swift b/LockdowniOS/UIKit+Extensions.swift new file mode 100644 index 0000000..47249b7 --- /dev/null +++ b/LockdowniOS/UIKit+Extensions.swift @@ -0,0 +1,33 @@ +// +// UIKit+Extensions.swift +// LockdowniOS +// +// Created by Oleg Dreyman on 28.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import UIKit + +extension UIDevice { + + static var is4InchIphone: Bool { + return UIScreen.main.nativeBounds.height == 1136 + } +} + +extension Bundle { + var versionString: String { + return "v" + (infoDictionary?["CFBundleShortVersionString"] as? String ?? "") + } +} + +extension UIStoryboard { + func instantiate(_ viewControllerType: ViewController.Type) -> ViewController { + let identifier = String.init(describing: viewControllerType) + if let resolved = instantiateViewController(withIdentifier: identifier) as? ViewController { + return resolved + } else { + fatalError("No ViewController with Storyboard ID = \(identifier). Please make sure your Storyboard ID is the same as class name!") + } + } +} diff --git a/LockdowniOS/UIStackView+Ext.swift b/LockdowniOS/UIStackView+Ext.swift new file mode 100644 index 0000000..5bfff64 --- /dev/null +++ b/LockdowniOS/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/UIVIew+Extensions.swift b/LockdowniOS/UIVIew+Extensions.swift new file mode 100644 index 0000000..4480a82 --- /dev/null +++ b/LockdowniOS/UIVIew+Extensions.swift @@ -0,0 +1,31 @@ +// +// UIVIew+Extensions.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 20.04.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +// MARK: ClickListener +class ClickListener: UITapGestureRecognizer { + var onClick : (() -> Void)? = nil +} + + +// MARK: UIView Extension +extension UIView { + + func setOnClickListener(action :@escaping () -> Void){ + let tapRecogniser = ClickListener(target: self, action: #selector(onViewClicked(sender:))) + tapRecogniser.onClick = action + self.addGestureRecognizer(tapRecogniser) + } + + @objc func onViewClicked(sender: ClickListener) { + if let onClick = sender.onClick { + onClick() + } + } +} diff --git a/LockdowniOS/UIView+Corners.swift b/LockdowniOS/UIView+Corners.swift new file mode 100644 index 0000000..ccf1a19 --- /dev/null +++ b/LockdowniOS/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/UIView+Ext.swift b/LockdowniOS/UIView+Ext.swift new file mode 100644 index 0000000..d2ef658 --- /dev/null +++ b/LockdowniOS/UIView+Ext.swift @@ -0,0 +1,169 @@ +// +// 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 = gradient.points.start + gradientLayer.endPoint = gradient.points.end + 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() + }) { [weak self] isCompleted in + self?.isUserInteractionEnabled = true + guard isCompleted else { return } + completion?() + } + } + } + + func showAnimatedSpring(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 / 3, animations: { + self.transformToIdentity() + }) { [weak self] isCompleted in + self?.isUserInteractionEnabled = true + guard isCompleted else { return } + 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) + ) + } + + static func dynamicColor(light: UIColor, dark: UIColor) -> UIColor { + guard #available(iOS 13.0, *) else { return light } + return UIColor { $0.userInterfaceStyle == .dark ? dark : light } + } +} + +class GradientView: UIView { + var gradient: LockdownGradient? { + didSet { + guard let gradient else { return } + gradientLayer = applyGradient(gradient) + } + } + var gradientLayer: CALayer? + + override func layoutSubviews() { + super.layoutSubviews() + gradientLayer?.frame = bounds + } +} diff --git a/LockdowniOS/UIViewController+Ext.swift b/LockdowniOS/UIViewController+Ext.swift new file mode 100644 index 0000000..6f75701 --- /dev/null +++ b/LockdowniOS/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/UserDefault.swift b/LockdowniOS/UserDefault.swift new file mode 100644 index 0000000..6db118f --- /dev/null +++ b/LockdowniOS/UserDefault.swift @@ -0,0 +1,99 @@ +// +// UserDefault.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import CocoaLumberjackSwift +import Foundation + +@propertyWrapper +struct UserDefault { + private let key: String + private let defaultValue: T + private let isLogged: Bool + private let userDefaults: UserDefaults + + init(_ key: String, defaultValue: T, + isLogged: Bool = false, + userDefaults: UserDefaults = UserDefaults(suiteName: LockdownStorageIdentifier.userDefaultsId)!) { + self.key = key + self.defaultValue = defaultValue + self.isLogged = isLogged + self.userDefaults = userDefaults + } + + var wrappedValue: T { + get { + let value = userDefaults.object(forKey: key) as? T ?? defaultValue + if isLogged { + DDLogInfo("Reading UserDefaults.\(key) of value \(value).") + } + return value + } + set { + if isLogged { + DDLogInfo("Setting UserDefaults.\(key) value as \(newValue).") + } + userDefaults.set(newValue, forKey: key) + } + } +} + +extension UserDefaults { + + @UserDefault("homeScreenLastPaywallDisplayDate", defaultValue: Date(), isLogged: true) + static var lastPaywallDisplayDate + + @UserDefault("hasSeenPaywallOnHomeScreen", defaultValue: false, isLogged: true) + static var hasSeenPaywallOnHomeScreen + + @UserDefault("hasSeenLTO", defaultValue: false, isLogged: true) + static var hasSeenLTO + + @UserDefault("hasSeenAdvancedPaywall", defaultValue: false, isLogged: true) + static var hasSeenAdvancedPaywall + + @UserDefault("hasSeenAnonymousPaywall", defaultValue: false, isLogged: true) + static var hasSeenAnonymousPaywall + + @UserDefault("hasSeenUniversalPaywall", defaultValue: false, isLogged: true) + static var hasSeenUniversalPaywall + + @UserDefault("hasSeenStartupOneTimeOffer", defaultValue: false, isLogged: true) + static var hasSeenStartupOneTimeOffer + + @UserDefault("onboardingCompleted", defaultValue: false, isLogged: true, userDefaults: UserDefaults.standard) + static var onboardingCompleted + + @UserDefault("hasPurchasedFromOnboarding", defaultValue: false, isLogged: true, userDefaults: UserDefaults.standard) + static var hasPurchasedFromOnboarding + + @UserDefault("shouldShowMultipleSubscriptionAlert", defaultValue: false, isLogged: true, userDefaults: UserDefaults.standard) + static var shouldShowMultipleSubscriptionAlert + + @UserDefault("didShowMultipleSubscriptionAlert", defaultValue: false, isLogged: true, userDefaults: UserDefaults.standard) + static var didShowMultipleSubscriptionAlert +} + +// MARK: - Content Blocker + +extension UserDefaults { + struct ContentBlocking { + + @UserDefault("hasSeenContentBlockerPageBefore", defaultValue: false) + static var hasSeenContentBlockerPageBefore + + @UserDefault("AdBlockingEnabled", defaultValue: true) + static var adBlockingEnabled + + @UserDefault("PrivacyBlockingEnabled", defaultValue: true) + static var privacyBlockingEnabled + + @UserDefault("SocialBlockingEnabled", defaultValue: true) + static var socialBlockingEnabled + } +} + diff --git a/LockdowniOS/UserService.swift b/LockdowniOS/UserService.swift new file mode 100644 index 0000000..2308a3d --- /dev/null +++ b/LockdowniOS/UserService.swift @@ -0,0 +1,70 @@ +// +// UserService.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 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) { + firstly { + 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) + // TODO: to change later when there will be data on the server + completion(nil) + } + } + + private func signIn() -> Promise<()> { + if let apiCredentials = getAPICredentials(), getAPICredentialsConfirmed() == true { + DDLogInfo("plan status: have confirmed API credentials, using them") + return 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 in + DDLogInfo("plan status: subscriptionevent result: \(result)") + return Promise { seal in seal.fulfill(()) } + } + } else { + return firstly { + try Client.signIn() + }.then { _ in + Promise { seal in seal.fulfill(()) } + } + } + } +} + diff --git a/LockdowniOS/VPNController.swift b/LockdowniOS/VPNController.swift index 5fd93d7..c569489 100644 --- a/LockdowniOS/VPNController.swift +++ b/LockdowniOS/VPNController.swift @@ -8,6 +8,7 @@ import UIKit import NetworkExtension import CocoaLumberjackSwift +import WidgetKit let kVPNLocalizedDescription = "Lockdown VPN" @@ -34,10 +35,19 @@ class VPNController: NSObject { VPNController.shared.setEnabled(true) }) } + + func isConfigurationExisting(_ completion: @escaping (Bool) -> ()) { + manager.loadFromPreferences { (error) in + completion(self.manager.protocolConfiguration != nil) + } + } func setEnabled(_ enabled: Bool, completion: @escaping (_ error: Error?) -> Void = {_ in }) { DDLogInfo("VPNController set enabled: \(enabled)") setUserWantsVPNEnabled(enabled) + if #available(iOSApplicationExtension 14.0, iOS 14.0, *) { + WidgetCenter.shared.reloadAllTimelines() + } if (enabled) { setUpAndEnableVPN { error in completion(error) @@ -89,16 +99,23 @@ class VPNController: NSObject { p.ikeSecurityAssociationParameters.integrityAlgorithm = NEVPNIKEv2IntegrityAlgorithm.SHA512 p.ikeSecurityAssociationParameters.lifetimeMinutes = 1440 - p.deadPeerDetectionRate = NEVPNIKEv2DeadPeerDetectionRate.high + p.deadPeerDetectionRate = NEVPNIKEv2DeadPeerDetectionRate.medium p.identityData = identityData p.identityDataPassword = "" + if #available(iOS 13.0, *) { + p.enableFallback = false + } + self.manager.protocolConfiguration = p self.manager.isEnabled = true self.manager.isOnDemandEnabled = true + var onDemandRules:[NEOnDemandRule] = []; let connectRule = NEOnDemandRuleConnect() connectRule.interfaceTypeMatch = .any - self.manager.onDemandRules = [connectRule] + onDemandRules.append(connectRule) + self.manager.onDemandRules = onDemandRules + DDLogInfo("VPN status before loading: \(self.manager.connection.status)") self.manager.localizedDescription! = kVPNLocalizedDescription self.manager.saveToPreferences(completionHandler: {(_ error: Error?) -> Void in @@ -115,10 +132,35 @@ class VPNController: NSObject { } } else { - completion(nil) + self.loadFromPrefenrenceAndStartVPN(completion: completion) } }) }) } + private func loadFromPrefenrenceAndStartVPN(completion: @escaping (_ error: Error?) -> Void) { + manager.loadFromPreferences { [weak self] error in + if let error { + DDLogError("Read preference error before start vpn: " + error.localizedDescription) + } + 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() + let config = URLSessionConfiguration.default + config.requestCachePolicy = .reloadIgnoringLocalCacheData + 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 + return + } + task.resume() + } + catch { + DDLogError("Unable to start the tunnel after saving: " + error.localizedDescription) + } + completion(nil) + } + + } } diff --git a/LockdowniOS/VPNController.temp_caseinsensitive_rename.swift b/LockdowniOS/VPNController.temp_caseinsensitive_rename.swift deleted file mode 100644 index d47a931..0000000 --- a/LockdowniOS/VPNController.temp_caseinsensitive_rename.swift +++ /dev/null @@ -1,195 +0,0 @@ -// -// VPNController.swift -// Lockdown -// -// Copyright © 2018 Confirmed, Inc. All rights reserved. -// - -import UIKit -import NetworkExtension -import CocoaLumberjackSwift - -class VPNController: NSObject { - - static let shared = VPNController() - - private override init() { - super.init() - updateProtocol() - NotificationCenter.default.addObserver(self, selector: #selector(vpnStatusDidChange(_:)), name: .NEVPNStatusDidChange, object: nil) - - NETunnelProviderManager.loadAllFromPreferences { (managers, error) -> Void in - } - NEVPNManager.shared().loadFromPreferences(completionHandler: {(_ error: Error?) -> Void in - }) - } - - @objc func vpnStatusDidChange(_ notification: Notification) { - if let object = notification.object as? NETunnelProviderSession { - if object.manager.localizedDescription == IPSec.localizedName { - ipsec.ipsecManager = object.manager - } - print("DescriptionTunnel \(object.manager.localizedDescription)") - } - else if let object = notification.object as? NEVPNConnection{ - if object.manager.localizedDescription == IPSec.localizedName { - ipsec.ipsecManager = object.manager - } - } - } - - //MARK: - PROXY METHODS - /* - * proxy is used for whitelisting - * route traffic directly to site for approved domains - */ - func setupWhitelistingProxy() { - - //Utils.setupWhitelistedDefaults() - let vpnManager = NEVPNManager.shared() - vpnManager.loadFromPreferences(completionHandler: {(_ error: Error?) -> Void in - - NETunnelProviderManager.loadAllFromPreferences { (managers, error) -> Void in - if let managers = managers { - let manager: NETunnelProviderManager - if managers.count > 0 { - manager = managers[0] - } else { - manager = NETunnelProviderManager() - manager.protocolConfiguration = NETunnelProviderProtocol() - } - - manager.localizedDescription = "Lockdown Tunnel" - manager.protocolConfiguration?.serverAddress = "Lockdown" - manager.isEnabled = true - manager.isOnDemandEnabled = true - - let connectRule = NEOnDemandRuleConnect() - connectRule.interfaceTypeMatch = .any - manager.onDemandRules = [connectRule] - manager.saveToPreferences(completionHandler: { (error) -> Void in - }) - } - else{ - - } - } - }) - } - - /* - * disable & re-enable proxy - * primarily to reload whitelist rules - */ - func toggleWhitelistingProxy() { - disableWhitelistingProxy(proxyDisabledCallback: {(error) -> Void in - self.setupWhitelistingProxy() - }) - } - - /* - * disable proxy - * should be synchronized with VPN state - */ - func disableWhitelistingProxy(proxyDisabledCallback: ((_ error: Error?) -> Void)? = nil) { - ipsec.disableWhitelistingProxy(completion: {error in - proxyDisabledCallback?(error) - }) - - } - - //MARK: - VPN FUNCTIONS - - /* - * only called internally - * check for appropriate parameters and set up/install - * enable on completion of setup - */ - - /* - * master function for initiating VPN connections - */ - func connectToVPN() { - ipsec.connectToVPN() - } - - func disconnectFromVPN() { - ipsec.disconnectFromVPN() - } - - func connectToLockdown() { - let manager = NEVPNManager.shared() - manager.loadFromPreferences(completionHandler: {(_ error: Error?) -> Void in - VPNController.shared.setupWhitelistingProxy() - }) - } - - func stopLockdown() { - VPNController.shared.disableWhitelistingProxy() - } - - func forceVPNOff() { - let manager = NEVPNManager.shared() - manager.loadFromPreferences(completionHandler: {(_ error: Error?) -> Void in - if !manager.isEnabled { - return - } - DDLogInfo("Loading Error \(String(describing: error))") - var p = NEVPNProtocolIKEv2() - if let pc = manager.protocolConfiguration { - p = pc as! NEVPNProtocolIKEv2 - } - - //null the address - p.serverAddress = "local." + Global.vpnDomain - p.serverCertificateIssuerCommonName = "local." + Global.vpnDomain - p.remoteIdentifier = "local." + Global.vpnDomain - - p.deadPeerDetectionRate = NEVPNIKEv2DeadPeerDetectionRate.high - - manager.protocolConfiguration = p - manager.isOnDemandEnabled = false - manager.isEnabled = false - manager.localizedDescription! = Global.vpnName - - manager.saveToPreferences(completionHandler: {(_ error: Error?) -> Void in - if let e = error { - DDLogError("Error with saving config \(e)") - } - }) - }) - - disableWhitelistingProxy() - } - - func reloadBlockListRules() { - DispatchQueue.main.async { - self.toggleWhitelistingProxy() - } - } - - func updateProtocol() { - ipsec.endpointForRegion(region: Utils.getSavedRegion()) //calling this method will choose a new region if unsupported (rare) - } - - func lockdownState(completion: @escaping (_ status: NEVPNStatus) -> Void) { - NETunnelProviderManager.loadAllFromPreferences { (managers, error) -> Void in - if let managers = managers { - let manager: NETunnelProviderManager - if managers.count > 0 { - manager = managers[0] - } else { - manager = NETunnelProviderManager() - manager.protocolConfiguration = NETunnelProviderProtocol() - } - completion(manager.connection.status) - } - else { - completion(.invalid) - } - } - } - - let ipsec = IPSec(); - -} diff --git a/LockdowniOS/VPNPaywallViewController.swift b/LockdowniOS/VPNPaywallViewController.swift new file mode 100644 index 0000000..827f161 --- /dev/null +++ b/LockdowniOS/VPNPaywallViewController.swift @@ -0,0 +1,454 @@ +// +// AdvancedWallViewController.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 25.04.2023. +// + +import UIKit +import NetworkExtension +import PromiseKit +import CocoaLumberjackSwift +import StoreKit + +protocol VPNPaywallViewControllerCloseDelegate: AnyObject { + func didClosePaywall() +} + +final class VPNPaywallViewController: BaseViewController, Loadable { + private enum Tab { + case advanced + case anonymous + case universal + } + + let shared: UserService = BaseUserService.shared + + var parentVC: UIViewController? + + enum Mode { + case newSubscription + case upgrade(active: [Subscription.PlanType]) + } + + var mode = Mode.newSubscription + + weak var delegate: VPNPaywallViewControllerCloseDelegate? + var purchaseSuccessful: (()->Void)? + var purchaseFailed: ((Error)->Void)? + + private var selectedTab = Tab.advanced { + didSet { + updateTabs() + } + } + + private var needScrolToUniversal = false + + //MARK: Properties + private var titleName = NSLocalizedString("Lockdown", comment: "") + + //MARK: navigation + private lazy var navigationView: ConfiguredNavigationView = { + let view = ConfiguredNavigationView() + view.rightNavButton.setTitle(NSLocalizedString("RESTORE", comment: ""), for: .normal) + view.titleLabel.text = NSLocalizedString(titleName, comment: "") + view.leftNavButton.setTitle(NSLocalizedString("CLOSE", comment: ""), for: .normal) + view.leftNavButton.addTarget(self, action: #selector(closeButtonClicked), for: .touchUpInside) + view.rightNavButton.addTarget(self, action: #selector(restorePurchase), for: .touchUpInside) + return view + }() + + //MARK: horizontal scroll menu + private lazy var hScrollView: UIScrollView = { + let view = UIScrollView() + view.isScrollEnabled = true + return view + }() + + private lazy var advancedPlan: PlanView = { + let view = PlanView() + view.title.text = "Advanced" + view.isUserInteractionEnabled = true + + view.setOnClickListener { [unowned self] in + self.selectedTab = .advanced + } + return view + }() + + private lazy var anonymousPlan: PlanView = { + let view = PlanView() + view.title.text = "Anonymous" + view.isUserInteractionEnabled = true + view.setOnClickListener { [unowned self] in + selectedTab = .anonymous + } + return view + }() + + private lazy var universalPlan: PlanView = { + let view = PlanView() + view.title.text = "Universal" + view.isUserInteractionEnabled = true + view.setOnClickListener { [unowned self] in + selectedTab = .universal + } + return view + }() + + private lazy var plansStack: UIStackView = { + let stack = UIStackView() + stack.axis = .horizontal + stack.addArrangedSubview(advancedPlan) + stack.addArrangedSubview(anonymousPlan) + stack.addArrangedSubview(universalPlan) + stack.alignment = .leading + stack.distribution = .equalSpacing + stack.spacing = 16 + return stack + }() + + lazy var advancedView: PaywallView = { + let view = PaywallView(model: .advancedDetails()) + view.topProduct.setOnClickListener { [unowned self] in + selectAdvancedYearly() + selectYearlyProduct(view, model: .advancedDetails()) + } + view.bottomProduct.setOnClickListener { [unowned self] in + selectAdvancedMonthly() + selectMontlyProduct(view, model: .advancedDetails()) + } + + view.actionButton.setOnClickListener { [unowned self] in + startTrial() + } + return view + }() + + + + lazy var anonymousView: PaywallView = { + let view = PaywallView(model: .anonymousDetails()) + view.isHidden = true + view.topProduct.setOnClickListener { [unowned self] in + selectAnonymousYearly() + selectYearlyProduct(view, model: .anonymousDetails()) + } + view.bottomProduct.setOnClickListener { [unowned self] in + selectAnonymousMonthly() + selectMontlyProduct(view, model: .anonymousDetails()) + } + view.actionButton.setOnClickListener { [unowned self] in + startTrial() + } + return view + }() + + lazy var universalView: PaywallView = { + let view = PaywallView(model: .universalDetails()) + view.isHidden = true + view.topProduct.setOnClickListener { [unowned self] in + selectUniversalYearly() + selectYearlyProduct(view, model: .universalDetails()) + } + view.bottomProduct.setOnClickListener { [unowned self] in + selectUniversalMonthly() + selectMontlyProduct(view, model: .universalDetails()) + } + + view.actionButton.setOnClickListener { [unowned self] in + startTrial() + } + + return view + }() + + private func selectYearlyProduct(_ view: PaywallView, model: PaywallViewModel) { + view.topProduct.setSelected(true) + view.bottomProduct.setSelected(false) + let anualPrice = VPNSubscription.getProductIdPrice(productId: model.annualProductId) + let monthlyPrice = VPNSubscription.getProductIdPriceMonthly(productId: model.annualProductId) + let trialDuation = VPNSubscription.trialDuration(productId: model.annualProductId) ?? "" + let title = trialDuation + " " + NSLocalizedString("free trial", comment: "") + "," + " then \(anualPrice) (\(monthlyPrice)/mo)" + view.trialDescriptionLabel.text = title + view.trialDescriptionLabel.isHidden = VPNSubscription.trialDuration(productId: model.annualProductId) == nil + view.updateCTATitle(for: model.annualProductId) + } + + private func selectMontlyProduct(_ view: PaywallView, model: PaywallViewModel) { + view.bottomProduct.setSelected(true) + view.topProduct.setSelected(false) + let monthlyPrice = VPNSubscription.getProductIdPrice(productId: model.mounthProductId) + let trialDuation = VPNSubscription.trialDuration(productId: model.annualProductId) ?? "" + let title = trialDuation + " " + NSLocalizedString("free trial", comment: "") + "," + " then \(monthlyPrice)/mo" + view.trialDescriptionLabel.text = title + view.trialDescriptionLabel.isHidden = VPNSubscription.trialDuration(productId: model.mounthProductId) == nil + view.updateCTATitle(for: model.mounthProductId) + } + + private lazy var privacyLabel: UILabel = { + let label = UILabel() + label.font = fontMedium11 + label.textAlignment = .center + label.numberOfLines = 0 + + let attributedText = NSMutableAttributedString(string: NSLocalizedString("By continuing you agree with our ", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.smallGrey]) + let termsRange = NSRange(location: attributedText.length, length: NSLocalizedString("Terms of Service", comment: "").count) + attributedText.append(NSAttributedString(string: NSLocalizedString("Terms of Service", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.white])) + attributedText.append(NSAttributedString(string: NSLocalizedString(" and ", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.smallGrey])) + let privacyRange = NSRange(location: attributedText.length, length: NSLocalizedString("Privacy Policy", comment: "").count) + attributedText.append(NSAttributedString(string: NSLocalizedString("\nPrivacy Policy", comment: ""), attributes: [NSAttributedString.Key.font: fontMedium11, NSAttributedString.Key.foregroundColor: UIColor.white])) + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + attributedText.addAttributes([NSAttributedString.Key.paragraphStyle: paragraphStyle], range: NSRange(location: 0, length: attributedText.length)) + label.attributedText = attributedText + label.isUserInteractionEnabled = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(labelTapped(sender:))) + label.addGestureRecognizer(tapGesture) + return label + }() + + //MARK: Lificycle + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .paywallNew + configureUI() + updateCurrentSelectedTab() + updateTabs() + updateVisibleTabs() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + if needScrolToUniversal { + scrollToRightTabs() + needScrolToUniversal = false + } + } + + //MARK: ConfigureUI + private func configureUI() { + view.addSubview(navigationView) + navigationView.anchors.top.safeAreaPin(inset: 18) + navigationView.anchors.leading.marginsPin() + navigationView.anchors.trailing.marginsPin() + + view.addSubview(hScrollView) + hScrollView.anchors.top.spacing(24, to: navigationView.anchors.bottom) + hScrollView.anchors.leading.pin(inset: 16) + hScrollView.anchors.trailing.pin() + hScrollView.anchors.height.equal(60) + hScrollView.showsHorizontalScrollIndicator = false + + hScrollView.addSubview(plansStack) + plansStack.anchors.top.marginsPin() + plansStack.anchors.leading.equal(hScrollView.anchors.leading) + plansStack.anchors.trailing.equal(hScrollView.anchors.trailing) + plansStack.anchors.height.equal(hScrollView.anchors.height) + + view.addSubview(privacyLabel) + privacyLabel.anchors.bottom.safeAreaPin() + privacyLabel.anchors.leading.marginsPin() + privacyLabel.anchors.trailing.marginsPin() + privacyLabel.anchors.height.equal(36) + + view.addSubview(advancedView) + advancedView.anchors.top.spacing(24, to: hScrollView.anchors.bottom) + advancedView.anchors.leading.pin() + advancedView.anchors.trailing.pin() + advancedView.anchors.bottom.spacing(8, to: privacyLabel.anchors.top) + + view.addSubview(anonymousView) + anonymousView.anchors.top.spacing(24, to: hScrollView.anchors.bottom) + anonymousView.anchors.leading.pin() + anonymousView.anchors.trailing.pin() + anonymousView.anchors.bottom.spacing(8, to: privacyLabel.anchors.top) + + view.addSubview(universalView) + universalView.anchors.top.spacing(24, to: hScrollView.anchors.bottom) + universalView.anchors.leading.pin() + universalView.anchors.trailing.pin() + universalView.anchors.bottom.spacing(8, to: privacyLabel.anchors.top) + } + + //MARK: Functions + @objc func closeButtonClicked() { + dismiss(animated: true) + } + + @objc private func labelTapped(sender: UITapGestureRecognizer) { + let termsRange = NSRange(location: privacyLabel.attributedText!.length - NSLocalizedString("Terms of Service", comment: "").count - 18, length: NSLocalizedString("Terms of Service", comment: "").count) + let privacyRange = NSRange(location: privacyLabel.attributedText!.length - NSLocalizedString("Privacy Policy", comment: "").count, length: NSLocalizedString("Privacy Policy", comment: "").count) + + if sender.didTapAttributedTextInLabel(label: privacyLabel, inRange: privacyRange), + let url = URL(string: "https://lockdownprivacy.com/privacy") { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } else if sender.didTapAttributedTextInLabel(label: privacyLabel, inRange: termsRange), + let url = URL(string: "https://lockdownprivacy.com/terms") { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } + } + + private func disable(button: UIButton) { + button.isEnabled = false + button.backgroundColor = .lightGray + } + + private func isDisabledPlan(_ plan: Subscription.PlanType) -> Bool { + guard let subscription = shared.user.currentSubscription else { + return false + } + let planOrder: [Subscription.PlanType] = [ + .advancedMonthly, + .advancedAnnual, + .anonymousMonthly, + .anonymousAnnual, + .universalMonthly, + .universalAnnual + ] + guard let index = planOrder.firstIndex(of: plan), + let currentIndex = planOrder.firstIndex(of: subscription.planType) else { + return false + } + return index <= currentIndex + } + + private func update(_ planView: PlanView, isSelected: Bool) { + if isSelected { + planView.iconImageView.image = UIImage(named: "fill-1") + planView.backgroundView.layer.borderColor = UIColor.borderBlue.cgColor + } else { + planView.iconImageView.image = UIImage(named: "grey-ellipse-1") + planView.backgroundView.layer.borderColor = UIColor.borderGray.cgColor + } + } + + private func updateCurrentSelectedTab () { + guard let plan = shared.user.currentSubscription?.planType else { + selectedTab = .universal + needScrolToUniversal = true + return + } + if plan.isAdvanced { + selectedTab = .anonymous + } + if plan.isAnonymous { + selectedTab = .universal + } + if plan.isUniversal { + selectedTab = .universal + } + } + + private func scrollToRightTabs() { + DispatchQueue.main.async { + let offset = self.hScrollView.contentSize.width - self.hScrollView.frame.size.width + self.hScrollView.setContentOffset(CGPoint(x: offset, y: 0), animated: false) + } + } + + private func updateTabs() { + switch selectedTab { + case .advanced: + selectAdvancedYearly() + case .anonymous: + selectAnonymousYearly() + case .universal: + selectUniversalYearly() + } + + advancedView.isHidden = selectedTab != .advanced + anonymousView.isHidden = selectedTab != .anonymous + universalView.isHidden = selectedTab != .universal + + update(advancedPlan, isSelected: selectedTab == .advanced) + update(anonymousPlan, isSelected: selectedTab == .anonymous) + update(universalPlan, isSelected: selectedTab == .universal) + } + + private func updateVisibleTabs() { + guard let plan = shared.user.currentSubscription?.planType else { + advancedPlan.isHidden = false + anonymousPlan.isHidden = false + universalPlan.isHidden = false + return + } + if plan.isAdvanced { + advancedPlan.isHidden = false + anonymousPlan.isHidden = false + universalPlan.isHidden = false + } + if plan.isAnonymous { + advancedPlan.isHidden = true + anonymousPlan.isHidden = true + } + if plan.isUniversal { + advancedPlan.isHidden = true + anonymousPlan.isHidden = true + } + } +} + +extension VPNPaywallViewController: ProductPurchasable { + + @objc private func restorePurchase() { + restorePurchases() + } + + func selectAdvancedYearly() { + VPNSubscription.selectedProductId = VPNSubscription.productIdAdvancedYearly + updatePricingSubtitle() + } + + func selectAdvancedMonthly() { + VPNSubscription.selectedProductId = VPNSubscription.productIdAdvancedMonthly + updatePricingSubtitle() + } + + func selectAnonymousYearly() { + VPNSubscription.selectedProductId = VPNSubscription.productIdAnnual + updatePricingSubtitle() + } + + func selectAnonymousMonthly() { + VPNSubscription.selectedProductId = VPNSubscription.productIdMonthly + updatePricingSubtitle() + } + + func selectUniversalYearly() { + VPNSubscription.selectedProductId = VPNSubscription.productIdAnnualPro + updatePricingSubtitle() + } + + func selectUniversalMonthly() { + VPNSubscription.selectedProductId = VPNSubscription.productIdMonthlyPro + updatePricingSubtitle() + } + + func updatePricingSubtitle() { + let context: VPNSubscription.SubscriptionContext = { + switch mode { + case .newSubscription: + return .new + case .upgrade: + return .upgrade + } + }() + } + + @objc func startTrial() { + showLoadingView() + VPNSubscription.purchase( + succeeded: { + self.dismiss(animated: true, completion: { + self.purchaseSuccessful?() + }) + }, + errored: { error in + self.hideLoadingView() + self.purchaseFailed?(error) + }) + } +} diff --git a/LockdowniOS/VPNSubscription.swift b/LockdowniOS/VPNSubscription.swift index 9aacc37..c2b75b8 100644 --- a/LockdowniOS/VPNSubscription.swift +++ b/LockdowniOS/VPNSubscription.swift @@ -9,20 +9,176 @@ import UIKit import SwiftyStoreKit import PromiseKit import CocoaLumberjackSwift +import StoreKit + +struct OneTimeProducts: ToList { + let weekly: String + let weeklyTrial: String + let yearly: String + let yearlyTrial: String +} + +struct SpecialOfferProducts: ToList { + let yearly: String +} + +struct FeedbackProducts: ToList { + let weekly: String + let yearly: String +} + +protocol ToList { + func toList() -> [String] +} + +extension ToList { + func toList() -> [String] { + let otherSelf = Mirror(reflecting: self) + return otherSelf.children.compactMap { + $0.value as? String + } + } +} + +struct InternalSubscription: Hashable { + let productId: String + let period: SKProduct.PeriodUnit + let trialDuration: String? + let priceLocale: Locale + let price: NSDecimalNumber + let offer: NSDecimalNumber? + + static var mockWeekly: InternalSubscription { + InternalSubscription(productId: "lockdown.weekly.1.202408.no_trial.4hrs_offer", period: .week, trialDuration: nil, priceLocale: .current, price: 0.99, offer: nil) + } + + static var mockWeeklyTrial: InternalSubscription { + InternalSubscription(productId: "lockdown.weekly.1.202408.3_days_free_trial.4hrs_offer", period: .week, trialDuration: "3 days", priceLocale: .current, price: 0.99, offer: nil) + } + + static var mockYearly: InternalSubscription { + InternalSubscription(productId: "lockdown.yearly.40.202408.no_trial.4hrs_offer_", period: .year, trialDuration: nil, priceLocale: .current, price: 39.99, offer: nil) + } + + static var mockYearlTrial: InternalSubscription { + InternalSubscription(productId: "lockdown.yearly.40.202408.3_days_free_trial.4hrs_offer", period: .year, trialDuration: "3 days", priceLocale: .current, price: 39.99, offer: nil) + } + + static var mockYearlyBF: InternalSubscription { + InternalSubscription(productId: "lockdown.yearly.30.202412.1_year_no_trial.4h_screen_holiday", period: .year, trialDuration: nil, priceLocale: .current, price: 99.99, offer: 29.99) + } +} enum SubscriptionState: Int { case Uninitialized = 1, Subscribed, NotSubscribed } -class VPNSubscription: NSObject { +actor VPNSubscription: NSObject { + + enum SubscriptionType { + case oneTime + case feedback + case specialOffer + case onboarding + + var productIds: [String] { + switch self { + case .oneTime: VPNSubscription.oneTimeProducts.toList() + case .feedback: VPNSubscription.feedbackProducts.toList() + case .specialOffer: VPNSubscription.specialOfferProducts.toList() + case .onboarding: VPNSubscription.onboardingProducts.toList() + } + } + } + private var cachedSubscriptions: [SubscriptionType: [InternalSubscription]] = [:] + + static var shared = VPNSubscription() + + static let productIdAdvancedMonthly = "LockdowniOSFirewallMonthly" + static let productIdAdvancedYearly = "LockdowniOSFirewallAnnual" static let productIdMonthly = "LockdowniOSVpnMonthly" static let productIdAnnual = "LockdowniOSVpnAnnual" - static let productIds: Set = [productIdMonthly, productIdAnnual] - static var selectedProductId = productIdMonthly + static let productIdMonthlyPro = "LockdowniOSVpnMonthlyPro" + static let productIdAnnualPro = "LockdowniOSVpnAnnualPro" + static let productIds: Set = [productIdAdvancedMonthly, productIdAdvancedYearly, productIdMonthly, productIdAnnual, productIdMonthlyPro, productIdAnnualPro] + static let oneTimeProducts = OneTimeProducts(weekly: "lockdown.weekly.1.202408.no_trial.4hrs_offer", + weeklyTrial: "lockdown.weekly.1.202408.3_days_free_trial.4hrs_offer", + yearly: "lockdown.yearly.40.202408.no_trial.4hrs_offer_", + yearlyTrial: "lockdown.yearly.40.202408.3_days_free_trial.4hrs_offer") + static let onboardingProducts = OneTimeProducts(weekly: "lockdown.weekly.5.202502.no_trial.onboarding", + weeklyTrial: "lockdown.weekly.5.202502.3_days_free_trial.onboarding", + yearly: "lockdown.yearly.60.202502.no_trial.onboarding", + yearlyTrial: "lockdown.yearly.60.202502.3_days_free_trial.onboarding") + static let feedbackProducts = FeedbackProducts(weekly: "lockdown.weekly.1.202409.no_trial.feedback", + yearly: "lockdown.yearly.40.202409.no_trial.feedback") + static let specialOfferProducts = SpecialOfferProducts(yearly: "lockdown.yearly.30.202501.1_year_no_trial.4h_screen_newyear") + static var selectedProductId = productIdAdvancedMonthly + + // Advanced Level + static var defaultPriceStringAdvancedMonthly = "$4.99" + static var defaultPriceStringAdvancedYearly = "$29.99" + static var defaultPriceSubStringAdvancedYearly = "$2.49" + static var defaultUpgradePriceStringAdvancedMonthly = "$4.99" + static var defaultUpgradePriceStringAdvancedYearly = "$29.99" + + // Anonymous Level + static var defaultPriceStringMonthly = "$8.99" + static var defaultPriceStringAnnual = "$59.99" + static var defaultPriceSubStringAnnual = "$4.99" + static var defaultUpgradePriceStringAnnual = "$59.99" + static var defaultUpgradePriceStringMonthly = "$8.99" - static var defaultPriceMonthly = "$4.99" - static var defaultPriceAnnual = "$49.99" + + // Universal Level + static var defaultPriceStringMonthlyPro = "$11.99" + static var defaultPriceStringAnnualPro = "$99.99" + static var defaultPriceStringAnnualPro70Off = "$29.99" + static var defaultPriceSubStringAnnualPro = "$8.33" + static var defaultUpgradePriceStringAnnualPro = "$99.99" + static var defaultUpgradePriceStringMonthlyPro = "$11.99" + + @discardableResult + func loadSubscriptions(type: SubscriptionType) async -> [InternalSubscription]? { + if let subscriptions = cachedSubscriptions[type], !subscriptions.isEmpty { + return subscriptions + } + cachedSubscriptions[type] = await _loadSubscriptions(productIds: Set(type.productIds)) + return cachedSubscriptions[type] + } + + private func _loadSubscriptions(productIds: Set) async -> [InternalSubscription]? { + DDLogInfo("cache localized price for productIds: \(productIds)") + let currencyFormatter = NumberFormatter() + currencyFormatter.usesGroupingSeparator = true + currencyFormatter.numberStyle = .currency + + return await withCheckedContinuation { continuation in + SwiftyStoreKit.retrieveProductsInfo(productIds) { result in + DDLogInfo("retrieve products results: \(result)") + var subs = [InternalSubscription]() + for product in result.retrievedProducts { + + let period = Self.subscriptionPeriod(product: product) + let trialDuration = Self.trialDuraion(for: product.introductoryPrice) + currencyFormatter.locale = product.priceLocale + if let period { + let ip = InternalSubscription(productId: product.productIdentifier, + period: period, + trialDuration: trialDuration, + priceLocale: product.priceLocale, + price: product.price, + offer: product.price) + subs.append(ip) + } + } + continuation.resume(returning: subs) + } + } + } + private override init() { + super.init() + } static func purchase(succeeded: @escaping () -> Void, errored: @escaping (Error) -> Void) { DDLogInfo("purchase") @@ -37,6 +193,7 @@ class VPNSubscription: NSObject { } .done { (getKey: GetKey) in try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64) + BaseUserService.shared.user.resetCache() succeeded() } .catch { error in @@ -49,53 +206,256 @@ class VPNSubscription: NSObject { } } - static func setProductIdPrice(productId: String, price: String?) { - DDLogInfo("Setting product id price for \(productId)") - if price != nil { - UserDefaults.standard.set(price, forKey: productId + "Price") + static func subscriptionPeriod(product: SKProduct) -> SKProduct.PeriodUnit? { + guard let subscriptionPeriod = product.subscriptionPeriod else { + return nil + } + + return subscriptionPeriod.unit + } + + static func setTrialDuration(productId: String, duration: String?) { + let trialKey = productId + "Trial" + + guard let duration else { + UserDefaults.standard.removeObject(forKey: trialKey) + DDLogInfo("Unavaible trial for \(productId)") + return + } + DDLogInfo("Setting trial a duration \(duration) for \(productId)") + UserDefaults.standard.set(duration, forKey: trialKey) + } + + static func setProductIdPrice(productId: String, price: String) { + DDLogInfo("Setting product id price \(price) for \(productId)") + UserDefaults.standard.set(price, forKey: productId + "Price") + } + + static func setProductIdUpgradePrice(productId: String, upgradePrice: String) { + DDLogInfo("Setting product id upgrade price \(upgradePrice) for \(productId)") + UserDefaults.standard.set(upgradePrice, forKey: productId + "UpgradePrice") + } + + static func setProductIdPriceAnnualMonthly(productId: String, price: String) { + DDLogInfo("Setting product id price yearly per month \(price) for \(productId)") + UserDefaults.standard.set(price, forKey: productId + "MonthlyPrice") + } + + static func setProductIdUpgradePriceAnnualMonthly(productId: String, price: String) { + DDLogInfo("Setting product id upgrade price yearly per month\(price) for \(productId)") + UserDefaults.standard.set(price, forKey: productId + "MonthlyUpgradePrice") + } + + enum SubscriptionContext { + case new + case upgrade + case monthlyNew + case monthlyUpgrade + } + + static func getProductIdPrice(productId: String, for context: SubscriptionContext) -> String { + switch context { + case .new: + return getProductIdPrice(productId: productId) + case .upgrade: + return getProductIdUpgradePrice(productId: productId) + case .monthlyNew: + return getProductIdPrice(productId: productId) + case .monthlyUpgrade: + return getProductIdUpgradePrice(productId: productId) + } + } + + static func getProductIdPriceMonthly(productId: String) -> String { + DDLogInfo("Getting product id price yearly per month \(productId)") + if let price = UserDefaults.standard.string(forKey: productId + "MonthlyPrice") { + DDLogInfo("Got product id price yearly per month for \(productId): \(price)") + return price } else { - DDLogError("Invalid nil localizedPrice for productId \(productId), returning default") - if productId == productIdMonthly { - UserDefaults.standard.set(defaultPriceMonthly, forKey: productId + "Price") - } - else if productId == productIdAnnual { - UserDefaults.standard.set(defaultPriceAnnual, forKey: productId + "Price") - } - else { + DDLogError("Found no cached price yearly per month for productId \(productId), returning default") + switch productId { + case productIdAdvancedYearly: + return defaultPriceSubStringAdvancedYearly + case productIdAnnual: + return defaultPriceSubStringAnnual + case productIdAnnualPro: + return defaultPriceSubStringAnnualPro + default: DDLogError("Invalid product Id: \(productId)") + return "Invalid Price" } } } + static func trialDuration(productId: String) -> String? { + UserDefaults.standard.string(forKey: productId + "Trial") + } + static func getProductIdPrice(productId: String) -> String { DDLogInfo("Getting product id price for \(productId)") - if let price = UserDefaults.standard.string(forKey: productId) { + if let price = UserDefaults.standard.string(forKey: productId + "Price") { + DDLogInfo("Got product id price for \(productId): \(price)") return price } else { DDLogError("Found no cached price for productId \(productId), returning default") - if productId == productIdMonthly { - return defaultPriceMonthly - } - else if productId == productIdAnnual { - return defaultPriceAnnual - } - else { + switch productId { + case productIdAdvancedMonthly: + return defaultPriceStringAdvancedMonthly + case productIdAdvancedYearly: + return defaultPriceStringAdvancedYearly + 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" } } } + static func getProductIdUpgradePrice(productId: String) -> String { + DDLogInfo("Getting product id upgrade price for \(productId)") + if let upgradePrice = UserDefaults.standard.string(forKey: productId + "UpgradePrice") { + DDLogInfo("Got product id upgrade price for \(productId): \(upgradePrice)") + return upgradePrice + } + else { + DDLogError("Found no cached upgrade price for productId \(productId), returning default") + switch productId { + case productIdAdvancedMonthly: + return defaultUpgradePriceStringAdvancedMonthly + case productIdAdvancedYearly: + return defaultUpgradePriceStringAdvancedYearly + 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" + } + } + } + static func cacheLocalizedPrices() -> Void { + + let currencyFormatter = NumberFormatter() + currencyFormatter.usesGroupingSeparator = true + currencyFormatter.numberStyle = .currency + DDLogInfo("cache localized price for productIds: \(productIds)") SwiftyStoreKit.retrieveProductsInfo(productIds) { result in DDLogInfo("retrieve products results: \(result)") for product in result.retrievedProducts { - DDLogInfo("product: \(product)") - DDLogInfo("productprice: \(product.localizedPrice)") - setProductIdPrice(productId: product.productIdentifier, price: product.localizedPrice) + DDLogInfo("product locale: \(product.priceLocale)") + DDLogInfo("productprice: \(product.localizedPrice ?? "n/a")") + + if product.productIdentifier == productIdAdvancedMonthly { + if product.localizedPrice != nil { + DDLogInfo("setting productIdAdvancedMonthly display price = " + product.localizedPrice!) + setProductIdPrice(productId: productIdAdvancedMonthly, price: "\(product.localizedPrice!)") + setProductIdUpgradePrice(productId: productIdAdvancedMonthly, upgradePrice: "\(product.localizedPrice!)") + } + else { + DDLogError("monthly nil localizedPrice, setting default") + setProductIdPrice(productId: productIdAdvancedMonthly, price: defaultPriceStringAdvancedMonthly) + setProductIdUpgradePrice(productId: productIdAdvancedMonthly, upgradePrice: defaultUpgradePriceStringAdvancedMonthly) + } + } + else if product.productIdentifier == productIdAdvancedYearly { + if product.localizedPrice != nil { + currencyFormatter.locale = product.priceLocale + let priceMonthly = product.price.dividing(by: 12) + if let priceString = currencyFormatter.string(from: priceMonthly) { + setProductIdPriceAnnualMonthly(productId: productIdAdvancedYearly, price: priceString) + DDLogInfo("setting productIdAdvancedAnnualMonthly display price = " + priceString) + } + DDLogInfo("setting productIdAdvancedYearly display price = " + product.localizedPrice!) + setProductIdPrice(productId: productIdAdvancedYearly, price: "\(product.localizedPrice!)") + setProductIdUpgradePrice(productId: productIdAdvancedYearly, upgradePrice: "\(product.localizedPrice!)") + } + else { + DDLogError("monthly nil localizedPrice, setting default") + setProductIdPrice(productId: productIdAdvancedYearly, price: defaultPriceStringAdvancedYearly) + setProductIdUpgradePrice(productId: productIdAdvancedYearly, upgradePrice: defaultUpgradePriceStringAdvancedYearly) + setProductIdPriceAnnualMonthly(productId: productIdAdvancedYearly, price: defaultPriceSubStringAdvancedYearly) + } + } + else if product.productIdentifier == productIdAnnual { + if product.localizedPrice != nil { + currencyFormatter.locale = product.priceLocale + let priceMonthly = product.price.dividing(by: 12) + if let priceString = currencyFormatter.string(from: priceMonthly) { + setProductIdPriceAnnualMonthly(productId: productIdAnnual, price: priceString) + DDLogInfo("setting productIdAnnualAnnualMonthly display price = " + priceString) + } + DDLogInfo("setting productIdAnnualAnnual display price = annual product price / 12 = " + product.localizedPrice!) + setProductIdPrice(productId: productIdAnnual, price: "\(product.localizedPrice!)") + setProductIdUpgradePrice(productId: productIdAnnual, upgradePrice: "\(product.localizedPrice!)") + } + + else { + DDLogError("unable to format price with currencyformatter: " + product.price.stringValue) + setProductIdPrice(productId: productIdAnnual, price: defaultPriceStringAnnual) + setProductIdUpgradePrice(productId: productIdAnnual, upgradePrice: defaultUpgradePriceStringAnnual) + setProductIdPriceAnnualMonthly(productId: productIdAnnual, price: defaultPriceSubStringAnnual) + } + } + else if product.productIdentifier == productIdMonthly { + if product.localizedPrice != nil { + DDLogInfo("setting productIdMonthly display price = " + product.localizedPrice!) + setProductIdPrice(productId: productIdMonthly, price: "\(product.localizedPrice!)") + setProductIdUpgradePrice(productId: productIdMonthly, upgradePrice: "\(product.localizedPrice!)") + } + else { + DDLogError("monthly nil localizedPrice, setting default") + setProductIdPrice(productId: productIdMonthly, price: defaultPriceStringMonthly) + setProductIdUpgradePrice(productId: productIdMonthly, upgradePrice: defaultUpgradePriceStringMonthly) + } + } + else if product.productIdentifier == productIdMonthlyPro { + if product.localizedPrice != nil { + DDLogInfo("setting productIdMonthlyPro display price = " + product.localizedPrice!) + setProductIdPrice(productId: productIdMonthlyPro, price: "\(product.localizedPrice!)") + setProductIdUpgradePrice(productId: productIdMonthlyPro, upgradePrice: "\(product.localizedPrice!)") + } + else { + DDLogError("monthlyPro nil localizedPrice, setting default") + setProductIdPrice(productId: productIdMonthlyPro, price: defaultPriceStringMonthlyPro) + setProductIdUpgradePrice(productId: productIdMonthlyPro, upgradePrice: defaultUpgradePriceStringMonthlyPro) + } + } + else if product.productIdentifier == productIdAnnualPro { + if product.localizedPrice != nil { + currencyFormatter.locale = product.priceLocale + let priceMonthly = product.price.dividing(by: 12) + if let priceString = currencyFormatter.string(from: priceMonthly) { + DDLogInfo("setting productIdAnnualPro display price = annualPro product price / 12 = " + priceString) + setProductIdPriceAnnualMonthly(productId: productIdAnnualPro, price: priceString) + } + DDLogInfo("setting productIdAnnualPro display price = " + product.localizedPrice!) + setProductIdPrice(productId: productIdAnnualPro, price: "\(product.localizedPrice!)") + setProductIdUpgradePrice(productId: productIdAnnualPro, upgradePrice: "\(product.localizedPrice!)") + } + else { + DDLogError("unable to format price with currencyformatter: " + product.price.stringValue) + setProductIdPrice(productId: productIdAnnualPro, price: defaultPriceStringAnnualPro) + setProductIdUpgradePrice(productId: productIdAnnualPro, upgradePrice: defaultUpgradePriceStringAnnualPro) + setProductIdPriceAnnualMonthly(productId: productIdAnnualPro, price: defaultPriceSubStringAnnualPro) + } + } + setTrialDuration(productId: product.productIdentifier, duration: trialDuraion(for: product.introductoryPrice)) } for invalidProductId in result.invalidProductIDs { DDLogError("invalid product id: \(invalidProductId)"); @@ -103,4 +463,80 @@ class VPNSubscription: NSObject { } } + private static func trialDuraion(for trial: SKProductDiscount?) -> String? { + guard let trial, + trial.paymentMode == .freeTrial else { + return nil + } + let unit = switch trial.subscriptionPeriod.unit { + case .day: NSLocalizedString("day", comment: "day") + case .week: NSLocalizedString("week", comment: "week") + case .month: NSLocalizedString("month", comment: "month") + case .year: NSLocalizedString("year", comment: "year") + } + return "\(trial.subscriptionPeriod.numberOfUnits)" + "-" + unit + } +} + +extension Subscription.PlanType { + var productId: String? { + switch self { + case .advancedMonthly: + return VPNSubscription.productIdAdvancedMonthly + case .advancedAnnual: + return VPNSubscription.productIdAdvancedYearly + case .anonymousMonthly: + return VPNSubscription.productIdMonthly + case .anonymousAnnual: + return VPNSubscription.productIdAnnual + case .universalMonthly: + return VPNSubscription.productIdMonthlyPro + case .universalAnnual: + return VPNSubscription.productIdAnnualPro + default: + return nil + } + } + + static var supported: [Subscription.PlanType] { + return [.advancedMonthly, .advancedAnnual, .anonymousMonthly, .anonymousAnnual, .universalMonthly, .universalAnnual] + } + + var availableUpgrades: [Subscription.PlanType]? { + switch self { + case .advancedMonthly: + return [.advancedAnnual, .anonymousMonthly, .anonymousAnnual, .universalMonthly, .universalAnnual] + case .advancedAnnual: + return [.anonymousMonthly, .anonymousAnnual, .universalMonthly, .universalAnnual] + case .anonymousMonthly: + return [.anonymousAnnual, .universalMonthly, .universalAnnual] + case .anonymousAnnual: + return [.universalMonthly, .universalAnnual] + case .universalMonthly: + return [.universalAnnual] + case .universalAnnual: + return [] + default: + return nil + } + } + + var unavailableToUpgrade: [Subscription.PlanType]? { + guard let upgrades = availableUpgrades else { + return nil + } + + var candidates = Subscription.PlanType.supported + candidates.removeAll(where: { upgrades.contains($0) }) + return candidates + } + + func canUpgrade(to newPlan: Subscription.PlanType) -> Bool { + return availableUpgrades?.contains(newPlan) == true + } +} + +enum PurchasePlacement { + case onboarding + case homeScreen } diff --git a/LockdowniOS/Views/AdvancedPlansViews.swift b/LockdowniOS/Views/AdvancedPlansViews.swift new file mode 100644 index 0000000..404880b --- /dev/null +++ b/LockdowniOS/Views/AdvancedPlansViews.swift @@ -0,0 +1,131 @@ +// +// AdvancedPlansViews.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 29.04.2023. +// + +import UIKit + +final class AdvancedPlansViews: UIView { + + //MARK: Properties + + var isSelected: Bool = false + + lazy var backgroundView: UIView = { + let view = UIView() + view.isUserInteractionEnabled = true + view.layer.cornerRadius = 8 + view.layer.borderWidth = 2 + view.layer.borderColor = isSelected ? UIColor.white.cgColor : UIColor.borderGray.cgColor + + return view + }() + + lazy var iconImageView: UIImageView = { + let image = UIImageView() + image.contentMode = .scaleAspectFit + image.image = isSelected ? UIImage(named: "fill-1") : UIImage(named: "grey-ellipse-1") + image.layer.masksToBounds = true + return image + }() + + lazy var title: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = fontMedium17 + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + lazy var detailTitle: UILabel = { + let label = UILabel() + label.textAlignment = .left + label.textColor = .white + label.font = fontBold17 + return label + }() + + lazy var detailTitle2: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = fontMedium13 + label.numberOfLines = 0 + label.textAlignment = .left + + return label + }() + + lazy var discountImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var titleStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(title) + stackView.addArrangedSubview(iconImageView) + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + stackView.alignment = .center + stackView.spacing = 0 + stackView.anchors.width.equal(130) + + return stackView + }() + + private lazy var detailsStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(detailTitle) + stackView.addArrangedSubview(detailTitle2) + stackView.axis = .vertical + stackView.distribution = .fillProportionally + stackView.alignment = .leading + stackView.spacing = 2 + return stackView + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(titleStackView) + stackView.addArrangedSubview(detailsStackView) + stackView.axis = .vertical + stackView.distribution = .fillEqually + stackView.alignment = .leading + stackView.spacing = 8 + stackView.anchors.height.equal(74) + return stackView + }() + + //MARK: Initialization + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: Functions + + private func configureUI() { + + addSubview(backgroundView) + backgroundView.anchors.edges.pin() + + backgroundView.addSubview(stackView) + stackView.anchors.top.marginsPin(inset: 16) + stackView.anchors.bottom.marginsPin(inset: 16) + stackView.anchors.leading.marginsPin(inset: 16) + stackView.anchors.trailing.pin() + + addSubview(discountImageView) + discountImageView.anchors.top.spacing(-14, to: backgroundView.anchors.bottom) + discountImageView.anchors.centerX.equal(backgroundView.anchors.centerX) + } +} diff --git a/LockdowniOS/Views/AnnualPlanView.swift b/LockdowniOS/Views/AnnualPlanView.swift new file mode 100644 index 0000000..4571395 --- /dev/null +++ b/LockdowniOS/Views/AnnualPlanView.swift @@ -0,0 +1,154 @@ +// +// AnnualPlanView.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 30.04.2023. +// + +import UIKit + +final class AnnualPlanView: UIView { + //MARK: Properties + + lazy var scrollView: UIScrollView = { + let view = UIScrollView() + view.isScrollEnabled = true + return view + }() + + lazy var contentView: UIView = { + let view = UIView() + view.anchors.height.equal(400) + return view + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Unlock Advanced Level Protection", comment: "") + label.textColor = .white + label.font = fontBold34 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("And Get", comment: "") + label.textColor = .white + label.font = fontSemiBold22 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + lazy var bulletView1: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Custom block lists")) + return view + }() + + lazy var bulletView2: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Advanced malware & ads blocking")) + return view + }() + + lazy var bulletView3: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Unlimited blocking")) + return view + }() + + lazy var bulletView4: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "App-specific block lists")) + return view + }() + + lazy var bulletView5: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Advanced encryption protocols")) + return view + }() + + lazy var bulletView6: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Import/Export your own block lists")) + return view + }() + + private lazy var bulletsStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(bulletView1) + stackView.addArrangedSubview(bulletView2) + stackView.addArrangedSubview(bulletView3) + stackView.addArrangedSubview(bulletView4) + stackView.addArrangedSubview(bulletView5) + stackView.addArrangedSubview(bulletView6) + stackView.axis = .vertical + stackView.alignment = .leading + stackView.distribution = .equalSpacing + stackView.spacing = 8 + return stackView + }() + + lazy var firsttimeLabel: UILabel = { + let label = UILabel() + label.font = fontSemiBold13 + label.textAlignment = .center + label.numberOfLines = 0 + let attributedText = NSMutableAttributedString(string: NSLocalizedString("First time subscribers start with a ", comment: ""), attributes: [NSAttributedString.Key.font: fontSemiBold13, NSAttributedString.Key.foregroundColor: UIColor.white]) + attributedText.append(NSAttributedString(string: NSLocalizedString("7-Day Free Trial", comment: ""), attributes: [NSAttributedString.Key.font: fontSemiBold13, NSAttributedString.Key.foregroundColor: UIColor.paywallOrange])) + label.attributedText = attributedText + + return label + }() + + //MARK: Initialization + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: ConfigureUI + private func configureUI() { + + addSubview(firsttimeLabel) + firsttimeLabel.anchors.bottom.pin(inset: 8) + firsttimeLabel.anchors.leading.marginsPin() + firsttimeLabel.anchors.trailing.marginsPin() + + addSubview(scrollView) + scrollView.anchors.top.pin() + scrollView.anchors.leading.pin(inset: 16) + scrollView.anchors.trailing.pin() + scrollView.showsVerticalScrollIndicator = false + scrollView.anchors.bottom.spacing(24, to: firsttimeLabel.anchors.top) + + scrollView.addSubview(contentView) + contentView.anchors.top.pin() + contentView.anchors.centerX.align() + contentView.anchors.width.equal(scrollView.anchors.width) + contentView.anchors.bottom.pin() + + contentView.addSubview(bulletsStackView) + bulletsStackView.anchors.leading.marginsPin() + bulletsStackView.anchors.trailing.marginsPin() + + contentView.addSubview(descriptionLabel) + descriptionLabel.anchors.bottom.spacing(24, to: bulletsStackView.anchors.top) + descriptionLabel.anchors.leading.marginsPin() + descriptionLabel.anchors.trailing.marginsPin() + + contentView.addSubview(titleLabel) + titleLabel.anchors.bottom.spacing(24, to: descriptionLabel.anchors.top) + titleLabel.anchors.leading.marginsPin() + titleLabel.anchors.trailing.marginsPin() + titleLabel.anchors.top.marginsPin() + } +} diff --git a/LockdowniOS/Views/MonthlyPlanView.swift b/LockdowniOS/Views/MonthlyPlanView.swift new file mode 100644 index 0000000..d3e77dd --- /dev/null +++ b/LockdowniOS/Views/MonthlyPlanView.swift @@ -0,0 +1,158 @@ +// +// MonthlyPlanView.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 30.04.2023. +// + +import UIKit + +final class MonthlyPlanView: UIView { + //MARK: Properties + + lazy var scrollView1: UIScrollView = { + let view = UIScrollView() + view.isScrollEnabled = true + return view + }() + + lazy var contentView: UIView = { + let view = UIView() + view.anchors.height.equal(400) + return view + }() + + lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Unlock Advanced Level Protection", comment: "") + label.textColor = .white + label.font = fontBold34 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("And Get", comment: "") + label.textColor = .white + label.font = fontSemiBold22 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + lazy var bulletView1: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Custom block lists")) + return view + }() + + lazy var bulletView2: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Advanced malware & ads blocking")) + return view + }() + + lazy var bulletView3: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Unlimited blocking")) + return view + }() + + lazy var bulletView4: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "App-specific block lists")) + return view + }() + + lazy var bulletView5: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Advanced encryption protocols")) + return view + }() + + lazy var bulletView6: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Import/Export your own block lists")) + return view + }() + + private lazy var bulletsStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(bulletView1) + stackView.addArrangedSubview(bulletView2) + stackView.addArrangedSubview(bulletView3) + stackView.addArrangedSubview(bulletView4) + stackView.addArrangedSubview(bulletView5) + stackView.addArrangedSubview(bulletView6) + stackView.axis = .vertical + stackView.alignment = .leading + stackView.distribution = .equalSpacing + stackView.spacing = 8 + return stackView + }() + + lazy var firsttimeLabel: UILabel = { + let label = UILabel() + label.font = fontSemiBold13 + label.textAlignment = .center + label.numberOfLines = 0 + let attributedText = NSMutableAttributedString(string: NSLocalizedString("First time subscribers start with a ", comment: ""), attributes: [NSAttributedString.Key.font: fontSemiBold13, NSAttributedString.Key.foregroundColor: UIColor.white]) + attributedText.append(NSAttributedString(string: NSLocalizedString("7-Day Free Trial", comment: ""), attributes: [NSAttributedString.Key.font: fontSemiBold13, NSAttributedString.Key.foregroundColor: UIColor.paywallOrange])) + label.attributedText = attributedText + + return label + }() + + + + //MARK: Initialization + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: ConfigureUI + private func configureUI() { + + addSubview(firsttimeLabel) + firsttimeLabel.anchors.leading.marginsPin() + firsttimeLabel.anchors.trailing.marginsPin() + firsttimeLabel.anchors.bottom.pin(inset: 8) + + addSubview(scrollView1) + scrollView1.anchors.top.pin() + scrollView1.anchors.leading.pin(inset: 16) + scrollView1.anchors.trailing.pin() + scrollView1.showsVerticalScrollIndicator = false + scrollView1.anchors.bottom.spacing(24, to: firsttimeLabel.anchors.top) + + scrollView1.addSubview(contentView) + contentView.anchors.top.pin() + contentView.anchors.centerX.align() + contentView.anchors.width.equal(scrollView1.anchors.width) + contentView.anchors.bottom.pin() + + contentView.addSubview(bulletsStackView) + //bulletsStackView.anchors.top.marginsPin() + bulletsStackView.anchors.leading.marginsPin() + bulletsStackView.anchors.trailing.marginsPin() + + contentView.addSubview(descriptionLabel) + descriptionLabel.anchors.bottom.spacing(24, to: bulletsStackView.anchors.top) + descriptionLabel.anchors.leading.marginsPin() + descriptionLabel.anchors.trailing.marginsPin() + + contentView.addSubview(titleLabel) + titleLabel.anchors.bottom.spacing(24, to: descriptionLabel.anchors.top) + titleLabel.anchors.leading.marginsPin() + titleLabel.anchors.trailing.marginsPin() + titleLabel.anchors.top.marginsPin() + + } +} diff --git a/LockdowniOS/WeakObject.swift b/LockdowniOS/WeakObject.swift new file mode 100644 index 0000000..80d7bde --- /dev/null +++ b/LockdowniOS/WeakObject.swift @@ -0,0 +1,16 @@ +// +// WeakObject.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 4.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +final class WeakObject { + + weak var object : T? + + init (_ object: T) { + self.object = object + } +} diff --git a/LockdowniOS/WebViewViewController.swift b/LockdowniOS/WebViewViewController.swift index 4cbf3ac..26100ff 100644 --- a/LockdowniOS/WebViewViewController.swift +++ b/LockdowniOS/WebViewViewController.swift @@ -44,5 +44,4 @@ class WebViewViewController: BaseViewController, WKNavigationDelegate { @IBAction func dismiss() { self.dismiss(animated: true, completion: nil) } - } diff --git a/LockdowniOS/WelcomeView.swift b/LockdowniOS/WelcomeView.swift new file mode 100644 index 0000000..c6d86de --- /dev/null +++ b/LockdowniOS/WelcomeView.swift @@ -0,0 +1,181 @@ +// +// WelcomeView.swift +// LockdownSandbox +// +// Created by Алишер Ахметжанов on 08.05.2023. +// + +import UIKit + +final class WelcomeView: UIView { + + //MARK: Properties + + lazy var backgroundView: UIView = { + let view = UIView() + return view + }() + + lazy var gradientView: UIView = { + let view = UIView() + return view + }() + + private lazy var titleLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("Welcome to \nLockdown 2.0!", comment: "") + label.textColor = .white + label.font = fontBold26 + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + private lazy var imageView: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage(named: "welcome-image") + imageView.contentMode = .scaleAspectFit + return imageView + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("You will notice your Lockdown app looks a bit different. We’ve been working hard on creating a more powerful, usable and informative privacy tool. We are fully open source and open audited (Feb 2023).", comment: "") + label.textColor = .white + label.font = fontMedium15 + label.textAlignment = .left + label.minimumScaleFactor = 0.5 + label.numberOfLines = 0 + return label + }() + + private lazy var subTitleLable: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("With the new Lockdown:", comment: "") + label.textColor = .white + label.font = fontSemiBold22 + label.numberOfLines = 0 + label.textAlignment = .left + return label + }() + + private lazy var bulletView1: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "Your blocking engine is faster and more robust")) + return view + }() + + private lazy var bulletView2: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "You can import + export lists")) + return view + }() + + private lazy var bulletView3: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "You can create your own groups")) + return view + }() + + private lazy var bulletView4: BulletView = { + let view = BulletView() + view.configure(with: BulletViewModel(image: UIImage(named: "Checkbox")!, title: "You can select app specific lists")) + return view + }() + + private lazy var bulletsStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(bulletView1) + stackView.addArrangedSubview(bulletView2) + stackView.addArrangedSubview(bulletView3) + stackView.addArrangedSubview(bulletView4) + stackView.axis = .vertical + stackView.spacing = 10 + return stackView + }() + + private lazy var descriptionLabel2: UILabel = { + let label = UILabel() + label.text = NSLocalizedString("You can access these new firewall features by starting a trial for our new “Advanced” subscription plan. We are hard at work improving these features and fixing any bugs that arise. Please give it a try and reach out with any feedback to team@lockdownhq.com.", comment: "") + label.textColor = .white + label.font = fontMedium15 + label.textAlignment = .left + label.numberOfLines = 0 + return label + }() + + lazy var continueButton: UIButton = { + let button = UIButton(type: .system) + button.tintColor = .white + button.backgroundColor = .tunnelsBlue + button.layer.cornerRadius = 28 + let titleLabel = UILabel() + titleLabel.text = NSLocalizedString("Continue", comment: "") + titleLabel.font = fontSemiBold17 + titleLabel.textColor = .white + titleLabel.textAlignment = .center + + button.addSubview(titleLabel) + titleLabel.anchors.top.pin(inset: 16) + titleLabel.anchors.bottom.pin(inset: 16) + titleLabel.anchors.leading.pin(inset: 24) + titleLabel.anchors.trailing.pin(inset: 24) + button.anchors.height.equal(56) + return button + }() + + //MARK: Initialization + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //MARK: Functions + private func configureUI() { + + addSubview(backgroundView) + backgroundView.anchors.top.pin() + backgroundView.anchors.leading.pin() + backgroundView.anchors.trailing.pin() + backgroundView.anchors.bottom.pin() + + backgroundView.addSubview(titleLabel) + titleLabel.anchors.top.pin(inset: 24) + titleLabel.anchors.centerX.equal(backgroundView.anchors.centerX) + + backgroundView.addSubview(imageView) + imageView.anchors.leading.marginsPin() + imageView.anchors.centerY.equal(titleLabel.anchors.centerY) + + backgroundView.addSubview(descriptionLabel) + descriptionLabel.anchors.top.spacing(16, to: titleLabel.anchors.bottom) + descriptionLabel.anchors.trailing.marginsPin() + descriptionLabel.anchors.leading.marginsPin() + + backgroundView.addSubview(subTitleLable) + subTitleLable.anchors.top.spacing(16, to: descriptionLabel.anchors.bottom) + subTitleLable.anchors.trailing.marginsPin() + subTitleLable.anchors.leading.marginsPin() + + backgroundView.addSubview(bulletsStackView) + bulletsStackView.anchors.top.spacing(8, to: subTitleLable.anchors.bottom) + bulletsStackView.anchors.trailing.marginsPin() + bulletsStackView.anchors.leading.marginsPin() + + backgroundView.addSubview(descriptionLabel2) + descriptionLabel2.anchors.top.spacing(16, to: bulletsStackView.anchors.bottom) + descriptionLabel2.anchors.trailing.marginsPin() + descriptionLabel2.anchors.leading.marginsPin() + + backgroundView.addSubview(continueButton) + continueButton.anchors.bottom.pin(inset: 24) + continueButton.anchors.top.spacing(16, to: descriptionLabel2.anchors.bottom) + continueButton.anchors.leading.marginsPin() + continueButton.anchors.trailing.marginsPin() + } +} diff --git a/LockdowniOS/WelcomeViewController.swift b/LockdowniOS/WelcomeViewController.swift new file mode 100644 index 0000000..35e87a5 --- /dev/null +++ b/LockdowniOS/WelcomeViewController.swift @@ -0,0 +1,48 @@ +// +// WelcomeViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 18.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class WelcomeViewController: UIViewController { + + private lazy var bkgView: UIView = { + let view = UIView() + view.layer.cornerRadius = 15 + return view + }() + + let welcomeView = WelcomeView() + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .clear + view.addSubview(bkgView) + bkgView.anchors.leading.marginsPin() + bkgView.anchors.trailing.marginsPin() + bkgView.anchors.centerY.equal(view.anchors.centerY) + + bkgView.addSubview(welcomeView) + welcomeView.anchors.top.pin() + welcomeView.anchors.leading.pin() + welcomeView.anchors.trailing.pin() + welcomeView.anchors.bottom.pin() + OneTimeActions.markAsSeen(.welcomeScreen) + + welcomeView.continueButton.addTarget(self, action: #selector(dismissed), for: .touchUpInside) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + bkgView.applyGradient(.welcomePurple, corners: .continuous(15.0)) + } + + @objc func dismissed() { + dismiss(animated: false) + } +} diff --git a/LockdowniOS/WhatIsVpnViewController.swift b/LockdowniOS/WhatIsVpnViewController.swift new file mode 100644 index 0000000..1273828 --- /dev/null +++ b/LockdowniOS/WhatIsVpnViewController.swift @@ -0,0 +1,198 @@ +// +// WhatIsVpnViewController.swift +// Lockdown +// +// Created by Johnny Lin on 08/23/19. +// Copyright © 2019 Confirmed. All rights reserved. +// + +import Foundation +import UIKit +import AwesomeSpotlightView +import NicoProgress + +class WhatIsVpnViewController: BaseViewController, AwesomeSpotlightViewDelegate { + + var is4InchIphone = UIDevice.is4InchIphone + + @IBOutlet weak var getStartedButton: UIButton! + @IBOutlet weak var dataFlow: NicoProgressBar! + @IBOutlet weak var descriptionLabel: UILabel! + + @IBOutlet var descriptionLabelHeight: NSLayoutConstraint! + @IBOutlet weak var vpnActiveLabel: UILabel! + var privacyEnabled = false + @IBOutlet weak var locationLabel: UILabel! + @IBOutlet weak var ipLabel: UILabel! + @IBOutlet weak var dataLabel: UILabel! + @IBOutlet weak var toggleCircle: UIButton! + @IBOutlet weak var toggleAnimatedCircle: NVActivityIndicatorView! + @IBOutlet weak var button: UIButton! + +// var parentVC: HomeViewController? = nil + var parentVC: VPNPaywallViewController? = nil + + override func viewDidLoad() { + super.viewDidLoad() + + // iPhone SE + if self.is4InchIphone { + descriptionLabelHeight.constant = 0 + } + + if UserDefaults.hasSeenAnonymousPaywall + || UserDefaults.hasSeenUniversalPaywall { + self.getStartedButton.alpha = 0 + } + + setPrivacyState(state: false) + + dataFlow.primaryColor = .orange + dataFlow.secondaryColor = .tunnelsWarning + } + + @IBAction func getStartedTapped(_ sender: Any) { + let vc = VPNPaywallViewController() + present(vc, animated: true) + } + + @IBAction func learnMoreTapped(_ sender: Any) { + showVPNDetails() + } + + 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 spotlightView = AwesomeSpotlightView(frame: view.frame, + spotlight: [s1]) + spotlightView.cutoutRadius = 8 + spotlightView.spotlightMaskColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75); + spotlightView.enableArrowDown = true + spotlightView.textLabelFont = fontMedium16 + spotlightView.labelSpacing = 24; + spotlightView.delegate = self + view.addSubview(spotlightView) + spotlightView.start() + } + + func setPrivacyState(state: Bool) { + privacyEnabled = state + if (state == true) { + vpnActiveLabel.text = NSLocalizedString("Activating", comment: "").uppercased() + vpnActiveLabel.backgroundColor = .tunnelsBlue + + toggleCircle.isHidden = true + toggleAnimatedCircle.color = .tunnelsBlue + toggleAnimatedCircle.startAnimating() + button.tintColor = .tunnelsBlue + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: { + self.toggleAnimatedCircle.stopAnimating() + + self.toggleCircle.isHidden = false + self.toggleCircle.tintColor = .tunnelsBlue + + self.vpnActiveLabel.text = NSLocalizedString("Tunnel On", comment: "").uppercased() + self.vpnActiveLabel.backgroundColor = .tunnelsBlue + self.locationLabel.text = NSLocalizedString("Location: 🇯🇵", comment: "") + self.ipLabel.text = NSLocalizedString("IP: [Anonymized]", comment: "") + 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: "") + ], + font: fontSemiBold15_5, + bulletFont: fontMedium18, + bullet: "•", + textColor: .tunnelsBlue, + bulletColor: .tunnelsBlue) + }) + } + 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: "") + dataFlow.primaryColor = .orange + dataFlow.secondaryColor = .tunnelsWarning + vpnActiveLabel.text = NSLocalizedString("Tunnel Off", comment: "").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: "") + ], + font: fontSemiBold15_5, + bulletFont: fontMedium18, + bullet: "•", + textColor: .tunnelsWarning, + bulletColor: .tunnelsWarning) + } + } + + @IBAction func privTapped(_ sender: Any) { + if(privacyEnabled == true) { + setPrivacyState(state: false) + } + else { + setPrivacyState(state: true) + } + } + + @IBAction func closeTapped(_ sender: Any) { + self.dismiss(animated: true, completion: nil) + } + + func add(stringList: [String], + font: UIFont, + bulletFont: UIFont, + bullet: String = "\u{2022}", + indentation: CGFloat = 17, + lineSpacing: CGFloat = 1.35, + paragraphSpacing: CGFloat = 6, + 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 paragraphStyle = NSMutableParagraphStyle() + let nonOptions = [NSTextTab.OptionKey: Any]() + paragraphStyle.tabStops = [ + NSTextTab(textAlignment: .left, location: indentation, options: nonOptions)] + paragraphStyle.defaultTabInterval = indentation + paragraphStyle.lineSpacing = lineSpacing + paragraphStyle.paragraphSpacing = paragraphSpacing + paragraphStyle.headIndent = indentation + + let bulletList = NSMutableAttributedString() + for string in stringList { + let formattedString = "\(bullet)\t\(string)\n" + let attributedString = NSMutableAttributedString(string: formattedString) + + attributedString.addAttributes( + [NSAttributedString.Key.paragraphStyle : paragraphStyle], + range: NSMakeRange(0, attributedString.length)) + + attributedString.addAttributes( + textAttributes, + range: NSMakeRange(0, attributedString.length)) + + let string:NSString = NSString(string: formattedString) + let rangeForBullet:NSRange = string.range(of: bullet) + attributedString.addAttributes(bulletAttributes, range: rangeForBullet) + bulletList.append(attributedString) + } + + return bulletList + } +} + diff --git a/LockdowniOS/WhatsNewDescriptionLabel.swift b/LockdowniOS/WhatsNewDescriptionLabel.swift new file mode 100644 index 0000000..a460b16 --- /dev/null +++ b/LockdowniOS/WhatsNewDescriptionLabel.swift @@ -0,0 +1,74 @@ +// +// WhatsNewDescriptionLabel.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 30.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +struct WhatsNewDescriptionLabelViewModel { + let text: String +} + +final class WhatsNewDescriptionLabel: UIView { + + // MARK: - Properties + + private lazy var checkmarkImage: UIImageView = { + let image = UIImageView() + image.image = UIImage(named: "icn_checkmark") + image.contentMode = .left + image.layer.masksToBounds = true + image.isHidden = true + return image + }() + + private lazy var descriptionLabel: UILabel = { + let label = UILabel() + label.textColor = .label + label.font = fontMedium15 + label.textAlignment = .left + return label + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(checkmarkImage) + stackView.addArrangedSubview(descriptionLabel) + stackView.axis = .horizontal + stackView.distribution = .fillProportionally + stackView.alignment = .leading + stackView.spacing = 12 + return stackView + }() + + // MARK: - Initializer + + override init(frame: CGRect) { + super.init(frame: frame) + configureUI() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Functions + + private func configureUI() { + + addSubview(stackView) + stackView.anchors.top.pin() + stackView.anchors.bottom.pin() + stackView.anchors.leading.pin() + stackView.anchors.trailing.pin() + } + + func configure(with model: WhatsNewDescriptionLabelViewModel) { + descriptionLabel.text = model.text + } +} + diff --git a/LockdowniOS/WhatsNewViewController.swift b/LockdowniOS/WhatsNewViewController.swift new file mode 100644 index 0000000..cb1f869 --- /dev/null +++ b/LockdowniOS/WhatsNewViewController.swift @@ -0,0 +1,205 @@ +// +// WhatsNewViewController.swift +// Lockdown +// +// Created by Aliaksandr Dvoineu on 30.05.23. +// Copyright © 2023 Confirmed Inc. All rights reserved. +// + +import UIKit + +class WhatsNewViewController: UIViewController { + + private lazy var navigationView: CustomNavigationView = { + let view = CustomNavigationView() + view.title = NSLocalizedString("What's New", comment: "") + view.titleView.font = .boldLockdownFont(size: 17) + view.buttonTitle = NSLocalizedString("CLOSE", comment: "") + view.onButtonPressed { [unowned self] in + self.closeButtonClicked() + } + return view + }() + + private lazy var headerView: UIView = { + headerView( + withTitle: NSLocalizedString("Lockdown 2.0", comment: "Lockdown title"), + andSubtitle: NSLocalizedString("Lockdown 2.0 brings a variety of new features to the world's first fully audited and Openly Operated privacy app!", comment: "Lockdown subtitle") + ) + }() + + private lazy var protectionLevelsView: UIView = { + let titleLable = UILabel() + titleLable.font = .boldLockdownFont(size: 22) + titleLable.textColor = .label + titleLable.numberOfLines = 0 + titleLable.text = NSLocalizedString("Protection Levels", comment: "") + + let footerLabel = secondaryLabel() + footerLabel.text = NSLocalizedString("The new Advanced level features are available under the Anonymous and Universal plans.", comment: "") + footerLabel.highlight( + NSLocalizedString("Advanced", comment: ""), + NSLocalizedString("Anonymous", comment: ""), + NSLocalizedString("Universal", comment: ""), + font: .boldLockdownFont(size: 15) + ) + + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .leading + stackView.spacing = 8 + stackView.addArrangedSubview(titleLable) + bulletList.forEach { stackView.addArrangedSubview($0) } + stackView.addArrangedSubview(footerLabel) + return stackView + }() + + private lazy var bulletList = [ + bulletView1, + bulletView2, + bulletView3, + bulletView4 + ] + + private lazy var bulletView1: BulletView = { + bulletView( + with: BulletViewModel( + image: UIImage(named: "icn_checkmark_bold")!, + title: NSLocalizedString("Basic allows you to use our firewall, and block custom domains as well as domains in our basic curated lists. ", comment: ""), + highlightedStrings: [NSLocalizedString("Basic", comment: "")] + ) + ) + }() + + private lazy var bulletView2: BulletView = { + bulletView( + with: BulletViewModel( + image: UIImage(named: "icn_checkmark_bold")!, + title: NSLocalizedString("With Advanced protection, you get the benefit of blocking domains in our advanced block lists and creating/importing/exporting block lists.", comment: ""), + highlightedStrings: [NSLocalizedString("Advanced", comment: "")] + ) + ) + }() + + private lazy var bulletView3: BulletView = { + bulletView( + with: + BulletViewModel( + image: UIImage(named: "icn_checkmark_bold")!, + title: NSLocalizedString("VPN subscriptions are now known as Anonymous.", comment: ""), + highlightedStrings: [NSLocalizedString("Anonymous", comment: "")] + ) + ) + }() + + private lazy var bulletView4: BulletView = { + bulletView( + with: BulletViewModel( + image: UIImage(named: "icn_checkmark_bold")!, + title: NSLocalizedString("Pro subscriptions are now known as Universal", comment: ""), + highlightedStrings: [NSLocalizedString("Universal", comment: "")] + ) + ) + }() + + private lazy var footerView: UIView = { + headerView( + withTitle: NSLocalizedString("Blocking Engine", comment: ""), + andSubtitle: NSLocalizedString("The firewall is now much more powerful, efficient, and secure. It also comes with new capabilities like importing/exporting and large curated advanced blocklists.", comment: ""), + titleFontSize: 22 + ) + }() + + private lazy var bulletsStackView: UIStackView = { + let stackView = UIStackView() + stackView.addArrangedSubview(headerView) + stackView.addArrangedSubview(protectionLevelsView) + stackView.addArrangedSubview(footerView) + stackView.axis = .vertical + stackView.alignment = .leading + stackView.distribution = .equalSpacing + stackView.spacing = 24 + return stackView + }() + + override func viewDidLoad() { + super.viewDidLoad() + + configureUI() + } + + //MARK: ConfigureUI + private func configureUI() { + view.backgroundColor = .systemBackground + + view.addSubview(navigationView) + navigationView.anchors.top.safeAreaPin() + navigationView.anchors.leading.pin() + navigationView.anchors.trailing.pin() + + let scrollView = UIScrollView() + view.addSubview(scrollView) + scrollView.anchors.top.spacing(18, to: navigationView.anchors.bottom) + scrollView.anchors.leading.marginsPin() + scrollView.anchors.trailing.marginsPin() + scrollView.anchors.bottom.marginsPin() + + let contentView = UIView() + scrollView.addSubview(contentView) + contentView.anchors.top.pin() + contentView.anchors.leading.pin() + contentView.anchors.trailing.pin() + contentView.anchors.bottom.pin() + contentView.anchors.width.equal(scrollView.anchors.width) + + contentView.addSubview(bulletsStackView) + bulletsStackView.anchors.top.pin() + bulletsStackView.anchors.leading.pin() + bulletsStackView.anchors.trailing.pin() + bulletsStackView.anchors.bottom.pin() + } + + //MARK: Functions + @objc private func closeButtonClicked() { + dismiss(animated: true) + } + + private func headerView( + withTitle title: String, + andSubtitle subtitle: String, + titleFontSize: CGFloat = 26 + ) -> UIView { + let titleLable = UILabel() + titleLable.font = .boldLockdownFont(size: titleFontSize) + titleLable.textColor = .label + titleLable.numberOfLines = 0 + titleLable.text = title + + let subtitleLabel = secondaryLabel() + subtitleLabel.text = subtitle + + let stackView = UIStackView() + stackView.axis = .vertical + stackView.alignment = .leading + stackView.spacing = 16.0 + stackView.addArrangedSubview(titleLable) + stackView.addArrangedSubview(subtitleLabel) + return stackView + } + + private func secondaryLabel() -> UILabel { + let label = UILabel() + label.font = .regularLockdownFont(size: 15) + label.textColor = .label + label.numberOfLines = 0 + return label + } + + private func bulletView(with model: BulletViewModel) -> BulletView { + let view = BulletView() + view.titleLabel.textColor = .label + view.titleLabel.font = fontRegular15 + view.configure(with: model) + return view + } + } diff --git a/LockdowniOS/WhitelistAddCell.swift b/LockdowniOS/WhitelistAddCell.swift index 4daa25d..83f2ca8 100644 --- a/LockdowniOS/WhitelistAddCell.swift +++ b/LockdowniOS/WhitelistAddCell.swift @@ -20,5 +20,4 @@ class WhitelistAddCell: UITableViewCell { } @IBOutlet weak var addWhitelistDomain: UITextField! - } diff --git a/LockdowniOS/WhitelistCell.swift b/LockdowniOS/WhitelistCell.swift index 9f7a452..09343e6 100644 --- a/LockdowniOS/WhitelistCell.swift +++ b/LockdowniOS/WhitelistCell.swift @@ -11,5 +11,4 @@ class WhitelistCell: UITableViewCell { @IBOutlet weak var whitelistDomain: UILabel? @IBOutlet weak var whitelistStatus: UILabel? - } diff --git a/LockdowniOS/WhitelistViewController.swift b/LockdowniOS/WhitelistViewController.swift index 5fc3fa8..f6f1bba 100644 --- a/LockdowniOS/WhitelistViewController.swift +++ b/LockdowniOS/WhitelistViewController.swift @@ -31,22 +31,33 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl @IBAction func save() { self.dismiss(animated: true, completion: { if self.didMakeChange == true { - if VPNController.shared.status() == .connected { + if getIsCombinedBlockListEmpty() { + FirewallController.shared.setEnabled(false, isUserExplicitToggle: true) + } else if VPNController.shared.status() == .connected { FirewallController.shared.restart() } } }) } - func saveNewDomain() { - // TODO: Check it's a valid domain format - if let text = addDomainTextField?.text { - if text.count > 0 { - didMakeChange = true - DDLogInfo("Adding custom whitelist domain - \(text)") - addUserWhitelistedDomain(domain: text.lowercased()) - addDomainTextField!.text = "" - tableView.reloadData() + func saveNewDomain(userEnteredDomainName: String) { + let validation = DomainNameValidator.validate(userEnteredDomainName) + + switch validation { + case .valid: + didMakeChange = true + DDLogInfo("Adding custom whitelist domain - \(userEnteredDomainName)") + addUserWhitelistedDomain(domain: userEnteredDomainName.lowercased()) + addDomainTextField?.text = "" + tableView.reloadData() + case .notValid(let reason): + DDLogWarn("Custom whitelist domain not valid - \(userEnteredDomainName), reason - \(reason)") + 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: "") + ) { + self.addDomainTextField?.becomeFirstResponder() } } } @@ -69,7 +80,7 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl if indexPath.section == 0 { // Add Domain row if indexPath.row == tableView.numberOfRows(inSection: indexPath.section) - 1 { - return 70 + return 75 } else { return 50 @@ -93,14 +104,14 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl 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 = UIFont.init(name: "Montserrat-Medium", size: 14) + label.font = fontMedium14 label.textColor = UIColor.darkGray if section == 0 { - label.text = "Your settings".localized() + label.text = NSLocalizedString("Your Settings", comment: "") } else { - label.text = "Pre-configured Suggestions".localized() + label.text = NSLocalizedString("Pre-configured Suggestions", comment: "") } view.addSubview(label) @@ -199,10 +210,10 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl 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 = "Whitelisted".localized() + cell.whitelistStatus?.text = NSLocalizedString("Whitelisted", comment: "") } else { - cell.whitelistStatus?.text = "Not Whitelisted".localized() + cell.whitelistStatus?.text = NSLocalizedString("Not Whitelisted", comment: "") } return cell } @@ -211,12 +222,17 @@ class WhitelistViewController: BaseViewController, UITableViewDataSource, UITabl @IBAction func textFieldDidEndOnExit(textField: UITextField) { self.dismissKeyboard() - saveNewDomain() + + guard let text = textField.text else { + DDLogError("Text is empty on add domain text field") + return + } + + saveNewDomain(userEnteredDomainName: text) } @objc func didSelectTextField(textField: UITextField) { let addDomainRow = tableView.numberOfRows(inSection: 0) - 1 self.tableView.scrollToRow(at: IndexPath.init(row: addDomainRow, section: 0), at: .middle, animated: true) } - } diff --git a/LockdowniOS/WhyTrustViewController.swift b/LockdowniOS/WhyTrustViewController.swift index 70d432c..4373be0 100644 --- a/LockdowniOS/WhyTrustViewController.swift +++ b/LockdowniOS/WhyTrustViewController.swift @@ -9,7 +9,7 @@ import Foundation import UIKit -class WhyTrustViewController: UIViewController, UIScrollViewDelegate { +class WhyTrustViewController: BaseViewController, UIScrollViewDelegate { @IBOutlet weak var pageControl: UIPageControl! @IBOutlet weak var scrollView: UIScrollView! @@ -42,5 +42,4 @@ class WhyTrustViewController: UIViewController, UIScrollViewDelegate { let pageIndex = round(scrollView.contentOffset.x/view.frame.width) pageControl.currentPage = Int(pageIndex) } - } diff --git a/LockdowniOS/advanced_analytics.txt b/LockdowniOS/advanced_analytics.txt new file mode 100644 index 0000000..7d5b86d --- /dev/null +++ b/LockdowniOS/advanced_analytics.txt @@ -0,0 +1,14 @@ +cdn.mxpnl.com +hotjar.com +script.hotjar.com +googletagmanager.com +firebase-settings.crashlytics.com +vc.hotjar.io +sessions.bugsnag.com +pendo.io +bounceexchange.com +wunderkind.co +hexagon-analytics.com +cdn.attn.tv +bounce.net +brand-metrics.com diff --git a/LockdowniOS/advanced_gaming.txt b/LockdowniOS/advanced_gaming.txt new file mode 100644 index 0000000..d5301d1 --- /dev/null +++ b/LockdowniOS/advanced_gaming.txt @@ -0,0 +1,259 @@ +a4.applovin.com +assets.applovin.com +cdn.liftoff-creatives.io +supersonics.com +outcome-sssp.supersonicads.com +4.adsco.re +6.adsco.re +a4g.com +indexexchange.com +pubmatic.com +media.net +sharethrough.com +rubiconproject.com +onetag.com +appnexus.com +inmobi.com +engagebdr.com +vidoomy.com +loopme.com +thebrave.io +verve.com +spotx.tv +peak226.com +smartadserver.com +spotxchange.com +conversantmedia.com +bidmachine.io +advertising.com +axonix.com +gamaigroup.com +applovin.com +adcolony.com +openx.com +pubnative.net +aps.amazon.com +smaato.com +sonobi.com +yieldmo.com +admanmedia.com +beachfront.com +improvedigital.com +mintegral.com +themediagrid.com +contextweb.com +pokkt.com +rhythmone.com +somoaudience.com +startapp.com +xad.com +mobilefuse.com +adview.com +blis.com +mars.media +videoheroes.tv +smartyads.com +video.unrulymedia.com +webeyemob.com +freewheel.tv +bold-win.com +triplelift.com +adelement.com +gothamads.com +e-planning.net +fyber.com +acd.op.hicloud.com +adx-dra.op.hicloud.com +adx-dre.op.hicloud.com +adx-drru.op.hicloud.com +algorix.co +rhebus.works +smartclip.net +smartstream.tv +ucfunnel.com +undertone.com +xandr.com +ogury.com +velismedia.com +outbrain.com +lkqd.net +tremorhub.com +lemmatechnologies.com +olaex.biz +ssp.e-volution.ai +admixer.co.kr +aralego.com +eskimi.com +appads.in +sovrn.com +synacor.com +lijit.com +betweendigital.com +adform.com +mediafuse.com +meitu.com +criteo.com +bksn.se +iqzone.com +tpmn.io +yandex.com +start.io +adiiix.com +beapup.com +mobismarter.com +advlion.com +growintech.co +agon.mobi +liftoff.io +lkqd.com +yeahmobi.com +sabio.us +admixer.net +prequel.tv +tappx.com +lunamedia.io +districtm.io +ssp.logan.ai +adverty.com +nobid.io +zedo.com +targetspot.com +emxdgt.com +bidscube.com +kubient.com +pubwise.io +gumgum.com +zeststack.com +onairglobal.com +omnifytv.com +limpid.tv +ssp.smartyads.com +152media.info +app-stock.com +mobupps.com +singularads.com +brightcom.com +showheroes.com +9dotsmedia.com +bridgeupp.com +carbonatix.com +krushmedia.com +connekt.ai +minutemedia.com +geofy.ai +33across.com +target.my.com +admixer.com +admixplay.com +bidease.com +exchange.admazing.co +kidoz.net +reforge.in +telaria.com +xapads.com +vungle.com +adswizz.com +adtiming.com +adtonos.com +bidence.com +bigo.sg +blueseasx.com +chartboost.com +consumable.com +flat-ads.com +ignitemediatech.com +pangleglobal.com +tritondigital.com +uis.mobfox.com +emodoinc.com +epom.com +newborntown.com +groundtruth.com +mman.kr +metaxads.com +instal.com +silvermob.com +castify.ai +mobimight.com +elixirvideo.co +ubrikvideo.com +gamoshi.io +hyperad.tech +keenkale.com +my-cast.tv +smartivi.ai +elixirvideo.com +saharmedia.net +streambidmedia.com +brightmountainmedia.com +waardex.com +taipeidigital.com +whildey.com +adstxtmarket.com +alpineinteractivegroup.com +instreamatic.com +adtelligent.com +aniview.com +bizzclick.com +cgnl.io +cmcm.com +decenterads.com +gammassp.com +haxmediapartners.com +mgid.com +mobuppsrtb.com +ninthdecimal.com +penx.com +quantumdex.io +richaudience.com +supply.colossusssp.com +teads.tv +mcoreads.com +zeta.com +madopi.media +movve.com +motionspots.com +milkywase.com +digitalpiee.com +display.io +displayio.cloud +aceex.io +revlift.io +medialink-x.com +thirdpresence.com +acexchange.co.kr +mediaverse.ai +vidazoo.com +risecodes.com +adtarget.com.tr +admatic.com.tr +stroeer.com +springserve.com +imds.tv +stitchvideo.tv +supermidas.com +nativo.com +max-mobi.com +yieldnexus.com +chocolateplatform.com +appbroda.com +ad.plus +adpone.com +connectad.io +adtrue.com +adtech.com +ssp.decenterads.com +pixfuture.com +aolcloud.net +coxmt.com +sunmedia.tv +adagio.io +adyoulike.com +projectagora.com +themoneytizer.com +amxrtb.com +ironsrc.com +adbility-media.com +yieldlab.net +markappmedia.site +orangeclickmedia.com diff --git a/LockdowniOS/amazon_trackers.txt b/LockdowniOS/amazon_trackers.txt new file mode 100644 index 0000000..dae01d0 --- /dev/null +++ b/LockdowniOS/amazon_trackers.txt @@ -0,0 +1 @@ +amazon-adsystem.com diff --git a/LockdowniOS/clickbait.txt b/LockdowniOS/clickbait.txt index 8d12427..0c4cf89 100644 --- a/LockdowniOS/clickbait.txt +++ b/LockdowniOS/clickbait.txt @@ -9,5 +9,6 @@ newsmaxfeednetwork.com outbrain.com revcontent.com taboola.com -zemanta +zemanta.com zergnet.com +img-taboola.com diff --git a/LockdowniOS/crypto_mining.txt b/LockdowniOS/crypto_mining.txt index 95e6e2f..8b13789 100644 --- a/LockdowniOS/crypto_mining.txt +++ b/LockdowniOS/crypto_mining.txt @@ -1,404 +1 @@ -aeon.pool.minergate.com -arcticrise.com -at01.supportxmr.com -at02.supportxmr.com -au.ltcrabbit.com -aus01.supportxmr.com -axiom.br.nicehash.com -axiom.eu.nicehash.com -axiom.hk.nicehash.com -axiom.in.nicehash.com -axiom.jp.nicehash.com -axiom.LOCATION.nicehash.com -axiom.usa.nicehash.com -bcn.pool.minergate.com -bitconnectpool.co -blake256r14.br.nicehash.com -blake256r14.eu.nicehash.com -blake256r14.hk.nicehash.com -blake256r14.in.nicehash.com -blake256r14.jp.nicehash.com -blake256r14.LOCATION.nicehash.com -blake256r14.usa.nicehash.com -blake256r8.br.nicehash.com -blake256r8.eu.nicehash.com -blake256r8.hk.nicehash.com -blake256r8.in.nicehash.com -blake256r8.jp.nicehash.com -blake256r8.LOCATION.nicehash.com -blake256r8.usa.nicehash.com -blake256r8vnl.br.nicehash.com -blake256r8vnl.eu.nicehash.com -blake256r8vnl.hk.nicehash.com -blake256r8vnl.in.nicehash.com -blake256r8vnl.jp.nicehash.com -blake256r8vnl.LOCATION.nicehash.com -blake256r8vnl.usa.nicehash.com -blake2s.LOCATION.nicehash.com -ca01.supportxmr.com -cn.ss.btc.com -cn.stratum.slushpool.com -coins.arstechnica.com -coinspool.cu.cc -cryptonight.br.nicehash.com -cryptonight.eu.nicehash.com -cryptonight.hk.nicehash.com -cryptonight.in.nicehash.com -cryptonight.jp.nicehash.com -cryptonight.LOCATION.nicehash.com -cryptonight.usa.nicehash.com -cure.cryptopools.com -daggerhashimoto.br.nicehash.com -daggerhashimoto.eu.nicehash.com -daggerhashimoto.hk.nicehash.com -daggerhashimoto.in.nicehash.com -daggerhashimoto.jp.nicehash.com -daggerhashimoto.LOCATION.nicehash.com -daggerhashimoto.usa.nicehash.com -dash.suprnova.cc -dash80.suprnova.cc -dcr.coinmine.pl -dcr.pool.mn -dcr.suprnova.cc -de.moriaxmr.com -de01.supportxmr.com -de02.supportxmr.com -de03.supportxmr.com -de2.moriaxmr.com -decred.br.nicehash.com -decred.eu.nicehash.com -decred.hk.nicehash.com -decred.in.nicehash.com -decred.jp.nicehash.com -decred.LOCATION.nicehash.com -decred.usa.nicehash.com -doge.netcodepool.org -dsh.pool.minergate.com -equihash.br.nicehash.com -equihash.eu.nicehash.com -equihash.hk.nicehash.com -equihash.in.nicehash.com -equihash.jp.nicehash.com -equihash.LOCATION.nicehash.com -equihash.usa.nicehash.com -errantshed.co.uk -eu-stratum.btcguild.com -eu.btgpool.pro -eu.ltcrabbit.com -eu.miningfield.com -eu.multipool.us -eu.stratum.bitcoin.cz -eu.stratum.slushpool.com -eu.stratum.viaxmr.com -eu.wafflepool.com -eu.zec.slushpool.com -eu1.altminer.net -eu2.ethermine.org -fcn-dsh.pool.minergate.com -fcn-inf8.pool.minergate.com -fcn-qcn.pool.minergate.com -fcn-xmr.pool.minergate.com -fcn.pool.minergate.com -fr01.supportxmr.com -fr02.supportxmr.com -fr04.supportxmr.com -fr05.supportxmr.com -fr06.supportxmr.com -frankfurt-1.xmrpool.net -ftc.give-me-coins.com -gulf.moneroocean.stream -hash-to-coins.com -hash.rigpool.com -hashbag.cc -hk.ltcrabbit.com -hk01.supportxmr.com -hk02.supportxmr.com -hodl.blockquarry.com -hodl.br.nicehash.com -hodl.eu.nicehash.com -hodl.hk.nicehash.com -hodl.in.nicehash.com -hodl.jp.nicehash.com -hodl.LOCATION.nicehash.com -hodl.usa.nicehash.com -hub.miningpoolhub.com -inf8.pool.minergate.com -iwantcoins.myvnc.com -jp-stratum.btcc.com -jp.lapool.me -jp.stratum.viaxmr.com -keccak.br.nicehash.com -keccak.eu.nicehash.com -keccak.hk.nicehash.com -keccak.in.nicehash.com -keccak.jp.nicehash.com -keccak.LOCATION.nicehash.com -keccak.usa.nicehash.com -kmd.suprnova.cc -lbry.br.nicehash.com -lbry.eu.nicehash.com -lbry.hk.nicehash.com -lbry.in.nicehash.com -lbry.jp.nicehash.com -lbry.LOCATION.nicehash.com -lbry.usa.nicehash.com -litecoinpool.org -louhimo.club -lpool.name -ltc-eu.give-me-coins.com -ltc.coinat.com -ltc.coinfoundry.org -ltc.give-me-coins.com -ltc.pool.minergate.com -ltc.viabtc.com -luckpool.org -lycheebit.com -lyra2re.br.nicehash.com -lyra2re.eu.nicehash.com -lyra2re.hk.nicehash.com -lyra2re.in.nicehash.com -lyra2re.jp.nicehash.com -lyra2re.LOCATION.nicehash.com -lyra2re.usa.nicehash.com -lyra2rev2.br.nicehash.com -lyra2rev2.eu.nicehash.com -lyra2rev2.hk.nicehash.com -lyra2rev2.in.nicehash.com -lyra2rev2.jp.nicehash.com -lyra2rev2.LOCATION.nicehash.com -lyra2rev2.usa.nicehash.com -mcn-dsh.pool.minergate.com -mcn-inf8.pool.minergate.com -mcn-qcn.pool.minergate.com -mcn.pool.minergate.com -mine.cc.st -mine.moneropool.com -mine.xmrpool.net -mine2.coinmine.pl -mineshaft1.cryptopia.co.nz -mining.usa.hypernova.pw -miningpool.thruhere.net -mint.bitminter.com -mmpool.org -monerohash.com -mro.extremepool.org -neoscrypt.br.nicehash.com -neoscrypt.eu.nicehash.com -neoscrypt.hk.nicehash.com -neoscrypt.in.nicehash.com -neoscrypt.jp.nicehash.com -neoscrypt.LOCATION.nicehash.com -neoscrypt.usa.nicehash.com -new.brutum-pool.com -nist5.br.nicehash.com -nist5.eu.nicehash.com -nist5.hk.nicehash.com -nist5.in.nicehash.com -nist5.jp.nicehash.com -nist5.LOCATION.nicehash.com -nist5.usa.nicehash.com -nyc01.supportxmr.com -nyc02.supportxmr.com -nyc03.supportxmr.com -nyc04.supportxmr.com -nyc05.supportxmr.com -p2pool.org -pascal.br.nicehash.com -pascal.eu.nicehash.com -pascal.hk.nicehash.com -pascal.in.nicehash.com -pascal.jp.nicehash.com -pascal.LOCATION.nicehash.com -pascal.usa.nicehash.com -peercoin.ecoining.com -phx01.supportxmr.com -phx02.supportxmr.com -pickaxe.online -pivx.cryptopool.io -pool.chaucha.cl -pool.ekanembtc.com -pool.groupfabric.com -pool.hashbag.cc -pool.ipominer.com -pool.monero.hashvault.pro -pool.paycoinalt.com -pool.stalwartbucks.com -pool.supportxmr.com -pool.zeropool.org -pool1.cryptopoolmining.com -pop.pools.triplehexxx.net -profit.pool.bitcoin.com -qcn.pool.minergate.com -quark.br.nicehash.com -quark.eu.nicehash.com -quark.hk.nicehash.com -quark.in.nicehash.com -quark.jp.nicehash.com -quark.LOCATION.nicehash.com -quark.usa.nicehash.com -qubit.br.nicehash.com -qubit.eu.nicehash.com -qubit.hk.nicehash.com -qubit.in.nicehash.com -qubit.jp.nicehash.com -qubit.LOCATION.nicehash.com -qubit.usa.nicehash.com -ru.moriaxmr.com -sc.f2pool.com -scrypt.br.nicehash.com -scrypt.eu.nicehash.com -scrypt.hk.nicehash.com -scrypt.in.nicehash.com -scrypt.jp.nicehash.com -scrypt.LOCATION.nicehash.com -scrypt.usa.nicehash.com -scryptjanenf16.br.nicehash.com -scryptjanenf16.eu.nicehash.com -scryptjanenf16.hk.nicehash.com -scryptjanenf16.in.nicehash.com -scryptjanenf16.jp.nicehash.com -scryptjanenf16.LOCATION.nicehash.com -scryptjanenf16.usa.nicehash.com -scryptnf.br.nicehash.com -scryptnf.eu.nicehash.com -scryptnf.hk.nicehash.com -scryptnf.in.nicehash.com -scryptnf.jp.nicehash.com -scryptnf.LOCATION.nicehash.com -scryptnf.usa.nicehash.com -sea.wafflepool.com -serenity.hiddenjadestone.com -server.com -sg.stratum.slushpool.com -sg1.supportxmr.com -sha.eobot.com -sha256.br.nicehash.com -sha256.eu.nicehash.com -sha256.hk.nicehash.com -sha256.in.nicehash.com -sha256.jp.nicehash.com -sha256.LOCATION.nicehash.com -sha256.usa.nicehash.com -sia.br.nicehash.com -sia.eu.nicehash.com -sia.hk.nicehash.com -sia.in.nicehash.com -sia.jp.nicehash.com -sia.LOCATION.nicehash.com -sia.usa.nicehash.com -siamining.com -skunk.LOCATION.nicehash.com -solo.antpool.com -solo.ckpool.org -sopool.us -steamoctanepool.com -stratum-eu.coin-miners.info -stratum-zec.antpool.com -stratum.1ex.trade -stratum.aikapool.com -stratum.antpool.com -stratum.asicpool.info -stratum.bcmonster.com -stratum.bitcoin.cz -stratum.bitcoin.inc.hk -stratum.btcc.com -stratum.btcchina.com -stratum.bw.com -stratum.chainworksindustries.com -stratum.ckpool.org -stratum.coinspool.cu.cc -stratum.f2pool.com -stratum.f2xtpool.com -stratum.kano.is -stratum.myBTCcoin.com -stratum.slushpool.com -stratum.solo.prohashing.com -stratum.sye.host -stratum.viaxmr.com -theminingpool.thruhere.net -tsdjuuytw7udw.ru -us-backup.supportxmr.com -us-east.multipool.us -us-east.stratum.bitcoin.cz -us-east.stratum.slushpool.com -us-east.zec.slushpool.com -us-west.multipool.us -us-west.siamining.com -us.clevermining.com -us.litecoinpool.org -us.ltcrabbit.com -us.miningfield.com -us.moriaxmr.com -us.ss.btc.com -us.stratum.viaxmr.com -us1-zcash.flypool.org -us3.wemineltc.com -usa.tompool.org -useast.wafflepool.com -uswest.wafflepool.com -vegas-1.xmrpool.net -vegas-backup.xmrpool.net -vip.antpool.com -vtc.alwayshashing.com -vtc.coinfoundry.org -vtc.poolmining.org -whirlpoolx.br.nicehash.com -whirlpoolx.eu.nicehash.com -whirlpoolx.hk.nicehash.com -whirlpoolx.in.nicehash.com -whirlpoolx.jp.nicehash.com -whirlpoolx.LOCATION.nicehash.com -whirlpoolx.usa.nicehash.com -x11.br.nicehash.com -x11.eu.nicehash.com -x11.hk.nicehash.com -x11.in.nicehash.com -x11.jp.nicehash.com -x11.LOCATION.nicehash.com -x11.usa.nicehash.com -x11gost.br.nicehash.com -x11gost.eu.nicehash.com -x11gost.hk.nicehash.com -x11gost.in.nicehash.com -x11gost.jp.nicehash.com -x11gost.LOCATION.nicehash.com -x11gost.usa.nicehash.com -x13.br.nicehash.com -x13.eu.nicehash.com -x13.hk.nicehash.com -x13.in.nicehash.com -x13.jp.nicehash.com -x13.LOCATION.nicehash.com -x13.usa.nicehash.com -x15.br.nicehash.com -x15.eu.nicehash.com -x15.hk.nicehash.com -x15.in.nicehash.com -x15.jp.nicehash.com -x15.LOCATION.nicehash.com -x15.usa.nicehash.com -xdn.pool.minergate.com -xmr-asia1.nanopool.org -xmr-au1.nanopool.org -xmr-eu.dwarfpool.com -xmr-eu.suprnova.cc -xmr-eu1.nanopool.org -xmr-eu2.nanopool.org -xmr-jp1.nanopool.org -xmr-us-east1.nanopool.org -xmr-us-west1.nanopool.org -xmr-us.mixpools.org -xmr-usa.dwarfpool.com -xmr.crypto-pool.fr -xmr.mixpools.org -xmr.pool.minergate.com -xst.argakiig.us -xxx.xxx.xxx.xxx -xzc.suprnova.cc -yiimp.ccminer.org -ypool.net -zec-hk.f2pool.com -zec.f2pool.com -zec.slushpool.com -zec.suprnova.cc -zmine.io + diff --git a/LockdowniOS/data_trackers.txt b/LockdowniOS/data_trackers.txt new file mode 100644 index 0000000..9a48c2c --- /dev/null +++ b/LockdowniOS/data_trackers.txt @@ -0,0 +1,25 @@ +amplitude.com +branch.io +applovin.com +applvn.com +bluekai.com +chartboost.com +flurry.com +fitanalytics.com +analytics.localytics.com +mob.com +mtrtb.com +newrelic.com +nr-data.net +play.googleapis.com +plugco.in +scarabresearch.com +sdk.foursquare.com +umeng.com +umengcloud.com +voicefive.com +segment.io +api.myendpoint.io +bin5y4muil.execute-api.us-east-1.amazonaws.com +smart-sense.org +eum-appdynamics.com diff --git a/LockdowniOS/email_opens.txt b/LockdowniOS/email_opens.txt index a5d1bc9..d43513d 100644 --- a/LockdowniOS/email_opens.txt +++ b/LockdowniOS/email_opens.txt @@ -4,7 +4,6 @@ app.yesware.com bl-1.com go.rjmetrics.com go.toutapp.com -list-manage.com mailstat.us t.hsms06.com t.yesware.com diff --git a/LockdowniOS/en.lproj/Main.storyboard b/LockdowniOS/en.lproj/Main.storyboard new file mode 100644 index 0000000..1703d2c --- /dev/null +++ b/LockdowniOS/en.lproj/Main.storyboard @@ -0,0 +1,3592 @@ + + + + + + + + + + + + + Montserrat-Bold + + + Montserrat-Medium + + + Montserrat-Regular + + + Montserrat-SemiBold + + + SFProRounded-Bold + + + SFProRounded-Medium + + + SFProRounded-Regular + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LockdowniOS/en.lproj/Main.strings b/LockdowniOS/en.lproj/Main.strings new file mode 100644 index 0000000..6d295c9 --- /dev/null +++ b/LockdowniOS/en.lproj/Main.strings @@ -0,0 +1,443 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "I agree to the"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "Password"; + +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "TODAY"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "Title"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "NOT ACTIVE"; + +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "Learn"; + +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "Warning"; + +/* 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 Annual (Save ~50%)"; + +/* 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."; + +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "Tap to agree to this"; + +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "Not Whitelisted"; + +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "World's Simplest Privacy Policy"; + +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "February 2023"; + +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "Agree"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "CANCEL"; + +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "Accounts get the following benefits:"; + +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "Block Log Disabled"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "Why Trust Lockdown?"; + +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "Pro Annual (Save ~30%)"; + +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "Sign Up"; + +/* 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 Annual supports iPads and iPhones. Double tap using VoiceOver to select."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "Checkbox for \"iOS Annual\" Plan"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "Why Trust 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" = "Instructs user to tap the blue circle to the left of this label to activate."; + +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "A label that says Tap To Activate. Not the actual button."; + +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "Tap To Activate"; + +/* 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" = "CLOSE"; + +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "Restore Purchase"; + +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "Forgot Password?"; + +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "United States - West"; + +/* 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" = "CLOSE"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "Sign In"; + +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "View Log"; + +/* 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" = "Already have a Lockdown account?
Sign in below."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "Email Address"; + +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "Get Started"; + +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "Blocking Enabled"; + +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "Set Region"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "Digital Shield Firewall"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "CANCEL"; + +/* 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" = "CANCEL"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "Button"; + +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "Protect"; + +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "Location: 🇺🇸"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "iPads and 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" = "Block digital intruders"; + +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; + +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "Sign Up"; + +/* 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" = "Privacy Policy"; + +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "ALL TIME"; + +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "iPads, iPhones, and 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 is 100% on-device, so it does not collect or transmit any data to any servers - everything stays on your device."; + +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "Protect Good Connections"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "SAVE"; + +/* 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 is 100% open source, fully audited, and has a strict no-logs policy. Proof of your data protection is in the Privacy Policy."; + +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "Get Started Free"; + +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "Learn More"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "Password"; + +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "Checkbox for \"iOS Monthly\" Plan"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "CLOSE"; + +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "THIS WEEK"; + +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "$49.99/year after (~$4.17/month)"; + +/* 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" = "Block Group Title"; + +/* 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" = "✓ Get new block lists for trackers
✓ Access Lockdown Mac and Desktop
✓ Critical announcements and features"; + +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "Block List"; + +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "Account"; + +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "Account"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "Tap This Button To Activate Secure Tunnel"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "Privacy Policy"; + +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "Set Region"; + +/* 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" = "CANCEL"; + +/* 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 Monthly"; + +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "CANCEL"; + +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "TUNNEL OFF"; + +/* 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" = "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."; + +/* 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 Annual supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "Checkbox for \"Pro Annual\" Plan"; + +/* 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" = "Enter your email below and we'll send you an email to reset your password."; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "Sign In"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "Password"; + +/* 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" = "Tap This Button To Activate 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" = "For fastest speeds, choose a region closest to you. You can also anonymize your IP through other regions."; + +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "Block Log"; + +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "Enhanced Tracking Prevention"; + +/* 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" = "Password must be at least 8 characters, contain at least one uppercase letter, one lowercase letter, one number, and one symbol."; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "Why Trust 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 Monthly supports iPads and iPhones and Macs. Double tap using VoiceOver to select."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "Checkbox for \"Pro Monthly\" Plan"; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "Submit"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "Enable Block Log"; + +/* 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" = "Tap This Button To Activate Secure Tunnel"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "Hide your location & IP"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "NOT ACTIVE"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "Tap This Button To Activate Firewall"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "VIEW OPENAUDIT REPORT"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "You'll automatically be credited for your existing subscription."; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "Terms of Service"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "To: joe@email.com
Re: Q4 2019 Finance Review"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "SAVE"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "iPads and iPhones"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "Forgot Password"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "Sign In"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "Blocked today:"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "CLOSE"; + +/* 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" = "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."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "Email Address"; + +/* 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" = "See How It Works"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "Password"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "Over 1 Billion Trackers Blocked"; + +/* 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" = "Continue"; + +/* 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 encrypts your connections, anonymizes browsing history, and hides your location and unique IP address."; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "Privacy Policy"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "Start 1 Week Free Trial"; + +/* 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" = "Add a domain to whitelist"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "Email Address"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "iOS Monthly"; + +/* 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" = "Browse Safer - Fully Audited Privacy"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "iPad, iPhones, and Macs"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "CANCEL"; diff --git a/LockdowniOS/es.lproj/LaunchScreen.strings b/LockdowniOS/es.lproj/LaunchScreen.strings deleted file mode 100644 index 8b13789..0000000 --- a/LockdowniOS/es.lproj/LaunchScreen.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/LockdowniOS/es.lproj/Localizable.strings b/LockdowniOS/es.lproj/Localizable.strings deleted file mode 100644 index 394596d..0000000 --- a/LockdowniOS/es.lproj/Localizable.strings +++ /dev/null @@ -1,97 +0,0 @@ -/* - Strings.strings - TunnelsiOS - - Copyright © 2018 Confirmed Inc. All rights reserved. -*/ - - -"Up to five devices on any platform" = "Hasta cinco dispositivos en cualquier plataforma"; -"%@ per month after" = "Despues, por %@ al mes"; - -"Up to three of your iPhones and iPads" = "Hasta tres de tus iPhones y iPads"; - -"Up to five devices on any platform for %@ per year" = "Hasta cinco dispositivos en cualquier plataforma"; -"%@ per year after" = "Despues, por %@ al año"; -"Up to three of your iPhones and iPads" = "Hasta tres de tus iPhones y iPads"; - - - -"Hold On..." = "Espere..."; -"Please make sure your Internet connection is active. Otherwise, please e-mail team@confirmedvpn.com" = "Por Favor asegúrese de que su conexión a internet esté activa. De lo contrario, por favor envíe un correo a team@confirmedvpn.com"; -"Continue" = "Continuar"; -"Save & Continue" = "Guardar y Continuar"; -"Get Updates" = "Recibir Actualizaciones"; -"DISCONNECTED" = "DESCONECTADO"; -"Discnonected" = "Desconectado"; -"PROTECTED" = "PROTEGIDO"; -"Protected" = "Protegido"; -"CONNECTING" = "CONECTANDO"; -"Connecting..." = "Conectando..."; -"Disconnecting..." = "Desconectando..."; -"Your settings" = "Configuración personal"; -"Recommended by Confirmed" = "Recomendado por Confirmed"; -"Whitelisted" = "Sitio excluido"; -"Not whitelisted" = "Sitio no excluido"; -"Content Blocker" = "Bloqueo de contenido"; -"Block Tracking Scripts" = "Bloquear procesos de rastreo"; -"Block Social Trackers" = "Bloquear rastreadores sociales"; -"IP" = "Direccion IP"; -"Account" = "Cuenta"; -"Help" = "Ayuda"; -"Privacy" = "Privacidad"; -"Benefits" = "Beneficios"; -"Speed Test" = "Prueba de Velocidad"; -"Install Widget" = "Instalar Widget"; -"Whitelisting" = "Sitios Excluidos"; -"Content Blocker" = "Bloqueo de contenido"; -"Get Secure" = "Asegurar"; -"Block Ads" = "Bloquear anuncios"; -"Your traffic is now encrypted with bank-level 256-bit encryption and your uniquely identifiable IP address is hidden to protect your privacy." = "Su tráfico ahora está encriptado con encriptación de 256 bits a nivel bancario y su dirección IP identificable de manera única, está oculta para proteger su privacidad"; - -"All Devices" = "Todos los dispositivos"; -"iOS Only" = "Solamente iOS"; -"Up to five devices on any platform" = "Hasta cinco dispositivos en cualquier plataforma"; -"Up to three of your iPhones and iPads" = "Hasta tres de tus iPhones y iPads"; -"Monthly" = "Mensual"; -"Annual" = "Anual"; -"No Active Subscription" = "Sin Suscripcion Activa"; -"Loading" = "Cargando"; -"Please make sure your Internet connection is active and that you have an active subscription already. Otherwise, please start your free trial or e-mail team@confirmedvpn.com" = "Por favor asegúrese que su conexión de internet esté activa y que ya tiene una suscripción con nosotros. De lo contrario, comience su prueba gratuita o envíe un correo electrónico team@confirmedvpn.com"; -"Please enter a valid e-mail." = "Por favor ingrese un e-mail valido"; -"Couldn't load plan" = "No se pudo cargar un plan."; -"Couldn't load plan description" = "No se pudo cargar una descripción de plan."; - -"IP Address Visible" = "Direccion IP visible"; -"IP Address Hidden" = "Direccion IP escondida"; - -"Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously." = "Su dirección de IP es un número únicamente identificable para rastrear sus actividades. Confirmed enmascara este número para que pueda navegar de forma anónima."; -"Some Unencrypted Traffic" = "Algo de trafico sin encriptar"; -"Encrypted Traffic" = "Trafico encriptado"; -"Confirmed uses 256-bit encryption to prevent snoopers and ISPs from viewing your data or browsing history." = "Confimed usa una encriptación de 256 bits para prevenir hackeos, espiadores y su proveedor de ver sus datos o historial de búsqueda"; - -"Ads Allowed" = "Anuncios Permitidos"; -"Confirmed provides an ad blocker to speed up your Internet and prevent obtrusive ads from ruining your Internet experience." = "Confirmed provee un bloqueador de anuncios para aumentar la velocidad de su internet y prevenir anuncios innecesarios para mejorar su experiencia."; - -"Tracking Scripts Blocked" = "Guiones de rastreo bloqueados"; -"Tracking Scripts Enabled" = "Guiones de rastreo habilitados"; - -"Many websites include tracking scripts from Facebook, Google, and other sites that allow companies to track you across the Internet. Confirmed blocks these scripts, allowing you to have a privacy-focused experience." = "Muchas páginas incluyen guiones de rastreo de Facebook, Google y otras páginas similares que permiten a compañías rastreen su actividad en el Internet. Confirmed bloquea estos guiones, habilitando que su experiencia se enfoque en privacidad."; - -"To enable, go to Settings > Safari > Content Blocker" = "Para habilitar, ir a configuraciones > Safari > bloqueador de contenido"; - - -"two months free" = "dos meses gratis"; - -"United States - West" = "Estados Unidos - Oeste"; -"United States - East" = "Estados Unidos - Este"; -"United Kingdom" = "Reino Unido"; -"Ireland" = "Irlanda"; -"Germany" = "Alemania"; -"Canada" = "Canada"; -"Japan" = "Japón"; -"Australia" = "Australia"; -"South Korea" = "Corea del Sur"; -"Singapore" = "Singapur"; -"India" = "India"; -"Brazil" = "Brasil"; diff --git a/LockdowniOS/es.lproj/Main.strings b/LockdowniOS/es.lproj/Main.strings index 9de67ee..6790e29 100644 --- a/LockdowniOS/es.lproj/Main.strings +++ b/LockdowniOS/es.lproj/Main.strings @@ -1,306 +1,443 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "Estoy de acuerdo con la"; -/* Class = "UILabel"; text = "*.hulu.com"; ObjectID = "0Ij-sV-FOr"; */ -"0Ij-sV-FOr.text" = "*.hulu.com"; +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "Contraseña"; -/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "0Xb-62-FB6"; */ -"0Xb-62-FB6.normalTitle" = "Política de privacidad"; +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "HOY"; -/* Class = "UILabel"; text = "Up to three of your iPhones and iPads for only $4.99 per month."; ObjectID = "0gF-hr-Rzj"; */ -"0gF-hr-Rzj.text" = "Up to three of your iPhones and iPads for only $4.99 per month."; +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "Título"; -/* Class = "UIButton"; normalTitle = "Sign Out"; ObjectID = "2rP-4I-yso"; */ -"2rP-4I-yso.normalTitle" = "Sign Out"; +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "NOT ACTIVO"; -/* Class = "UILabel"; text = "IP Address Hidden"; ObjectID = "37I-9Y-wgf"; */ -"37I-9Y-wgf.text" = "IP Address Hidden"; +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "Aprender"; -/* Class = "UILabel"; text = "Unknown error."; ObjectID = "3mc-rm-zyi"; */ -"3mc-rm-zyi.text" = "Unknown error."; +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "Advertencia"; -/* Class = "UILabel"; text = "Add E-Mail"; ObjectID = "41J-yY-nDE"; */ -"41J-yY-nDE.text" = "Agregar Correo"; +/* Class = "UIButton"; normalTitle = "􀈂"; ObjectID = "3tZ-rh-ZOa"; */ +"3tZ-rh-ZOa.normalTitle" = "􀈂"; -/* Class = "UILabel"; text = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; ObjectID = "42e-yG-VLg"; */ -"42e-yG-VLg.text" = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; +/* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ +"5Ce-KH-z1j.text" = "iOS Anual (Ahorre ~50%)"; -/* Class = "UIButton"; normalTitle = "Save"; ObjectID = "5aF-Mo-YV9"; */ -"5aF-Mo-YV9.normalTitle" = "Guardar"; +/* 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."; -/* Class = "UILabel"; text = "Plan"; ObjectID = "5en-s8-3Z6"; */ -"5en-s8-3Z6.text" = "Plan"; +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "Toca para aceptar esto"; -/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "6QS-8R-9Zb"; */ -"6QS-8R-9Zb.normalTitle" = "Inicia Sesion"; +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "No Incluido En La Lista Blanca"; -/* Class = "UITextField"; placeholder = "Add domain (i.e. netflix.com)"; ObjectID = "7bS-tB-jYh"; */ -"7bS-tB-jYh.placeholder" = "Agregar dominio (i.e. netflix.com)"; +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "La Política De Privacidad Más Sencilla Del Mundo"; -/* Class = "UIButton"; normalTitle = "Get Two Months Free"; ObjectID = "8ng-qK-etN"; */ -"8ng-qK-etN.normalTitle" = "Obtenga dos meses gratis"; +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "Febrero 2023"; -/* Class = "UITextField"; placeholder = "Email"; ObjectID = "8x7-vO-xGo"; */ -"8x7-vO-xGo.placeholder" = "Email"; +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "De acuerdo"; -/* Class = "UILabel"; text = "Whitelisted"; ObjectID = "9tn-2p-xi9"; */ -"9tn-2p-xi9.text" = "Sitio excluido"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "CANCELAR"; -/* Class = "UILabel"; text = "Try the most trusted way to browse securely and privately."; ObjectID = "Cxt-QW-V1N"; */ -"Cxt-QW-V1N.text" = "Pruebe la manera más confiable para navegar segura y privadamente."; +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "Las cuentas obtienen los siguientes beneficios:"; -/* Class = "UILabel"; text = "IP Address Hidden"; ObjectID = "Dbh-wh-0gV"; */ -"Dbh-wh-0gV.text" = "IP Address Hidden"; +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "Registro De Bloqueos Deshabilitado"; -/* Class = "UILabel"; text = "Confirmed includes a Content Blocker that protects your privacy and increases performance in Safari by blocking invasive code."; ObjectID = "EAR-ME-6mT"; */ -"EAR-ME-6mT.text" = "Confirmed VPN incluye el Bloqueo de contenido que protege su privacidad y aumenta el rendimiento en Safari mediante el bloqueo de código invasivo."; +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "¿Por qué confiar en Lockdown?"; -/* Class = "UILabel"; text = "All Devices"; ObjectID = "ELD-E3-Yae"; */ -"ELD-E3-Yae.text" = "Todos los dispositivos"; +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "Pro Anual (Ahorre ~30%)"; -/* Class = "UITextField"; placeholder = "Email"; ObjectID = "Edz-d7-Ygb"; */ -"Edz-d7-Ygb.placeholder" = "Email"; +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "Regístrate"; -/* Class = "UIButton"; normalTitle = "Save"; ObjectID = "Egx-w0-cwP"; */ -"Egx-w0-cwP.normalTitle" = "Guardar"; +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "VPN Secure Tunnel"; -/* Class = "UILabel"; text = "iOS Only"; ObjectID = "FCU-Ip-FHE"; */ -"FCU-Ip-FHE.text" = "Solamente iOS"; +/* Class = "UIView"; accessibilityHint = "iOS Annual supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityHint" = "iOS Anual es compatible con iPads y iPhones. Toca dos veces con VoiceOver para seleccionar."; -/* Class = "UILabel"; text = "Confirmed VPN is different: our Privacy Policy explicitly and legally prohibits us from logging any personal data or information."; ObjectID = "FGo-KU-kDG"; */ -"FGo-KU-kDG.text" = "Confirmed VPN es diferente; nuestra política de privacidad nos prohíbe, explícita y legalmente, archivar cualquier información o datos personales."; +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "Casilla de Verificación del Plan \"iOS Anual\""; -/* Class = "UILabel"; text = "Plan"; ObjectID = "Fnz-ZP-M8f"; */ -"Fnz-ZP-M8f.text" = "Plan"; +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "¿Por qué confiar en Lockdown?"; -/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "GCK-Lf-jdj"; */ -"GCK-Lf-jdj.normalTitle" = "Conoce Mas"; +/* 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" = "Indica al usuario que toque el círculo azul a la izquierda de esta etiqueta para activar."; -/* Class = "UILabel"; text = "Plan"; ObjectID = "GoC-c8-4Ib"; */ -"GoC-c8-4Ib.text" = "Plan"; +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "Una etiqueta que dice Tocar Para Activar. No es el botón real."; -/* Class = "UILabel"; text = "Email"; ObjectID = "HDP-bQ-VFf"; */ -"HDP-bQ-VFf.text" = "Email"; +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "Tocar Para Activar"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "HbQ-tP-FHd"; */ -"HbQ-tP-FHd.normalTitle" = "×"; +/* Class = "UILabel"; text = "0"; ObjectID = "C0f-ye-V9U"; */ +"C0f-ye-V9U.text" = "0"; -/* Class = "UILabel"; text = "Privacy Policy"; ObjectID = "Ilb-j2-Evl"; */ -"Ilb-j2-Evl.text" = "Política de privacidad"; +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Cjo-hk-1dr"; */ +"Cjo-hk-1dr.normalTitle" = "CERRAR"; -/* Class = "UIButton"; normalTitle = "Get Secure"; ObjectID = "Ju2-He-5w7"; */ -"Ju2-He-5w7.normalTitle" = "Asegurar"; +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "Restaurar Compra"; -/* Class = "UIButton"; normalTitle = "Upgrade To All Devices"; ObjectID = "JzC-HQ-N0r"; */ -"JzC-HQ-N0r.normalTitle" = "Actualizar a todos los dispositivos"; +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "¿Se te olvidó tu contraseña?"; -/* Class = "UILabel"; text = "Please add an e-mail and password to sign in on other devices."; ObjectID = "Kuw-Uq-3Pb"; */ -"Kuw-Uq-3Pb.text" = "Agregue un correo electrónico y una contraseña para iniciar sesión en otros dispositivos."; +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "United States - West"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "LfX-F3-M2C"; */ -"LfX-F3-M2C.normalTitle" = "×"; +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "12:22 PM"; -/* Class = "UILabel"; text = "Certain websites don't operate well with a VPN or specifically block a VPN's use. For the most seamless experience, we recommend not sending this small percentage of traffic through a VPN. This is configurable and you can add or remove sites as you would like."; ObjectID = "NEL-fV-wRa"; */ -"NEL-fV-wRa.text" = "Ciertos sitios web no funcionan bien con una VPN o bloquean específicamente el uso de los VPN. Para la experiencia más fluida, recomendamos no enviar este pequeño porcentaje de tráfico a través de la VPN. Esto es configurable y puede agregar o eliminar sitios como desee."; +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "CERRAR"; -/* Class = "UILabel"; text = "team@confirmedvpn.com"; ObjectID = "NYs-1f-By4"; */ -"NYs-1f-By4.text" = "team@confirmedvpn.com"; +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "Iniciar Sesión"; -/* Class = "UIButton"; normalTitle = "Create Sign In"; ObjectID = "P58-dx-7zu"; */ -"P58-dx-7zu.normalTitle" = "Crear sesion"; +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "Ver Registro"; -/* Class = "UILabel"; text = "Protect Your Data"; ObjectID = "PGC-jx-bKV"; */ -"PGC-jx-bKV.text" = "Protege tus datos"; +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "VPN Secure Tunnel"; -/* Class = "UILabel"; text = "Plan"; ObjectID = "PIq-cK-jjA"; */ -"PIq-cK-jjA.text" = "Plan"; +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "¿Ya tienes una cuenta de Lockdown? Iniciar sesión a continuación."; -/* Class = "UILabel"; text = "DISCONNECTED"; ObjectID = "QT0-vo-fNN"; */ -"QT0-vo-fNN.text" = "DESCONECTADO"; +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "Dirección de Correo Electrónico"; -/* Class = "UILabel"; text = "Our privacy policy is focused on protecting the user and ensuring he or she understands our company and product. To learn more about our privacy policy and how we handle and protect your personal information when you use our services, please tap the link below."; ObjectID = "Uve-eR-XXB"; */ -"Uve-eR-XXB.text" = "Nuestra política de privacidad se centra en proteger al usuario y garantizar que ella o el comprenda nuestra compañía y producto. Para obtener más información, presione el enlace a continuación."; +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "Empezar"; -/* Class = "UILabel"; text = "Add Widget"; ObjectID = "VDZ-qq-YQO"; */ -"VDZ-qq-YQO.text" = "Agregar Widget"; +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "Bloqueo Habilitado"; -/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "VIN-ck-6sP"; */ -"VIN-ck-6sP.normalTitle" = "Registrate"; +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "Establecer Región"; -/* Class = "UILabel"; text = "Get Confirmed on Mac, PC, and Android too for only $9.99 a month"; ObjectID = "VNP-CM-Axq"; */ -"VNP-CM-Axq.text" = "Obtenga Confirmed en Mac, PC y Android también por solo $ 9.99 al mes"; +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "Firewall"; -/* Class = "UILabel"; text = "Confirmed VPN"; ObjectID = "Ve6-h6-unZ"; */ -"Ve6-h6-unZ.text" = "Confirmed VPN"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "CANCELAR"; -/* Class = "UILabel"; text = "Let's set up Confirmed to get the best possible experience."; ObjectID = "ViV-f2-U7q"; */ -"ViV-f2-U7q.text" = "Configuremos Confirmed VPN para obtener la mejor experiencia posible."; +/* Class = "UILabel"; text = "0"; ObjectID = "Msx-Nc-p9M"; */ +"Msx-Nc-p9M.text" = "0"; -/* Class = "UIButton"; normalTitle = "United States - West"; ObjectID = "VmW-aE-Qnf"; */ -"VmW-aE-Qnf.normalTitle" = "United States - West"; +/* Class = "UILabel"; text = "Lockdown"; ObjectID = "NAN-dg-xBj"; */ +"NAN-dg-xBj.text" = "Lockdown"; -/* Class = "UIButton"; normalTitle = "Restore Purchases"; ObjectID = "VuN-rN-BWG"; */ -"VuN-rN-BWG.normalTitle" = "Restaurar compras"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "NvC-GW-2NJ"; */ +"NvC-GW-2NJ.normalTitle" = "CANCELAR"; -/* Class = "UILabel"; text = "Confirmed VPN uses bank-level encryption to stop ISPs, hackers, and snoopers from accessing your personal data."; ObjectID = "Wdy-uC-C21"; */ -"Wdy-uC-C21.text" = "Confirmed VPN utiliza encriptación para evitar que los ISP, hackers y fisgones accedan a sus datos personales."; +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "Botón"; -/* Class = "UILabel"; text = "Open"; ObjectID = "WjY-1n-JrX"; */ -"WjY-1n-JrX.text" = "Open"; +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "Proteger"; -/* Class = "UILabel"; text = "Email"; ObjectID = "Wtx-yP-jGA"; */ -"Wtx-yP-jGA.text" = "Email"; +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "Ubicación: 🇺🇸"; -/* Class = "UILabel"; text = "Sign up for essential updates from Confirmed, including security updates, new features, and ways to protect yourself on the Internet."; ObjectID = "Xoa-mx-q7J"; */ -"Xoa-mx-q7J.text" = "Regístrese para obtener actualizaciones esenciales de Confirmed VPN, incluyendo actualizaciones de seguridad, nuevas funciones y formas de protegerse en Internet."; +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "iPads y iPhones"; -/* Class = "UILabel"; text = "To enable, go to Settings > Safari > Content Blocker"; ObjectID = "Y5p-7G-DfK"; */ -"Y5p-7G-DfK.text" = "To enable, go to Settings > Safari > Content Blocker"; +/* Class = "UIButton"; normalTitle = "􀍟"; ObjectID = "OIa-zY-ALo"; */ +"OIa-zY-ALo.normalTitle" = "􀍟"; -/* Class = "UILabel"; text = "Content Blocker"; ObjectID = "Yrd-Bw-bAW"; */ -"Yrd-Bw-bAW.text" = "Bloquear"; +/* Class = "UIButton"; normalTitle = "􀁝"; ObjectID = "OLt-18-OkW"; */ +"OLt-18-OkW.normalTitle" = "􀁝"; -/* Class = "UILabel"; text = "Confirmed VPN is a service to gain unlimited data for our VPN service. Confirmed subscriptions have a free one week trial, after which you will be charged to your credit card through your iTunes account. Price may vary by location. Your subscription will automatically renew unless canceled at least 24 hours before the end of the current period. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication. Manage Confirmed in Account Settings after the optional upgrade."; ObjectID = "ZIU-Xd-vDj"; */ -"ZIU-Xd-vDj.text" = "Confirmed VPN es un servicio para obtener datos ilimitados desde nuestro servicio VPN. Las suscripciones de Confirmed tienen una prueba gratuita de una semana, después del cual se le cargará a su tarjeta de crédito a través de su cuenta de iTunes. El precio puede variar según la ubicación. Su suscripción se renovará automáticamente a menos que se cancele al menos 24 horas antes del final del período actual. Cualquier porción no utilizada de un período de prueba gratuito, si se ofrece, se perderá cuando el usuario compre una suscripción a esa publicación. Administre Confirmed en la configuración de la cuenta después de la mejora opcional."; +/* Class = "UILabel"; text = "Block Invasive Tracking"; ObjectID = "PGe-EF-Ixy"; */ +"PGe-EF-Ixy.text" = "Bloquear Rastreo Invasivo"; -/* Class = "UIButton"; normalTitle = "Terms & Conditions"; ObjectID = "ZRi-ok-YKh"; */ -"ZRi-ok-YKh.normalTitle" = "Terminos y condiciones"; +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; -/* Class = "UILabel"; text = "Secure your data now with Confirmed, the openly operated VPN audited by countless security professionals. "; ObjectID = "aB1-BA-j8K"; */ -"aB1-BA-j8K.text" = "Asegura tu información ahora con Confirmed; el VPN operado abiertamente que es continuamente auditado por profesionales de seguridad."; +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "Regístrate"; -/* Class = "UILabel"; text = "Label"; ObjectID = "adI-JY-P95"; */ -"adI-JY-P95.text" = "Label"; +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "Lista Blanca"; -/* Class = "UILabel"; text = "Whitelist"; ObjectID = "agz-pH-D2H"; */ -"agz-pH-D2H.text" = "Sitios Excluidos"; +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "Política de Privacidad"; -/* Class = "UILabel"; text = "Confirmed VPN"; ObjectID = "ari-H5-3gJ"; */ -"ari-H5-3gJ.text" = "Confirmed VPN"; +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "TODO EL TIEMPO"; -/* Class = "UILabel"; text = "Confirmed is the only Openly Operated VPN. Our servers are available for public audit, and countless security professionals have verified the security and integrity of our company."; ObjectID = "b81-0P-0Mt"; */ -"b81-0P-0Mt.text" = "Confirmed is the only Openly Operated VPN. Our servers are available for public audit, and countless security professionals have verified the security and integrity of our company."; +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "iPads, iPhones, Y Macs"; -/* Class = "UILabel"; text = "Mac, Windows, iOS, and Android for $9.99 per month"; ObjectID = "ch7-UR-ecd"; */ -"ch7-UR-ecd.text" = "Mac, Windows, iOS, and Android for $9.99 per month"; +/* Class = "UILabel"; text = "3421"; ObjectID = "Sar-GU-wUS"; */ +"Sar-GU-wUS.text" = "3421"; -/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dDl-UE-3ud"; */ -"dDl-UE-3ud.normalTitle" = "Inicia Sesion"; +/* 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 está 100% en el dispositivo, por lo que no recopila ni transmite ningún dato a ningún servidor; todo permanece en su dispositivo."; -/* Class = "UIButton"; normalTitle = "Forgot Password"; ObjectID = "e9e-xo-AtR"; */ -"e9e-xo-AtR.normalTitle" = "Contraseña olvidada"; +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "Proteja Las Buenas Conexiones"; -/* Class = "UIButton"; normalTitle = "Later"; ObjectID = "eM1-ht-UL1"; */ -"eM1-ht-UL1.normalTitle" = "Despues"; +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "GUARDAR"; -/* Class = "UIButton"; normalTitle = "Add Email"; ObjectID = "f2i-dV-64I"; */ -"f2i-dV-64I.normalTitle" = "Agregar Correo"; +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domain-to-whitelist.com"; -/* Class = "UIButton"; normalTitle = "Restore Purchases"; ObjectID = "f2x-Fr-Ead"; */ -"f2x-Fr-Ead.normalTitle" = "Restaurar compras"; +/* 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 es 100% de código abierto, totalmente auditado y tiene una política estricta de no guardar registros. La prueba de la protección de sus datos está en la Política de Privacidad."; -/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "fDL-ho-Zgm"; */ -"fDL-ho-Zgm.normalTitle" = "Button"; +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "Empiece Gratis"; -/* Class = "UIButton"; normalTitle = "Start 1 Week Trial"; ObjectID = "fEA-js-9LV"; */ -"fEA-js-9LV.normalTitle" = "Prueba Gratis"; +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "Aprende Más"; -/* Class = "UILabel"; text = "Loading..."; ObjectID = "fPM-ls-bhY"; */ -"fPM-ls-bhY.text" = "Cargando..."; +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "Contraseña"; -/* Class = "UILabel"; text = "191.23.142.142"; ObjectID = "fhx-Lb-Pnf"; */ -"fhx-Lb-Pnf.text" = "191.23.142.142"; +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "iOS Mensual es compatible con iPads y iPhones. Toca dos veces con VoiceOver para seleccionar."; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "g85-ac-fK4"; */ -"g85-ac-fK4.normalTitle" = "×"; +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "Casilla de verificación para el plan \"iOS Mensual\""; -/* Class = "UILabel"; text = "1"; ObjectID = "gRJ-Rg-jAz"; */ -"gRJ-Rg-jAz.text" = "1"; +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "CERRAR"; -/* Class = "UILabel"; text = "Confirmed VPN"; ObjectID = "geO-1A-pWS"; */ -"geO-1A-pWS.text" = "Confirmed VPN"; +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "ESTA SEMANA"; -/* Class = "UILabel"; text = "1.0"; ObjectID = "gqH-fl-SDW"; */ -"gqH-fl-SDW.text" = "1.0"; +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "$49.99/año después (~$4.17/mes)"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "gvN-v9-bsD"; */ -"gvN-v9-bsD.normalTitle" = "×"; +/* Class = "UILabel"; text = "0"; ObjectID = "XSr-oZ-hkd"; */ +"XSr-oZ-hkd.text" = "0"; -/* Class = "UILabel"; text = "42.4 Mbps"; ObjectID = "hdu-lb-4qp"; */ -"hdu-lb-4qp.text" = "42.4 Mbps"; +/* Class = "UILabel"; text = "Block Group Title"; ObjectID = "XcQ-Zo-7hE"; */ +"XcQ-Zo-7hE.text" = "Título Del Grupo De Bloqueo"; -/* Class = "UILabel"; text = "Swipe to learn why Confirmed is the only VPN you can trust."; ObjectID = "i4b-sZ-Gy4"; */ -"i4b-sZ-Gy4.text" = "Desliza y descubre porque Confirmed es el único VPN en el puedes confiar."; +/* 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" = "✓ Obtenga nuevas listas de bloqueo para rastreadores
✓ Acceda a Lockdown Mac y ordenador
✓ Anuncios y funciones fundamentales"; -/* Class = "UILabel"; text = "Safe"; ObjectID = "jdh-lO-T7i"; */ -"jdh-lO-T7i.text" = "Safe"; +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "Lista De Bloqueos"; -/* Class = "UILabel"; text = "We want our privacy policy to be simple and readable. We are focused on protecting all of our users data and not distributing it with other companies for profit."; ObjectID = "kK5-2Y-8jv"; */ -"kK5-2Y-8jv.text" = "Queremos que nuestra política de privacidad sea simple y legible. Estamos enfocados en proteger todos los datos de nuestros usuarios y no compartirlos con otras compañías con fines de lucro."; +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "Cuenta"; -/* Class = "UILabel"; text = "2"; ObjectID = "kcn-RM-c89"; */ -"kcn-RM-c89.text" = "2"; +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "Cuenta"; -/* Class = "UIButton"; normalTitle = "Later"; ObjectID = "lvj-gp-owd"; */ -"lvj-gp-owd.normalTitle" = "Despues"; +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "Toque Este Botón Para Activar Secure Tunnel"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "pp8-VE-IM6"; */ -"pp8-VE-IM6.normalTitle" = "×"; +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "Política de Privacidad"; -/* Class = "UILabel"; text = "IP Address Hidden"; ObjectID = "pzg-XH-kRa"; */ -"pzg-XH-kRa.text" = "IP Address Hidden"; +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "Establecer Región"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "qG4-8Z-FpB"; */ -"qG4-8Z-FpB.normalTitle" = "×"; +/* Class = "UILabel"; text = "VPN"; ObjectID = "aXL-VV-lJa"; */ +"aXL-VV-lJa.text" = "VPN"; -/* Class = "UILabel"; text = "Block ads"; ObjectID = "qsd-L2-6zG"; */ -"qsd-L2-6zG.text" = "Bloquear anuncios"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "anO-Te-34C"; */ +"anO-Te-34C.normalTitle" = "CANCELAR"; -/* Class = "UILabel"; text = "We do not log, share, or sell any of your website traffic."; ObjectID = "rg7-oq-8EX"; */ -"rg7-oq-8EX.text" = "No registramos, compartimos ni vendemos el tráfico de sus sitios web,"; +/* Class = "UILabel"; text = "Email"; ObjectID = "asx-fS-ufQ"; */ +"asx-fS-ufQ.text" = "Correo Electrónico"; -/* Class = "UITextField"; placeholder = "Password"; ObjectID = "rhT-XQ-ffo"; */ -"rhT-XQ-ffo.placeholder" = "Contraseña"; +/* Class = "UILabel"; text = "🇺🇸"; ObjectID = "bDO-jU-rIH"; */ +"bDO-jU-rIH.text" = "🇺🇸"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "tdt-TT-gDW"; */ -"tdt-TT-gDW.normalTitle" = "×"; +/* Class = "UILabel"; text = "Pro Monthly"; ObjectID = "c6Y-nW-xhv"; */ +"c6Y-nW-xhv.text" = "Pro Mensual"; -/* Class = "UILabel"; text = "Updates"; ObjectID = "two-nE-LPa"; */ -"two-nE-LPa.text" = "Actualizaciones"; +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; -/* Class = "UILabel"; text = "Get Started"; ObjectID = "uJo-T5-FIB"; */ -"uJo-T5-FIB.text" = "Comenzar"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "CANCELAR"; -/* Class = "UILabel"; text = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; ObjectID = "upL-BX-BCS"; */ -"upL-BX-BCS.text" = "Su tráfico ahora está encriptado con encriptación de 256 bits a nivel bancario y su dirección IP identificable de manera única, está oculta para proteger su privacidad."; +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "TUNNEL APAGADO"; -/* Class = "UILabel"; text = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; ObjectID = "vTq-mf-eoG"; */ -"vTq-mf-eoG.text" = "Su tráfico ahora está encriptado con encriptación de 256 bits a nivel bancario y su dirección IP identificable de manera única, está oculta para proteger su privacidad."; +/* 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" = "Las conexiones bloqueadas por Lockdown durante el último día se muestran a continuación. Según nuestra Política de Privacidad, todo el bloqueo se realiza en el dispositivo y nunca se transmite a ningún servidor para su procesamiento.."; -/* Class = "UILabel"; text = "Confirmed VPN"; ObjectID = "vbK-ab-y2a"; */ -"vbK-ab-y2a.text" = "Confirmed VPN"; +/* 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 Anual es compatible con iPads, iPhones y Macs. Toca dos veces con VoiceOver para seleccionar."; -/* Class = "UILabel"; text = "3"; ObjectID = "vvw-R0-IOa"; */ -"vvw-R0-IOa.text" = "3"; +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "Casilla de verificación para plan \"Pro Anual\""; -/* Class = "UILabel"; text = "Upgrade Confirmed to an Annual plan and get two months free."; ObjectID = "wNt-4a-okh"; */ -"wNt-4a-okh.text" = "Actualice a el Plan anual para recibir dos meses gratis"; +/* 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" = "Ingrese su correo electrónico a continuación y le enviaremos un correo electrónico para restablecer su contraseña."; -/* Class = "UILabel"; text = "IP Address Hidden"; ObjectID = "wrZ-aS-efH"; */ -"wrZ-aS-efH.text" = "IP Address Hidden"; +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "Iniciar Sesión"; -/* Class = "UILabel"; text = "Account"; ObjectID = "wty-cI-SuR"; */ -"wty-cI-SuR.text" = "Cuenta"; +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "Contraseña"; -/* Class = "UITextField"; placeholder = "Password"; ObjectID = "xCw-QW-K0K"; */ -"xCw-QW-K0K.placeholder" = "Contraseña"; +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "blocked-domain.com"; -/* Class = "UIButton"; normalTitle = "Setup"; ObjectID = "xMY-0a-SHd"; */ -"xMY-0a-SHd.normalTitle" = "Configurar"; +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "Toque Este Botón Para Activar El Firewall"; -/* Class = "UIButton"; normalTitle = "ⓘ What does this mean?"; ObjectID = "yns-bx-ZkI"; */ -"yns-bx-ZkI.normalTitle" = "ⓘ Que significa esto?"; +/* 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" = "Para velocidades más rápidas, elija una región más cercana a usted. También puede anonimizar su IP a través de otras regiones."; -/* Class = "UILabel"; text = "|"; ObjectID = "yxg-bE-IK7"; */ -"yxg-bE-IK7.text" = "|"; +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "Registro de Bloqueos"; -/* Class = "UILabel"; text = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; ObjectID = "zCp-MF-Ozw"; */ -"zCp-MF-Ozw.text" = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "Prevención De Rastreo Mejorada"; + +/* 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" = "La contraseña debe tener al menos 8 caracteres, contener al menos una letra mayúscula, una letra minúscula, un número y un símbolo."; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "¿Por qué confiar en 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 Mensual es compatible con iPads, iPhones y Macs. Toca dos veces con VoiceOver para seleccionar."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "Casilla de verificación para plan \"Pro Mensual\""; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "Enviar"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "Habilitar Registro De Bloqueos"; + +/* Class = "UILabel"; text = "Email"; ObjectID = "jMa-F3-KUG"; */ +"jMa-F3-KUG.text" = "Correo Electrónico"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "jXy-3G-JaC"; */ +"jXy-3G-JaC.accessibilityLabel" = "Toque Este Botón Para Activar Secure Tunnel"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "Navegación Privada + Ocultar Ubicación e IP"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "NO ACTIVA"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "Toque Este Botón Para Activar El Firewall"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "VER INFORME DE AUDITORÍA"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "Se le acreditará automáticamente su suscripción existente."; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "Términos De Servicio"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "Para: joe@email.com
Re: Q4 2019 Revisión Financiera"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "GUARDAR"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "iPads y iPhones"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "Olvide Mi Contraseña"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Secure Tunnel VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "Iniciar Sesión"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "Bloqueado Hoy:"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "CERRAR"; + +/* 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" = "Algunos sitios o aplicaciones no funcionan bien con VPN. La lista blanca a continuación le permite incluir sitios en la lista blanca para que omitan la VPN para una mejor experiencia."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "Dirección De Correo Electrónico"; + +/* 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" = "Vea Cómo Funciona"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "Contraseña"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "Más De 1 Billón De Rastreadores Bloqueados"; + +/* 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" = "Correo Electrónico"; + +/* Class = "UIButton"; normalTitle = "Continue"; ObjectID = "tzL-mY-IJs"; */ +"tzL-mY-IJs.normalTitle" = "Continuar"; + +/* Class = "UILabel"; text = "Tunnel Whitelist"; ObjectID = "uA3-l4-Qe0"; */ +"uA3-l4-Qe0.text" = "Lista Blanca de 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 cifra sus conexiones, anonimiza el historial de navegación y oculta su ubicación y dirección IP única."; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "Política De Privacidad"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "Comience La Prueba Gratuita De 1 Semana"; + +/* 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" = "Agregar un dominio a la lista blanca"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "Dirección De Correo Electrónico"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "iOS Mensual"; + +/* 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" = "Navegue De Forma Más Segura - Privacidad Totalmente Auditada"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "iPad, iPhones, y Macs"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "CANCELAR"; diff --git a/LockdowniOS/facebook_inc.txt b/LockdowniOS/facebook_inc.txt index f534a5e..de23cf6 100644 --- a/LockdowniOS/facebook_inc.txt +++ b/LockdowniOS/facebook_inc.txt @@ -21,3 +21,4 @@ sac-h-ct-m-fbx.fbsbx.com.online-metrix.net static.ak.facebook.com.edgesuite.net tfbnw.net whatsapp.com +graph.instagram.com \ No newline at end of file diff --git a/LockdowniOS/facebook_sdk.txt b/LockdowniOS/facebook_sdk.txt index 6c9ae4a..cd951c3 100644 --- a/LockdowniOS/facebook_sdk.txt +++ b/LockdowniOS/facebook_sdk.txt @@ -1,3 +1,2 @@ api.facebook.com -connect.facebook.net graph.facebook.com diff --git a/LockdowniOS/fr.lproj/Main.strings b/LockdowniOS/fr.lproj/Main.strings new file mode 100644 index 0000000..6ecb5b6 --- /dev/null +++ b/LockdowniOS/fr.lproj/Main.strings @@ -0,0 +1,445 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "J'accepte le"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "Mot de passe"; + +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "AUJOURD'HUI"; + +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "Titre"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "INACTIF"; + +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "En savoir plus"; + +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "Avertissement"; + +/* 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 Annuel (Economisez ~50%)"; + +/* 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."; + +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "Appuyez pour accepter"; + +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "Pas sur Liste Blanche"; + +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "La politique de confidentialité la plus simple du monde"; + +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "Février 2023"; + +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "Accepter"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "ANNULER"; + +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "Les comptes bénéficient des avantages suivants :"; + +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "Journal de Blocage Désactivé"; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "Pourquoi Faire Confiance à Lockdown ?"; + +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "Pro Annuel (Economisez ~30%)"; + +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "Inscription"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "Tunnel Securisé VPN"; + +/* Class = "UIView"; accessibilityHint = "iOS Annual supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityHint" = "L'abonnement iOS Annuel prend en charge les iPads et les iPhones. Tapez deux fois sur le bouton VoiceOver pour sélectionner."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "Case à cocher pour \"Abonnement iOS Annuel\""; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "Pourquoi Faire Confiance à 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" = "Indique à l'utilisateur de toucher le cercle bleu à gauche de cette étiquette pour l'activer."; + +/* Class = "UILabel"; accessibilityLabel = "A label that says Tap To Activate. Not the actual button."; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.accessibilityLabel" = "Une étiquette qui dit \" Appuyez pour activer \". Pas le bouton lui-même."; + +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "Appuyez pour activer"; + +/* 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" = "FERMER"; + +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "Restaurer l'Achat"; + +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "Mot de passe oublié ?"; + +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "États-Unis - Ouest"; + +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "12h22"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "FERMER"; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "Connexion"; + +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "Afficher Journal"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "Tunnel Securisé VPN"; + +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "Vous avez déjà un compte Lockdown ?
 Connectez-vous ci-dessous."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "Adresse E-mail"; + +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "Démarrer"; + +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "Blocage activé"; + +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "Définir Région"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "Pare-feu"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "ANNULER"; + +/* 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" = "ANNULER"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "Bouton"; + +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "Protéger"; + +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "Localisation : 🇺🇸"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "iPads et 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" = "Bloqueur de Traqueurs Intrusifs"; + +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; + +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "Inscription"; + +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "Liste Blanche"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "Politique de Confidentialité"; + +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "TOUT LE TEMPS"; + +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "iPads, iPhones et 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" = "Le pare-feu Lockdown est 100% sur votre appareil, il ne collecte, ni ne transmet aucune donnée à aucun serveur. Tout reste sur votre appareil."; + +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "Protéger Connexions Sûres"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "SAUVEGARDER"; + +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domaine-en-liste-blanche.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 est 100% open source, intégralement audité et a une politique stricte de non-conservation des logs. Consultez la politique de confidentialité pour en savoir plus."; + +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "Commencez"; + +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "En Savoir Plus"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "Mot de Passe"; + +/* Class = "UIView"; accessibilityHint = "iOS Monthly supports iPads and iPhones. Double tap using VoiceOver to select."; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityHint" = "L'abonnement iOS Mensuel supporte les iPads et iPhones. Tapez deux fois sur le bouton VoiceOver pour sélectionner."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "Case à cocher pour \"Abonnement iOS Mensuel\""; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "FERMER"; + +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "CETTE SEMAINE"; + +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "$49.99/an après (~$4.17/mois)"; + +/* 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" = "Titre du Groupe de Blocage"; + +/* 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" = "✓ Recevez de nouvelles listes de blocage pour les traqueurs +✓ Accédez à Lockdown Mac et Desktop +✓ Recevez les annonces critiques et les dernières fonctionnalités"; + +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "Liste de Blocages"; + +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "Compte"; + +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "Compte"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "Appuyez sur ce bouton pour activer le tunnel sécurisé"; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "Politique de Confidentialité"; + +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "Définir Région"; + +/* 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" = "ANNULER"; + +/* 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 Mensuel"; + +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "ANNULER"; + +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "TUNNEL INACTIF"; + +/* 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" = "Les connexions bloquées par Lockdown pour le dernier jour sont indiquées ci-dessous. Conformément à notre politique de confidentialité, tous les blocages sont effectués sur l'appareil et ne sont jamais transmis à un serveur pour traitement."; + +/* 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" = "L'abonnement Pro Annuel prend en charge les iPads, les iPhones et les Macs. Tapez deux fois sur le bouton VoiceOver pour sélectionner."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "Case à cocher pour \"Abonnement Pro Annuel\""; + +/* 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" = "Entrez votre adresse e-mail ci-dessous et nous vous enverrons un e-mail pour réinitialiser votre mot de passe."; + +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "Connexion"; + +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "Mot de passe"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "domain-bloque.com"; + +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "Appuyez sur ce bouton pour activer le pare-feu"; + +/* 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" = "Pour les vitesses les plus rapides, choisissez la région la plus proche de chez vous. Vous pouvez également anonymiser votre IP en passant par d'autres régions."; + +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "Journal de Blocage"; + +/* Class = "UILabel"; text = "Enhanced Tracking Prevention"; ObjectID = "gHS-VV-2R1"; */ +"gHS-VV-2R1.text" = "Prévention Améliorée du Suivi"; + +/* 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" = "Le mot de passe doit comporter au moins 8 caractères, au moins une lettre majuscule, une lettre minuscule, un chiffre et un symbole."; + +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "h6y-cc-pyT"; */ +"h6y-cc-pyT.normalTitle" = "Pourquoi Faire Confiance à 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" = "L'abonnement Pro Mensuel supporte les iPads, iPhones et Macs. Tapez deux fois sur le bouton VoiceOver pour sélectionner."; + +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Monthly\" Plan"; ObjectID = "hqL-Xy-82O"; */ +"hqL-Xy-82O.accessibilityLabel" = "Case à cocher pour \"Abonnement Pro Mensuel\""; + +/* Class = "UIButton"; normalTitle = "Submit"; ObjectID = "j43-ba-oDJ"; */ +"j43-ba-oDJ.normalTitle" = "Envoyer"; + +/* Class = "UIButton"; normalTitle = "Enable Block Log"; ObjectID = "j7V-kr-ymm"; */ +"j7V-kr-ymm.normalTitle" = "Activer le Journal de Blocage"; + +/* 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" = "Appuyez sur ce bouton pour activer le tunnel sécurisé"; + +/* Class = "UILabel"; text = "Private Browsing + Hide Location & IP"; ObjectID = "ked-BF-PXx"; */ +"ked-BF-PXx.text" = "Navigation privée + Masquer la localisation et l'IP"; + +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "kmJ-b4-n81"; */ +"kmJ-b4-n81.text" = "INACTIF"; + +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "koL-dc-mZr"; */ +"koL-dc-mZr.accessibilityLabel" = "Appuyez Sur Ce Bouton Pour Activer Le Pare-feu"; + +/* Class = "UILabel"; text = "VIEW OPENAUDIT REPORT"; ObjectID = "lDV-k9-rkj"; */ +"lDV-k9-rkj.text" = "VOIR LE RAPPORT D'AUDIT"; + +/* Class = "UILabel"; text = "You'll automatically be credited for your existing subscription."; ObjectID = "lEt-3l-yTH"; */ +"lEt-3l-yTH.text" = "Vous serez automatiquement crédité pour votre abonnement existant."; + +/* Class = "UIButton"; normalTitle = "Terms of Service"; ObjectID = "lOA-PB-b5V"; */ +"lOA-PB-b5V.normalTitle" = "Conditions d'utilisation"; + +/* Class = "UILabel"; text = "To: joe@email.com
Re: Q4 2019 Finance Review"; ObjectID = "mRH-Ie-0qb"; */ +"mRH-Ie-0qb.text" = "To: joe@email.com
Re: Q4 2019 Finance Review"; + +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "mU9-zL-O80"; */ +"mU9-zL-O80.normalTitle" = "SAUVEGARDER"; + +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "naG-mg-g14"; */ +"naG-mg-g14.text" = "iPads et iPhones"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "Mot de passe oublié"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "Tunnel Securisé VPN"; + +/* Class = "UILabel"; text = "Sign In"; ObjectID = "oPf-b7-d1V"; */ +"oPf-b7-d1V.text" = "Connexion"; + +/* Class = "UILabel"; text = "Blocked today:"; ObjectID = "oYx-Wf-i0q"; */ +"oYx-Wf-i0q.text" = "Blocages du jour :"; + +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "pMu-va-97O"; */ +"pMu-va-97O.normalTitle" = "FERMER"; + +/* 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" = "Certains sites ou applications ne fonctionnent pas bien avec les VPN. La liste des sites ci-dessous contournent le VPN pour une meilleure expérience."; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "pXd-nf-bzR"; */ +"pXd-nf-bzR.placeholder" = "Adresse E-mail"; + +/* Class = "UILabel"; text = "whitelisted-domain.com"; ObjectID = "pao-zq-e98"; */ +"pao-zq-e98.text" = "domain-en-liste-blanche.com"; + +/* Class = "UIButton"; normalTitle = "See How It Works"; ObjectID = "qBb-qF-2nJ"; */ +"qBb-qF-2nJ.normalTitle" = "Voir Le Fonctionnement"; + +/* Class = "UILabel"; text = "Password"; ObjectID = "rCb-D1-AbS"; */ +"rCb-D1-AbS.text" = "Mot de passe"; + +/* Class = "UILabel"; text = "Over 1 Billion Trackers Blocked"; ObjectID = "rJd-Ql-Mu9"; */ +"rJd-Ql-Mu9.text" = "Plus d'un milliard de traqueurs bloqués"; + +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "rJk-m6-qqA"; */ +"rJk-m6-qqA.text" = "domaine-bloque.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" = "Continue"; + +/* Class = "UILabel"; text = "Tunnel Whitelist"; ObjectID = "uA3-l4-Qe0"; */ +"uA3-l4-Qe0.text" = "Liste Blanche"; + +/* 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" = "Le tunnel sécurisé crypte vos connexions, anonymise votre navigation, masque votre localisation et votre adresse IP unique."; + +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "v8T-bw-mkT"; */ +"v8T-bw-mkT.normalTitle" = "Politique de Confidentialité"; + +/* Class = "UIButton"; normalTitle = "Start 1 Week Free Trial"; ObjectID = "v8r-0N-ycQ"; */ +"v8r-0N-ycQ.normalTitle" = "Démarrer 1 semaine d'essai gratuit"; + +/* 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" = "Ajouter un domaine à la liste blanche"; + +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "wPU-g5-s3s"; */ +"wPU-g5-s3s.placeholder" = "Adresse E-mail"; + +/* Class = "UILabel"; text = "iOS Monthly"; ObjectID = "xt8-uP-JgS"; */ +"xt8-uP-JgS.text" = "iOS Mensuel"; + +/* Class = "UILabel"; text = "Firewall"; ObjectID = "yIB-Tk-mf1"; */ +"yIB-Tk-mf1.text" = "Pare-feu"; + +/* Class = "UILabel"; text = "Browse Safer - Fully Audited Privacy"; ObjectID = "yXI-U1-vLz"; */ +"yXI-U1-vLz.text" = "Naviguer en toute sécurité - Protection de la vie privée vérifiée"; + +/* Class = "UILabel"; text = "iPad, iPhones, and Macs"; ObjectID = "yvR-wl-rIt"; */ +"yvR-wl-rIt.text" = "iPad, iPhones, et Macs"; + +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "ziI-ly-5qb"; */ +"ziI-ly-5qb.normalTitle" = "ANNULER"; diff --git a/LockdowniOS/game_ads.txt b/LockdowniOS/game_ads.txt new file mode 100644 index 0000000..52c2a73 --- /dev/null +++ b/LockdowniOS/game_ads.txt @@ -0,0 +1,7 @@ +ads.mopub.com +ads.nexage.com +vungle.com +moatads.com +tenjin.io +voodoo-analytics.io +anzuinfra.com diff --git a/LockdowniOS/general_ads.txt b/LockdowniOS/general_ads.txt new file mode 100644 index 0000000..cef442d --- /dev/null +++ b/LockdowniOS/general_ads.txt @@ -0,0 +1,21 @@ +rayjump.com +mopub.com +unityads.unity3d.com +ssacdn.com +supersonic.com +supersonicads.com +starbolt.io +tapjoy.com +tapjoyads.com +comscoreresearch.com +dianomi.com +g.doubleclick.net +static.doubleclick.net +jetpackdigital.com +adtechus.com +atwola.com +brealtime.com +concert.io +revjet.com +rs-stripe.com +specless.tech diff --git a/LockdowniOS/google_shopping_ads.txt b/LockdowniOS/google_shopping_ads.txt new file mode 100644 index 0000000..c518151 --- /dev/null +++ b/LockdowniOS/google_shopping_ads.txt @@ -0,0 +1,3 @@ +googleadservices.com +ad.doubleclick.net +adservice.google.com diff --git a/LockdowniOS/ifunny_trackers.txt b/LockdowniOS/ifunny_trackers.txt new file mode 100644 index 0000000..f1525ab --- /dev/null +++ b/LockdowniOS/ifunny_trackers.txt @@ -0,0 +1,252 @@ +a4g.com +indexexchange.com +pubmatic.com +media.net +sharethrough.com +rubiconproject.com +onetag.com +appnexus.com +inmobi.com +engagebdr.com +vidoomy.com +loopme.com +thebrave.io +verve.com +spotx.tv +peak226.com +smartadserver.com +spotxchange.com +conversantmedia.com +bidmachine.io +advertising.com +axonix.com +gamaigroup.com +applovin.com +adcolony.com +openx.com +pubnative.net +aps.amazon.com +smaato.com +sonobi.com +yieldmo.com +admanmedia.com +beachfront.com +improvedigital.com +mintegral.com +themediagrid.com +contextweb.com +pokkt.com +rhythmone.com +somoaudience.com +startapp.com +xad.com +mobilefuse.com +adview.com +blis.com +mars.media +videoheroes.tv +smartyads.com +video.unrulymedia.com +webeyemob.com +freewheel.tv +bold-win.com +triplelift.com +adelement.com +gothamads.com +e-planning.net +fyber.com +acd.op.hicloud.com +adx-dra.op.hicloud.com +adx-dre.op.hicloud.com +adx-drru.op.hicloud.com +algorix.co +rhebus.works +smartclip.net +smartstream.tv +ucfunnel.com +undertone.com +xandr.com +ogury.com +velismedia.com +outbrain.com +lkqd.net +tremorhub.com +lemmatechnologies.com +olaex.biz +ssp.e-volution.ai +admixer.co.kr +aralego.com +eskimi.com +appads.in +sovrn.com +synacor.com +lijit.com +betweendigital.com +adform.com +mediafuse.com +meitu.com +criteo.com +bksn.se +iqzone.com +tpmn.io +yandex.com +start.io +adiiix.com +beapup.com +mobismarter.com +advlion.com +growintech.co +agon.mobi +liftoff.io +lkqd.com +yeahmobi.com +sabio.us +admixer.net +prequel.tv +tappx.com +lunamedia.io +districtm.io +ssp.logan.ai +adverty.com +nobid.io +zedo.com +targetspot.com +emxdgt.com +bidscube.com +kubient.com +pubwise.io +gumgum.com +zeststack.com +onairglobal.com +omnifytv.com +limpid.tv +ssp.smartyads.com +152media.info +app-stock.com +mobupps.com +singularads.com +brightcom.com +showheroes.com +9dotsmedia.com +bridgeupp.com +carbonatix.com +krushmedia.com +connekt.ai +minutemedia.com +geofy.ai +33across.com +target.my.com +admixer.com +admixplay.com +bidease.com +exchange.admazing.co +kidoz.net +reforge.in +telaria.com +xapads.com +vungle.com +adswizz.com +adtiming.com +adtonos.com +bidence.com +bigo.sg +blueseasx.com +chartboost.com +consumable.com +flat-ads.com +ignitemediatech.com +pangleglobal.com +tritondigital.com +uis.mobfox.com +emodoinc.com +epom.com +newborntown.com +groundtruth.com +mman.kr +metaxads.com +instal.com +silvermob.com +castify.ai +mobimight.com +elixirvideo.co +ubrikvideo.com +gamoshi.io +hyperad.tech +keenkale.com +my-cast.tv +smartivi.ai +elixirvideo.com +saharmedia.net +streambidmedia.com +brightmountainmedia.com +waardex.com +taipeidigital.com +whildey.com +adstxtmarket.com +alpineinteractivegroup.com +instreamatic.com +adtelligent.com +aniview.com +bizzclick.com +cgnl.io +cmcm.com +decenterads.com +gammassp.com +haxmediapartners.com +mgid.com +mobuppsrtb.com +ninthdecimal.com +penx.com +quantumdex.io +richaudience.com +supply.colossusssp.com +teads.tv +mcoreads.com +zeta.com +madopi.media +movve.com +motionspots.com +milkywase.com +digitalpiee.com +display.io +displayio.cloud +aceex.io +revlift.io +medialink-x.com +thirdpresence.com +acexchange.co.kr +mediaverse.ai +vidazoo.com +risecodes.com +adtarget.com.tr +admatic.com.tr +stroeer.com +springserve.com +imds.tv +stitchvideo.tv +supermidas.com +nativo.com +max-mobi.com +yieldnexus.com +chocolateplatform.com +appbroda.com +ad.plus +adpone.com +connectad.io +adtrue.com +adtech.com +ssp.decenterads.com +pixfuture.com +aolcloud.net +coxmt.com +sunmedia.tv +adagio.io +adyoulike.com +projectagora.com +themoneytizer.com +amxrtb.com +ironsrc.com +adbility-media.com +yieldlab.net +markappmedia.site +orangeclickmedia.com diff --git a/LockdowniOS/ja.lproj/LaunchScreen.strings b/LockdowniOS/ja.lproj/LaunchScreen.strings deleted file mode 100644 index 8b13789..0000000 --- a/LockdowniOS/ja.lproj/LaunchScreen.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/LockdowniOS/ja.lproj/Localizable.strings b/LockdowniOS/ja.lproj/Localizable.strings deleted file mode 100644 index 0f6b09e..0000000 --- a/LockdowniOS/ja.lproj/Localizable.strings +++ /dev/null @@ -1,94 +0,0 @@ -/* - Strings.strings - TunnelsiOS - - Copyright © 2018 Confirmed Inc. All rights reserved. -*/ - - -"Up to five devices on any platform for %@ per month" = "月額%@でお持ちのデバイス5台までインストール可能です。"; -"Up to three of your iPhones and iPads for %@ per month" = "月額%@でお持ちのiPhone又はiPad3台までインストール可能です。"; - -"Up to five devices on any platform for %@ per year" = "年額%@でOSを問わずデバイス5台まで使用可能"; -"Up to three of your iPhones and iPads for %@ per year" = "月額%@でiPhoneまたはiPad3台まで使用可能です。"; - - -"Hold On..." = "ご確認ください"; -"Please make sure your Internet connection is active. Otherwise, please e-mail team@confirmedvpn.com" = "インターネット接続が正常に作動している事をご確認ください。インターネット接続が正常にも関わらずVPNが動作しない場合、team@confirmedvpn.comまでご連絡ください。"; -"Continue" = "続ける"; -"Save & Continue" = "セーブして続ける"; -"Get Updates" = "アップデートする"; -"DISCONNECTED" = "接続されていません"; -"Discnonected" = "接続されていません"; -"PROTECTED" = "保護されています"; -"Protected" = "保護されています"; -"CONNECTING" = "接続中です"; -"Connecting..." = "接続中です..."; -"Disconnecting..." = "接続を解除しています..."; -"Your settings" = "設定"; -"Recommended by Confirmed" = "ホワイトリスト推奨サイト"; -"Whitelisted" = "追加済み"; -"Not whitelisted" = "未設定"; -"Content Blocker" = "コンテンツブロッカー"; -"Block Tracking Scripts" = "追跡プログラムをブロック"; -"Block Social Trackers" = "ソーシャルトラッカーをブロック"; -"IP" = "Direccion IP"; -"Account" = "アカウント"; -"Help" = "ヘルプ"; -"Privacy" = "個人情報"; -"Benefits" = "特徴"; -"Speed Test" = "速度計測"; -"Install Widget" = "ウィジェット"; -"Whitelisting" = "ホワイトリスト"; -"Content Blocker" = "コンテンツブロッカー"; -"Get Secure" = "安全性を手に入れる"; -"Block Ads" = "広告をブロック"; -"Your traffic is now encrypted with bank-level 256-bit encryption and your uniquely identifiable IP address is hidden to protect your privacy." = "貴方の通信は金融機関レベルのAES-256bitで守られます。IPアドレスもプライバシーの保護の為に秘匿されます。"; - -"All Devices" = "マルチデバイス版"; -"iOS Only" = "iOS版"; -"Up to five devices on any platform" = "OSを問わずデバイス5台まで使用可能"; -"Up to three of your iPhones and iPads" = "iPhoneまたはiPad3台まで使用可能です。"; -"Monthly" = "月額プラン"; -"Annual" = "年額プラン"; -"No Active Subscription" = "現在購読がありません"; -"Loading" = "Cargando"; -"Please make sure your Internet connection is active and that you have an active subscription already. Otherwise, please start your free trial or e-mail team@confirmedvpn.com" = "インターネット接続が正常に作動している事をご確認ください。インターネット接続が正常にも関わらずVPNが動作しない場合、team@confirmedvpn.comまでご連絡ください。"; -"Please enter a valid e-mail." = "無効なメールアドレスのようです。"; -"Couldn't load plan" = "プランをロード出来ませんでした。"; -"Couldn't load plan description" = "プランの詳細をロード出来ませんでした -"; - -"IP Address Visible" = " IPアドレスが見える状態です"; -"IP Address Hidden" = "IPアドレスが秘匿されています"; - -"Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously." = "IPアドレスは、貴方の通信元を示す文字列です。Confirmedはこれを不可視化し貴方の通信を秘匿します。"; -"Some Unencrypted Traffic" = " 暗号化されていない通信"; -"Encrypted Traffic" = "暗号化されている通信"; -"Confirmed uses 256-bit encryption to prevent snoopers and ISPs from viewing your data or browsing history." = "Confirmedは256ビットの暗号化技術を持ちいて貴方の通信をハッカーやインターネットサービスプロバイダーから守ります。"; - -"Ads Allowed" = "広告を許可"; -"Confirmed provides an ad blocker to speed up your Internet and prevent obtrusive ads from ruining your Internet experience." = "Confirmedのアドブロッカーは邪魔な広告をブロックして通信速度を上げ、貴方の快適なブラウジングを助けます。"; - -"Tracking Scripts Blocked" = "追跡プログラムをブロック"; -"Tracking Scripts Enabled" = "追跡プログラムを許可"; - -"Many websites include tracking scripts from Facebook, Google, and other sites that allow companies to track you across the Internet. Confirmed blocks these scripts, allowing you to have a privacy-focused experience." = "多くのウェブサイトにはGoogleやFacebook等の企業が提供する追跡プログラムが含まれています。Confirmedはこれらから貴方のプライバシーを守ります。"; - -"To enable, go to Settings > Safari > Content Blocker" = "有効にする為には、設定>Safari>コンテンツブロッカーへとお進みください。"; - - -"two months free" = "2ヶ月間無料"; - -"United States - West" = "米国西部"; -"United States - East" = "米国東部"; -"United Kingdom" = "英国"; -"Ireland" = "アイルランド"; -"Germany" = "ドイツ"; -"Canada" = "カナダ"; -"Japan" = "日本"; -"Australia" = "オーストラリア"; -"South Korea" = "韓国"; -"Singapore" = "シンガポール"; -"India" = "インド"; -"Brazil" = "ブラジル"; diff --git a/LockdowniOS/ja.lproj/Main.strings b/LockdowniOS/ja.lproj/Main.strings index 0d31fe1..88809f6 100644 --- a/LockdowniOS/ja.lproj/Main.strings +++ b/LockdowniOS/ja.lproj/Main.strings @@ -1,306 +1,443 @@ +/* Class = "UILabel"; text = "I agree to the"; ObjectID = "04y-ib-kAj"; */ +"04y-ib-kAj.text" = "に同意する。"; -/* Class = "UILabel"; text = "*.hulu.com"; ObjectID = "0Ij-sV-FOr"; */ -"0Ij-sV-FOr.text" = "*.hulu.com"; +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "0XQ-Qd-SAy"; */ +"0XQ-Qd-SAy.placeholder" = "パスワード"; -/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "0Xb-62-FB6"; */ -"0Xb-62-FB6.normalTitle" = "個人情報保護方針"; +/* Class = "UILabel"; text = "TODAY"; ObjectID = "0id-50-Eu3"; */ +"0id-50-Eu3.text" = "本日"; -/* Class = "UILabel"; text = "Up to three of your iPhones and iPads for only $4.99 per month."; ObjectID = "0gF-hr-Rzj"; */ -"0gF-hr-Rzj.text" = "Up to three of your iPhones and iPads for only $4.99 per month."; +/* Class = "UILabel"; text = "Title"; ObjectID = "0m3-NA-IPB"; */ +"0m3-NA-IPB.text" = "タイトル"; -/* Class = "UIButton"; normalTitle = "Sign Out"; ObjectID = "2rP-4I-yso"; */ -"2rP-4I-yso.normalTitle" = "Sign Out"; +/* Class = "UILabel"; text = "NOT ACTIVE"; ObjectID = "2I5-jL-jQr"; */ +"2I5-jL-jQr.text" = "非アクティブ"; -/* Class = "UILabel"; text = "IP Address Hidden"; ObjectID = "37I-9Y-wgf"; */ -"37I-9Y-wgf.text" = "IP Address Hidden"; +/* Class = "UITabBarItem"; title = "Learn"; ObjectID = "2sg-zD-KTu"; */ +"2sg-zD-KTu.title" = "学ぶ"; -/* Class = "UILabel"; text = "Unknown error."; ObjectID = "3mc-rm-zyi"; */ -"3mc-rm-zyi.text" = "Unknown error."; +/* Class = "UILabel"; text = "Warning"; ObjectID = "3P8-IH-ggH"; */ +"3P8-IH-ggH.text" = "ご注意"; -/* Class = "UILabel"; text = "Add E-Mail"; ObjectID = "41J-yY-nDE"; */ -"41J-yY-nDE.text" = "Add E-Mail"; +/* Class = "UIButton"; normalTitle = "􀈂"; ObjectID = "3tZ-rh-ZOa"; */ +"3tZ-rh-ZOa.normalTitle" = "􀈂"; -/* Class = "UILabel"; text = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; ObjectID = "42e-yG-VLg"; */ -"42e-yG-VLg.text" = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; +/* Class = "UILabel"; text = "iOS Annual (Save ~50%)"; ObjectID = "5Ce-KH-z1j"; */ +"5Ce-KH-z1j.text" = "iOSの年次 (約50%割引)"; -/* Class = "UIButton"; normalTitle = "Save"; ObjectID = "5aF-Mo-YV9"; */ -"5aF-Mo-YV9.normalTitle" = "Save"; +/* 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 = "Plan"; ObjectID = "5en-s8-3Z6"; */ -"5en-s8-3Z6.text" = "プラン"; +/* Class = "UILabel"; text = "Tap to agree to this"; ObjectID = "6Sd-rw-Pyl"; */ +"6Sd-rw-Pyl.text" = "同意する場合はタップしてください。"; -/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "6QS-8R-9Zb"; */ -"6QS-8R-9Zb.normalTitle" = "Sign In"; +/* Class = "UILabel"; text = "Not Whitelisted"; ObjectID = "6TE-i7-iPd"; */ +"6TE-i7-iPd.text" = "ホワイトリストに登録されていません"; -/* Class = "UITextField"; placeholder = "Add domain (i.e. netflix.com)"; ObjectID = "7bS-tB-jYh"; */ -"7bS-tB-jYh.placeholder" = "ドメインを追加する (例:netflix.com)"; +/* Class = "UILabel"; text = "World's Simplest Privacy Policy"; ObjectID = "6sx-m3-XUn"; */ +"6sx-m3-XUn.text" = "世界で最もシンプルなプライバシーポリシー"; -/* Class = "UIButton"; normalTitle = "Get Two Months Free"; ObjectID = "8ng-qK-etN"; */ -"8ng-qK-etN.normalTitle" = "2ヶ月間の無料体験に申し込む"; +/* Class = "UIButton"; normalTitle = "February 2022"; ObjectID = "7NS-46-hCo"; */ +"7NS-46-hCo.normalTitle" = "2023年2月"; -/* Class = "UITextField"; placeholder = "Email"; ObjectID = "8x7-vO-xGo"; */ -"8x7-vO-xGo.placeholder" = "Email"; +/* Class = "UIButton"; normalTitle = "Agree"; ObjectID = "7nc-lb-z6u"; */ +"7nc-lb-z6u.normalTitle" = "同意する"; -/* Class = "UILabel"; text = "Whitelisted"; ObjectID = "9tn-2p-xi9"; */ -"9tn-2p-xi9.text" = "Whitelisted"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "8FF-0m-G4n"; */ +"8FF-0m-G4n.normalTitle" = "キャンセル"; -/* Class = "UILabel"; text = "Try the most trusted way to browse securely and privately."; ObjectID = "Cxt-QW-V1N"; */ -"Cxt-QW-V1N.text" = "最も優れた安全性と匿名性をお試しください。"; +/* Class = "UILabel"; text = "Accounts get the following benefits:"; ObjectID = "8xZ-K2-buV"; */ +"8xZ-K2-buV.text" = "アカウントには以下のような特典があります:"; -/* Class = "UILabel"; text = "IP Address Hidden"; ObjectID = "Dbh-wh-0gV"; */ -"Dbh-wh-0gV.text" = "IP Address Hidden"; +/* Class = "UILabel"; text = "Block Log Disabled"; ObjectID = "9Mg-Xn-BmP"; */ +"9Mg-Xn-BmP.text" = "ブロックログが無効"; -/* Class = "UILabel"; text = "Confirmed includes a Content Blocker that protects your privacy and increases performance in Safari by blocking invasive code."; ObjectID = "EAR-ME-6mT"; */ -"EAR-ME-6mT.text" = "コンテンツブロッカーは貴方のプライバシーを守りSafariの動作を向上させる為に侵略的なコードをブロックします。"; +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "A3S-Ro-tUz"; */ +"A3S-Ro-tUz.normalTitle" = "なぜロックダウンを信頼するのですか?"; -/* Class = "UILabel"; text = "All Devices"; ObjectID = "ELD-E3-Yae"; */ -"ELD-E3-Yae.text" = "マルチデバイス版"; +/* Class = "UILabel"; text = "Pro Annual (Save ~30%)"; ObjectID = "ASZ-so-4BJ"; */ +"ASZ-so-4BJ.text" = "プロの年次 (約30%割引)"; -/* Class = "UITextField"; placeholder = "Email"; ObjectID = "Edz-d7-Ygb"; */ -"Edz-d7-Ygb.placeholder" = "Email"; +/* Class = "UILabel"; text = "Sign Up"; ObjectID = "AVA-H3-vX1"; */ +"AVA-H3-vX1.text" = "サインアップ"; -/* Class = "UIButton"; normalTitle = "Save"; ObjectID = "Egx-w0-cwP"; */ -"Egx-w0-cwP.normalTitle" = "Save"; +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "BGP-ej-jz6"; */ +"BGP-ej-jz6.text" = "セキュアトンネルVPN"; -/* Class = "UILabel"; text = "iOS Only"; ObjectID = "FCU-Ip-FHE"; */ -"FCU-Ip-FHE.text" = "iOS版"; +/* 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 = "UILabel"; text = "Confirmed VPN is different: our Privacy Policy explicitly and legally prohibits us from logging any personal data or information."; ObjectID = "FGo-KU-kDG"; */ -"FGo-KU-kDG.text" = "Confirmed VPNの個人情報保護方針は、私たちがあなたの通信を追跡、記録しない事を明示しています。"; +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Annual\" Plan"; ObjectID = "BI8-xc-diY"; */ +"BI8-xc-diY.accessibilityLabel" = "チェックボックス \"iOS年次\" プラン"; -/* Class = "UILabel"; text = "Plan"; ObjectID = "Fnz-ZP-M8f"; */ -"Fnz-ZP-M8f.text" = "プラン"; +/* Class = "UIButton"; normalTitle = "Why Trust Lockdown?"; ObjectID = "BhW-8r-pbC"; */ +"BhW-8r-pbC.normalTitle" = "なぜロックダウンを信頼するのですか?"; -/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "GCK-Lf-jdj"; */ -"GCK-Lf-jdj.normalTitle" = "もっと詳しく知る"; +/* 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"; text = "Plan"; ObjectID = "GoC-c8-4Ib"; */ -"GoC-c8-4Ib.text" = "プラン"; +/* 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 = "Email"; ObjectID = "HDP-bQ-VFf"; */ -"HDP-bQ-VFf.text" = "Email"; +/* Class = "UILabel"; text = "Tap To Activate"; ObjectID = "BoV-0k-BS5"; */ +"BoV-0k-BS5.text" = "タップしてアクティブにする"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "HbQ-tP-FHd"; */ -"HbQ-tP-FHd.normalTitle" = "×"; +/* Class = "UILabel"; text = "0"; ObjectID = "C0f-ye-V9U"; */ +"C0f-ye-V9U.text" = "0"; -/* Class = "UILabel"; text = "Privacy Policy"; ObjectID = "Ilb-j2-Evl"; */ -"Ilb-j2-Evl.text" = "個人情報保護方針"; +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Cjo-hk-1dr"; */ +"Cjo-hk-1dr.normalTitle" = "閉じる"; -/* Class = "UIButton"; normalTitle = "Get Secure"; ObjectID = "Ju2-He-5w7"; */ -"Ju2-He-5w7.normalTitle" = "安全性を手に入れる"; +/* Class = "UIButton"; normalTitle = "Restore Purchase"; ObjectID = "Dm9-pY-Snm"; */ +"Dm9-pY-Snm.normalTitle" = "購入商品を復元する"; -/* Class = "UIButton"; normalTitle = "Upgrade To All Devices"; ObjectID = "JzC-HQ-N0r"; */ -"JzC-HQ-N0r.normalTitle" = "マルチデバイス版にアップグレード"; +/* Class = "UIButton"; normalTitle = "Forgot Password?"; ObjectID = "Dxs-1n-lKD"; */ +"Dxs-1n-lKD.normalTitle" = "パスワードをお忘れですか?"; -/* Class = "UILabel"; text = "Please add an e-mail and password to sign in on other devices."; ObjectID = "Kuw-Uq-3Pb"; */ -"Kuw-Uq-3Pb.text" = "他のデバイスからサインインする為には、Emailアドレスとパスワードの設定が必要となります。"; +/* Class = "UILabel"; text = "United States - West"; ObjectID = "E9c-eB-V7Z"; */ +"E9c-eB-V7Z.text" = "アメリカ西部"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "LfX-F3-M2C"; */ -"LfX-F3-M2C.normalTitle" = "×"; +/* Class = "UILabel"; text = "12:22 PM"; ObjectID = "G2S-PC-kMS"; */ +"G2S-PC-kMS.text" = "午後12時22分"; -/* Class = "UILabel"; text = "Certain websites don't operate well with a VPN or specifically block a VPN's use. For the most seamless experience, we recommend not sending this small percentage of traffic through a VPN. This is configurable and you can add or remove sites as you would like."; ObjectID = "NEL-fV-wRa"; */ -"NEL-fV-wRa.text" = "ウェブサイトによってはVPNの使用中には正常に動作しない場合が考えられます。ホワイトリストに追加する事でそのウェブサイトに限りVPNを使用しない通信を行う事が出来ます。"; +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "GBo-WQ-fTN"; */ +"GBo-WQ-fTN.normalTitle" = "閉じる"; -/* Class = "UILabel"; text = "team@confirmedvpn.com"; ObjectID = "NYs-1f-By4"; */ -"NYs-1f-By4.text" = "team@confirmedvpn.com"; +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "J7f-4S-hHD"; */ +"J7f-4S-hHD.normalTitle" = "サインイン"; -/* Class = "UIButton"; normalTitle = "Create Sign In"; ObjectID = "P58-dx-7zu"; */ -"P58-dx-7zu.normalTitle" = "アカウントを設定する"; +/* Class = "UIButton"; normalTitle = "View Log"; ObjectID = "JVD-sS-lUY"; */ +"JVD-sS-lUY.normalTitle" = "ログを見る"; -/* Class = "UILabel"; text = "Protect Your Data"; ObjectID = "PGC-jx-bKV"; */ -"PGC-jx-bKV.text" = "プロテクション"; +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "Jsv-mc-ptE"; */ +"Jsv-mc-ptE.text" = "セキュアトンネルVPN"; -/* Class = "UILabel"; text = "Plan"; ObjectID = "PIq-cK-jjA"; */ -"PIq-cK-jjA.text" = "プラン"; +/* Class = "UILabel"; text = "Already have a Lockdown account?
Sign in below."; ObjectID = "KDA-UD-f4u"; */ +"KDA-UD-f4u.text" = "すでにLockdownアカウントをお持ちですか??
以下からサインインしてください。"; -/* Class = "UILabel"; text = "DISCONNECTED"; ObjectID = "QT0-vo-fNN"; */ -"QT0-vo-fNN.text" = "DISCONNECTED"; +/* Class = "UITextField"; placeholder = "Email Address"; ObjectID = "KMj-n0-VnV"; */ +"KMj-n0-VnV.placeholder" = "電子メールアドレス"; -/* Class = "UILabel"; text = "Our privacy policy is focused on protecting the user and ensuring he or she understands our company and product. To learn more, please tap the link below."; ObjectID = "Uve-eR-XXB"; */ -"Uve-eR-XXB.text" = "私達のプライバシーポリシーはユーザーを保護し彼らが私達と製品に対する理解を深める事を目的としています。私達が貴方の個人情報をどのように守り、扱うかをより詳しく知りたい場合は以下のボタンをタップしてください。"; +/* Class = "UIButton"; normalTitle = "Get Started"; ObjectID = "KbM-Pn-EXN"; */ +"KbM-Pn-EXN.normalTitle" = "はじめに"; -/* Class = "UILabel"; text = "Add Widget"; ObjectID = "VDZ-qq-YQO"; */ -"VDZ-qq-YQO.text" = "ウィジェットを追加する"; +/* Class = "UILabel"; text = "Blocking Enabled"; ObjectID = "Kd7-nB-tAb"; */ +"Kd7-nB-tAb.text" = "ブロッキング有効"; -/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "VIN-ck-6sP"; */ -"VIN-ck-6sP.normalTitle" = "Sign Up"; +/* Class = "UILabel"; text = "Set Region"; ObjectID = "KtJ-Jg-Z3X"; */ +"KtJ-Jg-Z3X.text" = "地域を設定します。"; -/* Class = "UILabel"; text = "Get Confirmed on Mac, PC, and Android too for only $9.99 a month"; ObjectID = "VNP-CM-Axq"; */ -"VNP-CM-Axq.text" = "月額$9.99でMac、PC、AndroidでもConfirmedをお使い頂けます。"; +/* Class = "UILabel"; text = "Firewall"; ObjectID = "LE2-86-d4U"; */ +"LE2-86-d4U.text" = "ファイアウォール"; -/* Class = "UILabel"; text = "Confirmed VPN"; ObjectID = "Ve6-h6-unZ"; */ -"Ve6-h6-unZ.text" = "Confirmed VPN"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "MRU-xk-A2p"; */ +"MRU-xk-A2p.normalTitle" = "キャンセル"; -/* Class = "UILabel"; text = "Let's set up Confirmed to get the best possible experience."; ObjectID = "ViV-f2-U7q"; */ -"ViV-f2-U7q.text" = "Confirmed VPNを使って最高の安全性を手に入れましょう。"; +/* Class = "UILabel"; text = "0"; ObjectID = "Msx-Nc-p9M"; */ +"Msx-Nc-p9M.text" = "0"; -/* Class = "UIButton"; normalTitle = "United States - West"; ObjectID = "VmW-aE-Qnf"; */ -"VmW-aE-Qnf.normalTitle" = "United States - West"; +/* Class = "UILabel"; text = "Lockdown"; ObjectID = "NAN-dg-xBj"; */ +"NAN-dg-xBj.text" = "Lockdown"; -/* Class = "UIButton"; normalTitle = "Restore Purchases"; ObjectID = "VuN-rN-BWG"; */ -"VuN-rN-BWG.normalTitle" = "以前にご購入された方"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "NvC-GW-2NJ"; */ +"NvC-GW-2NJ.normalTitle" = "キャンセル"; -/* Class = "UILabel"; text = "Confirmed VPN uses bank-level encryption to stop ISPs, hackers, and snoopers from accessing your personal data."; ObjectID = "Wdy-uC-C21"; */ -"Wdy-uC-C21.text" = "Confirmed VPNは金融機関等と同レベルの暗号化技術を使用しており、インターネットサービスプロバイダーや悪質なハッカー等からのアクセスをブロックします。"; +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "O4a-yi-uRp"; */ +"O4a-yi-uRp.normalTitle" = "ボタン"; -/* Class = "UILabel"; text = "Open"; ObjectID = "WjY-1n-JrX"; */ -"WjY-1n-JrX.text" = "Open"; +/* Class = "UITabBarItem"; title = "Protect"; ObjectID = "O5a-jC-Mj1"; */ +"O5a-jC-Mj1.title" = "プロテクト"; -/* Class = "UILabel"; text = "Email"; ObjectID = "Wtx-yP-jGA"; */ -"Wtx-yP-jGA.text" = "Email"; +/* Class = "UILabel"; text = "Location: 🇺🇸"; ObjectID = "O6b-GR-ijA"; */ +"O6b-GR-ijA.text" = "場所: アメリカ"; -/* Class = "UILabel"; text = "Sign up for essential updates from Confirmed, including security updates, new features, and ways to protect yourself on the Internet."; ObjectID = "Xoa-mx-q7J"; */ -"Xoa-mx-q7J.text" = "Confirmedのアップデートに登録しましょう。セキュリティのアップデート、新機能、インターネット上で貴方も守る様々な手段が含まれています。"; +/* Class = "UILabel"; text = "iPads and iPhones"; ObjectID = "ODT-wN-bGo"; */ +"ODT-wN-bGo.text" = "iPadとiPhone"; -/* Class = "UILabel"; text = "To enable, go to Settings > Safari > Content Blocker"; ObjectID = "Y5p-7G-DfK"; */ -"Y5p-7G-DfK.text" = "To enable, go to Settings > Safari > Content Blocker"; +/* Class = "UIButton"; normalTitle = "􀍟"; ObjectID = "OIa-zY-ALo"; */ +"OIa-zY-ALo.normalTitle" = "􀍟"; -/* Class = "UILabel"; text = "Content Blocker"; ObjectID = "Yrd-Bw-bAW"; */ -"Yrd-Bw-bAW.text" = "コンテンツブロッカー"; +/* Class = "UIButton"; normalTitle = "􀁝"; ObjectID = "OLt-18-OkW"; */ +"OLt-18-OkW.normalTitle" = "􀁝"; -/* Class = "UILabel"; text = "Confirmed VPN is a service to gain unlimited data for our VPN service. Confirmed subscriptions have a free one week trial, after which you will be charged to your credit card through your iTunes account. Price may vary by location. Your subscription will automatically renew unless canceled at least 24 hours before the end of the current period. Any unused portion of a free trial period, if offered, will be forfeited when the user purchases a subscription to that publication. Manage Confirmed in Account Settings after the optional upgrade."; ObjectID = "ZIU-Xd-vDj"; */ -"ZIU-Xd-vDj.text" = "Confirmed VPNはVPN接続のための無制限のデータ容量を提供します。アプリの購読料は1週間の無料期間を経た後にiTunesアカウントにご登録のクレジットカードからお支払い頂く形になり、地域によって価格が変動します。ご購読中のプランは満期となる24時間前までにキャンセルされない限り自動的に更新されますのでご注意ください。また、プランを購入された時点で無料体験期間は終了となりますのでご了承頂ければ幸いです。ご購入後の設定変更等はiOSのアカウント設定画面から行う事が出来ます。"; +/* Class = "UILabel"; text = "Block Invasive Tracking"; ObjectID = "PGe-EF-Ixy"; */ +"PGe-EF-Ixy.text" = "侵襲的追跡をブロック"; -/* Class = "UIButton"; normalTitle = "Terms & Conditions"; ObjectID = "ZRi-ok-YKh"; */ -"ZRi-ok-YKh.normalTitle" = "利用規約"; +/* Class = "UIButton"; normalTitle = "􀆅"; ObjectID = "RMP-Wh-oss"; */ +"RMP-Wh-oss.normalTitle" = "􀆅"; -/* Class = "UILabel"; text = "Secure your data now with Confirmed, the openly operated VPN audited by countless security professionals. "; ObjectID = "aB1-BA-j8K"; */ -"aB1-BA-j8K.text" = "Confirmedを起動してあなたのデータを守りましょう。"; +/* Class = "UIButton"; normalTitle = "Sign Up"; ObjectID = "RTE-DP-SL6"; */ +"RTE-DP-SL6.normalTitle" = "サインアップ"; -/* Class = "UILabel"; text = "Label"; ObjectID = "adI-JY-P95"; */ -"adI-JY-P95.text" = "Label"; +/* Class = "UIButton"; normalTitle = "Whitelist"; ObjectID = "Rhb-9H-gRT"; */ +"Rhb-9H-gRT.normalTitle" = "ホワイトリスト"; -/* Class = "UILabel"; text = "Whitelist"; ObjectID = "agz-pH-D2H"; */ -"agz-pH-D2H.text" = "ホワイトリスト"; +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "SCr-mD-x9f"; */ +"SCr-mD-x9f.normalTitle" = "個人情報保護方針"; -/* Class = "UILabel"; text = "Confirmed VPN"; ObjectID = "ari-H5-3gJ"; */ -"ari-H5-3gJ.text" = "Confirmed VPN"; +/* Class = "UILabel"; text = "ALL TIME"; ObjectID = "SGD-NH-9Mo"; */ +"SGD-NH-9Mo.text" = "全ての時間"; -/* Class = "UILabel"; text = "Confirmed is the only Openly Operated VPN. Our servers are available for public audit, and countless security professionals have verified the security and integrity of our company."; ObjectID = "b81-0P-0Mt"; */ -"b81-0P-0Mt.text" = "Confirmed is the only Openly Operated VPN. Our servers are available for public audit, and countless security professionals have verified the security and integrity of our company."; +/* Class = "UILabel"; text = "iPads, iPhones, and Macs"; ObjectID = "SLG-gG-QTX"; */ +"SLG-gG-QTX.text" = "iPad、iPhone、及びMac"; -/* Class = "UILabel"; text = "Mac, Windows, iOS, and Android for $9.99 per month"; ObjectID = "ch7-UR-ecd"; */ -"ch7-UR-ecd.text" = "Mac, Windows, iOS, and Android for $9.99 per month"; +/* Class = "UILabel"; text = "3421"; ObjectID = "Sar-GU-wUS"; */ +"Sar-GU-wUS.text" = "3421"; -/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dDl-UE-3ud"; */ -"dDl-UE-3ud.normalTitle" = "Sign In"; +/* 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 = "UIButton"; normalTitle = "Forgot Password"; ObjectID = "e9e-xo-AtR"; */ -"e9e-xo-AtR.normalTitle" = "パスワードを忘れた"; +/* Class = "UILabel"; text = "Protect Good Connections"; ObjectID = "TaD-Vg-S5A"; */ +"TaD-Vg-S5A.text" = "良好な接続を保護する"; -/* Class = "UIButton"; normalTitle = "Later"; ObjectID = "eM1-ht-UL1"; */ -"eM1-ht-UL1.normalTitle" = "あとで"; +/* Class = "UIButton"; normalTitle = "SAVE"; ObjectID = "Tls-Z3-Gnz"; */ +"Tls-Z3-Gnz.normalTitle" = "セーブ"; -/* Class = "UIButton"; normalTitle = "Add Email"; ObjectID = "f2i-dV-64I"; */ -"f2i-dV-64I.normalTitle" = "Emailアドレスを追加"; +/* Class = "UITextField"; placeholder = "domain-to-whitelist.com"; ObjectID = "U7q-Uc-gxE"; */ +"U7q-Uc-gxE.placeholder" = "domain-to-whitelist.com"; -/* Class = "UIButton"; normalTitle = "Restore Purchases"; ObjectID = "f2x-Fr-Ead"; */ -"f2x-Fr-Ead.normalTitle" = "以前にご購入された方"; +/* 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 = "Button"; ObjectID = "fDL-ho-Zgm"; */ -"fDL-ho-Zgm.normalTitle" = "Button"; +/* Class = "UIButton"; normalTitle = "Get Started Free"; ObjectID = "VK2-VJ-AkH"; */ +"VK2-VJ-AkH.normalTitle" = "無料で始める"; -/* Class = "UIButton"; normalTitle = "Start 1 Week Trial"; ObjectID = "fEA-js-9LV"; */ -"fEA-js-9LV.normalTitle" = "無料体験を開始する。"; +/* Class = "UIButton"; normalTitle = "Learn More"; ObjectID = "Vhr-9g-C3l"; */ +"Vhr-9g-C3l.normalTitle" = "詳細はこちら"; -/* Class = "UILabel"; text = "Loading..."; ObjectID = "fPM-ls-bhY"; */ -"fPM-ls-bhY.text" = "Loading..."; +/* Class = "UILabel"; text = "Password"; ObjectID = "W3y-bX-UkA"; */ +"W3y-bX-UkA.text" = "パスワード"; -/* Class = "UILabel"; text = "191.23.142.142"; ObjectID = "fhx-Lb-Pnf"; */ -"fhx-Lb-Pnf.text" = "191.23.142.142"; +/* 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 = "UIButton"; normalTitle = "×"; ObjectID = "g85-ac-fK4"; */ -"g85-ac-fK4.normalTitle" = "×"; +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"iOS Monthly\" Plan"; ObjectID = "WcD-2j-Lym"; */ +"WcD-2j-Lym.accessibilityLabel" = "チェックボックス \"iOS毎月\"プラン"; -/* Class = "UILabel"; text = "1"; ObjectID = "gRJ-Rg-jAz"; */ -"gRJ-Rg-jAz.text" = "1"; +/* Class = "UIButton"; normalTitle = "CLOSE"; ObjectID = "Wi8-F0-ahr"; */ +"Wi8-F0-ahr.normalTitle" = "閉じる"; -/* Class = "UILabel"; text = "Confirmed VPN"; ObjectID = "geO-1A-pWS"; */ -"geO-1A-pWS.text" = "Confirmed VPN"; +/* Class = "UILabel"; text = "THIS WEEK"; ObjectID = "WyU-rB-hEs"; */ +"WyU-rB-hEs.text" = "今週"; -/* Class = "UILabel"; text = "1.0"; ObjectID = "gqH-fl-SDW"; */ -"gqH-fl-SDW.text" = "1.0"; +/* Class = "UILabel"; text = "$49.99/year after (~$4.17/month)"; ObjectID = "XQI-S7-VjW"; */ +"XQI-S7-VjW.text" = "$49.99/y年後 (~$4.17/月)"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "gvN-v9-bsD"; */ -"gvN-v9-bsD.normalTitle" = "×"; +/* Class = "UILabel"; text = "0"; ObjectID = "XSr-oZ-hkd"; */ +"XSr-oZ-hkd.text" = "0"; -/* Class = "UILabel"; text = "42.4 Mbps"; ObjectID = "hdu-lb-4qp"; */ -"hdu-lb-4qp.text" = "42.4 Mbps"; +/* Class = "UILabel"; text = "Block Group Title"; ObjectID = "XcQ-Zo-7hE"; */ +"XcQ-Zo-7hE.text" = "ブロックグループタイトル"; -/* Class = "UILabel"; text = "Swipe to learn why Confirmed is the only VPN you can trust."; ObjectID = "i4b-sZ-Gy4"; */ -"i4b-sZ-Gy4.text" = "Confirm VPNの信頼性をご説明致します。"; +/* 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 = "UILabel"; text = "Safe"; ObjectID = "jdh-lO-T7i"; */ -"jdh-lO-T7i.text" = "安全性"; +/* Class = "UIButton"; normalTitle = "Block List"; ObjectID = "XvC-pH-UjX"; */ +"XvC-pH-UjX.normalTitle" = "ブロックリスト"; -/* Class = "UILabel"; text = "We want our privacy policy to be simple and readable. We are focused on protecting all of our users data and not distributing it with other companies for profit."; ObjectID = "kK5-2Y-8jv"; */ -"kK5-2Y-8jv.text" = "私たちは簡潔で読みやすいプライバシーポリシーの提供を心がけています。私たちは貴方のデータを守り、それを第三者に漏らす事はありません。"; +/* Class = "UINavigationItem"; title = "Account"; ObjectID = "Y6I-Ar-Q5y"; */ +"Y6I-Ar-Q5y.title" = "アカウント"; -/* Class = "UILabel"; text = "2"; ObjectID = "kcn-RM-c89"; */ -"kcn-RM-c89.text" = "2"; +/* Class = "UITabBarItem"; title = "Account"; ObjectID = "YAE-W9-FME"; */ +"YAE-W9-FME.title" = "アカウント"; -/* Class = "UIButton"; normalTitle = "Later"; ObjectID = "lvj-gp-owd"; */ -"lvj-gp-owd.normalTitle" = "あとで"; +/* Class = "UIView"; accessibilityLabel = "Tap This Button To Activate Secure Tunnel"; ObjectID = "ZPf-nR-X1a"; */ +"ZPf-nR-X1a.accessibilityLabel" = "このボタンをタップして、セキュアトンネルをアクティブにします"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "pp8-VE-IM6"; */ -"pp8-VE-IM6.normalTitle" = "×"; +/* Class = "UIButton"; normalTitle = "Privacy Policy"; ObjectID = "a37-qm-xpZ"; */ +"a37-qm-xpZ.normalTitle" = "個人情報保護方針"; -/* Class = "UILabel"; text = "IP Address Hidden"; ObjectID = "pzg-XH-kRa"; */ -"pzg-XH-kRa.text" = "IP Address Hidden"; +/* Class = "UIButton"; normalTitle = "Set Region"; ObjectID = "a4O-qT-yLk"; */ +"a4O-qT-yLk.normalTitle" = "地域を設定する"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "qG4-8Z-FpB"; */ -"qG4-8Z-FpB.normalTitle" = "×"; +/* Class = "UILabel"; text = "VPN"; ObjectID = "aXL-VV-lJa"; */ +"aXL-VV-lJa.text" = "VPN"; -/* Class = "UILabel"; text = "Block ads"; ObjectID = "qsd-L2-6zG"; */ -"qsd-L2-6zG.text" = "広告をブロック"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "anO-Te-34C"; */ +"anO-Te-34C.normalTitle" = "キャンセル"; -/* Class = "UILabel"; text = "We do not log, share, or sell any of your website traffic."; ObjectID = "rg7-oq-8EX"; */ -"rg7-oq-8EX.text" = "貴方の通信を追跡、記録しそれを売り渡す事はありません。"; +/* Class = "UILabel"; text = "Email"; ObjectID = "asx-fS-ufQ"; */ +"asx-fS-ufQ.text" = "電子メール"; -/* Class = "UITextField"; placeholder = "Password"; ObjectID = "rhT-XQ-ffo"; */ -"rhT-XQ-ffo.placeholder" = "”パスワード”"; +/* Class = "UILabel"; text = "🇺🇸"; ObjectID = "bDO-jU-rIH"; */ +"bDO-jU-rIH.text" = "アメリカ"; -/* Class = "UIButton"; normalTitle = "×"; ObjectID = "tdt-TT-gDW"; */ -"tdt-TT-gDW.normalTitle" = "×"; +/* Class = "UILabel"; text = "Pro Monthly"; ObjectID = "c6Y-nW-xhv"; */ +"c6Y-nW-xhv.text" = "Pro毎月"; -/* Class = "UILabel"; text = "Updates"; ObjectID = "two-nE-LPa"; */ -"two-nE-LPa.text" = "アップデート"; +/* Class = "UIButton"; normalTitle = "􀎬"; ObjectID = "cAQ-Dd-nCd"; */ +"cAQ-Dd-nCd.normalTitle" = "􀎬"; -/* Class = "UILabel"; text = "Get Started"; ObjectID = "uJo-T5-FIB"; */ -"uJo-T5-FIB.text" = "Get Started"; +/* Class = "UIButton"; normalTitle = "CANCEL"; ObjectID = "cNH-31-ne4"; */ +"cNH-31-ne4.normalTitle" = "キャンセル"; -/* Class = "UILabel"; text = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; ObjectID = "upL-BX-BCS"; */ -"upL-BX-BCS.text" = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; +/* Class = "UILabel"; text = "TUNNEL OFF"; ObjectID = "cVg-HS-AW8"; */ +"cVg-HS-AW8.text" = "トンネルオフ"; -/* Class = "UILabel"; text = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; ObjectID = "vTq-mf-eoG"; */ -"vTq-mf-eoG.text" = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; +/* 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 = "UILabel"; text = "Confirmed VPN"; ObjectID = "vbK-ab-y2a"; */ -"vbK-ab-y2a.text" = "Confirmed VPN"; +/* 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をサポートしています。 VoiceOverを使ってダブルタップして選択します。"; -/* Class = "UILabel"; text = "3"; ObjectID = "vvw-R0-IOa"; */ -"vvw-R0-IOa.text" = "3"; +/* Class = "UIView"; accessibilityLabel = "Checkbox for \"Pro Annual\" Plan"; ObjectID = "d44-u4-ZF6"; */ +"d44-u4-ZF6.accessibilityLabel" = "チェックボックス \"Pro年次\"プラン"; -/* Class = "UILabel"; text = "Upgrade Confirmed to an Annual plan and get two months free."; ObjectID = "wNt-4a-okh"; */ -"wNt-4a-okh.text" = "Confirmedの年間購読プラン"; +/* 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 = "UILabel"; text = "IP Address Hidden"; ObjectID = "wrZ-aS-efH"; */ -"wrZ-aS-efH.text" = "IP Address Hidden"; +/* Class = "UIButton"; normalTitle = "Sign In"; ObjectID = "dp2-kD-Scb"; */ +"dp2-kD-Scb.normalTitle" = "サインイン"; -/* Class = "UILabel"; text = "Account"; ObjectID = "wty-cI-SuR"; */ -"wty-cI-SuR.text" = "アカウント"; +/* Class = "UITextField"; placeholder = "Password"; ObjectID = "f3K-Kz-eVg"; */ +"f3K-Kz-eVg.placeholder" = "パスワード"; -/* Class = "UITextField"; placeholder = "Password"; ObjectID = "xCw-QW-K0K"; */ -"xCw-QW-K0K.placeholder" = "”パスワード”"; +/* Class = "UILabel"; text = "blocked-domain.com"; ObjectID = "fK4-lE-dks"; */ +"fK4-lE-dks.text" = "blocked-domain.com"; -/* Class = "UIButton"; normalTitle = "Setup"; ObjectID = "xMY-0a-SHd"; */ -"xMY-0a-SHd.normalTitle" = "設定"; +/* Class = "UIButton"; accessibilityLabel = "Tap This Button To Activate Firewall"; ObjectID = "fRG-nx-zRG"; */ +"fRG-nx-zRG.accessibilityLabel" = "このボタンをタップしてファイアウォールをアクティブにします"; -/* Class = "UIButton"; normalTitle = "ⓘ What does this mean?"; ObjectID = "yns-bx-ZkI"; */ -"yns-bx-ZkI.normalTitle" = "ⓘ 状況を確認する"; +/* 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 = "|"; ObjectID = "yxg-bE-IK7"; */ -"yxg-bE-IK7.text" = "|"; +/* Class = "UILabel"; text = "Block Log"; ObjectID = "fmE-6E-gnc"; */ +"fmE-6E-gnc.text" = "ブロックログ"; -/* Class = "UILabel"; text = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; ObjectID = "zCp-MF-Ozw"; */ -"zCp-MF-Ozw.text" = "Your IP address is a uniquely identifiable number to track your activities. Confirmed masks this number to let you browse anonymously. "; +/* 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文字以上で、大文字、小文字、数字、記号を1つ以上含む必要があります。"; + +/* 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をサポートしています。 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" = "このボタンをタップして、セキュアトンネルをアクティブにします"; + +/* 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
再: 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" = "iPadとiPhone"; + +/* Class = "UILabel"; text = "Forgot Password"; ObjectID = "nu2-zq-dta"; */ +"nu2-zq-dta.text" = "パスワードをお忘れですか"; + +/* Class = "UILabel"; text = "Secure Tunnel VPN"; ObjectID = "nuc-D4-P1M"; */ +"nuc-D4-P1M.text" = "セキュアトンネル"; + +/* 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" = "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" = "ファイアウォール"; + +/* 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" = "キャンセル"; diff --git a/LockdowniOS/junes_journey_trackers.txt b/LockdowniOS/junes_journey_trackers.txt new file mode 100644 index 0000000..4300c92 --- /dev/null +++ b/LockdowniOS/junes_journey_trackers.txt @@ -0,0 +1,167 @@ +adcolony.com +pubnative.net +peak226.com +pubmatic.com +loopme.com +appnexus.com +rubiconproject.com +openx.com +betweendigital.com +algorix.co +inmobi.com +ucfunnel.com +aralego.com +adiiix.com +smartadserver.com +xandr.com +tpmn.io +themediagrid.com +olaex.biz +gamoshi.io +opera.com +meitu.com +bidmachine.io +admixer.net +mman.kr +bold-win.com +triplelift.com +improvedigital.com +thebrave.io +hyperad.tech +brightcom.com +freewheel.tv +supply.colossusssp.com +advertising.com +onetag.com +conversantmedia.com +facebook.com +fyber.com +acd.op.hicloud.com +admanmedia.com +adx-dra.op.hicloud.com +adx-dre.op.hicloud.com +adx-drru.op.hicloud.com +axonix.com +blis.com +contextweb.com +indexexchange.com +mars.media +rhythmone.com +startapp.com +undertone.com +verve.com +video.unrulymedia.com +yieldmo.com  +sharethrough.com +rhebus.works +videoHeroes.tv +xad.com +adform.com +alpineinteractivegroup.com +gitberry.com +mediaverse.ai +prequel.tv +showheroes.com +visiblemeasures.com +ironsrc.com +adbility-media.com +appads.in +gamoshi.io  +pokkt.com +smaato.com +smartclip.net +smartstream.tv +spotxchange.com +stroeer.com +yandex.com +yieldlab.net +engagebdr.com +lijit.com +hyprmx.com +jungroup.com +spotx.tv +iqzone.com +krushmedia.com +imds.tv +synacor.com +springserve.com +digimedllc.com +lkqd.net +connekt.ai +adnuro.com +thirdpresence.com +newsusadigital.com +mobileapplied.com +advanttechnology.com +solex.io +ardenodemedia.com +adtonos.com +tritondigital.com +lkqd.com +balloonlabs.ai +digitalinnovationgroup.com +my-cast.tv +smartivi.ai +consumable.com +adswizz.com +unity.com +webeyemob.com +kidoz.net +yeahmobi.com +exchange.admazing.co +mintegral.com +reforge.in +tremorhub.com +velismedia.com +criteo.com +liftoff.io +telaria.com +lemmatechnologies.com +se7en.es +adview.com +adelement.com +advlion.com +admixplay.com +bidease.com +vungle.com +sonobi.com +vidoomy.com +targetspot.com +bigo.sg +pangleglobal.com +blueseasx.com +adtiming.com +uis.mobfox.com +admixer.co.kr +app-stock.com +ssp.e-volution.ai +smartyads.com +chartboost.com +bidence.com +flat-ads.com +e-planning.net +target.my.com +ignitemediatech.com +acexchange.co.kr +mobupps.com +aniview.com +media.net +lunamedia.io +ogury.com +gameloft.com +eskimi.com +q1media.com +q1connect.com +beachfront.com +admixer.com +adwmg.com +cmcm.com +limpid.tv +sabio.us +silvermob.com +sovrn.com +mobirtb.com +kubient.com +pubwheel.com +admatic.com.tr +gumgum.com diff --git a/LockdowniOS/marketing.txt b/LockdowniOS/marketing.txt index 27b101c..513f0cd 100644 --- a/LockdowniOS/marketing.txt +++ b/LockdowniOS/marketing.txt @@ -5,14 +5,10 @@ api.facebook.com api.mixpanel.com app-measurement.com appboy.com -braze.com -doubleclick.net fabric.io -facebook.net fb.com firebase.com google-analytics.com -googleadservices.com heapanalytics.com hm.baidu.com in.getclicky.com @@ -22,3 +18,8 @@ ping.chartbeat.net richmetrics.com sb.scorecardresearch.com sc-analytics.appspot.com +flashtalking.com +3lift.com +2mdn.net +googlesyndication.com +consumertrack.com diff --git a/LockdowniOS/marketing_beta.txt b/LockdowniOS/marketing_beta.txt new file mode 100644 index 0000000..3320121 --- /dev/null +++ b/LockdowniOS/marketing_beta.txt @@ -0,0 +1,22 @@ +events.appsflyer.com +sdk.appsflyer.com +t.appsflyer.com +launches.appsflyer.com +impression.appsflyer.com +inapps.appsflyer.com +api.instabug.com +e.crashlytics.com +settings.crashlytics.com +mobile-service.segment.com +api.segment.io +cdn-settings.segment.com +sdk.iad-01.braze.com +sdk.iad-02.braze.com +sdk.iad-03.braze.com +sdk.iad-04.braze.com +sdk.iad-05.braze.com +sdk.iad-06.braze.com +sdk.iad-08.braze.com +sdk.fra-01.braze.eu +imp.control.kochava.com +sdk-api-v1.singular.net diff --git a/LockdowniOS/ransomware.txt b/LockdowniOS/ransomware.txt index c485d9b..8b13789 100644 --- a/LockdowniOS/ransomware.txt +++ b/LockdowniOS/ransomware.txt @@ -1,66 +1 @@ -2bdfb.spinakrosa.at -2gdb4.leoraorage.at -5rport45vcdef345adfkksawe.bematvocal.at -6g4ds.froekuge.com -74nfnjhlq45nkgws4hbdbk45wekfjhqw4talefgnv.curryfort.at -88fga.ketteaero.com -8b4bb47tiaolhy4uhhlfaqerg.sofarany.at -94dbhbj3l4blaeyfgl7q45glbaer.giponfeste.at -974gfbjhb23hbfkyfaby3byqlyuebvly5q254y.mendilobo.com -9hrds.wolfcrap.at -a64gfdsjhb4htbiwaysbdvukyft5q.zobodine.at -aq3ef.goimocoa.at -as3ws.fopyirr.com -b4youfred5485jgsa3453f.italazudda.com -bfd45u8ehdklrfqwlhbhjbgqw.niptana.at -d34fa.lasmeio.com -dd7bsndhr45nfksdnkferfer.javakale.at -f4dsbjhb45wfiuqeib4fkqeg.meccaledgy.at -fl43s.toabolt.at -g4dhhg53jsdjnnkjwjrfyiouh3o4u4th.vinerteen.com -gfkuwflbhsjdabnu4nfukerfqwlfwr4rw.ringbalor.com -gwbak.nickymaru.com -gwe32fdr74bhfsyujb34gfszfv.zatcurr.com -h3ds4.maconslab.com -h54dc.leverdaze.at -h5nuwefkuh134ljngkasdbasfg.corolbugan.com -hrfgd74nfksjdcnnklnwefvdsf.materdunst.com -i5ndw.titlecorta.at -ibf4d.ukegaub.at -ik4dm.mazerunci.at -irhng84nfaslbv243ljtblwqjrb.pinnafaon.at -irudhkunrlfu25fhkaqw34blr5qlby4tgq43t.orrisbirth.com -k234s.ascotsprue.com -k34ew.keyedgell.com -k3cxd.pileanoted.com -k47d3.proporr.com -k4restportgonst34d23r.oftpony.at -kbv5s.kylepasse.at -kh5jfnvkk5twerfnku5twuilrnglnuw45yhlw.vealsithe.com -kkd47eh4hdjshb5t.angortra.at -kkr4hbwdklf234bfl84uoqleflqwrfqwuelfh.brazabaya.com -l123d.feustude.at -nn54djhfnrnm4dnjnerfsd.replylaten.at -nnrtsdf34dsjhb23rsdf.spannflow.com -o4dm3.leaama.at -oehknf74ohqlfnpq9rhfgcq93g.hateflux.com -p54dhkus4tlkfashdb6vjetgsdfg.greetingshere.at -po4dbsjbneljhrlbvaueqrgveatv.bonmawp.at -prest54538hnksjn4kjfwdbhwere.hotchunman.com -pts764gt354fder34fsqw45gdfsavadfgsfg.kraskula.com -rbg4hfbilrf7to452p89hrfq.boonmower.com -sondr5344ygfweyjbfkw4fhsefv.heliofetch.at -t54ndnku456ngkwsudqer.wallymac.com -tes543berda73i48fsdfsd.keratadze.at -tt54rfdjhb34rfbnknaerg.milerteddy.com -u24er.ovaarmor.com -u54bbnhf354fbkh254tbkhjbgy8258gnkwerg.tahaplap.com -uhufnlsad7bhf4ykqfbevmxergwrth.himfinn.com -uiredn4njfsa4234bafb32ygjdawfvs.frascuft.com -uj5nj.onanwhit.com -vewrb.italisumo.at -w6bfg4hahn5bfnlsafgchkvg5fwsfvrt.hareuna.at -wor4d.slewirk.at -y4bxj.adozeuds.com -ytrest84y5i456hghadefdsd.pontogrot.com -yyre45dbvn2nhbefbmh.begumvelic.at + diff --git a/LockdowniOS/reporting.txt b/LockdowniOS/reporting.txt new file mode 100644 index 0000000..705b322 --- /dev/null +++ b/LockdowniOS/reporting.txt @@ -0,0 +1,3 @@ +sessions.bugsnag.com +api.bugfender.com +firebaselogging-pa.googleapis.com diff --git a/LockdowniOS/scams.txt b/LockdowniOS/scams.txt new file mode 100644 index 0000000..ee5b7bb --- /dev/null +++ b/LockdowniOS/scams.txt @@ -0,0 +1,2 @@ +cheap-jewelry-online.com +cnsexpress.website diff --git a/LockdowniOS/snapchat_analytics.txt b/LockdowniOS/snapchat_analytics.txt new file mode 100644 index 0000000..d1cb44f --- /dev/null +++ b/LockdowniOS/snapchat_analytics.txt @@ -0,0 +1,5 @@ +snapads.com +analytics.snapchat.com +app-analytics.snapchat.com +sc-analytics.appspot.com +tr.snapchat.com diff --git a/LockdowniOS/tiktok_trackers.txt b/LockdowniOS/tiktok_trackers.txt new file mode 100644 index 0000000..9573ec0 --- /dev/null +++ b/LockdowniOS/tiktok_trackers.txt @@ -0,0 +1,3699 @@ +0pbxmo-useast2a.xzcs3zlph.com.edgesuite.net +0pbxmo-useast2a.xzcs3zlph.com +0pbxmo.xzcs3zlph.com +0pbxmo.xzcs3zlph.com.edgesuite.net +0rutzab.qfyf1toi.com +1uipkgq.mzfvozqybf.com +3dokby.toutiao50.com +472064670264-p830d2k61ivgab1ihaq1oe5u50jf54q9.apps.googleusercontent.com +5412ac7a44c679ea291e43368cac45de.byteoversea.com +908812512490-tqgub82rl7tuj6g8n7qvac1e21nqsiop.apps.googleusercontent.com +9pwigjmwo.xzcs3zlph.com +9pwigjmwo.xzcs3zlph.com.edgesuite.net +a.ipstatp.com +a.isnssdk.com +a.l.bytedns.net +a.pstatp.com +a.sgpstatp.com +a0.ipstatp.com +a0.pstatp.com +a0.pstatp.com.bsgslb.com +a0.pstatp.com.cloudcdn.net +a0.pstatp.com.cloudglb.com +a0.sgpstatp.com +a1-ipv6.pstatp.com.wsglb0.com +a1.ipstatp.com +a1.l.bytedns.net +a1.pstatp.com +a1.pstatp.com.wscdns.com +a15-tb.isnssdk.com +a15-tb.sgsnssdk.com +a15-wd.ipstatp.com +a16-tb.isnssdk.com +a16-tb.isnssdk.com.edgekey.net +a2.ipstatp.com +a2.pstatp.com +a3-ipv6.pstatp.com +a3-ipv6.pstatp.com.w.cdngslb.com +a3.bytecdn.cn +a3.pstatp.com +a3.pstatp.com.w.alikunlun.net +a4.bytecdn.cn +a4.pstatp.com +a5.bytecdn.cn +a5.pstatp.com +a6-ipv6.pstatp.com +a6.bytecdn.cn +a6.pstatp.com +a6.pstatp.com.download.ks-cdn.com +a6.pstatp.com.download.ks-cdn1.com +a8.pstatp.com +a9.pstatp.com +a9.pstatp.com.cdn.dnsv1.com +a9.pstatp.com.cloud.cdntip.com +abn.snssdk.com +abtest-ch-hl.snssdk.com +abtest-ch-hl.snssdk.com.w.kunluncan.com +abtest-ch.snssdk.com +abtest-sg-tiktok.byteoversea.com +abtest-sg.byteoversea.com +abtest-va-tiktok.byteoversea.com +abtest-va-tiktok.byteoversea.com.edgesuite.net +abtest-va.byteoversea.com +abtest-va.byteoversea.com.edgesuite.net +abtest.snssdk.com +accproxy.snssdk.com +acgccddacg.temp.p23.tc.cdntip.com +act.snssdk.com +act.toutiaocloud.com +activity-spring-api-1.b.bytedns.net +activity-spring-api-2.b.bytedns.net +activity-spring-api.b.bytedns.net +activity.ixigua.com +activity.musical.ly +activity.tiktok.com +activity.tiktok.com.edgekey.net +ad-lb-alisg.byteoversea.net +ad-lb-maliva.byteoversea.net +ad.toutiao.com +ad.toutiao.com.bytedns.net +admin.bytedance.com +ads.tiktok.com +ads.tiktok.com.edgekey.net +adshare.toutiao.com +adx.toutiao.com +agent.toutiao.com +aideas.toutiao.com +akcqacex.qfyf1toi.com +alisg-normal-lb.byteoversea.net +alisg-websocket-all-lb.byteoversea.net +aliva-sentry.byteoversea.com +all.amemv.com.w.kunluncan.com +all.aweme-core-lb-hl.l.bytedns.net +all.aweme-core-lb-lf.l.bytedns.net +all.aweme-core-lb-lq.l.bytedns.net +all.huodonghylivecdn.sched.ovscdns.net +all.hylivecdn.sched.ovscdns.net +all.snssdk.com.w.kunluncan.com +all.toutiao-activity-lb-hl.l.bytedns.net +all.toutiao-activity-lb-lf.l.bytedns.net +all.toutiao-activity-lb-lq.l.bytedns.net +all.toutiao-misc-lb-lf.l.bytedns.net +all.toutiao-other-lf.l.bytedns.net +all.webcast-core-lb-hl.l.bytedns.net +all.webcast-core-lb-lf.l.bytedns.net +all.webcast-core-lb-lq.l.bytedns.net +all.webcast-lb-hl.l.bytedns.net +all.webcast-lb-lf.l.bytedns.net +all.webcast-lb-lq-ttgw.l.bytedns.net +alog.umeng.com +amazex-aggdsz.snssdk.com +amazex-agjsnj.snssdk.com +amazex-aglnsy.snssdk.com +amazex-agsdqd.snssdk.com +amazex-agsxxa.snssdk.com +amazex-agsy.snssdk.com +amazex.snssdk.com +amd-res.musical.ly.edgesuite.net +amemv-quic-upload-hl.l.bytedns.net +amemv-quic-upload.l.bytedns.net +amemv.com +amemv.com.bsgslb.com +amemv.com.w.cdngslb.com +amemv.com.w.kunlunca.com +amemv.com.w.kunluncan.com +amemv.com.w.kunlunhuf.com +amemv.com.wsglb0.com +amemv.com.xi.zwtianshangm.com +amfr.snssdk.com +ammp.byteoversea.com +analytics.snssdk.com +analytics.tiktok.com +analytics.tiktok.com.edgekey.net +api-bee-hl.amemv.com +api-bee.amemv.com +api-c.amemv.com +api-c.amemv.com.w.kunluncan.com +api-core-va.tiktokv.com +api-core-va.tiktokv.com.edgesuite.net +api-dev.musical.ly +api-eagle-hl.amemv.com +api-eagle-hl.amemv.com.w.kunluncan.com +api-eagle-lq.amemv.com +api-eagle-lq.amemv.com.w.cdngslb.com +api-eagle.amemv.com +api-eagle.amemv.com.w.kunluncan.com +api-h2-eagle.tiktokv.com +api-h2-eagle.tiktokv.com.edgekey.net +api-h2.tiktokv.com +api-h2.tiktokv.com.edgekey.net +api-hl.amemv.com +api-hl.amemv.com.w.kunluncan.com +api-ipv6.amemv.com +api-ipv6.amemv.com.w.cdngslb.com +api-live-hl.amemv.com +api-live-hl.amemv.com.w.kunluncan.com +api-live.amemv.com +api-live.amemv.com.w.kunluncan.com +api-lq.amemv.com +api-lq.amemv.com.w.cdngslb.com +api-m.tiktok.com +api-mo.snssdk.com +api-s1-h2.musical.ly +api-s1-h2.tiktokv.com +api-s1-quic.musical.ly +api-s1-quic.tiktokv.com +api-sg.toutiao50.com +api-sg19.toutiao50.com +api-sg21.toutiao50.com +api-spe-a.isnssdk.com +api-spe-a.sgsnssdk.com +api-spe-b.isnssdk.com +api-spe-b.sgsnssdk.com +api-spe-b.snssdk.com +api-spe-c.isnssdk.com +api-spe-c.sgsnssdk.com +api-spe-c.snssdk.com +api-spe-d.snssdk.com +api-spe-e.snssdk.com +api-spe-f.snssdk.com +api-spe-g.snssdk.com +api-spe-h.snssdk.com +api-spe-i.snssdk.com +api-spe-j.snssdk.com +api-spe-k.snssdk.com +api-spe-l.snssdk.com +api-spe-m.snssdk.com +api-spe-p.snssdk.com +api-spe-q.snssdk.com +api-spe-ttl.ipstatp.com +api-spe-ttl.sgpstatp.com +api-spe.isnssdk.com +api-spe.sgsnssdk.com +api-spe.snssdk.com.w.kunluncan.com +api-spw-b.snssdk.com +api-swan-hl.amemv.com +api-swan-hl.amemv.com.bsgslb.com +api-swan-hl.amemv.com.w.kunluncan.com +api-swan.amemv.com +api-swan.amemv.com.download.ks-cdn.com +api-swan.amemv.com.w.kunluncan.com +api-t.tiktok.com +api-t.tiktokv.com +api-t.tiktokv.com.edgekey.net +api-t1.tiktokv.com +api-t2.tiktokv.com +api-t3.tiktokv.com +api-t3.tiktokv.com.cdn.cloudflare.net +api-test-pcs.snssdk.com +api-useast2a.toutiao50.com +api-useast2a.toutiao50.com.edgesuite.net +api-va.tiktokv.com +api-va.tiktokv.com.edgekey.net +api-yak-hl.amemv.com +api-yak.amemv.com +api.amemv.com +api.amemv.com.bytedns.net +api.amemv.com.w.kunluncan.com +api.amemv.com.xi.zwtianshangm.com +api.huoshan.com.bytedns.net +api.musical.ly +api.snssdk.com +api.snssdk.com.w.kunluncan.com +api.tiktok.com +api.tiktokv.com +api.tiktokv.com.edgekey.net +api.toutiao50.com +api.toutiao50.com.edgesuite.net +api100-core-c-hl.amemv.com +api100-core-c-lf.amemv.com +api100-core-c-lq.amemv.com +api100-quic-c-hl.snssdk.com +api100-quic-c-lf.snssdk.com +api100-quic-c-lq.snssdk.com +api100-quic-c.snssdk.com +api15-h2-eagle.tiktokv.com +api15-h2.tiktokv.com +api16-core-c-alisg.tiktokv.com +api16-core-c-alisg.tiktokv.com.edgekey.net +api16-core-c-useast1a.musical.ly +api16-core-c-useast1a.musical.ly.edgekey.net +api16-core-c-useast1a.tiktokv.com +api16-core-c-useast1a.tiktokv.com.edgekey.net +api16-core-c-useast2a.musical.ly +api16-core-c-useast2a.musical.ly.edgekey.net +api16-core-c-useast2a.tiktokv.com +api16-core-c-useast2a.tiktokv.com.edgekey.net +api16-core-c4-alisg.tiktokv.com +api16-core-h2.sgsnssdk.com +api16-core-i-alisg.sgsnssdk.com +api16-core-ipv6.sgsnssdk.com +api16-core-n-alisg.tiktokv.com +api16-core-n-alisg.tiktokv.com.edgekey.net +api16-core-va.tiktokv.com +api16-core-va.tiktokv.com.edgekey.net +api16-normal-c-alisg.tiktokv.com +api16-normal-c-alisg.tiktokv.com.edgekey.net +api16-normal-c-useast1a.musical.ly +api16-normal-c-useast1a.musical.ly.edgekey.net +api16-normal-c-useast1a.tiktokv.com +api16-normal-c-useast1a.tiktokv.com.edgekey.net +api16-normal-c-useast2a.musical.ly +api16-normal-c-useast2a.musical.ly.edgekey.net +api16-normal-c-useast2a.tiktokv.com +api16-normal-c-useast2a.tiktokv.com.edgekey.net +api16-normal-h2.sgsnssdk.com +api16-normal-i-alisg.sgsnssdk.com +api16-normal-i-sg.sgsnssdk.com +api16-normal-ipv4.sgsnssdk.com +api16-normal-ipv6.sgsnssdk.com +api16-normal-v4.tiktokv.com +api16-normal-v4.tiktokv.com.edgekey.net +api16-normal-v6.tiktokv.com +api16-normal-v6.tiktokv.com.edgekey.net +api16-va.tiktokv.com +api16-va.tiktokv.com.edgekey.net +api16.tiktokv.com +api19-core-c-alisg.tiktokv.com +api19-core-c-useast1a.musical.ly +api19-core-c-useast1a.tiktokv.com +api19-core-c-useast2a.musical.ly +api19-core-c-useast2a.tiktokv.com +api19-core-c6-alisg.tiktokv.com +api19-core-va.tiktokv.com +api19-normal-c-alisg.tiktokv.com +api19-normal-c-useast1a.musical.ly +api19-normal-c-useast1a.tiktokv.com +api19-normal-c-useast2a.musical.ly +api19-normal-c-useast2a.tiktokv.com +api19-va.tiktokv.com +api19.tiktokv.com +api19.toutiao50.com +api2-1.musical.ly +api2-16-h2-eagle-useast1a.musical.ly +api2-16-h2-eagle-useast1a.musical.ly.edgekey.net +api2-16-h2-eagle-useast2a.musical.ly +api2-16-h2-eagle-useast2a.musical.ly.edgekey.net +api2-16-h2-eagle.musical.ly +api2-16-h2-eagle.musical.ly.edgekey.net +api2-16-h2-useast1a.musical.ly +api2-16-h2-useast1a.musical.ly.edgekey.net +api2-16-h2-useast2a.musical.ly +api2-16-h2-useast2a.musical.ly.edgekey.net +api2-16-h2.musical.ly +api2-16-h2.musical.ly.edgekey.net +api2-16-quic-useast2a.musical.ly +api2-16-quic-useast2a.musical.ly.edgekey.net +api2-16-quic.musical.ly +api2-16-quic.musical.ly.edgekey.net +api2-16-useast2a.musical.ly +api2-16-useast2a.musical.ly.edgesuite.net +api2-16.musical.ly +api2-16.musical.ly.edgesuite.net +api2-19-h2-eagle-useast1a.musical.ly +api2-19-h2-eagle-useast2a.musical.ly +api2-19-h2-eagle.musical.ly +api2-19-h2-useast1a.musical.ly +api2-19-h2-useast2a.musical.ly +api2-19-h2.musical.ly +api2-19-useast2a.musical.ly +api2-19.musical.ly +api2-21-h2-eagle-useast1a.musical.ly +api2-21-h2-eagle-useast2a.musical.ly +api2-21-h2-eagle.musical.ly +api2-21-h2-useast1a.musical.ly +api2-21-h2-useast2a.musical.ly +api2-21-h2.musical.ly +api2-21-quic-useast2a.musical.ly +api2-21-quic.musical.ly +api2-21.musical.ly +api2-22-h2.musical.ly +api2-22-quic-eagle-useast1a.musical.ly +api2-22-quic-eagle-useast2a.musical.ly +api2-22-quic-eagle.musical.ly +api2-22-quic-useast1a.musical.ly +api2-22-quic-useast2a.musical.ly +api2-22-quic.musical.ly +api2-30-useast2a.musical.ly +api2-30.musical.ly +api2-31.musical.ly +api2-32.musical.ly +api2-core-useast1a.musical.ly +api2-core-useast1a.musical.ly.edgekey.net +api2-core-useast2a.musical.ly +api2-core-useast2a.musical.ly.edgekey.net +api2-core.musical.ly +api2-core.musical.ly.edgesuite.net +api2-gcpbr-quic.musical.ly +api2-h2-useast1a.musical.ly +api2-h2-useast1a.musical.ly.edgesuite.net +api2-h2-useast2a.musical.ly +api2-h2-useast2a.musical.ly.edgesuite.net +api2-h2.musical.ly +api2-h2.musical.ly.edgesuite.net +api2-t1.musical.ly +api2-t2-useast2a.musical.ly +api2-t2.musical.ly +api2-t3.musical.ly +api2-useast1a.musical.ly +api2-useast1a.musical.ly.edgekey.net +api2-useast2a.musical.ly +api2-useast2a.musical.ly.edgekey.net +api2.musical.ly +api2.musical.ly.edgekey.net +api21-core-c-alisg.tiktokv.com +api21-core-c-useast1a.musical.ly +api21-core-c-useast1a.tiktokv.com +api21-core-c-useast2a.musical.ly +api21-core-c-useast2a.tiktokv.com +api21-core-n-alisg.tiktokv.com +api21-core-va.tiktokv.com +api21-h2-eagle.tiktokv.com +api21-h2.tiktokv.com +api21-normal-c-alisg.tiktokv.com +api21-normal-c-useast1a.musical.ly +api21-normal-c-useast1a.tiktokv.com +api21-normal-c-useast2a.musical.ly +api21-normal-c-useast2a.tiktokv.com +api21-normal-n-alisg.tiktokv.com +api21-quic.tiktokv.com +api21-test.byteoversea.com +api21-va.tiktokv.com +api21.tiktokv.com +api21.toutiao50.com +api22-core-c-alisg.tiktokv.com +api22-core-c-useast1a.musical.ly +api22-core-c-useast1a.tiktokv.com +api22-core-c-useast2a.musical.ly +api22-core-c-useast2a.tiktokv.com +api22-core-c4-alisg.tiktokv.com +api22-core-c6-alisg.tiktokv.com +api22-core-i-alisg.sgsnssdk.com +api22-core-n-alisg.tiktokv.com +api22-core-va.tiktokv.com +api22-normal-c-alisg.tiktokv.com +api22-normal-c-useast1a.musical.ly +api22-normal-c-useast1a.tiktokv.com +api22-normal-c-useast2a.musical.ly +api22-normal-c-useast2a.tiktokv.com +api22-normal-i-alisg.sgsnssdk.com +api22-quic.sgsnssdk.com +api22-quic.tiktokv.com +api22-va.tiktokv.com +api3-core-c-alisg.tiktokv.com +api3-core-c-hl.amemv.com +api3-core-c-hl.amemv.com.w.cdngslb.com +api3-core-c-lf.amemv.com +api3-core-c-lf.amemv.com.w.cdngslb.com +api3-core-c-lq.amemv.com +api3-core-c-lq.amemv.com.w.cdngslb.com +api3-core-c-useast1a.tiktokv.com +api3-core-c.amemv.com +api3-core-c.amemv.com.w.cdngslb.com +api3-normal-c-alisg.tiktokv.com +api3-normal-c-hl.amemv.com +api3-normal-c-hl.amemv.com.w.cdngslb.com +api3-normal-c-hl.snssdk.com +api3-normal-c-lf.amemv.com +api3-normal-c-lf.snssdk.com +api3-normal-c-lq.amemv.com +api3-normal-c-lq.amemv.com.w.cdngslb.com +api3-normal-c-lq.snssdk.com +api3-normal-c-useast1a.tiktokv.com +api3-normal-c.amemv.com +api3-normal-c.amemv.com.w.cdngslb.com +api3-normal-c.snssdk.com +api3-trial-h2.snssdk.com +api30-h2-eagle.tiktokv.com +api30-h2.tiktokv.com +api30.glb.byteoversea.net +api30.tiktokv.com +api31.bytegeo.akadns.net +api31.tiktokv.com +api32.tiktokv.com +api39.zong.byteoversea.net +api5-core-c-lq.amemv.com +api5-core-c.amemv.com +api5-normal-c-hl.snssdk.com +api5-normal-c-lf.snssdk.com +api5-normal-c-lq.snssdk.com +api5-normal-c.snssdk.com +api53-core-c-alisg.tiktokv.com +api53-core-c-useast1a.tiktokv.com +api53-normal-c-alisg.tiktokv.com +api53-normal-c-useast1a.tiktokv.com +api55-normal-c-alisg.tiktokv.com +api55-normal-c-useast1a.tiktokv.com +api58-core-c-alisg.tiktokv.com +api58-normal-c-alisg.tiktokv.com +api58-normal-c-useast1a.tiktokv.com +api77-core-c-alisg.tiktokv.com +api77-normal-c-alisg.tiktokv.com +api77-normal-c-useast1a.tiktokv.com +api9-core-c-alisg.tiktokv.com +api9-normal-c-alisg.tiktokv.com +api9-normal-c-useast1a.tiktokv.com +apmlog.snssdk.com +app-beta.snssdk.com +app-measurement.com +app-test.musemuse.cn +app-va.tiktokv.com +app.musemuse.cn +app.musical.ly +app.snssdk.com +app.toutiao.com +app.toutiao.com.w.cdngslb.com +applog-useast1a.musical.ly +applog-useast1a.musical.ly.edgekey.net +applog-useast2a.musical.ly +applog-useast2a.musical.ly.edgekey.net +applog.musical.ly +applog.musical.ly.edgekey.net +applog.snssdk.com +applog.snssdk.com.w.kunluncan.com +applog.tiktokv.com +argus.byted.org +artist.tiktok.com +artists.tiktok.com +autolua.snssdk.com +automation.snssdk.com +aweme-activity-lb.t.bytedns.net +aweme-bee-hl.snssdk.com +aweme-bee.snssdk.com +aweme-beta.bytedance.net +aweme-c.snssdk.com +aweme-c.snssdk.com.w.kunluncan.com +aweme-cold.snssdk.com +aweme-core.b.bytedns.net +aweme-eagle-hl.snssdk.com +aweme-eagle-hl.snssdk.com.w.kunluncan.com +aweme-eagle-hl.snssdk.com.xi.zwtianshangm.com +aweme-eagle-ipv6.snssdk.com +aweme-eagle-lq.snssdk.com +aweme-eagle-lq.snssdk.com.xi.zwtianshangm.com +aweme-eagle.snssdk.com +aweme-eagle.snssdk.com.w.kunluncan.com +aweme-eagle.snssdk.com.xi.zwtianshangm.com +aweme-heartbeat.snssdk.com +aweme-hl.snssdk.com +aweme-hl.snssdk.com.w.kunluncan.com +aweme-hl.snssdk.com.xi.zwtianshangm.com +aweme-ipv6.snssdk.com +aweme-live-hl.snssdk.com +aweme-live-hl.snssdk.com.w.kunluncan.com +aweme-live-quic.snssdk.com +aweme-live.snssdk.com +aweme-live.snssdk.com.w.kunluncan.com +aweme-lq.snssdk.com +aweme-n.snssdk.com +aweme-o.snssdk.com +aweme-swan-hl.snssdk.com +aweme-swan-hl.snssdk.com.w.kunluncan.com +aweme-swan.snssdk.com +aweme-swan.snssdk.com.bsgslb.com +aweme-swan.snssdk.com.w.kunluncan.com +aweme-yak-hl.snssdk.com +aweme-yak.snssdk.com +aweme-yak.snssdk.com.w.kunluncan.com +aweme.isnssdk.com +aweme.snssdk.com +aweme.snssdk.com.bytedns.net +aweme.snssdk.com.xi.zwtianshangm.com +awssg.l.byteoversea.net +b-kds.bytedance.com +b.bytedns.net +b.l.bytedns.net +b6morlb.qfyf1toi.com +bcy-promo.snssdk.com +bcy.snssdk.com +bds-sg.byteoversea.com +bds.snssdk.com +bfc-test-dynamic.snssdk.com +bfc-test-gather.snssdk.com +bfc-test-hole.snssdk.com +bgacfgabbcd.temp.p23.tc.cdntip.com +bgp.aweme-core-eagle-lf.l.bytedns.net +bgp.httpdns-hl.l.bytedns.net +bgp.httpdns-lf.l.bytedns.net +bgp.misc-core-lb-lf.l.bytedns.net +bgp.toutiao-other-lf.l.bytedns.net +bgp.ttgw.webcast-lb-lf.l.bytedns.net +bhns1.toutiao.com +bhns2.toutiao.com +bianjixing.snssdk.com +bic.snssdk.com +bkbd-awsjp16-up-va.tiktokcdn.com +bkbd-awsjp16-up-va.tiktokcdn.com.edgesuite.net +bkbd-awsjp16-up.muscdn.com +bkbd-maliva16-up-va.tiktokcdn.com +bkbd-maliva16-up-va.tiktokcdn.com.edgesuite.net +bkbd-maliva16-up.muscdn.com +bluewhale.snssdk.com +boe-bcy.byted.org +boe-gateway.byted.org +boe-lb.bytedns.net +boe-ra.byteoversea.com +boe-verify.snssdk.com +boe.i.snssdk.com +bolt.snssdk.com +book.snssdk.com +book.snssdk.com.w.kunluncan.com +bos.bytegeo.akadns.net +brn-cgk-v1.l.byteoversea.net +bsdk.sgsnssdk.com +bsdk.snssdk.com +business-sg.topbuzz.com +business.tiktok.com +business.topbuzz.com +bvc-tos.pstatp.com +bytecamp.toutiao.com +bytecdn.cn +bytecdn.cn.bsgslb.com +byted.org +bytedance-tmcdn.tm.com.my +bytedance.akadns.net +bytedance.map.fastly.net +bytedanceapi.com +bytedns.net +bytegeo.akadns.net +byteicdn.com +byteimg.com +byteimg.com.bsgslb.com +bytemock-http-boe.byted.org +byteoversea.com +byteoversea.com.bytegeo.akadns.net +byteoversea.com.edgekey.net +byteoversea.com.edgesuite.net +byteoversea.com.srip.net +byteoversea.net +bytetcdn.com +bytewlb.akadns.net +bytps.snssdk.com +c.bytetcdn.com +c.l.bytedns.net +c.pstatp.com +c.pstatp.com.w.cdngslb.com +c.worldfcdn.com +c01.i07.arnic.lv3.cloudglb.com +c1.l.bytedns.net +calender.snssdk.com +capture.byteoversea.com +car.toutiao.com +car.toutiao.com.w.cdngslb.com +careers.tiktok.com +careers.tiktok.com.edgesuite.net +cars.toutiao.com +cars.toutiao.com.w.cdngslb.com +castmsc1.xzcs3zlph.com +castmsc1.xzcs3zlph.com.edgesuite.net +casttt1.xzcs3zlph.com +casttt1.xzcs3zlph.com.edgesuite.net +cc.tiktok.com +cc.toutiao.com +ccfdhgbagdc.bgacfgabbcd.temp.p23.tc.cdntip.com +cdn-feature.byted.org +cdn-t1.byteoversea.com +cdn-t2.byteoversea.com +cdn.isnssdk.com +cdn.sgsnssdk.com +cdn.tiktok.com.c.footprint.net +cdn.tiktok.com.c.secure.footprint.net +cdn1-effect.snssdk.com +cdn3-effect.snssdk.com +cdn9-effect.snssdk.com +cebhheaehhb.temp.p23.tc.cdntip.com +cg.snssdk.com +cgame-gameset1.snssdk.com +cgame-gameset2.snssdk.com +cgame-gssdk.snssdk.com +cgamede-battle1.snssdk.com +cgameus-battle1.snssdk.com +cgameus-gameset1.snssdk.com +cgameus-gssdk.snssdk.com +channel.muscdn.com +chaos-boe.byted.org +chengzijianzhan.com +chuangyi.toutiao.com +chuangyi.toutiao.com.w.cdngslb.com +ci.toutiao.com +cjwallet.snssdk.com +class.snssdk.com +client_monitor.isnssdk.com +client_monitor.sgsnssdk.com +clientmonitor.isnssdk.com +clinent_monitor.isnssdk.com +clue.toutiao.com +cmaf-source-fcdn-frankfurt.s.worldfcdn.com +code.byted.org +cognition-va.bytedance.com +cognition.byteoversea.com +collect-elb-1844102569.us-east-1.elb.amazonaws.com +comic.snssdk.com +compass-ns.byteoversea.com +compass-ns.byteoversea.net +coresite-iad-api1.l.byteoversea.net +coresite-iad-v1.l.byteoversea.net +crash.snssdk.com +creator.toutiao.com +creatormarketplace.tiktok.com +credit.snssdk.com +cs.snssdk.com +csp.snssdk.com +d.amemv.com +d.amemv.com.w.kunlunca.com +d.l.bytedns.net +d.pstatp.com +d.snssdk.com +d.toutiao.com +d0gsjbullz.xzcs3zlph.com +d1.l.bytedns.net +d10br2bir1hdgs.cloudfront.net +d1fet3u01ea8tr.cloudfront.net +d1p5t7ayuzmgma.cloudfront.net +d1q3ka81k9jzg1.cloudfront.net +d1tl82f62uycur.cloudfront.net +d2.pstatp.com +d2.pstatp.com.cloudcdn.net +d2.pstatp.com.cloudglb.com +d271osm3r86e5v.cloudfront.net +d273c72xo36dwp.cloudfront.net +d2mmxl4tf8u5lc.cloudfront.net +d2ojit41ttaj0e.cloudfront.net +d2zoqe1q7uv743.cloudfront.net +d3a5g80vg31har.cloudfront.net +d3rbtc9rqobfk5.cloudfront.net +d9xr2nrl1eodo.cloudfront.net +danpin.snssdk.com +dataedu.snssdk.com +datahub.tiktok.com +davinci.snssdk.com +dcd.snssdk.com +debaac4b.tt.x.bsgslb.cn +debaac5c.tt.x.bsgslb.cn +developer-hl.toutiao.com +developer-sg.byteoversea.com +developer-sg.byteoversea.com.edgesuite.net +developer-sg.toutiao.com +developer.sgsnssdk.com +developer.toutiao.com +developers.tiktok.com +developers.tiktok.com.edgesuite.net +devops.byted.org +dficimage.toutiao.com +diamond-share.snssdk.com +diamond.snssdk.com +diamond.snssdk.com.w.kunluncan.com +dig.bdurl.net +dig.bdurl.net.bytedns.net +dig.byteoversea.com +direct.musical.ly +dispatch.byteoversea.com +distribution.snssdk.com +dm-hl.toutiao.com +dm-lq.toutiao.com +dm-lq.toutiao.com.w.cdngslb.com +dm-maliva-quic.byteoversea.com +dm-maliva16.byteoversea.com +dm-maliva16.byteoversea.com.edgekey.net +dm-sg-quic.byteoversea.com +dm-test.toutiao.com +dm.bytedance.com +dm.bytedance.com.w.cdngslb.com +dm.bytedance.net +dm.byteoversea.com +dm.isnssdk.com +dm.pstatp.com +dm.sgsnssdk.com +dm.toutiao.com +dm.toutiao.com.bytedns.net +dm.toutiao.com.w.cdngslb.com +dm16-alisg.byteoversea.com +dm16-alisg.byteoversea.com.edgekey.net +dm16-alisg.tiktokv.com +dm16-alisg.tiktokv.com.edgekey.net +dm16-useast1a.byteoversea.com +dm16-useast1a.byteoversea.com.edgekey.net +dm16-useast1a.tiktokv.com +dm16-useast1a.tiktokv.com.edgekey.net +dm16-useast2a.byteoversea.com +dm16-useast2a.byteoversea.com.edgekey.net +dm16-useast2a.tiktokv.com +dm16-useast2a.tiktokv.com.edgekey.net +dm16-va.tiktokv.com +dm16-va.tiktokv.com.edgekey.net +dm16.byteoversea.com +dm16.byteoversea.com.edgekey.net +dm16.musical.ly +dm16.musical.ly.edgekey.net +dm16.tiktokv.com +dm16.tiktokv.com.edgekey.net +dmp.snssdk.com +doc.toutiao.com +docs-crc-frontier.snssdk.com +docs-frontier-hl.snssdk.com +docs-frontier-sg.byteoversea.com +docs-frontier.snssdk.com +doh.byteoversea.com +douyin.com +douyinact.com +douyincdn.com +douyinvideo.net +down.muscdn.com +dpprofile.snssdk.com +dpprofile.snssdk.com.w.kunluncan.com +drop-black-hole.snssdk.com +drop-l-aweme-hl.snssdk.com +drop-l-aweme-lf.snssdk.com +drop-l-aweme-lq.snssdk.com +drop-l-aweme.snssdk.com +drop-l-gather.snssdk.com +drop-l-misc-core-hl.snssdk.com +drop-l-misc-core-lf.snssdk.com +drop-l-misc-core-lq.snssdk.com +drop-l-misc-core.snssdk.com +drop-l-toutiao-hl.snssdk.com +drop-l-toutiao-lf.snssdk.com +drop-l-toutiao-lq.snssdk.com +drop-l-toutiao.snssdk.com +drop-l-webcast-hl.snssdk.com +drop-l-webcast-lf.snssdk.com +drop-l-webcast-lq.snssdk.com +drop-l-webcast.snssdk.com +drop-s-aweme-hl.snssdk.com +drop-s-aweme-lf.snssdk.com +drop-s-aweme-lq.snssdk.com +drop-s-aweme.snssdk.com +drop-s-gather.snssdk.com +drop-s-toutiao-hl.snssdk.com +drop-s-toutiao-lf.snssdk.com +drop-s-toutiao-lq.snssdk.com +drop-s-toutiao-misc-hl.snssdk.com +drop-s-toutiao-misc-lf.snssdk.com +drop-s-toutiao-misc-lq.snssdk.com +drop-s-toutiao-misc.snssdk.com +drop-s-toutiao.snssdk.com +drt-lhr-v1.l.byteoversea.net +dsa-video.traffic.bytedance.akadns.net +dsa01.snssdk.com +dsa02.snssdk.com +dsa06.snssdk.com +dsa11.snssdk.com +dsa13.snssdk.com +dsp.toutiao.com +dttorhs584ri4.cloudfront.net +dynamic-amemv.com.edgekey.net +dynamic-comm.snssdk.com.bdgslb.com +dynamic-comm2.snssdk.com.bdgslb.com +dynamic-comm3.snssdk.com.bdgslb.com +dynamic-d5snssdk.bwhero.com.bdgslb.com +dzsu313agxr9l.cloudfront.net +e.l.bytedns.net +e.snssdk.com +e.toutiao.com +eagleye.snssdk.com +eagleye.snssdk.com.w.kunluncan.com +ec.snssdk.com +ecbabdeeeab.temp.p23.tc.cdntip.com +ecomuser.snssdk.com +edccfdcccgb.temp.p23.tc.cdntip.com +edge-aliec1-oss.bytecdn.cn +edge-aliec1-oss.snssdk.com +edge-aliec2-oss.bytecdn.cn +edge-aliec2-oss.snssdk.com +edge-aliec2.snssdk.com +edge-alinc2-oss-slb1.bytecdn.cn +edge-alinc2-oss-slb2.bytecdn.cn +edge-alisc1-oss.bytecdn.cn +edge-alisc1-oss.snssdk.com +edux.snssdk.com +ee-lb.bytedns.net +eehbfdggdad.temp.p23.tc.cdntip.com +effect-hl.snssdk.com +effect-hl.snssdk.com.w.kunluncan.com +effect-lf.snssdk.com +effect-lq.snssdk.com +effect.sgsnssdk.com +effect.snssdk.com +effect.tiktok.com +elb-babe-934876860.ap-southeast-1.elb.amazonaws.com +elb-for-cdn-1922253052.ap-southeast-1.elb.amazonaws.com +elb-for-front-1752188976.us-east-1.elb.amazonaws.com +env-boe.byted.org +ep-s3.bytecdn.cn +equinix-fra-api1.l.byteoversea.net +equinix-fra-k8s-v1.l.byteoversea.net +equinix-fra-v3.l.byteoversea.net +equinix-iad-api.l.byteoversea.net +equinix-iad-api1.l.byteoversea.net +equinix-iad-cache01.l.byteoversea.net +equinix-iad-v1.l.byteoversea.net +equinix-sea-v1.l.byteoversea.net +equinix-sin-api1.l.byteoversea.net +equinix-sin-v1.l.byteoversea.net +equinix-sin-vo.l.byteoversea.net +equinix-sjc-api1.l.byteoversea.net +equinix-sjc-v1.l.byteoversea.net +equinix-sjc-v2.l.byteoversea.net +et.snssdk.com +eva.snssdk.com +experiment.tiktok.com +extlog.snssdk.com +eyeu.snssdk.com +f-charge-fee.snssdk.com +f-moneyloan.snssdk.com +f-moneyloantest.snssdk.com +f-p-sandbox.snssdk.com +f-p-va.isnssdk.com +f-p-va.isnssdk.com.edgekey.net +f-pay-rp.snssdk.com +f.l.bytedns.net +f3-ttcdn-tos.pstatp.com +f3rsrpl.qfyf1toi.com +fadedfcbcfb.temp.p23.tc.cdntip.com +fangchan.snssdk.com +fangchan.toutiao.com +fantasy-static.amemv.com +fantasy-static.amemv.com.w.cdngslb.com +fantasy-static.snssdk.com +fantasy3-activity-c.amemv.com +fantasy3-activity-c.amemv.com.w.cdngslb.com +fb89a3b6273106930d55e9747de3d9d4.byteoversea.com +fcdnns1.compass.byteoversea.net +fcdnns2.compass.byteoversea.net +fddfafhadd.temp.p23.tc.cdntip.com +feedback-va.byteoversea.com +feelgood-api.tiktok.com +feishu-tos.pstatp.com +fgbfchedbbb.temp.p23.tc.cdntip.com +financal-stock-web.snssdk.com +finance.snssdk.com +financial-stock-web.snssdk.com +flv-l11.tiktokcdn.liveplay.myqcloud.com +fortune-hl.snssdk.com +fortune.snssdk.com +fortune.snssdk.com.w.kunluncan.com +fr4.byteoversea.com +frontier-aweme-hl.snssdk.com +frontier-aweme.snssdk.com +frontier-hl.snssdk.com +frontier-lf.snssdk.com +frontier-ra-va.byteoversea.com +frontier-sg-edu.byteoversea.com +frontier-sgee.byteoversea.com +frontier-va-edu.byteoversea.com +frontier-va-useast2a.byteoversea.com +frontier-va.byteoversea.com +frontier-va.tiktokv.com +frontier.byteoversea.com +frontier.isnssdk.com +frontier.musical.ly +frontier.sgsnssdk.com +frontier.snssdk.com +frontier.tiktokv.com +ft.snssdk.com +ft.snssdk.com.w.kunluncan.com +fusioncdn-lf.snssdk.com +fxj-boe.snssdk.com +g.l.bytedns.net +gababhhcehc.temp.p23.tc.cdntip.com +game.snssdk.com +gate.snssdk.com +gecko-cold.snssdk.com +gecko-hl.snssdk.com +gecko-hl.snssdk.com.w.kunluncan.com +gecko-ipv6.snssdk.com +gecko-lf.snssdk.com +gecko-lf.snssdk.com.w.kunluncan.com +gecko-lq.snssdk.com +gecko-pangle-hl.snssdk.com +gecko-pangle-lf.snssdk.com +gecko-pangle-lq.snssdk.com +gecko-sg.byteoversea.com +gecko-sg.byteoversea.com.edgesuite.net +gecko-sg.tiktokv.com +gecko-sg.tiktokv.com.edgesuite.net +gecko-va-useast1a.musical.ly +gecko-va-useast2a.byteoversea.com +gecko-va-useast2a.musical.ly +gecko-va-useast2a.musical.ly.edgesuite.net +gecko-va.byteoversea.com +gecko-va.byteoversea.com.edgesuite.net +gecko-va.musical.ly +gecko-va.musical.ly.edgesuite.net +gecko-va.snssdk.com +gecko-va.tiktokv.com +gecko-va.tiktokv.com.edgesuite.net +gecko.snssdk.com +gecko16-normal-c-useast1a.tiktokv.com +gecko16-normal-c-useast1a.tiktokv.com.edgesuite.net +gecko16-normal-c-useast2a.tiktokv.com +gecko16-normal-c-useast2a.tiktokv.com.edgesuite.net +gedfaccdcf.temp.p23.tc.cdntip.com +getstarted.tiktok.com +getstarted.tiktok.com.edgekey.net +gf-sg.snssdk.com +gf.snssdk.com +gh2ypyxesf.mzfvozqybf.com +gini.snssdk.com +git.byted.org +global-h1.bytedance.map.fastly.net +gms-ns.byteoversea.com +gplog-sg.byteoversea.com +gplog-va.byteoversea.com +gplog.snssdk.com +grafana-boe.byted.org +grelationship-sg.snssdk.com +gs-sin-api1.l.byteoversea.net +gsdk.sgsnssdk.com +gsdk.snssdk.com +gss-us1.bytetcdn.com +gtf-eapi.byteoversea.com +gtfac.byteoversea.com +gtfstorage.byteoversea.com +gts-ns.byteoversea.com +gurd-hl.snssdk.com +gurd-lf.snssdk.com +gurd-lq.snssdk.com +gurd.snssdk.com +gyveepz.mzfvozqybf.com +h.l.bytedns.net +h1.bytedance.map.fastly.net +h5-ppx-activity-lq.snssdk.com +h5-ppx-activity.snssdk.com +h5.isnssdk.com +h5.sgsnssdk.com +h5.toutiao.com +haohuo.snssdk.com +helo.sgsnssdk.com +hggafcaacfc.temp.p23.tc.cdntip.com +hls-l11.tiktokcdn.liveplay.myqcloud.com +hn-cnc.polaris.byteoversea.com +holmes.snssdk.com +homed-hl.snssdk.com +homed-lq.snssdk.com +homed.snssdk.com +homed.snssdk.com.w.kunluncan.com +hotapi-cn.snssdk.com +hotapi-cn.snssdk.com.w.kunluncan.com +hotapi-va.isnssdk.com +hotapi-va.isnssdk.com.edgekey.net +hotapi.sgsnssdk.com +hotsoon-a-hl.snssdk.com +hotsoon-a-hl.snssdk.com.w.kunluncan.com +hotsoon-a-lq.snssdk.com +hotsoon-a.snssdk.com +hotsoon-a.snssdk.com.bytedns.net +hotsoon-cold.snssdk.com +hotsoon-hl.snssdk.com +hotsoon-hl.snssdk.com.w.kunluncan.com +hotsoon-lq.snssdk.com +hotsoon.snssdk.com +hotsoon.snssdk.com.bytedns.net +hotsoon.snssdk.com.w.kunluncan.com +hr.toutiao.com +hscdn-tos.pstatp.com +httpdns.byteoversea.com +htyy.toutiao.com +hypstarcdn.com +hypstarcdn.com.akamaized.net +hypstarcdn.com.edgesuite.net +i-hl.snssdk.com +i-hl.snssdk.com.w.kunluncan.com +i-ipv6.snssdk.com +i-lf.snssdk.com +i-lq.snssdk.com +i-sentry.byted.org +i-tb.isnssdk.com +i-tb.sgsnssdk.com +i.byteoversea.com +i.byteoversea.com.edgekey.net +i.isnssdk.com +i.isnssdk.com.edgekey.net +i.l.bytedns.net +i.pstatp.com +i.sgnssdk.com +i.sgsnssdk.com +i.snssdk.com +i.snssdk.com.w.kunluncan.com +i0.pstatp.com +i0.pstatp.com.cloudcdn.net +i0.pstatp.com.cloudglb.com +i0.pstatp.com.w.alikunlun.com +i1.isnssdk.com +i1.isnssdk.com.edgekey.net +i1.pstatp.com +i1.sgsnssdk.com +i15-tb.sgsnssdk.com +i16-core.sgsnssdk.com +i16-tb.isnssdk.com +i16-tb.isnssdk.com.edgekey.net +i16-tb.sgsnssdk.com +i2.pstatp.com +i22-tb-quic.sgsnssdk.com +i3.pstatp.com +i4.pstatp.com +i5.pstatp.com +i6.pstatp.com +iam-boe.byted.org +ib-hl.snssdk.com +ib-hl.snssdk.com.w.kunluncan.com +ib-ipv6.snssdk.com +ib-lf.snssdk.com +ib-lq.snssdk.com +ib.snssdk.com +ib.snssdk.com.edgekey.net +ib.snssdk.com.w.kunluncan.com +ib.tiktokv.com +ibytedtos.com +ibytedtos.com.edgekey.net +ibytedtos.com.edgesuite.net +ibyteimg.com +ibyteimg.com.akamaized.net +ibyteimg.com.edgesuite.net +ic-hl.snssdk.com +ic-hl.snssdk.com.w.kunluncan.com +ic-lq.snssdk.com +ic.snssdk.com +ic.snssdk.com.w.kunluncan.com +ichannel-tb.isnssdk.com +ichannel-tb.sgsnssdk.com +ichannel-va.tiktokv.com +ichannel.isnssdk.com +ichannel.musical.ly +ichannel.sgsnssdk.com +ichannel.snssdk.com +ichannel.snssdk.com.bytedns.net +icweiliimg1.pstatp.com +icweiliimg1.pstatp.com.wsglb0.com +icweiliimg6.pstatp.com +icweiliimg9.pstatp.com +id.snssdk.com +ie.snssdk.com +iesdouyin.com +iesios.byted.org +ii.snssdk.com +im-va.tiktokv.com +im-va.tiktokv.com.edgesuite.net +im.snssdk.com +im16-normal-c-useast1a.tiktokv.com +im16-normal-c-useast1a.tiktokv.com.edgesuite.net +im16-normal-c-useast2a.tiktokv.com +im16-normal-c-useast2a.tiktokv.com.edgesuite.net +im5bswhcgy.mzfvozqybf.com +image-cache-alisg.byteoversea.net +image-cache-maliva.byteoversea.net +image-sg.musical.ly +image-sg.tiktokv.com +image-va.musical.ly +image-va.tiktokv.com +imagex-settings.bytedanceapi.com +imagex.ap-singapore-1.bytedanceapi.com +imagex.bytedanceapi.com +imagex.us-east-1.bytedanceapi.com +imapi-16-useast1a.musical.ly +imapi-16-useast1a.musical.ly.edgesuite.net +imapi-16-useast2a.musical.ly +imapi-16-useast2a.musical.ly.edgesuite.net +imapi-16.musical.ly +imapi-16.musical.ly.edgesuite.net +imapi-16.tiktokv.com +imapi-16.tiktokv.com.edgesuite.net +imapi-cold.snssdk.com +imapi-hl.snssdk.com +imapi-hl.snssdk.com.w.kunluncan.com +imapi-ipv6.snssdk.com +imapi-lq.snssdk.com +imapi-mu.isnssdk.com +imapi-mu.isnssdk.com.edgekey.net +imapi-n.snssdk.com +imapi-sg.isnssdk.com +imapi.snssdk.com.w.kunluncan.com +imapi2.isnssdk.com +imapi2.isnssdk.com.edgekey.net +imapi2.snssdk.com +imapi2.snssdk.com.w.kunluncan.com +imapihotsoon.isnssdk.com +imapihotsoon.snssdk.com +imdcd.snssdk.com +img-bcy-qn.pstatp.com +in1.byteoversea.com +in2.byteoversea.com +index.toutiao.com +inf-portal-boe.byted.org +insurance.snssdk.com +internal-lb-all-maliva.bytedns.net +internal-lb-all-mix.bytedns.net +internal-lb-all.bytedns.net +io.snssdk.com +ipstatp.com +ipstatp.com.edgekey.net +ipstatp.com.edgesuite.net +ipv6-v16.muscdn.com +ipv6-v16.muscdn.com.akamaized.net +ipv6-v19.muscdn.com +is-dyn1.snssdk.com +is-dyn3.snssdk.com +is-dyn5-hl.snssdk.com +is-dyn5.snssdk.com +is-dyn9.snssdk.com +is-h2.snssdk.com +is-h2.snssdk.com.w.kunluncan.com +is-hl-ipv6.snssdk.com +is-hl.snssdk.com +is-hl.snssdk.com.w.kunluncan.com +is-lf.snssdk.com +is-lf.snssdk.com.w.kunluncan.com +is-lq.snssdk.com +is.snssdk.com +is.snssdk.com.bytedns.net +is.snssdk.com.xi.zwtianshangm.com +is1-ipv6.snssdk.com +is3-hl.snssdk.com +is3-ipv6.snssdk.com +is3-lf.snssdk.com +is3-lq.snssdk.com +is3.snssdk.com +isnssdk.com +isnssdk.com.edgekey.net +isub-hl.snssdk.com +isub-hl.snssdk.com.w.kunluncan.com +isub-lq.snssdk.com +isub-tb.isnssdk.com +isub-tb.sgsnssdk.com +isub.isnssdk.com +isub.sgsnssdk.com +isub.snssdk.com +isub.snssdk.com.bytedns.net +it-hl.snssdk.com +it-hl.snssdk.com.w.kunluncan.com +it-lq.snssdk.com +it.snssdk.com +it.snssdk.com.w.kunluncan.com +iu-hl.snssdk.com +iu-hl.snssdk.com.w.kunluncan.com +iu-ipv6.snssdk.com +iu-lf.snssdk.com.w.kunluncan.com +iu-lq.snssdk.com +iu.snssdk.com +iu.snssdk.com.w.kunluncan.com +ixigua.com +ixigua.com.bsgslb.com +ixigua.com.xi.zwtianshangm.com +ixiguavideo.com +ixiguavideo.com.bsgslb.com +j.l.bytedns.net +jinzhan.snssdk.com +jmj.toutiao.com +job.toutiao.com +jsb-hl.snssdk.com +jsb-lf.snssdk.com +jsb-lq.snssdk.com +jsb-sg.byteoversea.com +jsb-sg.byteoversea.com.edgesuite.net +jsb-sg.tiktokv.com +jsb-sg.tiktokv.com.edgekey.net +jsb-va.byteoversea.com +jsb-va.byteoversea.com.edgesuite.net +jsb-va.musical.ly +jsb-va.musical.ly.edgekey.net +jsb-va.tiktokv.com +jsb-va.tiktokv.com.edgekey.net +jsb.snssdk.com +jtjxpx.qfyf1toi.com +juliangyinqing.com +just-test-1.byteoversea.com +just-test.byteoversea.com +k.l.bytedns.net +k2-dy-ml.gslb.ksyuncdn.com +k8s.byted.org +kds-mva.bytedance.com +kds-mva.byteoversea.com +kds-sg.bytedance.com +kds-sg.byteoversea.com +kds-useast2a.byteoversea.com +kds.bytedance.com +ken.snssdk.com +krb5auth.byted.org +krb5auth1.byted.org +krb5auth2.byted.org +krb5auth3.byted.org +ksd.snssdk.com +kuaima.toutiao.com +l.bytedns.net +l.byteoversea.net +l.l.bytedns.net +l2-toutiao-ml.gslb.ks-cdn1.com +l2-toutiao-ml.gslb.ksyuncdn.com +l9bclvbns.xzcs3zlph.com +l9bclvbns.xzcs3zlph.com.edgesuite.net +lab.toutiao.com +landing.toutiao.com +lark-frontier-hl.snssdk.com +lark-frontier-sg.byteoversea.com +lark-frontier.byteoversea.com +lark-frontier.byteoversea.com.edgesuite.net +lark-frontier.snssdk.com +lb.jinritemai.com +lbapi.snssdk.com +lbapi.snssdk.com.w.kunluncan.com +learning-hl.snssdk.com +learning-lq.snssdk.com +learning.snssdk.com +legroup21.gslb.cdnle.com +letsencrypt.bytecdn.cn +lf-c.snssdk.com +lf-c.snssdk.com.w.kunluncan.com +lf-cold.snssdk.com +lf-hl-ipv6.snssdk.com +lf-hl.snssdk.com +lf-hl.snssdk.com.w.kunluncan.com +lf-hl.snssdk.com.xi.zwtianshangm.com +lf-hs-sg.ibytedtos.com +lf-hs-sg.ibytedtos.com.edgekey.net +lf-ipv6.snssdk.com +lf-lf.snssdk.com +lf-lq.snssdk.com +lf-n.snssdk.com +lf-tb-sg.ibytedtos.com +lf-tb-sg.ibytedtos.com.edgekey.net +lf-tk-sg.ibytedtos.com +lf-tk-sg.ibytedtos.com.edgekey.net +lf.snssdk.com +lf.snssdk.com.bytedns.net +lf.snssdk.com.xi.zwtianshangm.com +lf1-amcdn-tos.pstatp.com.wsglb0.com +lf1-dycdn-tos.pstatp.com +lf1-effectcdn-tos.pstatp.com +lf1-faceucdn-tos.pstatp.com +lf1-geckocdn-tos.pstatp.com +lf1-hscdn-tos.pstatp.com +lf1-hscdn-tos.pstatp.com.wsglb0.com +lf1-ipv6.snssdk.com +lf1-tccdn-tos.pstatp.com +lf1-tccdn-tos.pstatp.com.wsglb0.com +lf1-ttcdn-tos.pstatp.com +lf1-ttcdn-tos.pstatp.com.wsglb0.com +lf1-xgcdn-tos.pstatp.com +lf1-xgcdn-tos.pstatp.com.wsglb0.com +lf16-geckocdn-sg.ibytedtos.com +lf16-geckocdn-sg.ibytedtos.com.edgesuite.net +lf16-geckocdn-sg.tiktokcdn.com +lf16-geckocdn-sg.tiktokcdn.com.edgesuite.net +lf16-lark-va.ibytedtos.com +lf16-lark-va.ibytedtos.com.edgekey.net +lf16-mt-va.ibytedtos.com +lf16-muse-va.ibytedtos.com +lf16-muse-va.ibytedtos.com.edgekey.net +lf16-tcs.tiktokcdn.com +lf19-geckocdn-sg.ibytedtos.com +lf19-geckocdn-sg.tiktokcdn.com +lf2-hscdn-tos.pstatp.com +lf3-adcdn-tos.pstatp.com +lf3-adcdn-tos.pstatp.com.w.alikunlun.com +lf3-adcdn-tos.pstatp.com.xi.zwtianshangm.com +lf3-amcdn-tos.pstatp.com.w.cdngslb.com +lf3-eecdn-tos.pstatp.com.w.alikunlun.com +lf3-effectcdn-tos.pstatp.com +lf3-effectcdn-tos.pstatp.com.w.cdngslb.com +lf3-faceucdn-tos.pstatp.com +lf3-faceucdn-tos.pstatp.com.w.cdngslb.com +lf3-fpcdn-tos.pstatp.com.w.cdngslb.com +lf3-geckocdn-tos.pstatp.com +lf3-geckocdn-tos.pstatp.com.w.cdngslb.com +lf3-hscdn-tos.pstatp.com +lf3-hscdn-tos.pstatp.com.xi.zwtianshangm.com +lf3-ipv6.snssdk.com +lf3-ppcdn-tos.pstatp.com +lf3-tccdn-tos.pstatp.com +lf3-ttcdn-tos.pstatp.com +lf3-xgcdn-tos.pstatp.com +lf6-dycdn-tos.pstatp.com +lf6-geckocdn-tos.pstatp.com +lf6-hscdn-tos.pstatp.com +lf6-ttcdn-tos.pstatp.com +lf6-xgcdn-tos.pstatp.com +lf9-amcdn-tos.pstatp.com.bsgslb.com +lf9-effectcdn-tos.pstatp.com +lf9-effectcdn-tos.pstatp.com.bsgslb.com +lf9-geckocdn-tos.pstatp.com.bsgslb.com +lf9-tk-tos.tiktokcdn.com +lg-hl.snssdk.com +lg-hl.snssdk.com.w.kunluncan.com +lg-lf.snssdk.com +lg-lq.snssdk.com +lg.snssdk.com +lh-hl.snssdk.com +lh-hl.snssdk.com.w.kunluncan.com +lh-lq.snssdk.com +lh.snssdk.com +lianmeng.snssdk.com +lianmengapi-hl.snssdk.com +lianmengapi-lf.snssdk.com +lianmengapi-lq.snssdk.com +lianmengapi.snssdk.com +lianmengapi.snssdk.com.w.kunluncan.com +library.musical.ly +link-sg.byteoversea.com +link-va.byteoversea.com +link-va.byteoversea.com.edgesuite.net +lite.snssdk.com +liteshopads.com +live.bytedanceapi.com +live.musical.ly +live6.pstatp.com +live6a.pstatp.com +lively-ws-flv.muscdn.com +lively-ws-hls.muscdn.com +lms-ns.byteoversea.com +loc.snssdk.com +log-16.toutiao50.com +log-c.snssdk.com +log-c.snssdk.com.w.kunluncan.com +log-cold.snssdk.com +log-hl-ipv6.snssdk.com +log-hl.snssdk.com +log-hl.snssdk.com.w.kunluncan.com +log-lf.snssdk.com +log-lq.snssdk.com +log-sg.toutiao50.com +log-tb.isnssdk.com +log-tb.sgsnssdk.com +log-va.tiktokv.com +log-va.tiktokv.com.edgekey.net +log.byteoversea.com +log.byteoversea.com.edgekey.net +log.isnssdk.com +log.musical.ly +log.sgsnssdk.com +log.snssdk.com +log.snssdk.com.bytedns.net +log.tiktokv.com +log.tiktokv.com.edgesuite.net +log.toutiao50.com +log.xzcs3zlph.com +log.xzcs3zlph.com.edgesuite.net +log15.byteoversea.com +log16-normal-c-useast1a.tiktokv.com +log16-normal-c-useast1a.tiktokv.com.edgekey.net +log16-normal-c-useast2a.tiktokv.com +log16-normal-c-useast2a.tiktokv.com.edgekey.net +log16.byteoversea.com +log16.byteoversea.com.edgekey.net +log2-useast1a.musical.ly +log2-useast1a.musical.ly.edgekey.net +log2-useast2a.musical.ly +log2-useast2a.musical.ly.edgekey.net +log2.musical.ly +log2.musical.ly.edgesuite.net +log3-ipv6.snssdk.com +log3-normal-c-alisg.tiktokv.com +log3-normal-c-useast1a.tiktokv.com +log53-normal-c-alisg.tiktokv.com +log53-normal-c-useast1a.tiktokv.com +log58-normal-c-alisg.tiktokv.com +log77-normal-c-alisg.tiktokv.com +log9-normal-c-alisg.tiktokv.com +login.tiktok.com +logsg.xzcs3zlph.com +logsg.xzcs3zlph.com.edgesuite.net +logtrace.snssdk.com +lp1.pstatp.com +lt.snssdk.com +lts-ns.byteoversea.com +luban.snssdk.com +luban.snssdk.com.w.kunluncan.com +luckycat-activity-hl.snssdk.com +luckycat-activity-lf.snssdk.com +luckycat-activity.snssdk.com +luckycat-ameboon-hl.snssdk.com +luckycat-ameboon-lf.snssdk.com +luckycat-ameboon-lq.snssdk.com +luckycat-ameboon.snssdk.com +luckycat-power-hl.snssdk.com +luckycat-power-lf.snssdk.com +luckycat-power-lq.snssdk.com +luckycat-power.snssdk.com +lv.ulikecam.com +lvres.muscdn.com +m-p16.akamaized.net +m-v16.akamaized.net +m-v16.toutiao50.com +m.ixugua.com +m.musical.ly +m.pstatp.com +m.pstatp.com.w.kunluncan.com +m.tiktok.com +m.tiktok.com.edgesuite.net +m.toutiao.com +m.toutiaocdn.cn +m.toutiaocdn.com +m.toutiaocdn.net +m.zjurl.cn +maliva-mcs.byteoversea.com +maliva-mcs.byteoversea.com.edgesuite.net +maliva-normal-lb-useast2a.byteoversea.net +maliva-normal-lb.byteoversea.net +market.toutiao.com +mcast1.toutiao50.com +mcs-va-useast2a.tiktokv.com +mcs-va.tiktokv.com +mcs.snssdk.com +media.tiktok.com +mercury-sdk.snssdk.com +mercury-vivo.snssdk.com +mercury.snssdk.com +meteor.tiktok.com +metrics.snssdk.com +mevynuwj.mzfvozqybf.com +mg.tiktok.com +mh.snssdk.com +mh.snssdk.com.w.kunluncan.com +microgame.snssdk.com +microhttp.snssdk.com +midc-test-hl.snssdk.com +midc-test.snssdk.com +mlab.toutiao.com +mobile.e.l.bytedns.net +mobile.g.l.bytedns.net +mobile.ttgw.tuchong-normal-lb-lf.l.bytedns.net +mobile.x.l.bytedns.net +mofxg.snssdk.com +mon-alias1.snssdk.com +mon-cold.snssdk.com +mon-hl.snssdk.com +mon-hl.snssdk.com.w.kunluncan.com +mon-ipv6.snssdk.com +mon-va-useast2a.byteoversea.com +mon-va.byteoversea.com +mon-va.byteoversea.com.edgesuite.net +mon-va.tiktokv.com +mon-va.tiktokv.com.edgesuite.net +mon-ws.snssdk.com +mon.byteoversea.com +mon.byteoversea.com.edgesuite.net +mon.isnssdk.com +mon.musical.ly +mon.musical.ly.edgesuite.net +mon.sgsnssdk.com +mon.snssdk.com +mon.snssdk.com.bytedns.net +mon.snssdk.com.xi.zwtianshangm.com +mon.tiktokv.com +mon.tiktokv.com.edgesuite.net +mon.toutiao.com +mongoosev.byted.org +monitor.isnssdk.com +monsetting.toutiao.com +moss-sg.snssdk.com +mp.toutiao.com +mpak-ainw1-up.muscdn.com +mpak-odec1.akamaized.net +mpak-sinc1.akamaized.net +mpak-ssgc1.akamaized.net +mpak-suse1.akamaized.net +mpak-suse1.muscdn.com +mpak-suse1.muscdn.com.akamaized.net +mpal-ocne1-up.muscdn.com +mpal-ocne1.muscdn.com +mpal-odec1.muscdn.com +mpal-odec1.muscdn.com.edgesuite.net +mpal-osgc1.muscdn.com +mpaw-sinc1-up.muscdn.com +mpaw-sinc1.muscdn.com +mpaw-ssgc1-up.muscdn.com +mpaw-ssgc1.muscdn.com +mpaw-suse1-up.muscdn.com +mpaw-suse1.muscdn.com +mphw-odec1.muscdn.com +mphw-ssgc1.muscdn.com +mphw-suse1.muscdn.com +mphw-suse1.muscdn.com.akamaized.net +mptc-suse1.muscdn.com +mpud-suse1.muscdn.com +mr-hl.snssdk.com +mr-lf.snssdk.com +mr-lq.snssdk.com +mr.snssdk.com +mssdk-sg.byteoversea.com +mssdk.snssdk.com +mtg-bkk-v1.l.byteoversea.net +mu.isnssdk.com +mue.toutiao.com +mue.toutiao.com.w.cdngslb.com +mus-lib-oss.muscdn.com +mus-oss.muscdn.com +mus-prod-o-de.muscdn.com +mus-prod-sg.muscdn.com +muscdn.com +muscdn.com.akamaized.net +muscdn.com.cdn.cloudflare.net +muscdn.com.edgesuite.net +muscdn.com.srip.net +muscdn.musical.ly.akadns.net +musemuse.cn +musereview.byteoversea.com +music.muscdn.com +music.musical.ly +musical.ly +musical.ly.akadns.net +musical.ly.edgekey.net +musical.ly.edgesuite.net +musically-alternate.app.link +musically.app.link +musically.muscdn.com +musician.tiktok.com +mva.byteoversea.com +mva.isnssdk.com +mva2.byteoversea.com +mvaali-live.byteoversea.com +mvaali-wallet.byteoversea.com +mvaali1.l.byteoversea.net +mvaali1.ws.byteoversea.net +mvaali2.l.byteoversea.net +mzfvozqybf.com +n.byteoversea.com +n.l.bytedns.net +nameaweme.snssdk.com +nativeapp.toutiao.com +nb.byted.org +neihanshequ.com +new1-maliva-normal-lb.byteoversea.net +newsroom.tiktok.com +nlb-l4.bytedns.net +notifyback-cn.snssdk.com +notifyback-sg.isnssdk.com +notifyback-va.isnssdk.com +notifyback-va.isnssdk.com.edgekey.net +novel-hl.snssdk.com +novel-lq.snssdk.com +novel.snssdk.com +nqzeqb.xzcs3zlph.com +ns.s.bytetcdn.com +ns1.bsgslb.com +ns1.c.bytetcdn.com +ns2.bsgslb.com +ns2.c.bytetcdn.com +ns2.s.bytetcdn.com +ns3.bsgslb.com +ns3.c.bytetcdn.com +o-sg.ibytedtos.com +o-us.ibytedtos.com +onelink-1664648862.eu-west-1.elb.amazonaws.com +open-api.musical.ly +open-api.tiktok.com +open-hl.snssdk.com +open-hl.toutiao.com +open-hl.toutiao.com.w.cdngslb.com +open-lq.toutiao.com +open-maliva.byteoversea.com +open-useast2a.bytedanceapi.com +open.bytedanceapi.com +open.isnssdk.com +open.mp.toutiao.com +open.sgsnssdk.com +open.snssdk.com +open.snssdk.com.bytedns.net +open.toutiao.com +open.toutiao.com.bytedns.net +open16-va.tiktokv.com +open16-va.tiktokv.com.edgekey.net +ops.musical.ly +orange.isnssdk.com +order.snssdk.com +order.snssdk.com.w.kunluncan.com +origin.maliva-normal-lb.byteoversea.net +otfp.byteoversea.com +other.l.bytedns.net +owl-sg.snssdk.com +owl-sg.tiktokv.com +owl-va.snssdk.com +owl.snssdk.com +owl.snssdk.com.w.kunluncan.com +p-t2.byteoversea.com +p-vcloud.byteimg.com +p.hypstarcdn.com +p.ipstatp.com +p.pstatp.com +p.pstatp.com.w.alikunlun.com +p.sgpstatp.com +p.sgsnssdk.com +p.tiktokcdn.com +p0.ipstatp.com +p0.pstatp.com +p0.pstatp.com.w.kunlunhuf.com +p0.sgpstatp.com +p1-ad.bytecdn.cn +p1-bcy.bytecdn.cn +p1-bcy.byteimg.com +p1-bk.byteimg.com +p1-cdn-a.byteimg.com +p1-cdn-b.byteimg.com +p1-dcd.byteimg.com +p1-dlxb.byteimg.com +p1-ds.byteimg.com +p1-dy-a.byteimg.com +p1-dy-b.byteimg.com +p1-dy-ipv6.byteimg.com +p1-dy.bytecdn.cn +p1-dy.byteimg.com +p1-dy.bytexservice.com +p1-ec.byteimg.com +p1-et.byteimg.com +p1-faceu.byteimg.com +p1-fp.byteimg.com +p1-growth.byteimg.com +p1-hs.bytecdn.cn +p1-hs.byteimg.com +p1-juejin.byteimg.com +p1-lark-file.byteimg.com +p1-lite.byteimg.com +p1-monster.byteimg.com +p1-oe.byteimg.com +p1-open.byteimg.com +p1-ppx.bytecdn.cn +p1-ppx.byteimg.com +p1-reading.byteimg.com +p1-security.byteimg.com +p1-tt-ipv6.byteimg.com +p1-tt.bytecdn.cn +p1-tt.byteimg.com +p1-ttpush.byteimg.com +p1-webcast-dycdn.byteimg.com +p1-webcast-hscdn.byteimg.com +p1-webcast-ttcdn.byteimg.com +p1-webcast-xgcdn.byteimg.com +p1-welfare.byteimg.com +p1-xg.bytecdn.cn +p1-xg.byteimg.com +p1-xg.pstatp.com +p1-xg.pstatp.com.wsglb0.com +p1.ipstatp.com +p1.pstatp.com +p1.pstatp.com.wscdns.com +p10.pstatp.com +p10.pstatp.com.w.kunlunle.com +p13.ipstatp.com +p15.hypstarcdn.com +p15a.tiktokcdn.com +p16-ad.byteoversea.com +p16-amd-va.tiktokcdn.com +p16-cg-gaudit.ibyteimg.com +p16-mt-sg-a.ibyteimg.com +p16-mt-sg-b.ibyteimg.com +p16-mt-sg.ibyteimg.com +p16-mt-va-b.ibyteimg.com +p16-mt-va.ibyteimg.com +p16-mt-va.ibyteimg.com.edgesuite.net +p16-musical-sg.ibyteimg.com +p16-musical-sg.ibyteimg.com.edgesuite.net +p16-musical-va-ab.ibyteimg.com +p16-musical-va-ab.ibyteimg.com.edgesuite.net +p16-musical-va.ibyteimg.com +p16-musical-va.ibyteimg.com.edgesuite.net +p16-security-sg.ibyteimg.com +p16-security-sg.ibyteimg.com.edgesuite.net +p16-security-va.ibyteimg.com +p16-security-va.ibyteimg.com.edgesuite.net +p16-sg-default.akamaized.net +p16-sg-default.edgesuite.net +p16-sg.hypstarcdn.com +p16-sg.muscdn.com +p16-sg.muscdn.com.edgesuite.net +p16-sg.tiktokcdn.com +p16-sg.toutiao50.com +p16-sg.xzcs3zlph.com +p16-sg.xzcs3zlph.com.edgesuite.net +p16-sign-sg.tiktokcdn.com +p16-sign-va.tiktokcdn.com +p16-tcscdn.byteoversea.com +p16-tcscdn.byteoversea.com.edgekey.net +p16-test.hypstarcdn.com +p16-test.tiktokcdn.com +p16-tiktok-sg-ab.ibyteimg.com +p16-tiktok-sg-ab.ibyteimg.com.edgesuite.net +p16-tiktok-sg-h2.ibyteimg.com +p16-tiktok-sg-h2.ibyteimg.com.edgesuite.net +p16-tiktok-sg.ibyteimg.com +p16-tiktok-sg.ibyteimg.com.edgesuite.net +p16-tiktok-sign-sg-h2.ibyteimg.com +p16-tiktok-sign-sg-h2.ibyteimg.com.edgesuite.net +p16-tiktok-sign-va-h2.ibyteimg.com +p16-tiktok-sign-va-h2.ibyteimg.com.edgesuite.net +p16-tiktok-va-h2.ibyteimg.com +p16-tiktok-va-h2.ibyteimg.com.akamaized.net +p16-tiktok-va.ibyteimg.com +p16-tiktok-va.ibyteimg.com.edgesuite.net +p16-tiktokcdn-com.akamaized.net +p16-tiktokcdn-com.toutiao50.com +p16-tk-m.tiktokcdn.com +p16-tk-m.tiktokcdn.com.edgesuite.net +p16-tk.tiktokcdn.com +p16-tk.tiktokcdn.com.edgesuite.net +p16-ulike-sg.ibyteimg.com +p16-va-default.akamaized.net +p16-va-default.edgesuite.net +p16-va-tiktok.ibyteimg.com +p16-va-tiktok.ibyteimg.com.akamaized.net +p16-va.hypstarcdn.com +p16-va.tiktokcdn.com +p16-webcast-hypstarcdn.byteimg.com +p16-webcast-useast1a.muscdn.com +p16-webcast-useast1a.muscdn.com.edgesuite.net +p16-webcast-useast2a.muscdn.com +p16-webcast-useast2a.muscdn.com.edgesuite.net +p16-webcast.hypstarcdn.com +p16-webcast.muscdn.com +p16-webcast.muscdn.com.edgesuite.net +p16-webcast.tiktokcdn.com +p16-webcast.tiktokcdn.com.edgesuite.net +p16.hypstarcdn.com +p16.hypstarcdn.com.edgesuite.net +p16.muscdn.com +p16.pstatp.com +p16.pstatp.com.edgekey.net +p16.pstatp.com.w.alikunlun.com +p16.tiktokcdn.com +p16.tiktokcdn.com.edgesuite.net +p16.toutiao50.com +p16.xzcs3zlph.com +p16.xzcs3zlph.com.akamaized.net +p16a.hypstarcdn.com +p16a.hypstarcdn.com.edgesuite.net +p16a.tiktokcdn.com +p16a.tiktokcdn.com.edgesuite.net +p16b.hypstarcdn.com +p16b.tiktokcdn.com +p16b.tiktokcdn.com.edgesuite.net +p16hypstarcdn-a.akamaihd.net +p16hypstarcdn-a.akamaihd.net.edgesuite.net +p19-webcast.hypstarcdn.com +p19-webcast.muscdn.com +p19-webcast.tiktokcdn.com +p19.hypstarcdn.com +p2-dy.bytecdn.cn +p2-ml.gslb.ks-cdn1.com +p2-ml.gslb.ksyuncdn.com +p2-xg.pstatp.com +p2.ipstatp.com +p2.ipstatp.com.w.kunlunca.com +p2.pstatp.com +p2.pstatp.com.cloudcdn.net +p2.pstatp.com.w.kunlungr.com +p21-webcast.hypstarcdn.com +p21-webcast.muscdn.com +p21-webcast.tiktokcdn.com +p22-dy.bytecdn.cn +p26-dy.byteimg.com +p26-sg.tiktokcdn.com +p26-sign-sg.tiktokcdn.com +p26-tt.byteimg.com +p29-dy.byteimg.com +p29-tt.byteimg.com +p3-ad.bytecdn.cn +p3-bcy.byteimg.com +p3-bk.byteimg.com +p3-chameleon.byteimg.com +p3-dcd.byteimg.com +p3-ds.bytecdn.cn +p3-ds.byteimg.com +p3-dy-a.byteimg.com +p3-dy-b.byteimg.com +p3-dy-ipv6.byteimg.com +p3-dy.bytecdn.cn +p3-dy.byteimg.com +p3-faceu.byteimg.com +p3-fp.byteimg.com +p3-growth.byteimg.com +p3-hs.bytecdn.cn +p3-hs.byteimg.com +p3-juejin.byteimg.com +p3-lark-file.byteimg.com +p3-live-cdn.pstatp.com.m.alikunlun.com +p3-luckycat-oasis.byted.org +p3-open.byteimg.com +p3-ppx.bytecdn.cn +p3-ppx.byteimg.com +p3-reading.byteimg.com +p3-search.byteimg.com +p3-security.byteimg.com +p3-sg.tiktokcdn.com +p3-sign-sg.tiktokcdn.com +p3-test.bytecdn.cn +p3-tt-ipv6.byteimg.com +p3-tt.bytecdn.cn +p3-tt.byteimg.com +p3-ttpush.byteimg.com +p3-tvynyd.byteimg.com +p3-webcast-dycdn.byteimg.com +p3-webcast-hscdn.byteimg.com +p3-webcast-ppxcdn.byteimg.com +p3-webcast-ttcdn.byteimg.com +p3-webcast-xgcdn.byteimg.com +p3-xg-a.byteimg.com +p3-xg.bytecdn.cn +p3-xg.byteimg.com +p3-xg.pstatp.com +p3-xg.pstatp.com.w.kunlungr.com +p3.pstatp.com +p3.pstatp.com.w.alikunlun.com +p3.pstatp.com.xi.zwtianshangm.com +p3a.bytecdn.cn +p3a.pstatp.com +p3a.pstatp.com.w.kunlungr.com +p4-dy.bytecdn.cn +p4-xg.pstatp.com +p4.pstatp.com +p4.pstatp.com.w.alikunlun.com +p5-dy.bytecdn.cn +p5-xg.pstatp.com +p5.pstatp.com +p5.pstatp.com.w.kunlunpi.com +p53-sg.tiktokcdn.com +p58-sg.tiktokcdn.com +p58-sign-sg.tiktokcdn.com +p5a.pstatp.com +p5a.pstatp.com.w.kunlunle.com +p6-ad.bytecdn.cn +p6-bk.byteimg.com +p6-dcd.byteimg.com +p6-ds.byteimg.com +p6-dy-a.byteimg.com +p6-dy-b.byteimg.com +p6-dy-ipv6.byteimg.com +p6-dy.bytecdn.cn +p6-faceu.byteimg.com +p6-fp.byteimg.com +p6-juejin.byteimg.com +p6-lark-file.byteimg.com +p6-open.byteimg.com +p6-ppx.byteimg.com +p6-security.byteimg.com +p6-tt-ipv6.byteimg.com +p6-tt.bytecdn.cn +p6-tt.byteimg.com +p6-ttpush.byteimg.com +p6-webcast-dycdn.byteimg.com +p6-webcast-hscdn.byteimg.com +p6-webcast-ppxcdn.byteimg.com +p6-webcast-ttcdn.byteimg.com +p6-webcast-xgcdn.byteimg.com +p6-xg.bytecdn.cn +p6-xg.pstatp.com +p6.pstatp.com +p6.pstatp.com.w.kunlunpi.com +p61-sg.tiktokcdn.com +p61-sign-sg.tiktokcdn.com +p61-va.tiktokcdn.com +p7-dy.bytecdn.cn +p7-xg.pstatp.com +p7.pstatp.com +p7.pstatp.com.w.alikunlun.net +p77-sg.tiktokcdn.com +p77-sign-sg.tiktokcdn.com +p77-sign-va.tiktokcdn.com +p77-va.tiktokcdn.com +p8-dy.bytecdn.cn +p8-xg.pstatp.com +p8.pstatp.com +p8.pstatp.com.w.alikunlun.net +p9-ad.bytecdn.cn +p9-ad.bytecdn.cn.bsgslb.com +p9-bcy.byteimg.com +p9-bk.byteimg.com +p9-dcd.byteimg.com +p9-dlxb.byteimg.com +p9-ds.byteimg.com +p9-dy-a.byteimg.com +p9-dy-b.byteimg.com +p9-dy-ipv6.byteimg.com +p9-dy-ipv6.byteimg.com.bsgslb.com +p9-dy.bytecdn.cn +p9-dy.bytecdn.cn.bsgslb.com +p9-dy.byteimg.com +p9-dy.byteimg.com.bsgslb.com +p9-faceu.byteimg.com +p9-faceu.byteimg.com.bsgslb.com +p9-fp.byteimg.com +p9-hs.bytecdn.cn +p9-hs.byteimg.com +p9-hs.byteimg.com.bsgslb.com +p9-juejin.byteimg.com +p9-lark-file.byteimg.com +p9-ppx.bytecdn.cn +p9-ppx.bytecdn.cn.bsgslb.com +p9-ppx.byteimg.com +p9-reading.byteimg.com +p9-security.byteimg.com +p9-security.byteimg.com.bsgslb.com +p9-sg.tiktokcdn.com +p9-sign-sg.tiktokcdn.com +p9-tt-ipv6.byteimg.com +p9-tt-ipv6.byteimg.com.bsgslb.com +p9-tt.bytecdn.cn +p9-tt.bytecdn.cn.bsgslb.com +p9-tt.byteimg.com +p9-tt.byteimg.com.bsgslb.com +p9-ttpush.byteimg.com +p9-webcast-dycdn.byteimg.com +p9-webcast-dycdn.byteimg.com.bsgslb.com +p9-webcast-hscdn.byteimg.com +p9-webcast-ppxcdn.byteimg.com +p9-webcast-ttcdn.byteimg.com +p9-webcast-ttcdn.byteimg.com.bsgslb.com +p9-webcast-xgcdn.byteimg.com +p9-xg.bytecdn.cn +p9-xg.bytecdn.cn.bsgslb.com +p9-xg.byteimg.com +p9-xg.byteimg.com.bsgslb.com +p9-xg.pstatp.com +p9.pstatp.com +p9.pstatp.com.bsgslb.com +p92-tt.byteimg.com +p97-tt.bytecdn.cn +p98.pstatp.com +p98.pstatp.com.m.alikunlun.com +p99-tt.bytecdn.cn +p99.pstatp.com +p99.pstatp.com.m.alikunlun.com +page.snssdk.com +pangolin.snssdk.com +pangolin16.isnssdk.com +pangolin16.sgsnssdk.com +partner-share.toutiao.com +partner-share.toutiao.com.w.cdngslb.com +partner.isnssdk.com +partner.sgsnssdk.com +partner.toutiao.com +partner.toutiao.com.bytedns.net +passport-alisg.byteoversea.com +passport-maliva.byteoversea.com +passport.snssdk.com +patrol-sg.isnssdk.com +patrol-sg.snssdk.com +patrol.snssdk.com +patrol.snssdk.com.w.kunluncan.com +pay-boe.snssdk.com +pb1.pstatp.com +pb1.pstatp.com.wsglb0.com +pb2.pstatp.com +pb2.pstatp.com.w.kunlunle.com +pb3.pstatp.com +pb3.pstatp.com.w.kunlungr.com +pb9.pstatp.com +pb9.pstatp.com.bsgslb.com +pgc.isnssdk.com +phonemark.toutiao.com +picwebws.pstatp.com.wsglb0.com +pigeon.snssdk.com +pigeon.snssdk.com.w.kunluncan.com +polaris-ns.byteoversea.com +ppx-2.isnssdk.com +ppx-a.isnssdk.com +ppx-a.isnssdk.com.edgekey.net +ppx-b.isnssdk.com +ppx.isnssdk.com +ppx.snssdk.com +ppx.snssdk.com.w.kunluncan.com +presource.snssdk.com +press-dyn-ali-a.snssdk.com +press-dyn-ali-b.snssdk.com +press-dyn-ali-c.snssdk.com +press-dyn-ali-d.snssdk.com +press-dyn-ali-e.snssdk.com +press-dyn-ali.snssdk.com +pstatp.com +pstatp.com.bsgslb.com +pstatp.com.cdn.dnsv1.com +pstatp.com.cdnle.com +pstatp.com.cloud.cdntip.com +pstatp.com.cloudcdn.net +pstatp.com.cloudglb.com +pstatp.com.edgekey.net +pstatp.com.m.alikunlun.com +pstatp.com.w.alikunlun.com +pstatp.com.w.alikunlun.net +pstatp.com.w.cdngslb.com +pstatp.com.w.kunluncan.com +pstatp.com.w.kunlungr.com +pstatp.com.w.kunlunle.com +pstatp.com.w.kunlunpi.com +pstatp.com.wscdns.com +pstatp.com.wsglb0.com +pstatp.com.xi.zwtianshangm.com +pull-cmaf-f16-ab.tiktokcdn.com +pull-cmaf-f16-sg01.tiktokcdn.com +pull-cmaf-f16.tiktokcdn.com +pull-cmaf-f16.tiktokcdn.com.akamaized.net +pull-cmaf-f5-mus.pstatp.com +pull-cmaf-f5.tiktokcdn.com +pull-cmaf-f5.tiktokcdn.com.akamaized.net +pull-cmaf-f5.tiktokcdn.com.c.worldfcdn.com +pull-f3-hs.pstatp.com +pull-f3-hs.pstatp.com.w.alikunlun.net +pull-f3-xg.bytecdn.cn +pull-f5-ab.tiktokcdn.com +pull-f5-ab.tiktokcdn.com.c.bytetcdn.com +pull-f5-ab.tiktokcdn.com.c.worldfcdn.com +pull-f5-mus.pstatp.com +pull-f5-sg01.tiktokcdn.com +pull-f5-xg.bytecdn.cn +pull-f5.hypstarcdn.com +pull-f5.tiktokcdn.com +pull-f5.tiktokcdn.com.c.bytetcdn.com +pull-f5.tiktokcdn.com.c.worldfcdn.com +pull-fcdn-base-oversea.s.worldfcdn.com +pull-fcdn-self-oversea.s.bytetcdn.com +pull-fcdn.bytecdn.cn +pull-flv-f1-ab.tiktokcdn.com +pull-flv-f1-ab.tiktokcdn.com.wsdvs.com +pull-flv-f1-sg01.tiktokcdn.com +pull-flv-f1-xg.bytecdn.cn +pull-flv-f1.tiktokcdn.com +pull-flv-f1.tiktokcdn.com.wsdvs.com +pull-flv-f11-ab.tiktokcdn.com +pull-flv-f11.tiktokcdn.com +pull-flv-f11.tiktokcdn.liveplay.myqcloud.com +pull-flv-f6-xg.bytecdn.cn +pull-flv-l1-admin.tiktokcdn.com +pull-flv-l1-admin.tiktokcdn.com.wsdvs.com +pull-flv-l1-hs.pstatp.com +pull-flv-l1-mus.pstatp.com +pull-flv-l1-sg01.tiktokcdn.com +pull-flv-l1-xg.bytecdn.cn +pull-flv-l1.hypstarcdn.com +pull-flv-l1.tiktokcdn.com +pull-flv-l1.tiktokcdn.com.wsdvs.com +pull-flv-l11-sg01.tiktokcdn.com +pull-flv-l11.tiktokcdn.com +pull-flv-l6-act.pstatp.com +pull-flv-l6-hs.pstatp.com +pull-flv-l6-xg.bytecdn.cn +pull-flv-l7.pstatp.com +pull-hls-f1-ab.tiktokcdn.com +pull-hls-f1-ab.tiktokcdn.com.wsdvs.com +pull-hls-f1.tiktokcdn.com +pull-hls-f1.tiktokcdn.com.wsdvs.com +pull-hls-f11-ab.tiktokcdn.com +pull-hls-f11.tiktokcdn.com +pull-hls-f11.tiktokcdn.liveplay.myqcloud.com +pull-hls-f16-sg01.tiktokcdn.com +pull-hls-f5-ab.tiktokcdn.com +pull-hls-f5-ab.tiktokcdn.com.akamaized.net +pull-hls-f5.tiktokcdn.com +pull-hls-f5.tiktokcdn.com.akamaized.net +pull-hls-l1-hs.pstatp.com +pull-hls-l1-sg01.tiktokcdn.com +pull-hls-l1.hypstarcdn.com +pull-hls-l1.tiktokcdn.com +pull-hls-l1.tiktokcdn.com.wsdvs.com +pull-hls-l11-sg01.tiktokcdn.com +pull-hls-l11.tiktokcdn.com +pull-hls-l6-hs.pstatp.com +pull-hls-q5.tiktokcdn.com +pull-hls-q5.tiktokcdn.com.akamaized.net +pull-hls-w5.tiktokcdn.com +pull-hls-w5.tiktokcdn.com.akamaized.net +pull-i3-hs.pstatp.com +pull-l3-hs.pstatp.com +pull-l3-hs.pstatp.com.w.alikunlun.net +pull-l3-xg.ixigua.com.xi.zwtianshangm.com +pull-l3.hypstarcdn.com +pull-q5.tiktokcdn.com +pull-q5.tiktokcdn.com.c.worldfcdn.com +pull-rtmp-f1-ab.tiktokcdn.com +pull-rtmp-f1-ab.tiktokcdn.com.wsdvs.com +pull-rtmp-f1-xg.bytecdn.cn +pull-rtmp-f1.tiktokcdn.com +pull-rtmp-f1.tiktokcdn.com.wsdvs.com +pull-rtmp-f11-ab.tiktokcdn.com +pull-rtmp-f11.tiktokcdn.com +pull-rtmp-f11.tiktokcdn.liveplay.myqcloud.com +pull-rtmp-f6-hs.pstatp.com +pull-rtmp-f6-xg.bytecdn.cn +pull-rtmp-l1-hs.pstatp.com +pull-rtmp-l1-mus.pstatp.com +pull-rtmp-l1-sg01.tiktokcdn.com +pull-rtmp-l1-xg.bytecdn.cn +pull-rtmp-l1.hypstarcdn.com +pull-rtmp-l1.tiktokcdn.com +pull-rtmp-l1.tiktokcdn.com.wsdvs.com +pull-rtmp-l11-sg01.tiktokcdn.com +pull-rtmp-l11.tiktokcdn.com +pull-rtmp-l6-hs.pstatp.com +pull-rtmp-l6-xg.bytecdn.cn +pull-source-cmaf-f5.hypstarcdn.com +pull-w5.tiktokcdn.com +pull-w5.tiktokcdn.com.c.worldfcdn.com +push-fcdn-base-oversea.s.worldfcdn.com +push-fcdn.bytecdn.cn +push-rtmp-f5-ab.tiktokcdn.com +push-rtmp-f5-xg.bytecdn.cn +push-rtmp-f5.hypstarcdn.com +push-rtmp-f5.tiktokcdn.com +push-rtmp-f5.tiktokcdn.com.c.bytetcdn.com +push-rtmp-f5.tiktokcdn.com.c.worldfcdn.com +push-rtmp-l1-hs.pstatp.com +push-rtmp-l1-xg.bytecdn.cn +push-rtmp-l1.hypstarcdn.com +push-rtmp-l1.tiktokcdn.com +push-rtmp-l1.tiktokcdn.com.wsdvs.com +push-rtmp-l11.tiktokcdn.com +push-rtmp-l6-act.pstatp.com +push-rtmp-l6-hs.pstatp.com +push-rtmp-l6-xg.bytecdn.cn +put15-tb.isnssdk.com +pvbmgpkp.xzcs3zlph.com +pxmsag.qfyf1toi.com +qdns-v5-dy.polaris.byteoversea.com +qdns-v5-tt.polaris.byteoversea.com +qfyf1toi.com +quic-amemv-all-hl.snssdk.com +quic-amemv-all-lf.snssdk.com +quic-amemv-all-lq.snssdk.com +quic-amemv-all.snssdk.com +quic-amemv-hl.snssdk.com +quic-amemv.snssdk.com +quic-awsbr.byteoversea.com +quic-awsbr16-up.byteoversea.com +quic-awsbr16-up.byteoversea.com.srip.net +quic-awsin.byteoversea.com +quic-awsin16-up.byteoversea.com +quic-awsin16-up.byteoversea.com.srip.net +quic-awsjp.byteoversea.com +quic-awsjp16-up.byteoversea.com +quic-awsjp16-up.byteoversea.com.srip.net +quic-huoshan-all.snssdk.com +quic-huoshan.snssdk.com +quic-maliva16-up.muscdn.com +quic-maliva16-up.muscdn.com.srip.net +quic-normal-lb-alisg.byteoversea.net +quic-normal-lb-gcp.byteoversea.net +quic-normal-lb-useast4.byteoversea.net +quic-proxy-gcp.byteoversea.net +quic.snssdk.com +r.snssdk.com +ra.byteoversea.com +radiance.snssdk.com +rc.snssdk.com +reading-hl.snssdk.com +reading.snssdk.com +rela.pc.cdn.bitgravity.com +renzheng.snssdk.com +renzheng.toutiao.com +replay-all-l6-tt.pstatp.com +res01.musical.ly +res01.musical.ly.akadns.net +resource.muscdn.com +resource.snssdk.com +rocket.snssdk.com +rocket.snssdk.com.w.kunluncan.com +rtapplog.snssdk.com +rtapplog.snssdk.com.w.kunluncan.com +rtc-boe.byted.org +rtc.bytedanceapi.com +rtlog-va.tiktokv.com +rtlog.byteoversea.com +rtlog.isnssdk.com +rtlog.musical.ly +rtlog.musical.ly.edgekey.net +rtlog.sgsnssdk.com +rtlog.snssdk.com +rtlog.snssdk.com.w.kunluncan.com +rtlog.tiktokv.com +rtmp-l11.tiktokcdn.liveplay.myqcloud.com +rtmpup1.pstatp.com +rtmpup4.pstatp.com +rttoblog.snssdk.com +s-tb.sgpstatp.com +s.amemv.com +s.amemv.com.w.kunlunca.com +s.bytetcdn.com +s.ipstatp.com +s.musemuse.cn +s.pstatp.com +s.sgpstatp.com +s.tiktokcdn.com +s.toutiao.com +s.worldfcdn.com +s0.bytecdn.cn +s0.ipstatp.com +s0.pstatp.com +s0.pstatp.com.w.alikunlun.net +s0.pstatp.com.xi.zwtianshangm.com +s0.sgpstatp.com +s0z.pstatp.com +s0z.pstatp.com.w.kunlunle.com +s1-fs.pstatp.com +s1-fs.pstatp.com.wsglb0.com +s1.bytecdn.cn +s1.ipstatp.com +s1.pstatp.com +s1.pstatp.com.wscdns.com +s16-hypstarcdn-com.akamaized.net +s16-ies.tiktok.com +s16-ies.tiktok.com.edgesuite.net +s16.byteoversea.com +s16.byteoversea.com.edgekey.net +s16.hypstarcdn.com +s16.hypstarcdn.com.edgesuite.net +s16.ipstatp.com +s16.ipstatp.com.edgesuite.net +s16.tiktokcdn.com +s16.tiktokcdn.com.edgesuite.net +s16a.hypstarcdn.com +s16a.hypstarcdn.com.edgesuite.net +s16a.tiktokcdn.com +s16a.tiktokcdn.com.edgesuite.net +s16b.hypstarcdn.com +s16b.hypstarcdn.com.edgesuite.net +s16b.tiktokcdn.com +s16b.tiktokcdn.com.edgesuite.net +s2.bytecdn.cn +s2.ipstatp.com +s2.pstatp.com +s2.pstatp.com.w.alikunlun.net +s20.tiktokcdn.com +s3-dcd-sf.pstatp.com.m.alikunlun.com +s3-fs.pstatp.com +s3-fs.pstatp.com.w.cdngslb.com +s3.bytecdn.cn +s3.pstatp.com +s3.pstatp.com.w.kunlungr.com +s3.pstatp.com.xi.zwtianshangm.com +s3a.bytecdn.cn +s3a.pstatp.com +s3a.pstatp.com.w.kunlungr.com +s3b.bytecdn.cn +s3b.pstatp.com +s3b.pstatp.com.w.kunlungr.com +s4.pstatp.com +s4.pstatp.com.bsgslb.com +s5.pstatp.com +s5.pstatp.com.cloudglb.com +s6.pstatp.com +s6.pstatp.com.bsgslb.com +s6.pstatp.com.cloudglb.com +s7.pstatp.com +s8.pstatp.com +s9.pstatp.com +s9.pstatp.com.bsgslb.com +saveu.tiktokv.com +sdfp-sg.byteoversea.com +sdfp-sg.byteoversea.com.edgekey.net +sdfp-sg.tiktokv.com +sdfp-sg.tiktokv.com.edgekey.net +sdfp-va.byteoversea.com +sdfp-va.byteoversea.com.edgesuite.net +sdfp-va.musical.ly +sdfp-va.musical.ly.edgesuite.net +sdfp-va.tiktokv.com +sdfp-va.tiktokv.com.edgesuite.net +sdfp.snssdk.com +sdfp.snssdk.com.w.kunluncan.com +search-bj.snssdk.com +search-hl.amemv.com +search-hl.amemv.com.w.cdngslb.com +search-lf.amemv.com +search-lf.amemv.com.w.cdngslb.com +search.amemv.com +search.amemv.com.w.cdngslb.com +search.snssdk.com +search.tiktokv.com +search.tiktokv.com.edgekey.net +search16-normal-c-alisg.tiktokv.com +search16-normal-c-alisg.tiktokv.com.edgekey.net +search16-normal-c-useast1a.tiktokv.com +search16-normal-c-useast1a.tiktokv.com.edgekey.net +search16-normal-c-useast2a.tiktokv.com +search16-normal-c-useast2a.tiktokv.com.edgekey.net +search21-normal-c-alisg.tiktokv.com +security-api.snssdk.com +security-hl.snssdk.com +security-hl.snssdk.com.w.kunluncan.com +security-ipv6.snssdk.com +security-lb-maliva.byteoversea.net +security-lf.snssdk.com +security-lq.snssdk.com +security-o.snssdk.com +security.snssdk.com +security.snssdk.com.w.kunluncan.com +security1.snssdk.com +security1.snssdk.com.w.kunluncan.com +security2.snssdk.com +security2.snssdk.com.w.kunluncan.com +security3.snssdk.com +security3.snssdk.com.w.kunluncan.com +sf-hs-sg.ibytedtos.com +sf-hs-sg.ibytedtos.com.edgekey.net +sf-tb-sg.ibytedtos.com +sf-tb-sg.ibytedtos.com.edgesuite.net +sf-tk-sg.ibytedtos.com +sf-tk-sg.ibytedtos.com.edgekey.net +sf1-cdn-b-tos.pstatp.com.wsglb0.com +sf1-dycdn-tos.pstatp.com +sf1-dycdn-tos.pstatp.com.wsglb0.com +sf1-eecdn-tos.pstatp.com +sf1-hscdn-tos.pstatp.com +sf1-hscdn-tos.pstatp.com.wsglb0.com +sf1-space-eecdn-tos.pstatp.com +sf1-tccdn-tos.pstatp.com +sf1-tccdn-tos.pstatp.com.wsglb0.com +sf1-ttcdn-tos.pstatp.com +sf1-ttcdn-tos.pstatp.com.wsglb0.com +sf1-ugcdn-tos.pstatp.com +sf1-vcloudcdn.pstatp.com +sf1-vcloudcdn.pstatp.com.wsglb0.com +sf1-webcast-dycdn.byteimg.com +sf1-webcast-ttcdn.byteimg.com +sf1-webcast-xgcdn.byteimg.com +sf1-xgcdn-tos.pstatp.com +sf1-xgcdn-tos.pstatp.com.wsglb0.com +sf16-cgfe-sg.ibytedtos.com +sf16-cgfe-va.ibytedtos.com +sf16-eacdn-tos.pstatp.com +sf16-lark-va.ibytedtos.com +sf16-lark-va.ibytedtos.com.edgekey.net +sf16-muse-va.ibytedtos.com +sf16-muse-va.ibytedtos.com.edgekey.net +sf16-passport-sg.ibytedtos.com +sf16-passport-sg.ibytedtos.com.edgekey.net +sf16-passport-va.ibytedtos.com +sf16-passport-va.ibytedtos.com.edgekey.net +sf16-scmcdn-sg.ibytedtos.com +sf16-scmcdn-va.ibytedtos.com +sf16-secsvc-sg.ibytedtos.com +sf16-security-sg.ibytedtos.com +sf16-security-va.ibytedtos.com +sf16-security-va.ibytedtos.com.edgekey.net +sf16-sg-default.edgesuite.net +sf16-sg.hypstarcdn.com +sf16-sg.muscdn.com +sf16-sg.tiktokcdn.com +sf16-tk-m.tiktokcdn.com +sf16-tk-m.tiktokcdn.com.edgesuite.net +sf16-tk.tiktokcdn.com +sf16-tk.tiktokcdn.com.edgesuite.net +sf16-ttcdn-tos.ipstatp.com +sf16-ttcdn-tos.ipstatp.com.edgesuite.net +sf16-ttcdn-tos.pstatp.com +sf16-ttcdn-tos.pstatp.com.edgesuite.net +sf16-unpkg-va.ibytedtos.com +sf16-va.tiktokcdn.com +sf16-webcast-hypstarcdn.byteimg.com +sf16-webcast-useast1a.muscdn.com +sf16-webcast-useast1a.muscdn.com.edgesuite.net +sf16-webcast-useast2a.muscdn.com +sf16-webcast-useast2a.muscdn.com.edgesuite.net +sf16-webcast.hypstarcdn.com +sf16-webcast.hypstarcdn.com.edgesuite.net +sf16-webcast.muscdn.com +sf16-webcast.muscdn.com.edgesuite.net +sf16-webcast.tiktokcdn.com +sf16-webcast.tiktokcdn.com.edgesuite.net +sf19-scmcdn-sg.ibytedtos.com +sf19-scmcdn-va.ibytedtos.com +sf19-va.tiktokcdn.com +sf19-webcast.hypstarcdn.com +sf19-webcast.muscdn.com +sf19-webcast.tiktokcdn.com +sf2-dycdn-tos.pstatp.com +sf2-ttcdn-tos.pstatp.com +sf21-tk.tiktokcdn.com +sf21-va.tiktokcdn.com +sf21-webcast.hypstarcdn.com +sf21-webcast.muscdn.com +sf21-webcast.tiktokcdn.com +sf26-sg.tiktokcdn.com +sf3-bcycdn-tos.pstatp.com.w.alikunlun.com +sf3-dycdn-tos.pstatp.com +sf3-dycdn-tos.pstatp.com.w.kunlunle.com +sf3-dycdn-tos.pstatp.com.xi.zwtianshangm.com +sf3-eacdn-tos.pstatp.com +sf3-eecdn-tos.pstatp.com +sf3-hscdn-tos.pstatp.com +sf3-hscdn-tos.pstatp.com.w.kunlunle.com +sf3-hscdn-tos.pstatp.com.xi.zwtianshangm.com +sf3-nhcdn-tos.pstatp.com.w.kunlunle.com +sf3-sg.tiktokcdn.com +sf3-space-eecdn-tos.pstatp.com +sf3-space-eecdn-tos.pstatp.com.w.cdngslb.com +sf3-swcdn-tos.pstatp.com +sf3-swcdn-tos.pstatp.com.m.alikunlun.com +sf3-tccdn-tos.pstatp.com +sf3-tccdn-tos.pstatp.com.w.kunlunpi.com +sf3-ttcdn-tos.pstatp.com +sf3-ttcdn-tos.pstatp.com.w.kunlunpi.com +sf3-ugcdn-tos.pstatp.com +sf3-vccdn-tos.pstatp.com +sf3-vccdn-tos.pstatp.com.w.cdngslb.com +sf3-webcast-dycdn.byteimg.com +sf3-webcast-ttcdn.byteimg.com +sf3-webcast-xgcdn.byteimg.com +sf3-xgcdn-tos.pstatp.com +sf4-ttcdn-tos.pstatp.com +sf5-ttcdn-tos.pstatp.com +sf53-sg.tiktokcdn.com +sf53-va.tiktokcdn.com +sf6-dycdn-tos.pstatp.com +sf6-hscdn-tos.pstatp.com +sf6-ttcdn-tos.pstatp.com +sf6-ttcdn-tos.pstatp.com.download.ks-cdn.com +sf6-vccdn-tos.pstatp.com +sf6-webcast-dycdn.byteimg.com +sf6-webcast-hscdn.byteimg.com +sf6-webcast-ttcdn.byteimg.com +sf6-webcast-xgcdn.byteimg.com +sf6-xgcdn-tos.pstatp.com +sf77-sg.tiktokcdn.com +sf77-va.tiktokcdn.com +sf9-sg.tiktokcdn.com +sf9-webcast-dycdn.byteimg.com +sf9-webcast-dycdn.byteimg.com.bsgslb.com +sf9-webcast-ttcdn.byteimg.com +sf9-webcast-xgcdn.byteimg.com +sg-effect.byteoversea.com +sg-link.byteoversea.com +sg.byteoversea.com +sg.isnssdk.com +sg3.byteoversea.com +sgali-dpprofile.byteoversea.com +sgali-mcs.byteoversea.com +sgali-mcs.byteoversea.com.edgesuite.net +sgali-wallet.byteoversea.com +sgali1.ws.byteoversea.net +sgali3.l.byteoversea.net +sgaup.bytedance.com +sgee-mcs.byteoversea.com +sgpstatp.com +sgsnssdk.com +share-1070666956.ap-northeast-1.elb.amazonaws.com +share-sg.snssdk.com +share-test.musemuse.cn +share.musemuse.cn +shop.musical.ly +shop.snssdk.com +slb-lb.bytedns.net +slide.toutiao.com +smr-aliva-useast2a.byteoversea.com +smr-aliva.byteoversea.com +smr-aliva.byteoversea.com.edgekey.net +smr-sg.byteoversea.com +smr-sg.byteoversea.com.edgekey.net +smr.snssdk.com +sms-callback.byteoversea.com +sniper-boe.byted.org +snssdk.com +snssdk.com.bdgslb.com +snssdk.com.bsgslb.com +snssdk.com.bytedns.net +snssdk.com.w.kunluncan.com +snssdk.com.xi.zwtianshangm.com +snssdk1180.onelink.me +snssdk1233.onelink.me +so.toutiao.com +so.toutiao.com.w.cdngslb.com +space.toutiao.com +spe-f2-frontier-hl.snssdk.com +spe-frontier-a.isnssdk.com +spe-frontier-a.snssdk.com +spe-frontier-b.snssdk.com +spe-frontier-d.snssdk.com +spe-frontier.isnssdk.com +spe-frontier.snssdk.com +spe.isnssdk.com +spe.sgsnssdk.com +spotlight.tiktok.com +spring-alinc2-api-1.bytedns.net +srp1.snssdk.com +srp2.snssdk.com +srp3.snssdk.com +srp4.snssdk.com +ssl.cdn.tiktok.com.c.footprint.net +ssl.ipstatp.com.edgekey.net +ssl.tiktok.com.c.secure.footprint.net +sso.toutiao.com +sso.toutiao.com.w.cdngslb.com +st1-ttcdn-tos.pstatp.com +staging-openapi-boe.byted.org +star.snssdk.com +star.toutiao.com +starling-oversea-useast2a.byteoversea.com +starling-oversea.byteoversea.com +starling-oversea.byteoversea.com.edgekey.net +starling-sg.byteoversea.com +starling-sg.byteoversea.com.edgesuite.net +starling-va-useast2a.byteoversea.com +starling-va.byteoversea.com +starling-va.byteoversea.com.edgesuite.net +starling.snssdk.com +starling.snssdk.com.w.kunluncan.com +starsp.toutiao.com +static.muscdn.com +static.toutiao.com +stock-hl.snssdk.com +stock-ws.snssdk.com +stock.snssdk.com +stream.snssdk.com +support.musical.ly +support.tiktok.com +support.tiktok.com.edgesuite.net +sv10.byteoversea.com +t.tiktok.com +t.tiktok.com.edgesuite.net +ta.snssdk.com +tb.isnssdk.com +tb.sgsnssdk.com +tcs-maliva-lb-useast2a.byteoversea.net +techblog.toutiao.com +telecom.aweme-core-lb-hl.l.bytedns.net +telecom.b.l.bytedns.net +telecom.c.l.bytedns.net +telecom.e.l.bytedns.net +telecom.f.l.bytedns.net +telecom.g.l.bytedns.net +telecom.ttgw.tuchong-normal-lb-lf.l.bytedns.net +telecom.x.l.bytedns.net +telecom.y.l.bytedns.net +temai.snssdk.com +temai.snssdk.com.bytedns.net +temai.toutiao.com +temp.p23.tc.cdntip.com +test-ads.tiktok.com +test-aweme.snssdk.com +test-deploy-3.pstatp.com.m.alikunlun.com +test-switch-lf.snssdk.com +test-switch.snssdk.com +test1-up.byteoversea.com +test1.byteoversea.com +testserver.musical.ly +tfe-test.snssdk.com +third-api.amemv.com +third-api.amemv.com.w.cdngslb.com +thrift-doc.byted.org +ticast1.toutiao50.com +tiktok-core-lb-maliva-useast2a.byteoversea.net +tiktok-core-lb-maliva.byteoversea.net +tiktok-lb-alisg.byteoversea.net +tiktok-lb-maliva.byteoversea.net +tiktok.cdnvideo.ru +tiktok.com +tiktok.com.c.footprint.net +tiktok.com.c.secure.footprint.net +tiktok.com.edgekey.net +tiktok.com.edgesuite.net +tiktokcdn.com +tiktokcdn.com.akamaized.net +tiktokcdn.com.baishan-cloud.net +tiktokcdn.com.bytewlb.akadns.net +tiktokcdn.com.c.worldfcdn.com +tiktokcdn.com.edgesuite.net +tiktokcdn.com.w.kunlunsl.com +tiktokcdn.com.wsdvs.com +tiktokcdn.liveplay.myqcloud.com +tiktokv.com +tiktokv.com.cdn.cloudflare.net +tiktokv.com.edgekey.net +tiktokv.com.edgesuite.net +time1.byteoversea.com +time2.byteoversea.com +tm3.bytecdn.cn +tma.snssdk.com +tmaportal.snssdk.com +tmaportal.snssdk.com.w.kunluncan.com +tms3.bytecdn.cn +tms3.pstatp.com +tms3.pstatp.com.w.kunlunle.com +tnc16-alisg.tiktokv.com +tnc16-useast1a.tiktokv.com +tnc3-aliec2.snssdk.com +tnc3-alisc1.snssdk.com +tnc3-bjlgy.snssdk.com +tobapplog.ctobsnssdk.com +tobapplog.snssdk.com +tobapplog.snssdk.com.w.kunluncan.com +toblog.byteoversea.com +toblog.ctobsnssdk.com +toblog.snssdk.com +topbuzz-applog-alisg.l.byteoversea.net +topbuzz-lb-alisg.byteoversea.net +topbuzz-lb-maliva.byteoversea.net +tos-ag-cu-agsy.snssdk.com +tos-ali.snssdk.com +tos-alisg-awssgelb.byteoversea.net +tos-alisg.byteoversea.com +tos-alisg16-alisgtlb-up.tiktokcdn.com +tos-alisg16-alisgtlb-up.tiktokcdn.com.edgesuite.net +tos-alisg16-up.byteoversea.com +tos-alisg16-up.byteoversea.com.edgesuite.net +tos-alisg16-up.hypstarcdn.com +tos-alisg16-up.muscdn.com +tos-alisg16-up.tiktokcdn.com +tos-alisg16-up.tiktokcdn.com.edgesuite.net +tos-alisg19-alisgtlb-up.tiktokcdn.com +tos-alisg19-up.byteoversea.com +tos-alisg19-up.hypstarcdn.com +tos-alisg19-up.muscdn.com +tos-alisg19-up.tiktokcdn.com +tos-alisg21-alisgtlb-up.tiktokcdn.com +tos-alisg21-up.byteoversea.com +tos-alisg21-up.hypstarcdn.com +tos-alisg21-up.tiktokcdn.com +tos-alisg2lf.byteoversea.com +tos-awsbh-va.tiktokcdn.com +tos-awsbh.byteoversea.net +tos-awsbh.muscdn.com +tos-awsbh.tiktokcdn.com +tos-awsbh16-up-va.tiktokcdn.com +tos-awsbh16-up-va.tiktokcdn.com.edgesuite.net +tos-awsbh16-up.byteoversea.net +tos-awsbh16-up.muscdn.com +tos-awsbh16-up.tiktokcdn.com +tos-awsbh16-up.tiktokcdn.com.edgesuite.net +tos-awsbh19-up.byteoversea.net +tos-awsbh19-up.tiktokcdn.com +tos-awsbh21-up-va.tiktokcdn.com +tos-awsbh21-up.byteoversea.net +tos-awsbr.byteoversea.com +tos-awsbr.byteoversea.net +tos-awsbr16-up.byteoversea.com +tos-awsbr16-up.byteoversea.com.edgekey.net +tos-awsbr16-up.hypstarcdn.com +tos-awsbr16-up.muscdn.com +tos-awsbr16-up.tiktokcdn.com +tos-awsbr16-up.tiktokcdn.com.edgesuite.net +tos-awsbr16-up4.muscdn.com +tos-awsbr19-up.byteoversea.com +tos-awsbr19-up.hypstarcdn.com +tos-awsbr19-up.muscdn.com +tos-awsbr19-up.tiktokcdn.com +tos-awsbr21-up.byteoversea.com +tos-awsde.byteoversea.com +tos-awsde16-up.byteoversea.com +tos-awsde16-up.tiktokcdn.com +tos-awsde16-up.tiktokcdn.com.edgesuite.net +tos-awsde19-up.byteoversea.com +tos-awsde21-up.byteoversea.com +tos-awsfr-va.tiktokcdn.com +tos-awsfr.byteoversea.com +tos-awsfr.hypstarcdn.com +tos-awsfr.muscdn.com +tos-awsfr.tiktokcdn.com +tos-awsfr16-up-va.tiktokcdn.com +tos-awsfr16-up.byteoversea.com +tos-awsfr16-up.byteoversea.com.edgesuite.net +tos-awsfr16-up.muscdn.com +tos-awsfr16-up.muscdn.com.edgesuite.net +tos-awsfr16-up.tiktokcdn.com +tos-awsfr16-up.tiktokcdn.com.edgesuite.net +tos-awsfr19-up.byteoversea.com +tos-awsfr19-up.muscdn.com +tos-awsfr19-up.tiktokcdn.com +tos-awsfr21-up.byteoversea.com +tos-awsin-va.tiktokcdn.com +tos-awsin.byteoversea.com +tos-awsin.hypstarcdn.com +tos-awsin.muscdn.com +tos-awsin.tiktokcdn.com +tos-awsin16-up-va.tiktokcdn.com +tos-awsin16-up-va.tiktokcdn.com.edgesuite.net +tos-awsin16-up.byteoversea.com +tos-awsin16-up.byteoversea.com.edgesuite.net +tos-awsin16-up.hypstarcdn.com +tos-awsin16-up.muscdn.com +tos-awsin16-up.tiktokcdn.com +tos-awsin16-up.tiktokcdn.com.edgesuite.net +tos-awsin19-up.byteoversea.com +tos-awsin19-up.hypstarcdn.com +tos-awsin19-up.muscdn.com +tos-awsin19-up.tiktokcdn.com +tos-awsin21-up-va.tiktokcdn.com +tos-awsin21-up.byteoversea.com +tos-awsin21-up.muscdn.com +tos-awsin21-up.tiktokcdn.com +tos-awsjp.byteoversea.com +tos-awsjp16-up-va.tiktokcdn.com +tos-awsjp16-up-va.tiktokcdn.com.edgesuite.net +tos-awsjp16-up.byteoversea.com +tos-awsjp16-up.byteoversea.com.edgekey.net +tos-awsjp16-up.hypstarcdn.com +tos-awsjp16-up.muscdn.com +tos-awsjp16-up.tiktokcdn.com +tos-awsjp16-up.tiktokcdn.com.edgesuite.net +tos-awsjp19-up-va.tiktokcdn.com +tos-awsjp19-up.byteoversea.com +tos-awsjp19-up.hypstarcdn.com +tos-awsjp19-up.muscdn.com +tos-awsjp19-up.tiktokcdn.com +tos-awsjp21-up.byteoversea.com +tos-awssg-va.tiktokcdn.com +tos-awssg.muscdn.com +tos-awssg.tiktokcdn.com +tos-awssg16-up-va.tiktokcdn.com +tos-awssg16-up-va.tiktokcdn.com.edgesuite.net +tos-awssg16-up.byteoversea.net +tos-awssg16-up.muscdn.com +tos-awssg16-up.muscdn.com.edgesuite.net +tos-awssg16-up.tiktokcdn.com +tos-awssg16-up.tiktokcdn.com.edgesuite.net +tos-awssg19-up-va.tiktokcdn.com +tos-awssg19-up.byteoversea.net +tos-awssg19-up.muscdn.com +tos-awssg19-up.tiktokcdn.com +tos-awssg21-up.byteoversea.net +tos-awssg21-up.muscdn.com +tos-awssg21-up.tiktokcdn.com +tos-awsuseast16-up.tiktokcdn.com +tos-awsuseast19-up.byteoversea.net +tos-awsuseast2a16-up.byteoversea.net +tos-awsuseast2a16-up.muscdn.com +tos-awsuseast2a16-up.muscdn.com.edgesuite.net +tos-boei18n-va.byted.org +tos-cm-hl.snssdk.com +tos-ct-hl.snssdk.com +tos-cu-hl.snssdk.com +tos-gcpgb-va.tiktokcdn.com +tos-gcpgb.muscdn.com +tos-gcpin-va.tiktokcdn.com +tos-gcpin.byteoversea.net +tos-gcpin.hypstarcdn.com +tos-gcpin.muscdn.com +tos-gcpin.tiktokcdn.com +tos-gcpnl-va.tiktokcdn.com +tos-gcpnl.muscdn.com +tos-hl-x.snssdk.com +tos-hl-x.snssdk.com.w.kunluncan.com +tos-lf-x.snssdk.com +tos-lf-x.snssdk.com.w.kunluncan.com +tos-maliva16-up-va.tiktokcdn.com +tos-maliva16-up-va.tiktokcdn.com.edgesuite.net +tos-maliva16-up.hypstarcdn.com +tos-maliva16-up.muscdn.com +tos-maliva16-up.muscdn.com.edgesuite.net +tos-maliva16-up.musical.ly +tos-maliva16-up.musical.ly.edgesuite.net +tos-maliva16-up.tiktokcdn.com +tos-maliva16-up.tiktokcdn.com.edgesuite.net +tos-maliva19-up-va.tiktokcdn.com +tos-maliva19-up.hypstarcdn.com +tos-maliva19-up.muscdn.com +tos-maliva19-up.musical.ly +tos-maliva19-up.tiktokcdn.com +tos-maliva21-up.muscdn.com +tos-maliva21-up.tiktokcdn.com +tos-nc2-slb1.bytecdn.cn +tos-nc2-slb2.bytecdn.cn +tos-nc2.snssdk.com +tos-quic-sg.tiktokcdn.com +tos-useast2a.muscdn.com +tos-useast2a.tiktokcdn.com +tos-useast2a16-up.byteoversea.net +tos-useast2a16-up.muscdn.com +tos-useast2a16-up.tiktokcdn.com +tos-useast2a16-up.tiktokcdn.com.edgesuite.net +tos-useast2a19-up.byteoversea.net +tos-useast2a19-up.muscdn.com +tos-useast2a19-up.tiktokcdn.com +tos-useast2a21-up.byteoversea.net +tos-useast2a21-up.muscdn.com +tos-useast2a21-up.tiktokcdn.com +tos.byteoversea.com +tos.isnssdk.com +tos.pstatp.com +tos16-alisg2lf-up.byteoversea.com +tos16-alisg2lf-up.byteoversea.com.edgekey.net +tosv.byted.org +toutiao-frontier.snssdk.com +toutiao-lb.b.bytedns.net +toutiao.com +toutiao.com.bytedns.net +toutiao.com.w.cdngslb.com +toutiao50.com +toutiaocloud.com +toutiaocloud.net +tp-pay-mva.byteoversea.com +tp-pay-sg.byteoversea.com +tp-pay-test.snssdk.com +tp-pay.byteoversea.com +tp-pay.byteoversea.com.bytegeo.akadns.net +tp-pay.snssdk.com +tp-pay.snssdk.com.w.kunluncan.com +tp-paymva.snssdk.com +tp-paysg.snssdk.com +tpaw-use1.muscdn.com +track.toutiao.com +tsearch-hl.snssdk.com +tsearch-lf.snssdk.com +tsearch-quic-hl.snssdk.com +tsearch-quic-lf.snssdk.com +tsearch-quic.snssdk.com +tsearch.snssdk.com +tsearchold-hl.snssdk.com +tt-video.hrlb.ks-cdn.com +tt-video.hrlb.ks-cdn1.com +tt.dy1.com.xi.zwtianshangm.com +tt2-bkk-v1.l.byteoversea.net +ttcdn-tos.pstatp.com +ttopencdn.gshifen.com +ttopencdn.jomodns.com +ttpush-alisg.byteoversea.com +ttpush-maliva.byteoversea.com +ttv5opencdn.jomodns.com +tuchong.pstatp.com +tuchong.pstatp.com.w.cdngslb.com +u701.v.bsgslb.com +u702.v.bsgslb.com +u705.v.bsgslb.com +u705ipv6.v.bsgslb.com +u712.v.bsgslb.com +u713.v.bsgslb.com +u954.v.bsgslb.com +u9551.v.bsgslb.com +u999.v.bsgslb.com +ug-overseas.snssdk.com +ug.snssdk.com +unauthorized.snssdk.com +unavailable.tiktokv.com +unicom.h.l.bytedns.net +unicom.y.l.bytedns.net +unpkg.pstatp.com +unpkg.pstatp.com.m.alikunlun.com +updatewebws.pstatp.com.wsglb0.com +uz91.v.bsgslb.com +uz91a.v.bsgslb.com +uz91ipv6pic.v.bsgslb.com +uz91ipv6pic1.v.bsgslb.com +uz91tmp.v.bsgslb.com +uz95.v.bsgslb.com +v-akamai-audit-p.muscdn.com +v-all-sg.tiktokcdn.com +v-edgecast.tiktokcdn.com +v-edgecast.tiktokcdn.com.bytewlb.akadns.net +v-fastly.tiktokcdn.com +v-ies.tiktokcdn.com +v.bsgslb.com +v.hypstarcdn.com +v.ipstatp.com +v.musical.ly +v.pstatp.com +v.sgpstatp.com +v.tiktok.com +v.tiktokcdn.com +v.tiktokv.com +v0-dy.ixigua.com +v0-tt.ixigua.com +v0.pstatp.com +v0.pstatp.com.cloudcdn.net +v0.pstatp.com.cloudglb.com +v1-dc-ad.bytecdn.cn +v1-default.bytecdn.cn +v1-dy-a-x.bytecdn.cn +v1-dy-b-x.bytecdn.cn +v1-dy-x.ixigua.com +v1-dy-y.ixigua.com +v1-dy-z.ixigua.com +v1-dy.bytecdn.cn +v1-dy.ixigua.com +v1-dy.ixiguavideo.com +v1-hs.bytecdn.cn +v1-tt.bytecdn.cn +v1-tt.ixigua.com +v1-tt.ixiguavideo.com +v1-up.amemv.com +v1-up.amemv.com.wsglb0.com +v1-xg-p.bytecdn.cn +v1-xg.bytecdn.cn +v1.ipstatp.com +v1.pstatp.com +v1.pstatp.com.wscdns.com +v10-tt.ixigua.com +v10-tt.ixiguavideo.com +v10.pstatp.com +v10.pstatp.com.cdnle.com +v11-tt.ixigua.com +v11.tiktokcdn.com +v15-edge.tiktokcdn.com +v15-fr.tiktokcdn.com +v15.tiktokcdn.com +v16-ad.byteoversea.com +v16-byteoversea.muscdn.com +v16-common.muscdn.com +v16-common.tiktokcdn.com +v16-default.byteoversea.com +v16-dy.byteoversea.com +v16-dy.byteoversea.com.edgesuite.net +v16-ipv6.tiktokcdn.com +v16-mcheckout.muscdn.com +v16-muscdn.toutiao50.com +v16-tcscdn.byteoversea.com +v16-tcscdn.byteoversea.com.edgekey.net +v16-tiktokcdn-com.akamaized.net +v16-up.amemv.com +v16-up.tiktokv.com +v16-up.tiktokv.com.edgesuite.net +v16-vcheckout.muscdn.com +v16-vcheckout.muscdn.com.edgesuite.net +v16-web-newkey.tiktokcdn.com +v16-web.tiktokcdn.com +v16-zr.tiktokcdn.com +v16.byteoversea.com +v16.byteoversea.com.edgesuite.net +v16.hypstarcdn.com +v16.hypstarcdn.com.edgesuite.net +v16.muscdn.com +v16.musical.ly +v16.tiktokcdn.com +v16.tiktokcdn.com.edgesuite.net +v16.tiktokv.com +v16.tiktokv.com.edgesuite.net +v16.toutiao50.com +v16.xzcs3zlph.com +v16.xzcs3zlph.com.edgesuite.net +v16a.tiktokcdn.com +v16a.tiktokcdn.com.edgesuite.net +v16m-default.akamaized.net +v16m-t.tiktokcdn.com +v16m.byteicdn.com +v16m.byteicdn.com.akamaized.net +v16m.hypstarcdn.com +v16m.hypstarcdn.com.akamaized.net +v16m.tiktokcdn.com +v16m.tiktokcdn.com.akamaized.net +v16s.byteicdn.com +v16s.tiktokcdn.com +v17-babe.ipstatp.com +v19-hwp.muscdn.com +v19-hwp.tiktokcdn.com +v19-web-newkey.tiktokcdn.com +v19-web.tiktokcdn.com +v19.byteicdn.com +v19.hypstarcdn.com +v19.muscdn.com +v19.tiktokcdn.com +v2-dy-x.ixigua.com +v2-dy-y.ixigua.com +v2-dy-z.ixigua.com +v2-dy.ixigua.com +v2-dy.ixiguavideo.com +v2-tt.ixigua.com +v2-tt.ixiguavideo.com +v2.pstatp.com +v2.pstatp.com.cloudcdn.net +v2.pstatp.com.cloudglb.com +v20.muscdn.com +v20.tiktokcdn.com +v21-tcs.tiktokcdn.com +v21-vcheckout.muscdn.com +v21.muscdn.com +v21.tiktokcdn.com +v21a.muscdn.com +v23.muscdn.com +v23.tiktokcdn.com +v24.muscdn.com +v24.muscdn.com.cdn.cloudflare.net +v24.tiktokcdn.com +v25.muscdn.com +v25.tiktokcdn.com +v26-dy.bytecdn.cn +v26-dy.ixigua.com +v26-tt.bytecdn.cn +v26.muscdn.com +v26.tiktokcdn.com +v27-dy.bytecdn.cn +v27-dy.ixigua.com +v27-tt.bytecdn.cn +v27.tiktokcdn.com +v27.tiktokcdn.com.cdn.jcloudcdn.com +v28-dy.bytecdn.cn +v28-tt.bytecdn.cn +v29-dy.ixigua.com +v3-ad.bytecdn.cn +v3-ad.ixigua.com.xi.zwtianshangm.com +v3-dc-ad.bytecdn.cn +v3-dc-ad.ixigua.com.xi.zwtianshangm.com +v3-default.bytecdn.cn +v3-default.ixigua.com.xi.zwtianshangm.com +v3-dy-a-x.bytecdn.cn +v3-dy-a.ixigua.com +v3-dy-a.ixigua.com.xi.zwtianshangm.com +v3-dy-b-x.bytecdn.cn +v3-dy-c-x.bytecdn.cn +v3-dy-d.ixigua.com +v3-dy-d.ixigua.com.xi.zwtianshangm.com +v3-dy-m.bytecdn.cn +v3-dy-n.bytecdn.cn +v3-dy-x.bytecdn.cn +v3-dy-x.ixigua.com +v3-dy-x.ixigua.com.xi.zwtianshangm.com +v3-dy-y.bytecdn.cn +v3-dy-y.ixigua.com +v3-dy-y.ixigua.com.xi.zwtianshangm.com +v3-dy-z.bytecdn.cn +v3-dy-z.ixigua.com +v3-dy-z.ixigua.com.xi.zwtianshangm.com +v3-dy.bytecdn.cn +v3-dy.ixigua.com +v3-dy.ixigua.com.xi.zwtianshangm.com +v3-dy.ixiguavideo.com +v3-hs-m.bytecdn.cn +v3-hs-n.bytecdn.cn +v3-hs.bytecdn.cn +v3-hs.ixigua.com.xi.zwtianshangm.com +v3-ppx.bytecdn.cn +v3-ppx.ixigua.com.xi.zwtianshangm.com +v3-tt-m.bytecdn.cn +v3-tt-n.bytecdn.cn +v3-tt-p.bytecdn.cn +v3-tt.bytecdn.cn +v3-tt.ixigua.com +v3-tt.ixigua.com.xi.zwtianshangm.com +v3-tt.ixiguavideo.com +v3-xg-m.bytecdn.cn +v3-xg-n.bytecdn.cn +v3-xg-p.bytecdn.cn +v3-xg.bytecdn.cn +v3-xg.ixigua.com.xi.zwtianshangm.com +v3.muscdn.com +v3.pstatp.com +v3.pstatp.com.w.kunlungr.com +v3.tiktokcdn.com +v3.tiktokcdn.com.w.kunlunsl.com +v30.muscdn.com +v30.tiktokcdn.com +v31.muscdn.com +v31.tiktokcdn.com +v31.true.byteoversea.net +v31.tsel.byteoversea.net +v32.muscdn.com +v32.tiktokcdn.com +v33.muscdn.com +v33.tiktokcdn.com +v34.muscdn.com +v34.tiktokcdn.com +v35.muscdn.com +v35.tiktokcdn.com +v36.muscdn.com +v36.tiktokcdn.com +v37.tiktokcdn.com +v38.tiktokcdn.com +v39-ca.gts.byteoversea.net +v39-us.gts.byteoversea.net +v39-us.tiktokcdn.com +v39.muscdn.com +v39.tiktokcdn.com +v3a.pstatp.com +v3a.pstatp.com.w.kunlunpi.com +v3b.pstatp.com +v3b.pstatp.com.w.kunlunle.com +v4-dy-x.ixigua.com +v4-dy-y.ixigua.com +v4-dy-z.ixigua.com +v4-dy.ixigua.com +v4-dy.ixiguavideo.com +v4-tt.ixigua.com +v4-tt.ixiguavideo.com +v4.pstatp.com +v4.pstatp.com.wscdns.com +v5-dy-x.ixigua.com +v5-dy-y.ixigua.com +v5-dy-z.ixigua.com +v5-dy.bytecdn.cn +v5-dy.ixigua.com +v5-dy.ixiguavideo.com +v5-tt.bytecdn.cn +v5-tt.ixigua.com +v5-tt.ixiguavideo.com +v5.pstatp.com +v51.tiktokcdn.com +v53.tiktokcdn.com +v58.tiktokcdn.com +v6-dc-ad.bytecdn.cn +v6-default.bytecdn.cn +v6-dy-a-x.bytecdn.cn +v6-dy-b-x.bytecdn.cn +v6-dy-x.ixigua.com +v6-dy-y.ixigua.com +v6-dy-z.ixigua.com +v6-dy.bytecdn.cn +v6-dy.ixigua.com +v6-dy.ixiguavideo.com +v6-hs.bytecdn.cn +v6-ppx.bytecdn.cn +v6-tt.bytecdn.cn +v6-tt.ixigua.com +v6-tt.ixiguavideo.com +v6-xg-3p.bytecdn.cn +v6-xg-p.bytecdn.cn +v6-xg.bytecdn.cn +v6.pstatp.com +v61.tiktokcdn.com +v7-dy-x.ixigua.com +v7-dy-y.ixigua.com +v7-dy-z.ixigua.com +v7-dy.ixigua.com +v7-dy.ixiguavideo.com +v7-tt.ixigua.com +v7-tt.ixiguavideo.com +v7.pstatp.com +v7.pstatp.com.w.alikunlun.net +v77.tiktokcdn.com +v8-dy-x.ixigua.com +v8-dy-y.ixigua.com +v8-dy-z.ixigua.com +v8-dy.ixigua.com +v8-dy.ixiguavideo.com +v8-tt.ixigua.com +v8-tt.ixiguavideo.com +v8.pstatp.com +v9-dc-ad.bytecdn.cn +v9-default.bytecdn.cn +v9-default.ixigua.com.bsgslb.com +v9-del.tiktokcdn.com +v9-del.tiktokcdn.com.baishan-cloud.net +v9-dy-a-x.bytecdn.cn +v9-dy-b-x.bytecdn.cn +v9-dy-c-x.bytecdn.cn +v9-dy-ipv6.ixigua.com.bsgslb.com +v9-dy-x.bytecdn.cn +v9-dy-x.ixigua.com +v9-dy-x.ixigua.com.bsgslb.com +v9-dy-y.bytecdn.cn +v9-dy-y.ixigua.com +v9-dy-y.ixigua.com.bsgslb.com +v9-dy-z.bytecdn.cn +v9-dy-z.ixigua.com +v9-dy-z.ixigua.com.bsgslb.com +v9-dy.bytecdn.cn +v9-dy.ixigua.com +v9-dy.ixigua.com.bsgslb.com +v9-dy.ixiguavideo.com +v9-hs.bytecdn.cn +v9-hs.ixigua.com.bsgslb.com +v9-id.tiktokcdn.com +v9-in.tiktokcdn.com +v9-in.tiktokcdn.com.baishan-cloud.net +v9-ph.tiktokcdn.com +v9-ph.tiktokcdn.com.baishan-cloud.net +v9-rp.tiktokcdn.com +v9-th.tiktokcdn.com +v9-tt.bytecdn.cn +v9-tt.ixigua.com +v9-tt.ixigua.com.bsgslb.com +v9-tt.ixiguavideo.com +v9-tt.ixiguavideo.com.bsgslb.com +v9-vcheckout.muscdn.com +v9-vcheckout.tiktokcdn.com +v9-vcheckout.tiktokcdn.com.baishan-cloud.net +v9-vn.tiktokcdn.com +v9.pstatp.com +v9.pstatp.com.bsgslb.com +v9.tiktokcdn.com +v9.tiktokcdn.com.baishan-cloud.net +va-effect.byteoversea.com +va-effect.byteoversea.com.edgekey.net +va-link.byteoversea.com +va-proxy-ali.musical.ly +va.isnssdk.com +vaali-dpprofile-useast2a.byteoversea.com +vaali-dpprofile.byteoversea.com +vaali-mcs.byteoversea.com +vaali1.l.byteoversea.net +varticle-frontier.snssdk.com +vas-alisg.byteoversea.com +vas-alisg16.byteoversea.com +vas-alisg16.byteoversea.com.edgesuite.net +vas-alisg16.tiktokcdn.com +vas-alisg16.tiktokcdn.com.edgesuite.net +vas-alisg19.byteoversea.com +vas-alisg21.byteoversea.com +vas-alisg22-quic.byteoversea.com +vas-boei18n.bytedance.net +vas-hl-x.snssdk.com +vas-lf-x.snssdk.com +vas-lf-x.snssdk.com.w.kunluncan.com +vas-maliva-useast2a.byteoversea.com +vas-maliva.byteoversea.com +vas-maliva16-useast2a.byteoversea.com +vas-maliva16.byteoversea.com +vas-maliva16.byteoversea.com.edgekey.net +vas-maliva19.byteoversea.com +vas-maliva21.byteoversea.com +vas.isnssdk.com +vas.sgsnssdk.com +vas.snssdk.com +vas.tiktokcdn.com +vc-brain.snssdk.com +vc-foresight.snssdk.com +vc-foresight.snssdk.com.w.kunluncan.com +vc-gate.snssdk.com +vc-mirror.snssdk.com +vc-postal.snssdk.com +vc-pservice.snssdk.com +vc-tantan.snssdk.com +vcl-brain.snssdk.com +vcs-sg.byteoversea.com +vcs-sg.byteoversea.com.edgekey.net +vcs-sg.tiktokv.com +vcs-sg.tiktokv.com.edgekey.net +vcs-va.byteoversea.com +vcs-va.tiktokv.com +vcs.snssdk.com +verification-va-useast1a.musical.ly +verification-va-useast1a.musical.ly.edgesuite.net +verification-va-useast2a.byteoversea.com +verification-va-useast2a.musical.ly +verification-va-useast2a.musical.ly.edgesuite.net +verification-va.byteoversea.com +verification-va.byteoversea.com.edgesuite.net +verification-va.musical.ly +verification-va.musical.ly.edgesuite.net +verification-va.tiktokv.com +verification-va.tiktokv.com.edgesuite.net +verification16-normal-c-useast1a.tiktokv.com +verification16-normal-c-useast1a.tiktokv.com.edgesuite.net +verification16-normal-c-useast2a.tiktokv.com +verification16-normal-c-useast2a.tiktokv.com.edgesuite.net +verify-sg.byteoversea.com +verify-sg.byteoversea.com.edgesuite.net +verify-sg.tiktokv.com +verify-sg.tiktokv.com.edgesuite.net +verify.snssdk.com +verify.snssdk.com.w.kunluncan.com +vg23wcast.qfyf1toi.com +video-sg.musical.ly +video-sg.tiktokv.com +video-va.musical.ly +video-va.tiktokv.com +video.h1.bytedance.map.fastly.net +videoarch-aliec1-edge-lb.bytedns.net +videoarch-awsbh-edge-lb.byteoversea.net +videoarch-awsde-edge-lb.byteoversea.net +videoarch-awssg-edge-lb.byteoversea.net +videoarch-awssg2alisg-lb.byteoversea.net +videoarch-gcpgb-edge-lb.byteoversea.net +videoarch-gcpin-edge-lb.byteoversea.net +videoarch-inaws-edge-lb.byteoversea.net +videoarch-lb-alisg.byteoversea.net +videoarch-lb-maliva.byteoversea.net +videoarch-useast-lb-useast2a.byteoversea.net +vigo-lb-alisg.byteoversea.net +vigoreview.byteoversea.com +vm.tiktok.com +vm.tiktok.com.edgesuite.net +vm.xzcs3zlph.com +vm.xzcs3zlph.com.edgesuite.net +vnpt-han-api.l.byteoversea.net +vod-hl.bytedanceapi.com +vod-hl.snssdk.com +vod-hl.snssdk.com.w.kunluncan.com +vod-lf.bytedanceapi.com +vod-lf.snssdk.com +vod-lf.snssdk.com.w.kunluncan.com +vod.ap-singapore-1.bytedanceapi.com +vod.bytedanceapi.com +vod.snssdk.com +vod.us-east-1.bytedanceapi.com +voffline.pstatp.com +vpn2.byted.org +vs.snssdk.com +vt.tiktok.com +vt.tiktok.com.edgesuite.net +vv-t1.ipstatp.com +vv.ipstatp.com +vv1.ipstatp.com +vzbmx.xzcs3zlph.com +vzbmx.xzcs3zlph.com.edgesuite.net +w1bobealq.mzfvozqybf.com +wallet-hl.snssdk.com +wallet-hl.snssdk.com.w.kunluncan.com +wallet.amemv.com +wallet.amemv.com.w.kunlunhuf.com +wallet.snssdk.com +wallet.snssdk.com.w.kunluncan.com +wcast.xzcs3zlph.com +wcast.xzcs3zlph.com.edgesuite.net +wd.snssdk.com +wd3.bytecdn.cn +wd3.pstatp.com +web.toutiao.com +webcast-c-hl.amemv.com +webcast-c-hl.amemv.com.w.cdngslb.com +webcast-c-lf.amemv.com +webcast-c-lf.amemv.com.w.cdngslb.com +webcast-c-lq.amemv.com +webcast-c-lq.amemv.com.w.cdngslb.com +webcast-c.amemv.com +webcast-c.amemv.com.w.cdngslb.com +webcast-helo.sgsnssdk.com +webcast-hl.amemv.com +webcast-hl.amemv.com.w.kunluncan.com +webcast-hl.amemv.com.xi.zwtianshangm.com +webcast-hl.snssdk.com +webcast-i-hl.amemv.com +webcast-i-lf.amemv.com +webcast-i-lf.amemv.com.w.cdngslb.com +webcast-i-lq.amemv.com +webcast-i.amemv.com +webcast-i.amemv.com.w.cdngslb.com +webcast-lb-alisg.byteoversea.net +webcast-lf.amemv.com +webcast-lf.amemv.com.w.kunluncan.com +webcast-lq.amemv.com +webcast-lq.amemv.com.w.cdngslb.com +webcast-ppx-hl.snssdk.com +webcast-ppx-lq.snssdk.com +webcast-ppx.snssdk.com +webcast-ppx.snssdk.com.w.kunluncan.com +webcast-useast1a.musical.ly +webcast-useast1a.musical.ly.edgekey.net +webcast-useast1a.tiktokv.com +webcast-useast2a.musical.ly +webcast-useast2a.musical.ly.edgekey.net +webcast-useast2a.tiktokv.com +webcast-va.tiktokv.com +webcast-va.tiktokv.com.edgekey.net +webcast.amemv.com +webcast.amemv.com.w.kunluncan.com +webcast.amemv.com.xi.zwtianshangm.com +webcast.musical.ly +webcast.musical.ly.edgekey.net +webcast.snssdk.com +webcast.snssdk.com.w.kunluncan.com +webcast.tiktokv.com +webcast.tiktokv.com.edgekey.net +webcast100-core-c-hl.amemv.com +webcast100-core-c-lf.amemv.com +webcast100-core-c-lq.amemv.com +webcast100-core-c.amemv.com +webcast100-normal-c-hl.amemv.com +webcast100-normal-c-hl.snssdk.com +webcast100-normal-c-lf.amemv.com +webcast100-normal-c-lf.snssdk.com +webcast100-normal-c-lq.amemv.com +webcast100-normal-c-lq.snssdk.com +webcast100-normal-c.amemv.com +webcast100-normal-c.snssdk.com +webcast16-normal-c-alisg.tiktokv.com +webcast16-normal-c-useast1a.tiktokv.com +webcast16-normal-c-useast1a.tiktokv.com.edgekey.net +webcast16-normal-c-useast2a.tiktokv.com +webcast16-normal-c-useast2a.tiktokv.com.edgekey.net +webcast16-useast1a.musical.ly +webcast16-useast2a.musical.ly +webcast16-useast2a.musical.ly.edgekey.net +webcast16-va.tiktokv.com +webcast16-va.tiktokv.com.edgekey.net +webcast16.musical.ly +webcast16.musical.ly.edgekey.net +webcast16.tiktokv.com +webcast16.tiktokv.com.edgekey.net +webcast19-normal-c-alisg.tiktokv.com +webcast19-normal-c-useast1a.tiktokv.com +webcast19-normal-c-useast2a.tiktokv.com +webcast19-useast1a.musical.ly +webcast19-useast2a.musical.ly +webcast19-va.tiktokv.com +webcast19.musical.ly +webcast19.tiktokv.com +webcast21-normal-c-useast1a.tiktokv.com +webcast21-normal-c-useast2a.tiktokv.com +webcast21-useast1a.musical.ly +webcast21-useast2a.musical.ly +webcast21-va.tiktokv.com +webcast21.musical.ly +webcast21.tiktokv.com +webcast26-c-hl.amemv.com +webcast26-c-hl.amemv.com.w.cdngslb.com +webcast26-c-lf.amemv.com +webcast26-c-lf.amemv.com.w.cdngslb.com +webcast26-c-lq.amemv.com +webcast26-c-lq.amemv.com.w.cdngslb.com +webcast26-c.amemv.com +webcast26-c.amemv.com.w.cdngslb.com +webcast26-hl.amemv.com +webcast26-lf.amemv.com +webcast26-lq.amemv.com +webcast26-lq.amemv.com.w.cdngslb.com +webcast26.amemv.com +webcast3-c-hl.amemv.com +webcast3-c-hl.amemv.com.w.cdngslb.com +webcast3-c-ipv6.amemv.com +webcast3-c-ipv6.amemv.com.w.cdngslb.com +webcast3-c-lf.amemv.com +webcast3-c-lf.amemv.com.w.cdngslb.com +webcast3-c-lq.amemv.com +webcast3-c-lq.amemv.com.w.cdngslb.com +webcast3-c.amemv.com +webcast3-c.amemv.com.w.cdngslb.com +webcast3-core-c-hl.amemv.com +webcast3-core-c-hl.amemv.com.w.cdngslb.com +webcast3-core-c-lf.amemv.com +webcast3-core-c-lf.amemv.com.w.cdngslb.com +webcast3-core-c-lq.amemv.com +webcast3-core-c-lq.amemv.com.w.cdngslb.com +webcast3-core-c.amemv.com +webcast3-core-c.amemv.com.w.cdngslb.com +webcast3-hl.amemv.com +webcast3-hl.amemv.com.w.kunluncan.com +webcast3-ipv6.amemv.com +webcast3-ipv6.amemv.com.w.cdngslb.com +webcast3-lf.amemv.com +webcast3-lf.amemv.com.w.kunluncan.com +webcast3-lq.amemv.com +webcast3-lq.amemv.com.w.cdngslb.com +webcast3-normal-c-hl.amemv.com +webcast3-normal-c-hl.amemv.com.w.cdngslb.com +webcast3-normal-c-hl.snssdk.com +webcast3-normal-c-lf.amemv.com +webcast3-normal-c-lf.amemv.com.w.cdngslb.com +webcast3-normal-c-lf.snssdk.com +webcast3-normal-c-lq.amemv.com +webcast3-normal-c-lq.amemv.com.w.cdngslb.com +webcast3-normal-c-lq.snssdk.com +webcast3-normal-c.amemv.com +webcast3-normal-c.amemv.com.w.cdngslb.com +webcast3-normal-c.snssdk.com +webcast3-ws-c-hl.amemv.com +webcast3-ws-c-hl.amemv.com.w.cdngslb.com +webcast3-ws-c-lf.amemv.com +webcast3-ws-c-lf.amemv.com.w.cdngslb.com +webcast3-ws-c-lq.amemv.com +webcast3-ws-c-lq.amemv.com.w.cdngslb.com +webcast3.amemv.com +webcast3.amemv.com.w.kunluncan.com +weiliicimg1.pstatp.com +welcome.toutiao.com +wenda.toutiao.com +wildcard.musical.ly.edgekey.net +wj.byteoversea.com +wj.byteoversea.com.edgesuite.net +wj.toutiao.com +worldfcdn.com +ws-hl.l.bytedns.net +ws.byteoversea.net +ws.l.bytedns.net +www.amemv.com +www.amemv.com.w.kunluncan.com +www.bytecdn.cn +www.bytedanceapi.com +www.byteicdn.com +www.douyin.com +www.hypstarcdn.com +www.ibytedtos.com +www.iesdouyin.com +www.ipstatp.com +www.isnssdk.com +www.ixigua.com +www.muscdn.com +www.musemuse.cn +www.musical.ly +www.p3.pstatp.com +www.pstatp.com +www.sgsnssdk.com +www.snssdk.com +www.tiktok.com +www.tiktok.com.edgesuite.net +www.tiktokcdn.com +www.tiktokv.com +www.tiktokv.com.edgesuite.net +www.toutiao.com +www.toutiao.com.bytedns.net +www.worldfcdn.com +www.wshifen.com +www.xzcs3zlph.com +x.l.bytedns.net +xd.snssdk.com +xd.snssdk.com.w.kunluncan.com +xgapi-hl.snssdk.com +xgapi-hl.snssdk.com.w.kunluncan.com +xgapi-lf.snssdk.com +xgapi-lq.snssdk.com +xgapi.snssdk.com +xgapi.snssdk.com.w.kunluncan.com +xgapi1.snssdk.com +xgapi2.snssdk.com +xgfe.snssdk.com +xgfe.snssdk.com.w.kunluncan.com +xlog-cold.snssdk.com +xlog-ipv6.snssdk.com +xlog-o.byteoversea.com +xlog-va.byteoversea.com +xlog-va.byteoversea.com.edgesuite.net +xlog-va.musical.ly +xlog-va.musical.ly.edgesuite.net +xlog-va.tiktokv.com +xlog-va.tiktokv.com.edgesuite.net +xlog.byteoversea.com +xlog.byteoversea.com.edgesuite.net +xlog.snssdk.com +xlog.snssdk.com.w.kunluncan.com +xlog.tiktokv.com +xlog.tiktokv.com.edgesuite.net +xwzx.815181.com.bytedns.net +xxbg.snssdk.com +xxz.toutiao.com +xzcs3zlph.com +xzcs3zlph.com.akamaized.net +xzzfz.toutiao.com +y.l.bytedns.net +youdianyisi.com +yuntu.toutiao.com +z.l.bytedns.net +z.toutiao.com +z.toutiao.com.w.cdngslb.com +z6-hl.snssdk.com +z6-lf.snssdk.com +z6-lq.snssdk.com +z6.snssdk.com +zdchannel.snssdk.com +zdchannel.snssdk.com.w.kunluncan.com +zdmall.snssdk.com +zdmall.snssdk.com.w.kunluncan.com +zerxygpl.mzfvozqybf.com +zhanzhang.toutiao.com +zhenzhen.snssdk.com +zjmvi.v.bsgslb.com +zlink.toutiao.com +zlink.toutiao.com.w.cdngslb.com +zong-isb-v1.l.byteoversea.net +zong-khi-v1.l.byteoversea.net +zong-lhe-v1.l.byteoversea.net +zong.byteoversea.net +zqq1.v.bsgslb.com +zqq2.v.bsgslb.com +zqqnydl.v.bsgslb.com +zttapk.v.bsgslb.com +zttpic.v.bsgslb.com +zttv1.v.bsgslb.com +zttvad.v.bsgslb.com +zttvi.v.bsgslb.com +zttvideo.v.bsgslb.com +zttvipv6.v.bsgslb.com +zttvl.v.bsgslb.com +zttvm.v.bsgslb.com +zttvp.v.bsgslb.com +zttvq.v.bsgslb.com +zttvs.v.bsgslb.com +zttvu.v.bsgslb.com +zttvv.v.bsgslb.com +zttvw.v.bsgslb.com +pull-cmaf-f16-sg01.ttlivecdn.com +pull-f5-tt01.tiktokcdn.com +pull-flv-f1-va01.tiktokcdn.com +pull-flv-f1-va01.tiktokcdn.com.wsdvs.com +pull-flv-f11-va01.tiktokcdn.com +pull-flv-l1-sg01.ttlivecdn.com +pull-flv-l1-sg01.ttlivecdn.com.wsdvs.com +pull-flv-l1-va01.tiktokcdn.com +pull-flv-l1-va01.tiktokcdn.com.wsdvs.com +pull-flv-l1-va01.ttlivecdn.com +pull-flv-l10-sg01.tiktokcdn.com +pull-flv-l11-sg01.ttlivecdn.com +pull-hls-f11-gcp01.tiktokcdn.com +pull-hls-f11-va01.tiktokcdn.com +pull-hls-f16-gcp01.tiktokcdn.com +pull-hls-f16-gcp01.ttlivecdn.com +pull-hls-f16-va01.tiktokcdn.com +pull-hls-l10-sg01.tiktokcdn.com +pull-hls-l11-gcp01.tiktokcdn.com +pull-hls-l11-sg01.ttlivecdn.com +pull-hls-q5-va01.tiktokcdn.com +pull-hls-w16-gcp01.tiktokcdn.com +pull-lls-l11.tiktokcdn.com +pull-rtmp-l11-gcp01.ttlivecdn.com +push-rtmp-f5-sg01.ttlivecdn.com +push-rtmp-f5-tt01.fcdn.us.tiktokv.com +push-rtmp-l1-gcp01.ttlivecdn.com +push-rtmp-l1-gcp01.ttlivecdn.com.wsdvs.com +push-rtmp-l1-va01.tiktokcdn.com +push-rtmp-l1-va01.tiktokcdn.com.wsdvs.com +push-rtmp-l11-gcp01.ttlivecdn.com +api16-core-useast5.us.tiktokv.com +api16-normal-useast5.us.tiktokv.com +api19-core-useast5.us.tiktokv.com +api19-normal-useast5.us.tiktokv.com +dig.us.tiktokv.com +frontier.us.tiktokv.com +hotapi16-platform-useast5.us.tiktokv.com +im16-platform-useast5.us.tiktokv.com +image.us.tiktokv.com +jsb16-normal-c-useast1a.tiktokv.com +lf16-effectcdn-va.tiktokcdn.com +lf19-effectcdn-va.tiktokcdn.com +lf21-effectcdn-va.tiktokcdn.com +libra16-platform-useast5.us.tiktokv.com +log16-applog-useast5.us.tiktokv.com +log16-applog-useast5.us.tiktokv.com.localdomain +mon-sg.tiktokv.com +mon16-platform-useast5.us.tiktokv.com +mssdk16-platform-useast5.us.tiktokv.com +p16-sign.tiktokcdn-us.com +p16.tiktokcdn-us.com +p19-sign.tiktokcdn-us.com +p19.tiktokcdn-us.com +pull-c5-va01.tiktokcdn.com +pull-cmaf-f16-gcp01.tiktokcdn.com +pull-cmaf-f16-tt01.fcdn.us.tiktokv.com +pull-cmaf-f16-tt01.tiktokcdn.com +pull-cmaf-f16-va01.tiktokcdn.com +pull-cmaf-f16.tiktokcdn.com.localdomain +pull-cmaf-f77-va01.tiktokcdn.com +pull-cmaf-f77-va01.tiktokcdn.com.localdomain +pull-cmaf-l16-va01.tiktokcdn.com +pull-cmaf-l77-va01.tiktokcdn.com +pull-f5-gcp01.tiktokcdn.com +pull-f5-tt01.fcdn.us.tiktokv.com +pull-f5-va01.tiktokcdn.com +pull-flv-l16-va01.tiktokcdn.com +pull-hls-f16-tt01.fcdn.us.tiktokv.com +pull-hls-f16-tt01.tiktokcdn.com +pull-hls-f77-va01.tiktokcdn.com +pull-hls-l16-va01.tiktokcdn.com +pull-hls-l77-va01.tiktokcdn.com +pull-hls-q16-gcp01.tiktokcdn.com +pull-hls-q16-va01.tiktokcdn.com +pull-hls-w16-va01.tiktokcdn.com +pull-hls-w16-va01.tiktokcdn.com.localdomain +pull-hls-w5-va01.tiktokcdn.com +pull-q5-va01.tiktokcdn.com +pull-rtmp-l16-va01.tiktokcdn.com +pull-w5-va01.tiktokcdn.com +push-rtmp-f5-gcp01.tiktokcdn.com +push-rtmp-f5-va01.tiktokcdn.com +push-rtmp-l16-va01.tiktokcdn.com +push-rtmp-l77-va01.tiktokcdn.com +sf16-ies-music-sg.tiktokcdn.com +sf16-ies-music-va.tiktokcdn.com +sf16.tiktokcdn-us.com +sf19.tiktokcdn-us.com +tnc16-platform-alisg.tiktokv.com +tnc16-platform-useast1a.tiktokv.com +tnc16-platform-useast5.us.tiktokv.com +v16m.tiktokcdn-us.com +v19.tiktokcdn-us.com +v25.tiktokcdn-us.com +v39.tiktokcdn-us.com +vcs16-platform-useast5.us.tiktokv.com +video-useast5.tiktokv.com +webcast16-normal-useast5.us.tiktokv.com +webcast19-normal-useast5.us.tiktokv.com +mcs-va.tiktok.com.edgekey.net diff --git a/LockdowniOS/tracker_info.json b/LockdowniOS/tracker_info.json new file mode 100644 index 0000000..f7ef4c7 --- /dev/null +++ b/LockdowniOS/tracker_info.json @@ -0,0 +1,12 @@ +{ + "trackerIds": { + "googleadservices.com": "google_ads", + "doubleclick.net": "google_ads" + }, + "descriptions": { + "google_ads": { + "title": "Google Ads", + "description": "These are Google ad servers, some of which host ads you see in shopping search results. Many apps also report to these servers in the background, even when your device is locked." + } + } +} diff --git a/Metrics.swift b/Metrics.swift new file mode 100644 index 0000000..810541c --- /dev/null +++ b/Metrics.swift @@ -0,0 +1,71 @@ +// +// Metrics.swift +// Lockdown +// +// Created by Oleg Dreyman on 28.09.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation + +func getDayMetrics() -> Int { + return defaults.integer(forKey: kDayMetrics) +} + +func getDayMetricsString(commas: Bool = false) -> String { + return metricsToString(metric: getDayMetrics(), commas: commas) +} + +func getWeekMetrics() -> Int { + return defaults.integer(forKey: kWeekMetrics) +} + +func getWeekMetricsString() -> String { + return metricsToString(metric: getWeekMetrics()) +} + +func getTotalMetrics() -> Int { + return defaults.integer(forKey: kTotalMetrics) +} + +func getTotalMetricsString() -> String { + return metricsToString(metric: getTotalMetrics()) +} + +func getTotalEnabledMetrics() -> Int { + return defaults.integer(forKey: kTotalEnabledMetrics) +} + +func getTotalEnabledString() -> String { + return metricsToString(metric: getTotalEnabledMetrics()) +} + +func getTotalDisabledMetrics() -> Int { + return defaults.integer(forKey: kTotalDisabledMetrics) +} + +func getTotalDisabledString() -> String { + return metricsToString(metric: getTotalDisabledMetrics()) +} + +func metricsToString(metric : Int, commas: Bool = false) -> String { + if (commas) { + let commasFormatter = NumberFormatter() + commasFormatter.numberStyle = .decimal + guard let formattedNumber = commasFormatter.string(from: NSNumber(value: metric)) else { return "\(metric)" } + return formattedNumber + } + if metric < 1000 { + return "\(metric)" + } + else if metric < 1000000 { + return "\(Int(metric / 1000))k" + } + else { + return "\(String(format: "%.2f", (Double(metric) / Double(1000000))))m" + } +} + +enum Metrics { + +} diff --git a/OneTimeActions.swift b/OneTimeActions.swift new file mode 100644 index 0000000..cb69af4 --- /dev/null +++ b/OneTimeActions.swift @@ -0,0 +1,36 @@ +// +// OneTimeActions.swift +// LockdowniOS +// +// Created by Oleg Dreyman on 28.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation + +enum OneTimeActions { + + enum Flag: String { + case welcomeScreen = "LockdownHasSeenWelcomePopup" + case notificationAuthorizationRequestPopup = "LockdownHasSeenNotificationAuthorizationRequestPopup" + case oneHundredTrackingAttemptsBlockedNotification = "LockdownHasScheduledOneHundredTrackingAttemptsBlockedNotification" + case newEnableNotificationsController = "LockdownHasSeenNewEnableNotificationsController" + } + + static func hasSeen(_ flag: Flag) -> Bool { + return defaults.bool(forKey: flag.rawValue) + } + + static func markAsSeen(_ flag: Flag) { + defaults.set(true, forKey: flag.rawValue) + } + + static func performOnce(ifHasNotSeen flag: Flag, action: () -> ()) { + if hasSeen(flag) { + return + } else { + markAsSeen(flag) + action() + } + } +} diff --git a/Podfile b/Podfile index e3c531e..c7dcf45 100644 --- a/Podfile +++ b/Podfile @@ -7,43 +7,42 @@ target :'Lockdown' do plugin 'cocoapods-acknowledgements', :settings_bundle => true pod 'AwesomeSpotlightView' pod 'RQShineLabel' - pod 'ReachabilitySwift' + pod 'NicoProgress' pod 'SwiftMessages', '6.0.0' pod 'PromiseKit' pod 'SwiftyStoreKit', '0.13.1' pod 'KeychainAccess' - pod 'CocoaLumberjack' pod 'PopupDialog', '~> 0.9' + pod 'SwiftCSV' end target :'LockdownTunnel' do pod 'PromiseKit' - pod 'KeychainAccess' pod 'SwiftyStoreKit', '0.13.1' - pod 'ReachabilitySwift' - pod 'CocoaLumberjack' + pod 'KeychainAccess' end target :'Lockdown VPN Widget' do pod 'PromiseKit' pod 'SwiftyStoreKit', '0.13.1' pod 'KeychainAccess' - pod 'CocoaLumberjack' - pod 'ReachabilitySwift' end target :'Lockdown Firewall Widget' do pod 'PromiseKit' pod 'SwiftyStoreKit', '0.13.1' pod 'KeychainAccess' - pod 'CocoaLumberjack' - pod 'ReachabilitySwift' end -# post_install do |installer| -# installer.pods_project.targets.each do |target| -# target.build_configurations.each do |config| -# config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' -# end -# end -# 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' +end + + post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + end + end + end diff --git a/PrivacyInfo.xcprivacy b/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..8d6c2a4 --- /dev/null +++ b/PrivacyInfo.xcprivacy @@ -0,0 +1,67 @@ + + + + + NSPrivacyTracking + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + 3B52.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeEmailAddress + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + NSPrivacyCollectedDataTypePurposeAnalytics + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypePurchaseHistory + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeUserID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + + diff --git a/ProtectedFileAccess.swift b/ProtectedFileAccess.swift new file mode 100644 index 0000000..82f2db3 --- /dev/null +++ b/ProtectedFileAccess.swift @@ -0,0 +1,240 @@ +// +// AppGroupStorage.swift +// LockdowniOS +// +// Created by Oleg Dreyman on 16.10.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation +import CocoaLumberjackSwift + +func flushBlockLog( log: (String) -> Void) { + + let logFileDateFormatter = DateFormatter() + logFileDateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + logFileDateFormatter.timeZone = TimeZone(abbreviation: "UTC") + + let lastFlushDateKey = "lastBlockLogFlushTimeInterval" + + let fileManager = FileManager.default + let sharedDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.com.confirmed") + let blockLogFile = sharedDir!.appendingPathComponent("blocklist.log") + + var blockLogFileText = "" + do { + blockLogFileText = try String(contentsOf: blockLogFile, encoding: .utf8) + log("Read block log file") + } + catch { + log("ERROR - couldn't read block log file at: \(blockLogFile.path)") + } + + let blockedEntries = blockLogFileText.split(separator: "\n") + + let lastFlushDate = defaults.double(forKey: lastFlushDateKey) + + for blockedEntry in blockedEntries { + // parse this [2022-04-14 22:23:12] 127.0.0.1 example.com *.example.com + let splitLine = blockedEntry.split(separator: "\t") + + if (splitLine.count != 4) { + log("ERROR: invalid blocked log entry: \(splitLine)") + continue + } + else { + // [2022-04-14 22:23:12] + var entryDateString = String(splitLine[0]) + if (entryDateString.count < 3) { + log("ERROR: invalid entryDateString - too short: \(entryDateString)") + continue + } + entryDateString.removeLast() + entryDateString.removeFirst() + + let host = String(splitLine[2]) + if (host == testFirewallDomain) { + // skip this + continue + } + // 2022-04-14 22:23:12 + if let entryDate = logFileDateFormatter.date(from: entryDateString) { + // only log entries that are newer than lastFlushDate +// log("entry line: \(blockedEntry)") +// log("comparing entrydatetime \(entryDate.timeIntervalSince1970) to lastFlushDate \(lastFlushDate)") +// log("comparing entrydatetime \(entryDate) to lastFlushDate \(Date(timeIntervalSince1970: lastFlushDate))") + if entryDate.timeIntervalSince1970 <= lastFlushDate { + // log("entryDate is older, not logging it") + continue + } + //log("entryDate is newer, logging it") + updateMetrics(.incrementAndLog(host: host), rescheduleNotifications: .withEnergySaving) + } + else { + log("ERROR: couldnt format entryDateString: \(entryDateString)") + continue + } + } + } + + // update the last flushed time + defaults.set(Date().timeIntervalSince1970, forKey: lastFlushDateKey) + +// TODO: this doesn't work bc the file is being written to at the same time +// log("Flushing Block Log File") +// do { +// try "".write(to: blockLogFile, atomically: true, encoding: .utf8) +// log("Flushed Block Log File") +// } +// catch { +// log("Error flushing block log file: \(error)") +// } +} + +enum ProtectedFileAccess { + + private static let fileURL = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.com.confirmed")! + .appendingPathComponent("protectionAccess.check") + + static var isAvailable: Bool { + do { + let data = try Data.init(contentsOf: fileURL, options: [.mappedIfSafe]) + let string = String(data: data, encoding: .utf8) + return string == "CHECK" + } catch { + return false + } + } + + @available(iOS 10.0, *) + static func createProtectionAccessCheckFile() { + if !isAvailable { + let result = FileManager.default.createFile( + atPath: fileURL.path, + contents: "CHECK".data(using: .utf8), + attributes: [FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication] + ) + if result { + DDLogInfo("Created protectionAccess.check file") + } else { + DDLogError("Failed to create protectionAccess.check file") + } + } else { + DDLogInfo("protectionAccess.check file already exists") + } + } +} + +enum PacketTunnelProviderLogs { + + static let dateFormatter: DateFormatter = { + $0.dateFormat = "yyyy-MM-dd HH:mm:ss" + $0.formatterBehavior = .behavior10_4 + $0.locale = .init(identifier: "en_US_POSIX") + return $0 + }(DateFormatter()) + + static let userDefaultsKey = "com.confirmed.lockdown.ne_temporaryLogs" + + static func log(_ string: String) { + guard ProtectedFileAccess.isAvailable else { + return + } + + let string = "\(dateFormatter.string(from: Date())) \(string)" + if var array = defaults.stringArray(forKey: userDefaultsKey) { + // don't let it get too large + if array.count > 40000 { + array = Array(array.dropFirst(10000)) + } + array.append(string) + defaults.setValue(array, forKey: userDefaultsKey) + } else { + defaults.setValue([string], forKey: userDefaultsKey) + } + } + + static var allEntries: [String] { + return defaults.stringArray(forKey: userDefaultsKey) ?? [] + } + + static func clear() { + defaults.setValue(Array(), forKey: userDefaultsKey) + } +} + +#if DEBUG +final class AppGroupStorage { + + struct Key: RawRepresentable { + let rawValue: String + + init(rawValue: String) { + self.rawValue = rawValue + } + } + + private static let json: (encoder: JSONEncoder, decoder: JSONDecoder) = (JSONEncoder(), JSONDecoder()) + static let directoryURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.confirmed")!.appendingPathComponent("AppGroupStorage", isDirectory: true) + + static let shared = AppGroupStorage() + + init() { + AppGroupStorage.createDirectoryIfNeeded(at: AppGroupStorage.directoryURL) + } + + func read(key: Key) -> Content? { + do { + let url = self.url(forKey: key) + let data = try Data(contentsOf: url) + let content = try AppGroupStorage.json.decoder.decode(Content.self, from: data) + return content + } catch { + DDLogError(error) + return nil + } + } + + func write(content: Content, key: Key) { + do { + let url = self.url(forKey: key) + let data = try AppGroupStorage.json.encoder.encode(content) + try data.write(to: url, options: [.atomic, .noFileProtection]) + } catch { + DDLogError(error) + return + } + } + + func delete(forKey key: Key) { + let url = self.url(forKey: key) + let fileManager = FileManager() + do { + try fileManager.removeItem(at: url) + } catch { + DDLogError(error) + } + } + + func url(forKey key: Key) -> URL { + return AppGroupStorage.directoryURL.appendingPathComponent(key.rawValue) + } +} + +extension AppGroupStorage { + static func createDirectoryIfNeeded(at url: URL) { + if FileManager.default.fileExists(atPath: url.path) { + DDLogInfo("AppGroupStorage Directory exists: \(url.path)") + return + } else { + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil) + DDLogInfo("AppGroupStorage Directory created: \(url.path)") + } catch { + DDLogError(error) + } + } + } +} +#endif diff --git a/PushNotifications.swift b/PushNotifications.swift new file mode 100644 index 0000000..549f005 --- /dev/null +++ b/PushNotifications.swift @@ -0,0 +1,325 @@ +// +// PushNotifications.swift +// LockdowniOS +// +// Created by Oleg Dreyman on 26.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import Foundation +import UserNotifications +import CocoaLumberjackSwift + +final class PushNotifications { + + private let serialQueue = DispatchQueue(label: "PushNotifications-ScheduleQueue") + private let energySaving = PushNotifications.EnergySaving() + + enum Category: String { + case weeklyUpdate = "WeeklyUpdate" + } + + static let shared = PushNotifications() + + struct RescheduleOptions: OptionSet { + let rawValue: Int + + static let energySaving = RescheduleOptions(rawValue: 1 << 0) + } + + func rescheduleWeeklyUpdate(options: RescheduleOptions) { + serialQueue.async { + self.energySaving.rescheduleRequestDidArrive() + if options.contains(.energySaving) { + if self.energySaving.isAllowedToReschedule() { + self.energySaving.willScheduleNotification() + self.scheduleWeeklyUpdate(options: options) + } + } else { + self.energySaving.willScheduleNotification() + self.scheduleWeeklyUpdate(options: options) + } + } + } + + func scheduleOnboardingNotification(options: RescheduleOptions) { + serialQueue.async { + self.scheduleOnboardingPush(options: options) + } + } + + func userDidAuthorizeWeeklyUpdate() { + SchedulingHelper.calculateAndSaveNotificationsAllowedAfterDate() + serialQueue.asyncAfter(deadline: .now() + 1.0) { + self.energySaving.willScheduleNotification() + self.scheduleWeeklyUpdate(options: []) + } + } + + func removeAllPendingNotifications() { + serialQueue.async { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + } + } + + private func scheduleOnboardingPush(options: RescheduleOptions) { + + #if DEBUG + dispatchPrecondition(condition: .onQueue(serialQueue)) + #endif + + guard Authorization.getUserWantsNotificationsEnabledForAnyCategory() else { + if options.contains(.energySaving) == false { + DDLogWarn("Notifications are not approved by user, not scheduling onboarding") + } + return + } + + let totalMetrics = getTotalMetrics() + + guard totalMetrics >= 100 else { + if options.contains(.energySaving) == false { + DDLogError("Error: asked to schedule onboarding notification when total metrics are below 100") + } + return + } + + let content = ContentMaker.makeNotificationContentForOnboarding() + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 2.0, repeats: false) + let identifier = Identifier.onboarding + let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: trigger) + + UNUserNotificationCenter.current().add(request) { (error) in + if options.contains(.energySaving) { + return + } + + if let error = error { + DDLogError("Error scheduling notification: \(error)") + } else { + DDLogInfo("Succesfully scheduled onboarding notification") + } + } + } + + private func scheduleWeeklyUpdate(options: RescheduleOptions) { + + #if DEBUG + dispatchPrecondition(condition: .onQueue(serialQueue)) + #endif + + guard Authorization.getUserWantsNotificationsEnabled(forCategory: .weeklyUpdate) else { + if options.contains(.energySaving) == false { + DDLogWarn("Notifications are not approved by user for weekly updates, not scheduling") + } + return + } + + guard let upcomingWeeklyUpdateDateComponents = SchedulingHelper.upcomingWeeklyUpdateDateComponents() else { + return + } + + let weeklyMetrics: Int + if let weekOfYear = upcomingWeeklyUpdateDateComponents.weekOfYear { + if weekOfYear != defaults.integer(forKey: kActiveWeek) { + weeklyMetrics = 0 + } else { + weeklyMetrics = getWeekMetrics() + } + } else { + weeklyMetrics = getWeekMetrics() + } + + let content = ContentMaker.makeNotificationContent(weeklyMetrics: weeklyMetrics) + let trigger = UNCalendarNotificationTrigger(dateMatching: upcomingWeeklyUpdateDateComponents, repeats: false) + let identifier = Identifier.weeklyUpdate(dateComponents: upcomingWeeklyUpdateDateComponents) + let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: trigger) + + UNUserNotificationCenter.current().add(request) { (error) in + if options.contains(.energySaving) { + return + } + + if let error = error { + DDLogError("Error scheduling notification: \(error)") + } else { + self.logScheduledNotification(request: request, triggerDate: trigger.nextTriggerDate() ?? .distantPast, weeklyMetrics: weeklyMetrics) + } + } + } + + private func logScheduledNotification(request: UNNotificationRequest, triggerDate: Date, weeklyMetrics: Int) { + DDLogInfo("Scheduled notification with id \(request.identifier) for metrics: \(weeklyMetrics), \(triggerDate)") + } +} + +extension PushNotifications { + struct Identifier: RawRepresentable { + var rawValue: String + + init(rawValue: String) { + self.rawValue = rawValue + } + + static func weeklyUpdate(dateComponents: DateComponents) -> Identifier { + guard let year = dateComponents.year, let month = dateComponents.month, let day = dateComponents.day else { + DDLogError("Wrong dateComponents: \(dateComponents)") + return Identifier(rawValue: "weekly-update-invalid") + } + + return Identifier(rawValue: "weekly-update-\(year)-\(month)-\(day)") + } + + static let onboarding = Identifier(rawValue: "onboarding") + + var isWeeklyUpdate: Bool { + return rawValue.starts(with: "weekly-update") + } + } +} + +extension PushNotifications { + enum ContentMaker { + static func makeNotificationContent(weeklyMetrics: Int) -> UNMutableNotificationContent { + if weeklyMetrics > 0 { + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("Blocked Trackers Summary", comment: "") + content.body = "\(NSLocalizedString("You blocked", comment: "Used in the sentence: You blocked 500 tracking attempts this week.")) \(weeklyMetrics) \(NSLocalizedString("tracking attempts this week. Tap to update to the newest block lists.", comment: "Used in the sentence: You blocked 500 tracking attempts this week. Tap to update to the newsst block lists."))" + content.sound = .default + return content + } else { + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("Stay Protected", comment: "") + content.body = NSLocalizedString("Tap to activate Lockdown Firewall and update to the newest block lists.", comment: "") + content.sound = .default + return content + } + } + + static func makeNotificationContentForOnboarding() -> UNMutableNotificationContent { + let content = UNMutableNotificationContent() + content.title = NSLocalizedString("You've just blocked 100 tracking attempts!", comment: "") + content.body = NSLocalizedString("Tap to see what they are.", comment: "Used in the paragraph: You've just blocked 100 tracking attempts! Tap to see what they are.") + // No sound because the user will likely be using their phone + // when they see this notification + content.sound = .none + return content + } + } +} + +extension PushNotifications { + + enum SchedulingHelper { + + static func calculateAndSaveNotificationsAllowedAfterDate() { + let now = Date() + if isFridayOrSaturday(date: now) { + let sunday = firstSunday(after: now) + DDLogInfo("User allowed notifications on Friday/Saturday, so we will start scheduling notifications only on Sunday: \(sunday ?? .distantPast)") + defaults.set(sunday, forKey: kAllowNotificationsAfterDate) + } else { + DDLogInfo("User allowed notifications on Sunday-Thursday, we will start scheduling notifications immediately") + defaults.set(now, forKey: kAllowNotificationsAfterDate) + } + } + + static func upcomingWeeklyUpdateDateComponents() -> DateComponents? { + guard let notificationsAllowedAfter = self.notificationsAllowedAfter() else { + // notifications are likely not authorized + DDLogError("No 'notifications allowed after date' is stored. It likely means that the user did not authorize the use of notifications") + return nil + } + + let now = Date() + + guard now > notificationsAllowedAfter else { + // weekly updates did not go into action yet, so not counting this + DDLogInfo("Not scheduling because notifications are not allowed yet (probably will be allowed on Sunday)") + return nil + } + + if let saturday = firstSaturday(after: now) { + return dateComponents(from: saturday) + } else { + return nil + } + } + + static private func notificationsAllowedAfter() -> Date? { + return defaults.object(forKey: kAllowNotificationsAfterDate) as? Date + } + + static private func isFridayOrSaturday(date: Date) -> Bool { + let gregorian = Calendar(identifier: .gregorian) + let weekday = gregorian.component(.weekday, from: date) + return weekday == 6 || weekday == 7 + } + + static private func firstSaturday(after date: Date) -> Date? { + let gregorian = Calendar(identifier: .gregorian) + + var saturday3Pm = DateComponents() + saturday3Pm.weekday = 7 + saturday3Pm.hour = 15 + saturday3Pm.minute = 0 + saturday3Pm.second = 0 + + guard let date = gregorian.nextDate(after: date, matching: saturday3Pm, matchingPolicy: .nextTime) else { + return nil + } + + return date + } + + static private func firstSunday(after date: Date) -> Date? { + let gregorian = Calendar(identifier: .gregorian) + + var sunday3Am = DateComponents() + sunday3Am.weekday = 1 + sunday3Am.hour = 3 + sunday3Am.minute = 0 + sunday3Am.second = 0 + + guard let date = gregorian.nextDate(after: date, matching: sunday3Am, matchingPolicy: .nextTime) else { + return nil + } + + return date + } + + static private func dateComponents(from date: Date) -> DateComponents { + return Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second, .weekOfYear], from: date) + } + } +} + +extension PushNotifications { + + final class EnergySaving { + + private var requestCounter: Int { + get { + return defaults.integer(forKey: kLockdownNotificationsEnergySavingCounter) + } + set { + defaults.set(newValue, forKey: kLockdownNotificationsEnergySavingCounter) + } + } + + func rescheduleRequestDidArrive() { + requestCounter += 1 + } + + func willScheduleNotification() { + requestCounter = 0 + } + + func isAllowedToReschedule() -> Bool { + if requestCounter >= 40 { + return true + } + return false + } + } +} diff --git a/PushNotificationsAuthorization.swift b/PushNotificationsAuthorization.swift new file mode 100644 index 0000000..155faeb --- /dev/null +++ b/PushNotificationsAuthorization.swift @@ -0,0 +1,44 @@ +// +// PushNotificationsAuthorize.swift +// Lockdown +// +// Created by Oleg Dreyman on 28.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import UIKit +import UserNotifications +import CocoaLumberjackSwift + +extension PushNotifications { + + enum Authorization { + + static let kUserAuthorizedPrefix = "LockdownNotificationsUserAuthorizedCategory" + + enum Status { + case authorized + case notAuthorized + } + + static func getUserWantsNotificationsEnabled(forCategory category: PushNotifications.Category) -> Bool { + return defaults.bool(forKey: kUserAuthorizedPrefix + category.rawValue) + } + + static func getUserWantsNotificationsEnabledForAnyCategory() -> Bool { + return getUserWantsNotificationsEnabled(forCategory: .weeklyUpdate) + } + + static func setUserWantsNotificationsEnabled(_ userWantsNotificationsEnabled: Bool, forCategory category: PushNotifications.Category) { + defaults.set(userWantsNotificationsEnabled, forKey: kUserAuthorizedPrefix + category.rawValue) + if category == .weeklyUpdate { + if userWantsNotificationsEnabled { + PushNotifications.shared.userDidAuthorizeWeeklyUpdate() + } else { + DDLogInfo("Weekly updates notifications are turned off; removing all pending notifications") + PushNotifications.shared.removeAllPendingNotifications() + } + } + } + } +} diff --git a/PushNotificationsAuthorizationUI.swift b/PushNotificationsAuthorizationUI.swift new file mode 100644 index 0000000..51649a5 --- /dev/null +++ b/PushNotificationsAuthorizationUI.swift @@ -0,0 +1,135 @@ +// +// PushNotificationsAuthorizationUI.swift +// Lockdown +// +// Created by Oleg Dreyman on 28.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import UIKit +import UserNotifications +import PopupDialog +import PromiseKit +import CocoaLumberjackSwift + +extension PushNotifications.Authorization { + + static func requestWeeklyUpdateAuthorization(presentingDialogOn vc: UIViewController) -> Promise { + + guard getUserWantsNotificationsEnabled(forCategory: .weeklyUpdate) == false else { + // if user already approved, just check the system status and don't show the dialog + DDLogWarn("Requesting authorization but it is already approved by the user, requesting with the system to ensure") + return authorizeWithSystemAfterUserApproval().then({ (systemStatus) -> Promise in + switch systemStatus { + case .deniedPreviously: + return Promise { resolver in + self.setUserWantsNotificationsEnabled(false, forCategory: .weeklyUpdate) + self.showGoToSettingsPopup(on: vc) { + resolver.fulfill(.notAuthorized) + } + } + case .success: + return Promise.value(.authorized) + case .deniedNow, .undetermined: + return Promise.value(.notAuthorized) + } + }) + } + + return Promise { resolver in + + let popup = PopupDialog( + title: NSLocalizedString("Stay Protected", comment: ""), + message: NSLocalizedString("Enable notifications to get a once-a-week summary and the latest block list updates. You can disable this anytime.", comment: ""), + image: UIImage(named: "notification_example"), + buttonAlignment: .horizontal, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: false, + panGestureDismissal: false, + hideStatusBar: false, + completion: nil + ) + + let no = CancelButton(title: NSLocalizedString("No", comment: ""), dismissOnTap: true) { + DDLogInfo("User did not allow notifications") + self.setUserWantsNotificationsEnabled(false, forCategory: .weeklyUpdate) + resolver.fulfill(.notAuthorized) + } + let yes = DefaultButton(title: NSLocalizedString("Enable", comment: ""), dismissOnTap: true) { + self.setUserWantsNotificationsEnabled(true, forCategory: .weeklyUpdate) + DDLogInfo("User allowed notifications, authorizing with the system now...") + self.authorizeWithSystemAfterUserApproval().done { systemStatus in + switch systemStatus { + case .success: + resolver.fulfill(.authorized) + case .deniedNow, .undetermined: + resolver.fulfill(.notAuthorized) + case .deniedPreviously: + self.setUserWantsNotificationsEnabled(false, forCategory: .weeklyUpdate) + self.showGoToSettingsPopup(on: vc) { + resolver.fulfill(.notAuthorized) + } + } + }.catch { error in + resolver.reject(error) + } + } + yes.buttonColor = UIColor.tunnelsBlue + yes.titleColor = UIColor.white + + popup.addButtons([no, yes]) + + vc.present(popup, animated: true, completion: nil) + } + } + + 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, + image: nil, + buttonAlignment: .vertical, + transitionStyle: .bounceDown, + preferredWidth: 270, + tapGestureDismissal: true, + panGestureDismissal: false, + hideStatusBar: false + ) { + completion() + } + + let okayButton = DefaultButton(title: NSLocalizedString("Okay", comment: ""), dismissOnTap: true, action: nil) + popup.addButton(okayButton) + + vc.present(popup, animated: true, completion: nil) + } + + enum SystemAuthenticationStatus { + case success + case deniedPreviously + case deniedNow + case undetermined + } + + static private func authorizeWithSystemAfterUserApproval() -> Promise { + return Promise { resolver in + UNUserNotificationCenter.current().getNotificationSettings { (settings) in + switch settings.authorizationStatus { + case .authorized, .notDetermined: + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (isSuccess, error) in + if let error = error { + resolver.reject(error) + } else { + resolver.fulfill(isSuccess ? .success : .deniedNow) + } + } + case .denied: + resolver.fulfill(.deniedPreviously) + default: + resolver.fulfill(.undetermined) + } + } + } + } +} diff --git a/README.md b/README.md new file mode 100755 index 0000000..833d7b3 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Lockdown Privacy (iOS) + +Lockdown is an open source firewall that blocks trackers, ads, and badware in all apps. Product details at [lockdownprivacy.com](https://lockdownprivacy.com). + +### Feature Requests + Bugs + +Create an issue on Github for feature requests and bug reports. + +### Openly Operated + +Lockdown achieves the highest level of transparency for both client and server via the Openly Operated standard. It has also been audited multiple times, the latest audit in July 2020. See the full reports here: [Audit Kits](https://openlyoperated.org/report/confirmedvpn) + +### Contributing + +Pull requests are welcome - please document any changes and potential bugs. + +### Build Instructions + +1. `pod install` + +2. `carthage update --no-use-binaries --platform iOS` or for XCode 12 `./wcarthage update --no-use-binaries --platform iOS` (workaround for [this Carthage issue](https://github.com/Carthage/Carthage/issues/3019)) + +3. Open `LockdowniOS.xcworkspace` + +To sign the app for devices, you will need an Apple Developer account. + +### Limitations to Building Locally + +If you build Lockdown locally, you will not be able to access Secure Tunnel, because that requires a Production app store receipt. We will soon enable a DEV environment for Secure Tunnel with limited capacity and regions, designed only for testing. + +To use Secure Tunnel, you must download Lockdown from the [App Store](https://lockdownprivacy.com). + +### Contact + +[team@lockdownprivacy.com](mailto:team@lockdownprivacy.com) + +### License + +This project is licensed under the GPL License - see the [LICENSE.md](LICENSE.md) file for details. + + + diff --git a/Readme.md b/Readme.md deleted file mode 100755 index 205be9a..0000000 --- a/Readme.md +++ /dev/null @@ -1,22 +0,0 @@ -# Lockdown (iOS) - -Lockdown is an Open Source and free firewall for your iOS device. We built the product to help protect users from app developers and analytics companies that are monetizing user data without consent or transparency. - - -## Feedback - -If you have any questions, concerns, or other feedback, please let us know in Github issues or by e-mail at engineering@lockdownhq.com, or contact us through https://lockdownhq.com/. - - -## Roadmap -We have many planned features in the upcoming months, so if you are interested, please download our app from the App Store. If you are interested in helping develop these features, please reach out to us at engineering@lockdownhq.com. For any feature requests or bug reports, please create an issue on our Github repository. - -## How To Build -To build the app from source, please make sure to run a 'pod install' & 'carthage update --no-use-binaries --platform ios' and open the *.xcworkspace file to build. To sign the app, you will need an Apple Developer account. - -## License - -This project is licensed under the GPL License - see the [LICENSE.md](LICENSE.md) file for details - - - diff --git a/ReviewAlertManager.swift b/ReviewAlertManager.swift new file mode 100644 index 0000000..32fb7ae --- /dev/null +++ b/ReviewAlertManager.swift @@ -0,0 +1,126 @@ +// +// ReviewAlertManager.swift +// LockdowniOS +// +// Created by George Apostu on 5/3/25. +// Copyright © 2025 Confirmed Inc. All rights reserved. +// + +import Foundation +import StoreKit + +class ReviewAlertManager { + // UserDefaults keys + private enum Keys { + static let alertCount = "ReviewAlertCount" + static let lastAlertDate = "LastReviewAlertDate" + static let firstAlertDate = "FirstReviewAlertDate" + } + + private let userDefaults: UserDefaults + + // Initialize with optional custom UserDefaults for testing + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + // Check if we should show the review alert + func shouldShowReviewAlert() -> Bool { + let alertCount = userDefaults.integer(forKey: Keys.alertCount) + let lastAlertDate = userDefaults.object(forKey: Keys.lastAlertDate) as? Date + let firstAlertDate = userDefaults.object(forKey: Keys.firstAlertDate) as? Date + + // Check 365-day limit from first alert + if let firstDate = firstAlertDate, + Date().timeIntervalSince(firstDate) >= 365 * 24 * 60 * 60 { + return true // Allow reset after 1 year + } + + // Maximum 3 alerts per year +// if alertCount >= 3 { +// return false +// } + + // First alert (onboarding) + if alertCount == 0 { + return true + } + + guard let lastDate = lastAlertDate else { + return false + } + + let timeSinceLastAlert = Date().timeIntervalSince(lastDate) + let hoursSinceLastAlert = timeSinceLastAlert / 3600 + let daysSinceLastAlert = timeSinceLastAlert / (24 * 3600) + + switch alertCount { + case 1: // Alert #2 - 3 hours after #1 + return hoursSinceLastAlert >= 3 + + case 2: // Alert #3 - 2 days after #2 + return daysSinceLastAlert >= 2 + + default: + // For any alert after the yearly reset + return daysSinceLastAlert >= 2 + } + } + + // Show review alert and update tracking data + func showReviewAlert(delay: TimeInterval = 0.1) { + guard shouldShowReviewAlert() else { return } + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + guard let self = self else { return } + guard shouldShowReviewAlert() else { return } + guard let topController = UIApplication.getTopMostViewController(), !topController.description.localizedCaseInsensitiveContains("paywall") else { return } + + var alertCount = userDefaults.integer(forKey: Keys.alertCount) + let now = Date() + + // Handle yearly reset + if let firstDate = userDefaults.object(forKey: Keys.firstAlertDate) as? Date, + now.timeIntervalSince(firstDate) >= 365 * 24 * 60 * 60 { + alertCount = 0 + userDefaults.removeObject(forKey: Keys.firstAlertDate) + } + + // Show the StoreKit review prompt + if let windowScene = UIApplication.shared.windows.first?.windowScene { + SKStoreReviewController.requestReview(in: windowScene) + } + + // Update tracking data + alertCount += 1 + userDefaults.set(alertCount, forKey: Keys.alertCount) + userDefaults.set(now, forKey: Keys.lastAlertDate) + + if alertCount == 1 { + userDefaults.set(now, forKey: Keys.firstAlertDate) + } + } + } + + // For testing purposes - reset all data + func reset() { + userDefaults.removeObject(forKey: Keys.alertCount) + userDefaults.removeObject(forKey: Keys.lastAlertDate) + userDefaults.removeObject(forKey: Keys.firstAlertDate) + } +} + +// Usage example: +extension ReviewAlertManager { + static let shared = ReviewAlertManager() + + // Call this during onboarding + static func showOnboardingAlert() { + shared.showReviewAlert(delay: 0.1) + } + + // Call this when app opens + static func checkAndShowAlert() { + shared.showReviewAlert(delay: 5.0) + } +} diff --git a/Settings.bundle/Pods-Lockdown Firewall Widget-settings-metadata.plist b/Settings.bundle/Pods-Lockdown Firewall Widget-settings-metadata.plist new file mode 100644 index 0000000..d04b9f4 --- /dev/null +++ b/Settings.bundle/Pods-Lockdown Firewall Widget-settings-metadata.plist @@ -0,0 +1,102 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + 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) 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/Settings.bundle/Pods-Lockdown VPN Widget-settings-metadata.plist b/Settings.bundle/Pods-Lockdown VPN Widget-settings-metadata.plist new file mode 100644 index 0000000..d04b9f4 --- /dev/null +++ b/Settings.bundle/Pods-Lockdown VPN Widget-settings-metadata.plist @@ -0,0 +1,102 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + 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) 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/Settings.bundle/Pods-Lockdown-settings-metadata.plist b/Settings.bundle/Pods-Lockdown-settings-metadata.plist index e280213..719f168 100644 --- a/Settings.bundle/Pods-Lockdown-settings-metadata.plist +++ b/Settings.bundle/Pods-Lockdown-settings-metadata.plist @@ -12,31 +12,12 @@ Type PSGroupSpecifier - - FooterText - Copyright (c) 2016, Zhuhao Wang All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - 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. - - Neither the name of NEKit nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - - 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 - NEKit - Type - PSGroupSpecifier - FooterText Copyright (c) 2017 aleksandrshoshiashvili <aleksandr.shoshiashvili@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 +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 @@ -45,7 +26,7 @@ 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 +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 @@ -60,23 +41,31 @@ THE SOFTWARE. FooterText - BSD 3-Clause License - -Copyright (c) 2010-2019, Deusty, LLC -All rights reserved. + The MIT License (MIT) -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +Copyright (c) 2015 Kyohei Ito -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +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: -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. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -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. +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. -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 + DynamicBlurView Type PSGroupSpecifier @@ -84,10 +73,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS& FooterText The MIT License (MIT) -Copyright (c) 2015 Kyohei Ito +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 +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 @@ -96,7 +85,7 @@ 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 +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 @@ -106,37 +95,34 @@ SOFTWARE. Title - DynamicBlurView + KeychainAccess Type PSGroupSpecifier FooterText - The MIT License (MIT) - -Copyright (c) 2014 kishikawa katsumi + Copyright (c) 2018 Nicolas Richard <nicorichard@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 +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 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 +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. - +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. Title - KeychainAccess + NicoProgress Type PSGroupSpecifier @@ -146,7 +132,7 @@ SOFTWARE. Author - Martin Wildfeuer (http://www.mwfire.de) Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal +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 @@ -155,7 +141,7 @@ 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 +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 @@ -174,7 +160,7 @@ THE SOFTWARE. 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 +"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 @@ -183,7 +169,7 @@ 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 +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 @@ -203,7 +189,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Copyright (c) 2014 gk (gk@reteq.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 +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 @@ -212,7 +198,7 @@ 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 +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 @@ -226,28 +212,31 @@ SOFTWARE. FooterText - Copyright (c) 2016 Ashley Mills + The MIT License (MIT) + +Copyright (c) 2014 Naoto Kaneko. +Copyright (c) 2019 SwiftCSV Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal +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 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 +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 +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. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. Title - ReachabilitySwift + SwiftCSV Type PSGroupSpecifier @@ -256,11 +245,11 @@ THE SOFTWARE. Copyright (c) 2016 SwiftKick Mobile LLC -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: +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. +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 SwiftMessages Type @@ -270,11 +259,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRES 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: +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. +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 @@ -283,7 +272,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRES FooterText - Generated by CocoaPods - http://cocoapods.org + Generated by CocoaPods - https://cocoapods.org Title Type diff --git a/Settings.bundle/Pods-LockdownTests-settings-metadata.plist b/Settings.bundle/Pods-LockdownTests-settings-metadata.plist new file mode 100644 index 0000000..ea19b60 --- /dev/null +++ b/Settings.bundle/Pods-LockdownTests-settings-metadata.plist @@ -0,0 +1,58 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + MIT License + +Copyright (c) 2019 Point-Free, 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 + SnapshotTesting + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Settings.bundle/Pods-LockdownTunnel-settings-metadata.plist b/Settings.bundle/Pods-LockdownTunnel-settings-metadata.plist index 39adc5e..d04b9f4 100644 --- a/Settings.bundle/Pods-LockdownTunnel-settings-metadata.plist +++ b/Settings.bundle/Pods-LockdownTunnel-settings-metadata.plist @@ -12,28 +12,6 @@ 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) @@ -92,33 +70,6 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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 @@ -136,7 +87,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI FooterText - Generated by CocoaPods - http://cocoapods.org + Generated by CocoaPods - https://cocoapods.org Title Type diff --git a/Settings.bundle/en.lproj/Root.strings b/Settings.bundle/en.lproj/Root.strings index 31bcb37..8b13789 100644 Binary files a/Settings.bundle/en.lproj/Root.strings and b/Settings.bundle/en.lproj/Root.strings differ diff --git a/Shared.swift b/Shared.swift index b496abe..961400c 100644 --- a/Shared.swift +++ b/Shared.swift @@ -10,7 +10,6 @@ import Foundation import CocoaLumberjackSwift import KeychainAccess -import Reachability let isTestFlight = Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" @@ -23,9 +22,25 @@ var appInstallDate: Date? { return nil } -let reachability = Reachability() +@discardableResult +func appHasJustBeenUpgradedOrIsNewInstall() -> Bool { + let currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + let versionOfLastRun = UserDefaults.standard.object(forKey: kVersionOfLastRun) as? String + DDLogInfo("APP UPGRADED CHECK: LAST RUN \(versionOfLastRun ?? "n/a") | CURRENT \(currentVersion ?? "n/a")") + UserDefaults.standard.set(currentVersion, forKey: kVersionOfLastRun) + if (versionOfLastRun == nil || versionOfLastRun != currentVersion) { + // either first time this check has occurred, or app was updated since last run + DDLogInfo("APP UPGRADED CHECK: TRUE - LAST RUN \(versionOfLastRun ?? "n/a") | CURRENT \(currentVersion ?? "n/a")") + return true + } else { + // nothing changed + DDLogInfo("APP UPGRADED CHECK: FALSE") + return false + } +} + +let reachability = Availability() -let defaults = UserDefaults(suiteName: "group.com.confirmed")! let keychain = Keychain(service: "com.confirmed.tunnels").synchronizable(true) // MARK: - VPN Credentials @@ -90,148 +105,74 @@ func getVPNCredentials() -> VPNCredentials? { return VPNCredentials(id: id!, keyBase64: keyBase64!) } +// MARK: - API Credentials -// MARK: - User wants Firewall/VPN Enabled - -let kUserWantsFirewallEnabled = "user_wants_firewall_enabled" -let kUserWantsVPNEnabled = "user_wants_vpn_enabled" +let kAPICredentialsEmail = "APICredentialsEmail" +let kAPICredentialsPassword = "APICredentialsPassword" -func setUserWantsFirewallEnabled(_ enabled: Bool) { - defaults.set(enabled, forKey: kUserWantsFirewallEnabled) +struct APICredentials { + var email: String = "" + var password: String = "" } -func getUserWantsFirewallEnabled() -> Bool { - return defaults.bool(forKey: kUserWantsFirewallEnabled) -} - -func setUserWantsVPNEnabled(_ enabled: Bool) { - defaults.set(enabled, forKey: kUserWantsVPNEnabled) -} - -func getUserWantsVPNEnabled() -> Bool { - return defaults.bool(forKey: kUserWantsVPNEnabled) +func setAPICredentials(email: String, password: String) throws { + DDLogInfo("Setting API Credentials with email: \(email)") + if (email == "") { + throw "Email was blank" + } + if (password == "") { + throw "Password was blank" + } + do { + try keychain.set(email, key: kAPICredentialsEmail) + try keychain.set(password, key: kAPICredentialsPassword) + } + catch { + throw "Unable to set API credentials on keychain" + } } - -// MARK: - VPN Region - -let kSavedVPNRegionServerPrefix = "vpn_region_server_prefix" - -struct VPNRegion { - var regionDisplayName: String = "" - var regionDisplayNameShort: String = "" - var regionFlagEmoji: String = "" - var serverPrefix: String = "" +func clearAPICredentials() { + try? keychain.remove(kAPICredentialsEmail) + try? keychain.remove(kAPICredentialsPassword) } -let vpnRegions:[VPNRegion] = [ - VPNRegion(regionDisplayName: "United States - West", - regionDisplayNameShort: "USA West", - regionFlagEmoji: "🇺🇸", - serverPrefix: "us-west"), - VPNRegion(regionDisplayName: "United States - East", - regionDisplayNameShort: "USA East", - regionFlagEmoji: "🇺🇸", - serverPrefix: "us-east"), - VPNRegion(regionDisplayName: "United Kingdom", - regionDisplayNameShort: "United Kingdom", - regionFlagEmoji: "🇬🇧", - serverPrefix: "eu-london"), - VPNRegion(regionDisplayName: "Ireland", - regionDisplayNameShort: "Ireland", - regionFlagEmoji: "🇮🇪", - serverPrefix: "eu-ireland"), - VPNRegion(regionDisplayName: "Germany", - regionDisplayNameShort: "Germany", - regionFlagEmoji: "🇩🇪", - serverPrefix: "eu-frankfurt"), - VPNRegion(regionDisplayName: "Canada", - regionDisplayNameShort: "Canada", - regionFlagEmoji: "🇨🇦", - serverPrefix: "canada"), - VPNRegion(regionDisplayName: "Japan", - regionDisplayNameShort: "Japan", - regionFlagEmoji: "🇯🇵", - serverPrefix: "ap-tokyo"), - VPNRegion(regionDisplayName: "Australia", - regionDisplayNameShort: "Australia", - regionFlagEmoji: "🇦🇺", - serverPrefix: "ap-sydney"), - VPNRegion(regionDisplayName: "South Korea", - regionDisplayNameShort: "South Korea", - regionFlagEmoji: "🇰🇷", - serverPrefix: "ap-seoul"), - VPNRegion(regionDisplayName: "Singapore", - regionDisplayNameShort: "Singapore", - regionFlagEmoji: "🇸🇬", - serverPrefix: "ap-singapore"), - VPNRegion(regionDisplayName: "Brazil", - regionDisplayNameShort: "Brazil", - regionFlagEmoji: "🇧🇷", - serverPrefix: "sa") -] - -func getVPNRegionForServerPrefix(serverPrefix: String) -> VPNRegion { - DDLogError("Getting VPN region for server prefix: \(serverPrefix)") - for vpnRegion in vpnRegions { - if vpnRegion.serverPrefix == serverPrefix { - return vpnRegion +func getAPICredentials() -> APICredentials? { + print("Getting stored API credentials") + var email: String? = nil + do { + email = try keychain.get(kAPICredentialsEmail) + if email == nil { + print("No stored API credential email") + return nil } } - DDLogError("Could not find VPN region for server prefix: \(serverPrefix)") - return vpnRegions[0] -} - -func getSavedVPNRegion() -> VPNRegion { - DDLogInfo("getSavedVPNRegion") - if let savedVPNRegionServerPrefix = defaults.string(forKey: kSavedVPNRegionServerPrefix) { - return getVPNRegionForServerPrefix(serverPrefix: savedVPNRegionServerPrefix) + catch { + print("Error getting stored API credentials email: \(error)") + return nil } - - // get default savedRegion by locale - let locale = NSLocale.autoupdatingCurrent - if let regionCode = locale.regionCode { - switch regionCode { - case "US": - if let timezone = TimeZone.autoupdatingCurrent.abbreviation() { - if timezone == "EST" || timezone == "EDT" || timezone == "CST" { - return getVPNRegionForServerPrefix(serverPrefix: "us-east") - } - } - else { - return getVPNRegionForServerPrefix(serverPrefix: "us-west") - } - case "GB": - return getVPNRegionForServerPrefix(serverPrefix: "eu-london") - case "IE": - return getVPNRegionForServerPrefix(serverPrefix: "eu-london") - case "CA": - return getVPNRegionForServerPrefix(serverPrefix: "canada") - case "KO": - return getVPNRegionForServerPrefix(serverPrefix: "ap-seoul") - case "ID", "SG", "MY", "PH", "TH", "TW", "VN": - return getVPNRegionForServerPrefix(serverPrefix: "ap-singapore") - case "DE", "FR", "IT", "PT", "ES", "AT", "PL", "RU", "UA", "NG", "TR", "ZA": - return getVPNRegionForServerPrefix(serverPrefix: "eu-frankfurt") - case "AU", "NZ": - return getVPNRegionForServerPrefix(serverPrefix: "ap-sydney") - case "AE", "IN", "PK", "BD", "QA", "SA": - return getVPNRegionForServerPrefix(serverPrefix: "ap-mumbai") - case "EG": - return getVPNRegionForServerPrefix(serverPrefix: "eu-frankfurt") - case "JP": - return getVPNRegionForServerPrefix(serverPrefix: "ap-tokyo") - case "BR", "CO", "VE", "AR": - return getVPNRegionForServerPrefix(serverPrefix: "sa") - default: - return vpnRegions[0] + var password: String? = nil + do { + password = try keychain.get(kAPICredentialsPassword) + if password == nil { + print("No stored API credential password") + return nil } } - return vpnRegions[0] + catch { + print("Error getting stored API credentials password: \(error)") + return nil + } + print("Returning stored API credentials with email: \(email!)") + return APICredentials(email: email!, password: password!) +} + +func getAPICredentialsConfirmed() -> Bool { + return defaults.bool(forKey: kAPICredentialsConfirmed) } -func setSavedVPNRegion(vpnRegion: VPNRegion) { - defaults.set(vpnRegion.serverPrefix, forKey: kSavedVPNRegionServerPrefix) +func setAPICredentialsConfirmed(confirmed: Bool) { + defaults.set(confirmed, forKey: kAPICredentialsConfirmed) } // MARK: - Extensions @@ -244,6 +185,7 @@ extension String: Error { // Error makes it easy to throw errors as one-liners } return nil } + func base64Decoded() -> String? { if let data = Data(base64Encoded: self) { return String(data: data, encoding: .utf8) @@ -251,15 +193,31 @@ extension String: Error { // Error makes it easy to throw errors as one-liners return nil } - func localized(bundle: Bundle = .main, tableName: String = "Localizable") -> String { - //return NSLocalizedString(self, tableName: tableName, value: "***\(self)***", comment: "") // used for debug missing strings - return NSLocalizedString(self, tableName: tableName, value: "\(self)", comment: "") - } - } extension UIColor { - static let tunnelsBlue = UIColor.init(red: 0/255.0, green: 173/255.0, blue: 231/255.0, alpha: 1.0) + static let confirmedBlue = UIColor(named: "Confirmed Blue") ?? UIColor.tunnelsBlue + + static let tunnelsBlue = UIColor(red: 0/255.0, green: 173/255.0, blue: 231/255.0, alpha: 1.0) + static let tunnelsWarning = UIColor(red: 231/255.0, green: 76/255.0, blue: 68/255.0, alpha: 1.0) + static let tunnelsDarkBlue = UIColor(red: 0/255.0, green: 117/255.0, blue: 157/255.0, alpha: 1.0) + static let tunnelsLightBlue = UIColor(red: 223/255.0, green: 243/255.0, blue: 251/255.0, alpha: 1.0) + static let paywallOrange = UIColor(red: 255/255, green: 171/255, blue: 0/255, alpha: 1) + static let paywallNew = UIColor(red: 0.225, green: 0.219, blue: 0.6, alpha: 1.0) + static let borderGray = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) + static let borderBlue = UIColor(red: 0, green: 0.678, blue: 0.906, alpha: 1) + static let smallGrey = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1) + static var purplePaywall = UIColor(red: 134/255.0, green: 26/255.0, blue: 201/255.0, alpha: 1) + static var purplePaywall2 = UIColor(red: 103/255.0, green: 26/255.0, blue: 201/255.0, alpha: 1) + static var extraLightGray = UIColor(red: 242/255.0, green: 244/255.0, blue: 245/255.0, alpha: 1) + static var gradientPink1 = UIColor(red: 0.788, green: 0.102, blue: 0.788, alpha: 1) + static var gradientPink2 = UIColor(red: 0.405, green: 0.103, blue: 0.789, alpha: 1) + static let tunnelsBlueTest = UIColor(red: 0/255.0, green: 173/255.0, blue: 231/255.0, alpha: 0.0) + static let lockdownRed = UIColor(red: 214.0/255.0, green: 87.0/255.0, blue: 75.0/255.0, alpha: 1.0) + static let panelSecondaryBackground = UIColor(named: "Panel Secondary Background") + static let tableCellBackground = UIColor(named: "tableCellBackground") + static let tableCellSelectedBackground = UIColor(named: "tableCellSelectedBackground") + static let disabledGray = UIColor(red: 0.3, green: 0.3, blue: 0.3, alpha: 1) } extension UnicodeScalar { @@ -333,3 +291,31 @@ struct Config { } } } + +// MARK: - Fonts +let fontRegular12 = UIFont(name: "Montserrat-Regular", size: 12)! +let fontRegular14 = UIFont(name: "Montserrat-Regular", size: 14)! +let fontRegular15 = UIFont(name: "Montserrat-Regular", size: 15)! +let fontRegular17 = UIFont(name: "Montserrat-Regular", size: 17)! +let fontMedium14 = UIFont(name: "Montserrat-Medium", size: 14)! +let fontMedium11 = UIFont(name: "Montserrat-Medium", size: 11)! +let fontMedium13 = UIFont(name: "Montserrat-Medium", size: 13)! +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 fontSemiBold13 = UIFont(name: "Montserrat-SemiBold", size: 13)! +let fontSemiBold15 = UIFont(name: "Montserrat-SemiBold", size: 15)! +let fontSemiBold15_5 = UIFont(name: "Montserrat-SemiBold", size: 15.5)! +let fontSemiBold17 = UIFont(name: "Montserrat-SemiBold", size: 17)! +let fontSemiBold22 = UIFont(name: "Montserrat-SemiBold", size: 22)! +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 fontBold17 = UIFont(name: "Montserrat-Bold", size: 17)! +let fontBold18 = UIFont(name: "Montserrat-Bold", size: 18)! +let fontBold22 = UIFont(name: "Montserrat-Bold", size: 22)! +let fontBold24 = UIFont(name: "Montserrat-Bold", size: 24)! +let fontBold26 = UIFont(name: "Montserrat-Bold", size: 26)! +let fontBold28 = UIFont(name: "Montserrat-Bold", size: 28)! +let fontBold34 = UIFont(name: "Montserrat-Bold", size: 34)! diff --git a/SpeedTest.swift b/SpeedTest.swift index 419c785..5099203 100644 --- a/SpeedTest.swift +++ b/SpeedTest.swift @@ -10,7 +10,6 @@ import Foundation import SystemConfiguration import CoreTelephony -import Reachability import CocoaLumberjackSwift import PromiseKit @@ -70,5 +69,4 @@ public class SpeedTest: NSObject, URLSessionDelegate, URLSessionDataDelegate { throw "Invalid URL string: \(urlString)" } } - } diff --git a/Tests/LockdownTests/DomainNameValidatorTests.swift b/Tests/LockdownTests/DomainNameValidatorTests.swift new file mode 100644 index 0000000..f81340d --- /dev/null +++ b/Tests/LockdownTests/DomainNameValidatorTests.swift @@ -0,0 +1,133 @@ +// +// DomainNameValidatorTests.swift +// LockdownTests +// +// Created by Oleg Dreyman on 19.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import XCTest +@testable import Lockdown + +class DomainNameValidatorTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + private func isDomainNameValid(_ domainName: String) -> Bool { + let result = DomainNameValidator.validate(domainName) + print(domainName, result) + switch result { + case .valid: + return true + case .notValid: + return false + } + } + + func testValidatesValidDomains() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + + let validDomainNames = [ + "google.com", + "main.apple.uk", + "fieo.fwjo.qqq", + "8934jjaq.com", + "jfie---328943.com", + "g.com", + "fb.ru", + // this is punycode for "правительство.рф", existing cyrillic domain name + "xn--80aealotwbjpid2k.xn--p1ai", + ] + + for validDomain in validDomainNames { + XCTAssertTrue(isDomainNameValid(validDomain)) + } + } + + func testNotValidatesEmptyString() { + XCTAssertFalse(isDomainNameValid("")) + XCTAssertFalse(isDomainNameValid(".")) + XCTAssertFalse(isDomainNameValid("..")) + XCTAssertFalse(isDomainNameValid("...")) + XCTAssertFalse(isDomainNameValid(".a")) + XCTAssertFalse(isDomainNameValid("a..a")) + XCTAssertFalse(isDomainNameValid(".a")) + } + + func testNotValidatesOneLabelString() { + let invalidDomainNames = [ + "google", + "apple", + "iphone eleven", + "80aealotwbjpid2k", + "4242fwfw3", + "lockdown app com", + ] + + for invalidName in invalidDomainNames { + XCTAssertFalse(isDomainNameValid(invalidName)) + } + } + + func testNotValidatesWildcardString() { + let invalidDomainNames = [ + "api.*.com", + "*.", + ".*", + "**", + "*cloud.com", + ] + + for invalidName in invalidDomainNames { + XCTAssertFalse(isDomainNameValid(invalidName)) + } + } + + func testValidatesWildcardAsFirstLabel() { + let validDomainNames = [ + "*.apple.com", + "*.uk", + "*.paris.fr", + ] + + for validName in validDomainNames { + XCTAssertTrue(isDomainNameValid(validName)) + } + } + + func testNotValidatesHttpsString() { + let invalidDomainNames = [ + "https://google.com", + "https://apple.com", + "http://facebook.uk", + "https://xn--80aealotwbjpid2k.xn--p1ai", + "ssh://main.fr", + ] + + for invalidName in invalidDomainNames { + XCTAssertFalse(isDomainNameValid(invalidName)) + } + } + + func testNotValidatesInvalidCharacters() { + // These are not officially supported by the URL system. + // To use these, one need to convert them to punycode, for example: + // "человек.рф" -> "xn--b1afbucs4d.xn--p1ai" + let invalidDomainNames = [ + "человек.рф", + "бассейн.уа", + "méxico.com", + ] + + for invalidName in invalidDomainNames { + XCTAssertFalse(isDomainNameValid(invalidName)) + } + } +} diff --git a/Tests/LockdowniOSTests/Info.plist b/Tests/LockdownTests/Info.plist similarity index 84% rename from Tests/LockdowniOSTests/Info.plist rename to Tests/LockdownTests/Info.plist index 6c6c23c..56cf7e2 100644 --- a/Tests/LockdowniOSTests/Info.plist +++ b/Tests/LockdownTests/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,9 +13,9 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - BNDL + $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0 + 1.0.1 CFBundleVersion 1 diff --git a/Tests/LockdowniOSTests/ConfirmediOSTests.swift b/Tests/LockdownTests/LockdownTests.swift similarity index 54% rename from Tests/LockdowniOSTests/ConfirmediOSTests.swift rename to Tests/LockdownTests/LockdownTests.swift index 66d4eb4..b98bb75 100644 --- a/Tests/LockdowniOSTests/ConfirmediOSTests.swift +++ b/Tests/LockdownTests/LockdownTests.swift @@ -1,34 +1,25 @@ // -// ConfirmediOSTests.swift -// ConfirmediOSTests +// LockdownTests.swift +// LockdownTests // -// Copyright © 2019 Confirmed Inc. All rights reserved. +// Created by Oleg Dreyman on 19.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. // import XCTest -class ConfirmediOSTests: XCTestCase { - +class LockdownTests: XCTestCase { + override func setUp() { - super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. } - + override func tearDown() { // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() } - + func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - } diff --git a/Tests/LockdownTests/SnapshotTests.swift b/Tests/LockdownTests/SnapshotTests.swift new file mode 100644 index 0000000..e3518b9 --- /dev/null +++ b/Tests/LockdownTests/SnapshotTests.swift @@ -0,0 +1,166 @@ +// +// SnapshotTests.swift +// LockdownTests +// +// Created by Oleg Dreyman on 27.05.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import XCTest +@testable import SnapshotTesting +@testable import Lockdown + +// SIMULATOR: iPhone SE, 1st Generation +// OS: iOS 13.3 + +class SnapshotTests: XCTestCase { + + func testEmailSignupVC() { + let emailSignUpVC = make(EmailSignUpViewController.self, storyboardIdentifier: "emailSignUpViewController") + lockdownSnapshotTest(emailSignUpVC) + } + + func testEmailSignInVC() { + let emailSignInVC = make(EmailSignInViewController.self, storyboardIdentifier: "emailSignInViewController") + lockdownSnapshotTest(emailSignInVC) + } + + func testWhatIsVpnVC() { + // We are not using `lockdownSnapshotTest` here because WhatIsVpnViewController + // has changing logic based on raw screen size, and by defaults that's not something + // that snapshot testing library is designed to test. So we're "emulating" that + // two different modes (iPhone SE and not iPhone SE) by toggling `is4InchIphone` + // on and off. + + do { + let whatIsVpnVC = make(WhatIsVpnViewController.self, storyboardIdentifier: "whatIsVpnViewController") + whatIsVpnVC.is4InchIphone = true + assertSnapshot(matching: whatIsVpnVC, as: .image(on: .iPhoneSe, userInterfaceStyle: .light)) + } + + do { + let whatIsVpnVC = make(WhatIsVpnViewController.self, storyboardIdentifier: "whatIsVpnViewController") + whatIsVpnVC.is4InchIphone = false + assertSnapshot(matching: whatIsVpnVC, as: .image(on: .iPhone8, userInterfaceStyle: .light)) + } + + do { + let whatIsVpnVC = make(WhatIsVpnViewController.self, storyboardIdentifier: "whatIsVpnViewController") + whatIsVpnVC.is4InchIphone = false + assertSnapshot(matching: whatIsVpnVC, as: .image(on: .iPhoneXsMax, userInterfaceStyle: .light)) + } + + do { + let whatIsVpnVC = make(WhatIsVpnViewController.self, storyboardIdentifier: "whatIsVpnViewController") + whatIsVpnVC.is4InchIphone = true + assertSnapshot(matching: whatIsVpnVC, as: .image(on: .iPhoneSe, userInterfaceStyle: .dark)) + } + + do { + let whatIsVpnVC = make(WhatIsVpnViewController.self, storyboardIdentifier: "whatIsVpnViewController") + whatIsVpnVC.is4InchIphone = false + assertSnapshot(matching: whatIsVpnVC, as: .image(on: .iPhone8, userInterfaceStyle: .dark)) + } + + do { + let whatIsVpnVC = make(WhatIsVpnViewController.self, storyboardIdentifier: "whatIsVpnViewController") + whatIsVpnVC.is4InchIphone = false + assertSnapshot(matching: whatIsVpnVC, as: .image(on: .iPhoneXsMax, userInterfaceStyle: .dark)) + } + + } + + func testHomeVC() { + let homeVC = make(HomeViewController.self, storyboardIdentifier: "homeViewController") + lockdownHighQualitySnapshotTest(homeVC) + } + + func testFirewallPrivacyPolicyVC() { + let privacyPolicyVC = make(PrivacyPolicyViewController.self, storyboardIdentifier: "firewallPrivacyPolicyViewController") + privacyPolicyVC.parentVC = nil + privacyPolicyVC.privacyPolicyKey = kHasAgreedToFirewallPrivacyPolicy + lockdownSnapshotTest(privacyPolicyVC) + } + + func testTitleVC() { + let titleVC = make(TitleViewController.self, storyboardIdentifier: "titleViewController") + titleVC.isAnimatingOnAppear = false + lockdownSnapshotTest(titleVC) + } + + func testLogVC() { + BlockDayLog.shared.clear() + + let date = Calendar.current.date(bySettingHour: 9, minute: 41, second: 10, of: Date())! + + BlockDayLog.shared.append(host: "snapshot-test.com", date: date) + BlockDayLog.shared.append(host: "lockdown-test.com", date: date) + let logVC = make(BlockLogViewController.self, storyboardIdentifier: "blockLogViewController") + lockdownSnapshotTest(logVC) + BlockDayLog.shared.clear() + } +} + +extension SnapshotTests { + private func make(_ vc: ViewController.Type, storyboardIdentifier: String) -> ViewController { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let viewController = storyboard.instantiateViewController(withIdentifier: storyboardIdentifier) as! ViewController + return viewController + } + + private func lockdownHighQualitySnapshotTest( + _ viewController: UIViewController, + record: Bool = false, + file: StaticString = #file, + testName: String = #function, + line: UInt = #line + ) { + + guard UIDevice.is4InchIphone else { + XCTFail("These snapshot tests are designed to run on iPhone SE 1st Gen, iOS 13.3 simulator", file: file, line: line) + return + } + + assertSnapshot(matching: viewController, as: .keyWindowImage(on: .iPhoneSe, userInterfaceStyle: .light), record: record, file: file, testName: testName, line: line) + assertSnapshot(matching: viewController, as: .keyWindowImage(on: .iPhone8, userInterfaceStyle: .light), record: record, file: file, testName: testName, line: line) + assertSnapshot(matching: viewController, as: .keyWindowImage(on: .iPhoneXsMax, userInterfaceStyle: .light), record: record, file: file, testName: testName, line: line) + + assertSnapshot(matching: viewController, as: .keyWindowImage(on: .iPhoneSe, userInterfaceStyle: .dark), record: record, file: file, testName: testName, line: line) + assertSnapshot(matching: viewController, as: .keyWindowImage(on: .iPhone8, userInterfaceStyle: .dark), record: record, file: file, testName: testName, line: line) + assertSnapshot(matching: viewController, as: .keyWindowImage(on: .iPhoneXsMax, userInterfaceStyle: .dark), record: record, file: file, testName: testName, line: line) + } + + private func lockdownSnapshotTest( + _ viewController: UIViewController, + record: Bool = false, + file: StaticString = #file, + testName: String = #function, + line: UInt = #line + ) { + + guard UIDevice.is4InchIphone else { + XCTFail("These snapshot tests are designed to run on iPhone SE 1st Gen, iOS 13.3 simulator", file: file, line: line) + return + } + + assertSnapshot(matching: viewController, as: .image(on: .iPhoneSe, userInterfaceStyle: .light), record: record, file: file, testName: testName, line: line) + assertSnapshot(matching: viewController, as: .image(on: .iPhone8, userInterfaceStyle: .light), record: record, file: file, testName: testName, line: line) + assertSnapshot(matching: viewController, as: .image(on: .iPhoneXsMax, userInterfaceStyle: .light), record: record, file: file, testName: testName, line: line) + + assertSnapshot(matching: viewController, as: .image(on: .iPhoneSe, userInterfaceStyle: .dark), record: record, file: file, testName: testName, line: line) + assertSnapshot(matching: viewController, as: .image(on: .iPhone8, userInterfaceStyle: .dark), record: record, file: file, testName: testName, line: line) + assertSnapshot(matching: viewController, as: .image(on: .iPhoneXsMax, userInterfaceStyle: .dark), record: record, file: file, testName: testName, line: line) + } +} + +extension Snapshotting where Value == UIViewController, Format == UIImage { + static func image(on device: ViewImageConfig, userInterfaceStyle: UIUserInterfaceStyle) -> Snapshotting { + return image(on: device, traits: .init(userInterfaceStyle: userInterfaceStyle)) + } + + static func keyWindowImage(on device: ViewImageConfig, userInterfaceStyle: UIUserInterfaceStyle) -> Snapshotting { + let traits = UITraitCollection(traitsFrom: [device.traits, .init(userInterfaceStyle: userInterfaceStyle)]) + + return image(drawHierarchyInKeyWindow: true, precision: 0.995, size: device.size, traits: traits) + } +} diff --git a/Tests/LockdownTests/TrackerInfoTests.swift b/Tests/LockdownTests/TrackerInfoTests.swift new file mode 100644 index 0000000..07c7e35 --- /dev/null +++ b/Tests/LockdownTests/TrackerInfoTests.swift @@ -0,0 +1,57 @@ +// +// TrackerInfoTests.swift +// LockdownTests +// +// Created by Oleg Dreyman on 03.06.2020. +// Copyright © 2020 Confirmed Inc. All rights reserved. +// + +import XCTest +@testable import Lockdown + +class TrackerInfoTests: XCTestCase { + + private func loadFile() throws -> TrackerInfo { + guard let url = Bundle.main.url(forResource: "tracker_info", withExtension: "json") else { + throw "Test: no file on disk" + } + + let content = try Data(contentsOf: url) + let info = try JSONDecoder().decode(TrackerInfo.self, from: content) + return info + } + + func testTrackerInfoJSONFileValid() throws { + let info = try loadFile() + print(info) + } + + func testTrackerInfoValidateContents() throws { + let info = try loadFile() + + var process = "" + process += "------- TRACKER-INFO.JSON VALIDATION START -------" + process += "\n - Validating trackerIds" + for (domain, trackerId) in info.test_trackerIds { + if info.test_descriptions.keys.contains(trackerId) { + process += "\n ✅ Descriptions exists for tracker ID: \(trackerId) (domain: \(domain))" + } else { + process += "\n ❌ Description missing for tracker ID: \(trackerId) (domain: \(domain))" + XCTFail("Description missing for tracker ID: \(trackerId) (domain: \(domain))") + } + } + process += "\n\n - Validating descriptions used" + for descKey in info.test_descriptions.keys { + let usedDomains = info.test_trackerIds.filter({ $0.value == descKey }).keys + if usedDomains.isEmpty { + process += "\n ❌ No domain defines tracker ID: \(descKey)" + XCTFail("No domains with tracker ID: \(descKey)") + } else { + process += "\n ✅ Domains for tracker ID \"\(descKey)\" are: \(usedDomains.joined(separator: ", "))" + } + } + process += "\n-------- TRACKER-INFO.JSON VALIDATION END --------" + print(process) + } + +} diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.1.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.1.png new file mode 100644 index 0000000..2698828 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.1.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.2.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.2.png new file mode 100644 index 0000000..a5604f6 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.2.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.3.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.3.png new file mode 100644 index 0000000..ac13161 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.3.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.4.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.4.png new file mode 100644 index 0000000..90ea1f1 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.4.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.5.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.5.png new file mode 100644 index 0000000..d1cbc4f Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.5.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.6.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.6.png new file mode 100644 index 0000000..27cc421 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignInVC.6.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.1.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.1.png new file mode 100644 index 0000000..3266872 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.1.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.2.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.2.png new file mode 100644 index 0000000..7d9ec78 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.2.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.3.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.3.png new file mode 100644 index 0000000..a17e2ea Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.3.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.4.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.4.png new file mode 100644 index 0000000..dfad32a Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.4.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.5.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.5.png new file mode 100644 index 0000000..873b427 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.5.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.6.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.6.png new file mode 100644 index 0000000..7762986 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testEmailSignupVC.6.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.1.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.1.png new file mode 100644 index 0000000..c964e03 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.1.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.2.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.2.png new file mode 100644 index 0000000..8ce2a36 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.2.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.3.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.3.png new file mode 100644 index 0000000..f9c71d9 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.3.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.4.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.4.png new file mode 100644 index 0000000..4533b9b Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.4.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.5.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.5.png new file mode 100644 index 0000000..ce83b2a Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.5.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.6.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.6.png new file mode 100644 index 0000000..ed3bad2 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testFirewallPrivacyPolicyVC.6.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.1.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.1.png new file mode 100644 index 0000000..ab5b1bf Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.1.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.2.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.2.png new file mode 100644 index 0000000..9ff7cf4 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.2.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.3.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.3.png new file mode 100644 index 0000000..504a027 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.3.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.4.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.4.png new file mode 100644 index 0000000..a450170 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.4.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.5.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.5.png new file mode 100644 index 0000000..50530af Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.5.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.6.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.6.png new file mode 100644 index 0000000..5134624 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testHomeVC.6.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.1.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.1.png new file mode 100644 index 0000000..53c61a3 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.1.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.2.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.2.png new file mode 100644 index 0000000..c46e076 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.2.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.3.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.3.png new file mode 100644 index 0000000..f658e56 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.3.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.4.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.4.png new file mode 100644 index 0000000..b89bfde Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.4.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.5.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.5.png new file mode 100644 index 0000000..573c051 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.5.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.6.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.6.png new file mode 100644 index 0000000..a861754 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testLogVC.6.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.1.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.1.png new file mode 100644 index 0000000..b7daf48 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.1.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.2.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.2.png new file mode 100644 index 0000000..fda2c3e Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.2.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.3.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.3.png new file mode 100644 index 0000000..9f1753a Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.3.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.4.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.4.png new file mode 100644 index 0000000..7f1dcc3 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.4.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.5.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.5.png new file mode 100644 index 0000000..2700c51 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.5.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.6.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.6.png new file mode 100644 index 0000000..c44496e Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testTitleVC.6.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.1.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.1.png new file mode 100644 index 0000000..9e434d7 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.1.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.2.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.2.png new file mode 100644 index 0000000..91a9da8 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.2.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.3.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.3.png new file mode 100644 index 0000000..dc93bcc Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.3.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.4.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.4.png new file mode 100644 index 0000000..cc2e0bf Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.4.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.5.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.5.png new file mode 100644 index 0000000..6046033 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.5.png differ diff --git a/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.6.png b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.6.png new file mode 100644 index 0000000..7d8b854 Binary files /dev/null and b/Tests/LockdownTests/__Snapshots__/SnapshotTests/testWhatIsVpnVC.6.png differ diff --git a/Tests/LockdowniOSUITests/ConfirmediOSUITests.swift b/Tests/LockdowniOSUITests/ConfirmediOSUITests.swift deleted file mode 100644 index a0070fc..0000000 --- a/Tests/LockdowniOSUITests/ConfirmediOSUITests.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ConfirmediOSUITests.swift -// ConfirmediOSUITests -// -// Copyright © 2019 Confirmed Inc. All rights reserved. -// - -import XCTest - -class ConfirmediOSUITests: XCTestCase { - - override func setUp() { - super.setUp() - - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. - XCUIApplication().launch() - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - -} diff --git a/Tests/LockdowniOSUITests/Info.plist b/Tests/LockdowniOSUITests/Info.plist deleted file mode 100644 index 6c6c23c..0000000 --- a/Tests/LockdowniOSUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/Info.plist b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/Info.plist new file mode 100644 index 0000000..70dd4c8 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/Info.plist @@ -0,0 +1,40 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-arm64 + LibraryPath + CocoaAsyncSocket.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + LibraryIdentifier + ios-arm64_x86_64-simulator + LibraryPath + CocoaAsyncSocket.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/CocoaAsyncSocket b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/CocoaAsyncSocket new file mode 100755 index 0000000..858c3c8 Binary files /dev/null and b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/CocoaAsyncSocket differ diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/CocoaAsyncSocket.h b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/CocoaAsyncSocket.h new file mode 100644 index 0000000..c4e5ee8 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/CocoaAsyncSocket.h @@ -0,0 +1,18 @@ +// +// CocoaAsyncSocket.h +// CocoaAsyncSocket +// +// Created by Derek Clarkson on 10/08/2015. +// CocoaAsyncSocket project is in the public domain. +// + +#import + +//! Project version number for CocoaAsyncSocket. +FOUNDATION_EXPORT double cocoaAsyncSocketVersionNumber; + +//! Project version string for CocoaAsyncSocket. +FOUNDATION_EXPORT const unsigned char cocoaAsyncSocketVersionString[]; + +#import +#import diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/GCDAsyncSocket.h b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/GCDAsyncSocket.h new file mode 100644 index 0000000..c339f8a --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/GCDAsyncSocket.h @@ -0,0 +1,1226 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import +#import + +#include // AF_INET, AF_INET6 + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; +@protocol GCDAsyncSocketDelegate; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketUseCFStreamForTLS; +#endif +#define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName +#define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates +#define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer +extern NSString *const GCDAsyncSocketSSLPeerID; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; +extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; +extern NSString *const GCDAsyncSocketSSLCipherSuites; +extern NSString *const GCDAsyncSocketSSLALPN; +#if !TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif + +#define GCDAsyncSocketLoggingContext 65535 + + +typedef NS_ERROR_ENUM(GCDAsyncSocketErrorDomain, GCDAsyncSocketError) { + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (instancetype)init; +- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq NS_DESIGNATED_INITIALIZER; + +/** + * Create GCDAsyncSocket from already connect BSD socket file descriptor +**/ ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD socketQueue:(nullable dispatch_queue_t)sq error:(NSError**)error; + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq error:(NSError**)error; + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq error:(NSError **)error; + +#pragma mark Configuration + +@property (atomic, weak, readwrite, nullable) id delegate; +#if OS_OBJECT_USE_OBJC +@property (atomic, strong, readwrite, nullable) dispatch_queue_t delegateQueue; +#else +@property (atomic, assign, readwrite, nullable) dispatch_queue_t delegateQueue; +#endif + +- (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; +- (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * If you are setting the delegate to nil within the delegate's dealloc method, + * you may need to use the synchronous versions below. +**/ +- (void)synchronouslySetDelegate:(nullable id)delegate; +- (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. +**/ + +@property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; +@property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; + +@property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; + +/** + * When connecting to both IPv4 and IPv6 using Happy Eyeballs (RFC 6555) https://tools.ietf.org/html/rfc6555 + * this is the delay between connecting to the preferred protocol and the fallback protocol. + * + * Defaults to 300ms. +**/ +@property (atomic, assign, readwrite) NSTimeInterval alternateAddressDelay; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. +**/ +@property (atomic, strong, readwrite, nullable) id userData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(nullable NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Tells the socket to begin listening and accepting connections on the unix domain at the given url. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) + **/ +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(nullable NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(nullable NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; +/** + * Connects to the unix domain socket at the given url, using the specified timeout. + */ +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Iterates over the given NetService's addresses in order, and invokes connectToAddress:error:. Stops at the + * first invocation that succeeds and returns YES; otherwise returns NO. + */ +- (BOOL)connectToNetService:(NSNetService *)netService error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can be used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. +**/ +@property (atomic, readonly) BOOL isDisconnected; +@property (atomic, readonly) BOOL isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +@property (atomic, readonly, nullable) NSString *connectedHost; +@property (atomic, readonly) uint16_t connectedPort; +@property (atomic, readonly, nullable) NSURL *connectedUrl; + +@property (atomic, readonly, nullable) NSString *localHost; +@property (atomic, readonly) uint16_t localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * @seealso connectedHost + * @seealso connectedPort + * @seealso localHost + * @seealso localPort +**/ +@property (atomic, readonly, nullable) NSData *connectedAddress; +@property (atomic, readonly, nullable) NSData *localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +@property (atomic, readonly) BOOL isIPv4; +@property (atomic, readonly) BOOL isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. +**/ +@property (atomic, readonly) BOOL isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(nullable NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)writeData:(nullable NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfWriteReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * ==== The available TOP-LEVEL KEYS are: + * + * - GCDAsyncSocketManuallyEvaluateTrust + * The value must be of type NSNumber, encapsulating a BOOL value. + * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. + * Instead it will pause at the moment evaulation would typically occur, + * and allow us to handle the security evaluation however we see fit. + * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. + * + * Note that if you set this option, then all other configuration keys are ignored. + * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. + * + * For more information on trust evaluation see: + * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation + * https://developer.apple.com/library/ios/technotes/tn2232/_index.html + * + * If unspecified, the default value is NO. + * + * - GCDAsyncSocketUseCFStreamForTLS (iOS only) + * The value must be of type NSNumber, encapsulating a BOOL value. + * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. + * This gives us more control over the security protocol (many more configuration options), + * plus it allows us to optimize things like sys calls and buffer allocation. + * + * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption + * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket + * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property + * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. + * + * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, + * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. + * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. + * + * If unspecified, the default value is NO. + * + * ==== The available CONFIGURATION KEYS are: + * + * - kCFStreamSSLPeerName + * The value must be of type NSString. + * It should match the name in the X.509 certificate given by the remote party. + * See Apple's documentation for SSLSetPeerDomainName. + * + * - kCFStreamSSLCertificates + * The value must be of type NSArray. + * See Apple's documentation for SSLSetCertificate. + * + * - kCFStreamSSLIsServer + * The value must be of type NSNumber, encapsulationg a BOOL value. + * See Apple's documentation for SSLCreateContext for iOS. + * This is optional for iOS. If not supplied, a NO value is the default. + * This is not needed for Mac OS X, and the value is ignored. + * + * - GCDAsyncSocketSSLPeerID + * The value must be of type NSData. + * You must set this value if you want to use TLS session resumption. + * See Apple's documentation for SSLSetPeerID. + * + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. + * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. + * See also the SSLProtocol typedef. + * + * - GCDAsyncSocketSSLSessionOptionFalseStart + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionFalseStart. + * + * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. + * + * - GCDAsyncSocketSSLCipherSuites + * The values must be of type NSArray. + * Each item within the array must be a NSNumber, encapsulating an SSLCipherSuite. + * See Apple's documentation for SSLSetEnabledCiphers. + * See also the SSLCipherSuite typedef. + * + * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) + * The value must be of type NSData. + * See Apple's documentation for SSLSetDiffieHellmanParams. + * + * ==== The following UNAVAILABLE KEYS are: (with throw an exception) + * + * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsAnyRoot + * + * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredRoots + * + * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredCerts + * + * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetEnableCertVerify + * + * - kCFStreamSSLLevel (UNAVAILABLE) + * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. + * Corresponding deprecated method: SSLSetProtocolVersionEnabled + * + * + * Please refer to Apple's documentation for corresponding SSLFunctions. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * IMPORTANT SECURITY NOTE: + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * + * You can also perform additional validation in socketDidSecure. +**/ +- (void)startTLS:(nullable NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. +**/ +@property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket +**/ +- (nullable CFReadStreamRef)readStream; +- (nullable CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } +**/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. +**/ +- (nullable SSLContextRef)sslContext; + +#pragma mark Utilities + +/** + * The address lookup utility used by the class. + * This method is synchronous, so it's recommended you use it on a background thread/queue. + * + * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. + * + * @returns + * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. + * The addresses are specifically for TCP connections. + * You can filter the addresses, if needed, using the other utility methods provided by the class. +**/ ++ (nullable NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Extracting host and port information from raw address data. +**/ + ++ (nullable NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:( NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr fromAddress:(NSData *)address; + ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr family:(nullable sa_family_t *)afPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. +**/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; +**/ +- (nullable dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. +**/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. + **/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. +**/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * then an invocation of this delegate method will be enqueued on the delegateQueue + * before the disconnect method returns. + * + * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, + * and the delegate is not also deallocated, then this method will be invoked, + * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) + * This is a generally rare, but is possible if one writes code like this: + * + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * In this case it may preferrable to nil the delegate beforehand, like this: + * + * asyncSocket.delegate = nil; // Don't invoke my delegate method + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * Of course, this depends on how your state machine is configured. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. +**/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +/** + * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if startTLS is invoked with options that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * Thus this method uses a completionHandler block rather than a normal return value. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +@end +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/GCDAsyncUdpSocket.h b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/GCDAsyncUdpSocket.h new file mode 100644 index 0000000..af327e0 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Headers/GCDAsyncUdpSocket.h @@ -0,0 +1,1036 @@ +// +// GCDAsyncUdpSocket +// +// This class is in the public domain. +// Originally created by Robbie Hanson of Deusty LLC. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN +extern NSString *const GCDAsyncUdpSocketException; +extern NSString *const GCDAsyncUdpSocketErrorDomain; + +extern NSString *const GCDAsyncUdpSocketQueueName; +extern NSString *const GCDAsyncUdpSocketThreadName; + +typedef NS_ERROR_ENUM(GCDAsyncUdpSocketErrorDomain, GCDAsyncUdpSocketError) { + GCDAsyncUdpSocketNoError = 0, // Never used + GCDAsyncUdpSocketBadConfigError, // Invalid configuration + GCDAsyncUdpSocketBadParamError, // Invalid parameter was passed + GCDAsyncUdpSocketSendTimeoutError, // A send operation timed out + GCDAsyncUdpSocketClosedError, // The socket was closed + GCDAsyncUdpSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@class GCDAsyncUdpSocket; + +@protocol GCDAsyncUdpSocketDelegate +@optional + +/** + * By design, UDP is a connectionless protocol, and connecting is not needed. + * However, you may optionally choose to connect to a particular host for reasons + * outlined in the documentation for the various connect methods listed above. + * + * This method is called if one of the connect methods are invoked, and the connection is successful. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address; + +/** + * By design, UDP is a connectionless protocol, and connecting is not needed. + * However, you may optionally choose to connect to a particular host for reasons + * outlined in the documentation for the various connect methods listed above. + * + * This method is called if one of the connect methods are invoked, and the connection fails. + * This may happen, for example, if a domain name is given for the host and the domain name is unable to be resolved. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError * _Nullable)error; + +/** + * Called when the datagram with the given tag has been sent. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag; + +/** + * Called if an error occurs while trying to send a datagram. + * This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError * _Nullable)error; + +/** + * Called when the socket has received the requested datagram. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data + fromAddress:(NSData *)address + withFilterContext:(nullable id)filterContext; + +/** + * Called when the socket is closed. +**/ +- (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError * _Nullable)error; + +@end + +/** + * You may optionally set a receive filter for the socket. + * A filter can provide several useful features: + * + * 1. Many times udp packets need to be parsed. + * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. + * The end result is a parallel socket io, datagram parsing, and packet processing. + * + * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. + * The filter can prevent such packets from arriving at the delegate. + * And because the filter can run in its own independent queue, this doesn't slow down the delegate. + * + * - Since the udp protocol does not guarantee delivery, udp packets may be lost. + * Many protocols built atop udp thus provide various resend/re-request algorithms. + * This sometimes results in duplicate packets arriving. + * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. + * + * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. + * Such packets need to be ignored. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * @param data - The packet that was received. + * @param address - The address the data was received from. + * See utilities section for methods to extract info from address. + * @param context - Out parameter you may optionally set, which will then be passed to the delegate method. + * For example, filter block can parse the data and then, + * pass the parsed data to the delegate. + * + * @returns - YES if the received packet should be passed onto the delegate. + * NO if the received packet should be discarded, and not reported to the delegete. + * + * Example: + * + * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { + * + * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; + * + * *context = response; + * return (response != nil); + * }; + * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; + * +**/ +typedef BOOL (^GCDAsyncUdpSocketReceiveFilterBlock)(NSData *data, NSData *address, id __nullable * __nonnull context); + +/** + * You may optionally set a send filter for the socket. + * A filter can provide several interesting possibilities: + * + * 1. Optional caching of resolved addresses for domain names. + * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. + * + * 2. Reusable modules of code for bandwidth monitoring. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * @param data - The packet that was received. + * @param address - The address the data was received from. + * See utilities section for methods to extract info from address. + * @param tag - The tag that was passed in the send method. + * + * @returns - YES if the packet should actually be sent over the socket. + * NO if the packet should be silently dropped (not sent over the socket). + * + * Regardless of the return value, the delegate will be informed that the packet was successfully sent. + * +**/ +typedef BOOL (^GCDAsyncUdpSocketSendFilterBlock)(NSData *data, NSData *address, long tag); + + +@interface GCDAsyncUdpSocket : NSObject + +/** + * GCDAsyncUdpSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create its own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (instancetype)init; +- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq NS_DESIGNATED_INITIALIZER; + +#pragma mark Configuration + +- (nullable id)delegate; +- (void)setDelegate:(nullable id)delegate; +- (void)synchronouslySetDelegate:(nullable id)delegate; + +- (nullable dispatch_queue_t)delegateQueue; +- (void)setDelegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; + +- (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; +- (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * This means GCDAsyncUdpSocket automatically supports both protocols, + * and can send to IPv4 or IPv6 addresses, + * as well as receive over IPv4 and IPv6. + * + * For operations that require DNS resolution, GCDAsyncUdpSocket supports both IPv4 and IPv6. + * If a DNS lookup returns only IPv4 results, GCDAsyncUdpSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncUdpSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, then the protocol used depends on the configured preference. + * If IPv4 is preferred, then IPv4 is used. + * If IPv6 is preferred, then IPv6 is used. + * If neutral, then the first IP version in the resolved array will be used. + * + * Starting with Mac OS X 10.7 Lion and iOS 5, the default IP preference is neutral. + * On prior systems the default IP preference is IPv4. + **/ +- (BOOL)isIPv4Enabled; +- (void)setIPv4Enabled:(BOOL)flag; + +- (BOOL)isIPv6Enabled; +- (void)setIPv6Enabled:(BOOL)flag; + +- (BOOL)isIPv4Preferred; +- (BOOL)isIPv6Preferred; +- (BOOL)isIPVersionNeutral; + +- (void)setPreferIPv4; +- (void)setPreferIPv6; +- (void)setIPVersionNeutral; + +/** + * Gets/Sets the maximum size of the buffer that will be allocated for receive operations. + * The default maximum size is 65535 bytes. + * + * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. + * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. + * + * Since the OS/GCD notifies us of the size of each received UDP packet, + * the actual allocated buffer size for each packet is exact. + * And in practice the size of UDP packets is generally much smaller than the max. + * Indeed most protocols will send and receive packets of only a few bytes, + * or will set a limit on the size of packets to prevent fragmentation in the IP layer. + * + * If you set the buffer size too small, the sockets API in the OS will silently discard + * any extra data, and you will not be notified of the error. +**/ +- (uint16_t)maxReceiveIPv4BufferSize; +- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max; + +- (uint32_t)maxReceiveIPv6BufferSize; +- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max; + +/** + * Gets/Sets the maximum size of the buffer that will be allocated for send operations. + * The default maximum size is 65535 bytes. + * + * Given that a typical link MTU is 1500 bytes, a large UDP datagram will have to be + * fragmented, and that’s both expensive and risky (if one fragment goes missing, the + * entire datagram is lost). You are much better off sending a large number of smaller + * UDP datagrams, preferably using a path MTU algorithm to avoid fragmentation. + * + * You must set it before the sockt is created otherwise it won't work. + * + **/ +- (uint16_t)maxSendBufferSize; +- (void)setMaxSendBufferSize:(uint16_t)max; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally in any way. +**/ +- (nullable id)userData; +- (void)setUserData:(nullable id)arbitraryUserData; + +#pragma mark Diagnostics + +/** + * Returns the local address info for the socket. + * + * The localAddress method returns a sockaddr structure wrapped in a NSData object. + * The localHost method returns the human readable IP address as a string. + * + * Note: Address info may not be available until after the socket has been binded, connected + * or until after data has been sent. +**/ +- (nullable NSData *)localAddress; +- (nullable NSString *)localHost; +- (uint16_t)localPort; + +- (nullable NSData *)localAddress_IPv4; +- (nullable NSString *)localHost_IPv4; +- (uint16_t)localPort_IPv4; + +- (nullable NSData *)localAddress_IPv6; +- (nullable NSString *)localHost_IPv6; +- (uint16_t)localPort_IPv6; + +/** + * Returns the remote address info for the socket. + * + * The connectedAddress method returns a sockaddr structure wrapped in a NSData object. + * The connectedHost method returns the human readable IP address as a string. + * + * Note: Since UDP is connectionless by design, connected address info + * will not be available unless the socket is explicitly connected to a remote host/port. + * If the socket is not connected, these methods will return nil / 0. +**/ +- (nullable NSData *)connectedAddress; +- (nullable NSString *)connectedHost; +- (uint16_t)connectedPort; + +/** + * Returns whether or not this socket has been connected to a single host. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * If connected, the socket will only be able to send/receive data to/from the connected host. +**/ +- (BOOL)isConnected; + +/** + * Returns whether or not this socket has been closed. + * The only way a socket can be closed is if you explicitly call one of the close methods. +**/ +- (BOOL)isClosed; + +/** + * Returns whether or not this socket is IPv4. + * + * By default this will be true, unless: + * - IPv4 is disabled (via setIPv4Enabled:) + * - The socket is explicitly bound to an IPv6 address + * - The socket is connected to an IPv6 address +**/ +- (BOOL)isIPv4; + +/** + * Returns whether or not this socket is IPv6. + * + * By default this will be true, unless: + * - IPv6 is disabled (via setIPv6Enabled:) + * - The socket is explicitly bound to an IPv4 address + * _ The socket is connected to an IPv4 address + * + * This method will also return false on platforms that do not support IPv6. + * Note: The iPhone does not currently support IPv6. +**/ +- (BOOL)isIPv6; + +#pragma mark Binding + +/** + * Binds the UDP socket to the given port. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You may optionally pass a port number of zero to immediately bind the socket, + * yet still allow the OS to automatically assign an available port. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Binds the UDP socket to the given port and optional interface. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You may optionally pass a port number of zero to immediately bind the socket, + * yet still allow the OS to automatically assign an available port. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept packets from the local machine. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToPort:(uint16_t)port interface:(nullable NSString *)interface error:(NSError **)errPtr; + +/** + * Binds the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects the UDP socket to the given host and port. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific host/port has the following effect: + * - You will only be able to send data to the connected host/port. + * - You will only be able to receive data from the connected host/port. + * - You will receive ICMP messages that come from the connected host/port, such as "connection refused". + * + * The actual process of connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after it has been connected. + * You can only connect a socket once. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * + * This method is asynchronous as it requires a DNS lookup to resolve the given host name. + * If an obvious error is detected, this method immediately returns NO and sets errPtr. + * If you don't care about the error, you can pass nil for errPtr. + * Otherwise, this method returns YES and begins the asynchronous connection process. + * The result of the asynchronous connection process will be reported via the delegate methods. + **/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific address has the following effect: + * - You will only be able to send data to the connected address. + * - You will only be able to receive data from the connected address. + * - You will receive ICMP messages that come from the connected address, such as "connection refused". + * + * Connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after its been connected. + * You can only connect a socket once. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. + * + * Note: Unlike the connectToHost:onPort:error: method, this method does not require a DNS lookup. + * Thus when this method returns, the connection has either failed or fully completed. + * In other words, this method is synchronous, unlike the asynchronous connectToHost::: method. + * However, for compatibility and simplification of delegate code, if this method returns YES + * then the corresponding delegate method (udpSocket:didConnectToHost:port:) is still invoked. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +#pragma mark Multicast + +/** + * Join multicast group. + * Group should be an IP address (eg @"225.228.0.1"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr; + +/** + * Join multicast group. + * Group should be an IP address (eg @"225.228.0.1"). + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(nullable NSString *)interface error:(NSError **)errPtr; + +- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr; +- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(nullable NSString *)interface error:(NSError **)errPtr; + +/** + * Send multicast on a specified interface. + * For IPv4, interface should be the the IP address of the interface (eg @"192.168.10.1"). + * For IPv6, interface should be the a network interface name (eg @"en0"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ + +- (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr; +- (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr; + +#pragma mark Reuse Port + +/** + * By default, only one socket can be bound to a given IP address + port at a time. + * To enable multiple processes to simultaneously bind to the same address+port, + * you need to enable this functionality in the socket. All processes that wish to + * use the address+port simultaneously must all enable reuse port on the socket + * bound to that port. + **/ +- (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr; + +#pragma mark Broadcast + +/** + * By default, the underlying socket in the OS will not allow you to send broadcast messages. + * In order to send broadcast messages, you need to enable this functionality in the socket. + * + * A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is + * delivered to every host on the network. + * The reason this is generally disabled by default (by the OS) is to prevent + * accidental broadcast messages from flooding the network. +**/ +- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr; + +#pragma mark Sending + +/** + * Asynchronously sends the given data, with the given timeout and tag. + * + * This method may only be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given host and port. + * + * This method cannot be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param host + * The destination to send the udp packet to. + * May be specified as a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * You may also use the convenience strings of "loopback" or "localhost". + * + * @param port + * The port of the host to send to. + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data + toHost:(NSString *)host + port:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given address. + * + * This method cannot be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param remoteAddr + * The address to send the data to (specified as a sockaddr structure wrapped in a NSData object). + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * You may optionally set a send filter for the socket. + * A filter can provide several interesting possibilities: + * + * 1. Optional caching of resolved addresses for domain names. + * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. + * + * 2. Reusable modules of code for bandwidth monitoring. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * For more information about GCDAsyncUdpSocketSendFilterBlock, see the documentation for its typedef. + * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. + * + * Note: This method invokes setSendFilter:withQueue:isAsynchronous: (documented below), + * passing YES for the isAsynchronous parameter. +**/ +- (void)setSendFilter:(nullable GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue; + +/** + * The receive filter can be run via dispatch_async or dispatch_sync. + * Most typical situations call for asynchronous operation. + * + * However, there are a few situations in which synchronous operation is preferred. + * Such is the case when the filter is extremely minimal and fast. + * This is because dispatch_sync is faster than dispatch_async. + * + * If you choose synchronous operation, be aware of possible deadlock conditions. + * Since the socket queue is executing your block via dispatch_sync, + * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. + * For example, you can't query properties on the socket. +**/ +- (void)setSendFilter:(nullable GCDAsyncUdpSocketSendFilterBlock)filterBlock + withQueue:(nullable dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous; + +#pragma mark Receiving + +/** + * There are two modes of operation for receiving packets: one-at-a-time & continuous. + * + * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. + * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, + * where your state machine may not always be ready to process incoming packets. + * + * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. + * Receiving packets continuously is better suited to real-time streaming applications. + * + * You may switch back and forth between one-at-a-time mode and continuous mode. + * If the socket is currently in continuous mode, calling this method will switch it to one-at-a-time mode. + * + * When a packet is received (and not filtered by the optional receive filter), + * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. + * + * If the socket is able to begin receiving packets, this method returns YES. + * Otherwise it returns NO, and sets the errPtr with appropriate error information. + * + * An example error: + * You created a udp socket to act as a server, and immediately called receive. + * You forgot to first bind the socket to a port number, and received a error with a message like: + * "Must bind socket before you can receive data." +**/ +- (BOOL)receiveOnce:(NSError **)errPtr; + +/** + * There are two modes of operation for receiving packets: one-at-a-time & continuous. + * + * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. + * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, + * where your state machine may not always be ready to process incoming packets. + * + * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. + * Receiving packets continuously is better suited to real-time streaming applications. + * + * You may switch back and forth between one-at-a-time mode and continuous mode. + * If the socket is currently in one-at-a-time mode, calling this method will switch it to continuous mode. + * + * For every received packet (not filtered by the optional receive filter), + * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. + * + * If the socket is able to begin receiving packets, this method returns YES. + * Otherwise it returns NO, and sets the errPtr with appropriate error information. + * + * An example error: + * You created a udp socket to act as a server, and immediately called receive. + * You forgot to first bind the socket to a port number, and received a error with a message like: + * "Must bind socket before you can receive data." +**/ +- (BOOL)beginReceiving:(NSError **)errPtr; + +/** + * If the socket is currently receiving (beginReceiving has been called), this method pauses the receiving. + * That is, it won't read any more packets from the underlying OS socket until beginReceiving is called again. + * + * Important Note: + * GCDAsyncUdpSocket may be running in parallel with your code. + * That is, your delegate is likely running on a separate thread/dispatch_queue. + * When you invoke this method, GCDAsyncUdpSocket may have already dispatched delegate methods to be invoked. + * Thus, if those delegate methods have already been dispatch_async'd, + * your didReceive delegate method may still be invoked after this method has been called. + * You should be aware of this, and program defensively. +**/ +- (void)pauseReceiving; + +/** + * You may optionally set a receive filter for the socket. + * This receive filter may be set to run in its own queue (independent of delegate queue). + * + * A filter can provide several useful features. + * + * 1. Many times udp packets need to be parsed. + * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. + * The end result is a parallel socket io, datagram parsing, and packet processing. + * + * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. + * The filter can prevent such packets from arriving at the delegate. + * And because the filter can run in its own independent queue, this doesn't slow down the delegate. + * + * - Since the udp protocol does not guarantee delivery, udp packets may be lost. + * Many protocols built atop udp thus provide various resend/re-request algorithms. + * This sometimes results in duplicate packets arriving. + * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. + * + * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. + * Such packets need to be ignored. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * Example: + * + * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { + * + * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; + * + * *context = response; + * return (response != nil); + * }; + * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; + * + * For more information about GCDAsyncUdpSocketReceiveFilterBlock, see the documentation for its typedef. + * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. + * + * Note: This method invokes setReceiveFilter:withQueue:isAsynchronous: (documented below), + * passing YES for the isAsynchronous parameter. +**/ +- (void)setReceiveFilter:(nullable GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue; + +/** + * The receive filter can be run via dispatch_async or dispatch_sync. + * Most typical situations call for asynchronous operation. + * + * However, there are a few situations in which synchronous operation is preferred. + * Such is the case when the filter is extremely minimal and fast. + * This is because dispatch_sync is faster than dispatch_async. + * + * If you choose synchronous operation, be aware of possible deadlock conditions. + * Since the socket queue is executing your block via dispatch_sync, + * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. + * For example, you can't query properties on the socket. +**/ +- (void)setReceiveFilter:(nullable GCDAsyncUdpSocketReceiveFilterBlock)filterBlock + withQueue:(nullable dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous; + +#pragma mark Closing + +/** + * Immediately closes the underlying socket. + * Any pending send operations are discarded. + * + * The GCDAsyncUdpSocket instance may optionally be used again. + * (it will setup/configure/use another unnderlying BSD socket). +**/ +- (void)close; + +/** + * Closes the underlying socket after all pending send operations have been sent. + * + * The GCDAsyncUdpSocket instance may optionally be used again. + * (it will setup/configure/use another unnderlying BSD socket). +**/ +- (void)closeAfterSending; + +#pragma mark Advanced +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. + **/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket isn't connected, or explicity bound to a particular interface, + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Returns (creating if necessary) a CFReadStream/CFWriteStream for the internal socket. + * + * Generally GCDAsyncUdpSocket doesn't use CFStream. (It uses the faster GCD API's.) + * However, if you need one for any reason, + * these methods are a convenient way to get access to a safe instance of one. +**/ +- (nullable CFReadStreamRef)readStream; +- (nullable CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Example usage: + * + * [asyncUdpSocket performBlock:^{ + * [asyncUdpSocket enableBackgroundingOnSocket]; + * }]; + * + * + * NOTE : Apple doesn't currently support backgrounding UDP sockets. (Only TCP for now). +**/ +//- (BOOL)enableBackgroundingOnSockets; + +#endif + +#pragma mark Utilities + +/** + * Extracting host/port/family information from raw address data. +**/ + ++ (nullable NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; ++ (int)familyFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(uint16_t * __nullable)portPtr fromAddress:(NSData *)address; ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(uint16_t * __nullable)portPtr family:(int * __nullable)afPtr fromAddress:(NSData *)address; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Info.plist b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Info.plist new file mode 100644 index 0000000..5b7cf0a Binary files /dev/null and b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Info.plist differ diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Modules/module.modulemap b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Modules/module.modulemap new file mode 100644 index 0000000..6c90f59 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64/CocoaAsyncSocket.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module CocoaAsyncSocket { + umbrella header "CocoaAsyncSocket.h" + + export * + module * { export * } +} diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/CocoaAsyncSocket b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/CocoaAsyncSocket new file mode 100755 index 0000000..241283b Binary files /dev/null and b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/CocoaAsyncSocket differ diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/CocoaAsyncSocket.h b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/CocoaAsyncSocket.h new file mode 100644 index 0000000..c4e5ee8 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/CocoaAsyncSocket.h @@ -0,0 +1,18 @@ +// +// CocoaAsyncSocket.h +// CocoaAsyncSocket +// +// Created by Derek Clarkson on 10/08/2015. +// CocoaAsyncSocket project is in the public domain. +// + +#import + +//! Project version number for CocoaAsyncSocket. +FOUNDATION_EXPORT double cocoaAsyncSocketVersionNumber; + +//! Project version string for CocoaAsyncSocket. +FOUNDATION_EXPORT const unsigned char cocoaAsyncSocketVersionString[]; + +#import +#import diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/GCDAsyncSocket.h b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/GCDAsyncSocket.h new file mode 100644 index 0000000..c339f8a --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/GCDAsyncSocket.h @@ -0,0 +1,1226 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import +#import + +#include // AF_INET, AF_INET6 + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; +@protocol GCDAsyncSocketDelegate; + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketUseCFStreamForTLS; +#endif +#define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName +#define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates +#define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer +extern NSString *const GCDAsyncSocketSSLPeerID; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; +extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; +extern NSString *const GCDAsyncSocketSSLCipherSuites; +extern NSString *const GCDAsyncSocketSSLALPN; +#if !TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif + +#define GCDAsyncSocketLoggingContext 65535 + + +typedef NS_ERROR_ENUM(GCDAsyncSocketErrorDomain, GCDAsyncSocketError) { + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (instancetype)init; +- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq NS_DESIGNATED_INITIALIZER; + +/** + * Create GCDAsyncSocket from already connect BSD socket file descriptor +**/ ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD socketQueue:(nullable dispatch_queue_t)sq error:(NSError**)error; + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq error:(NSError**)error; + ++ (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq error:(NSError **)error; + +#pragma mark Configuration + +@property (atomic, weak, readwrite, nullable) id delegate; +#if OS_OBJECT_USE_OBJC +@property (atomic, strong, readwrite, nullable) dispatch_queue_t delegateQueue; +#else +@property (atomic, assign, readwrite, nullable) dispatch_queue_t delegateQueue; +#endif + +- (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; +- (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * If you are setting the delegate to nil within the delegate's dealloc method, + * you may need to use the synchronous versions below. +**/ +- (void)synchronouslySetDelegate:(nullable id)delegate; +- (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. +**/ + +@property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; +@property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; + +@property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; + +/** + * When connecting to both IPv4 and IPv6 using Happy Eyeballs (RFC 6555) https://tools.ietf.org/html/rfc6555 + * this is the delay between connecting to the preferred protocol and the fallback protocol. + * + * Defaults to 300ms. +**/ +@property (atomic, assign, readwrite) NSTimeInterval alternateAddressDelay; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. +**/ +@property (atomic, strong, readwrite, nullable) id userData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(nullable NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Tells the socket to begin listening and accepting connections on the unix domain at the given url. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) + **/ +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(nullable NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(nullable NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; +/** + * Connects to the unix domain socket at the given url, using the specified timeout. + */ +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Iterates over the given NetService's addresses in order, and invokes connectToAddress:error:. Stops at the + * first invocation that succeeds and returns YES; otherwise returns NO. + */ +- (BOOL)connectToNetService:(NSNetService *)netService error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can be used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. +**/ +@property (atomic, readonly) BOOL isDisconnected; +@property (atomic, readonly) BOOL isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +@property (atomic, readonly, nullable) NSString *connectedHost; +@property (atomic, readonly) uint16_t connectedPort; +@property (atomic, readonly, nullable) NSURL *connectedUrl; + +@property (atomic, readonly, nullable) NSString *localHost; +@property (atomic, readonly) uint16_t localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * @seealso connectedHost + * @seealso connectedPort + * @seealso localHost + * @seealso localPort +**/ +@property (atomic, readonly, nullable) NSData *connectedAddress; +@property (atomic, readonly, nullable) NSData *localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +@property (atomic, readonly) BOOL isIPv4; +@property (atomic, readonly) BOOL isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. +**/ +@property (atomic, readonly) BOOL isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(nullable NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer is nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(nullable NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)writeData:(nullable NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfWriteReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * ==== The available TOP-LEVEL KEYS are: + * + * - GCDAsyncSocketManuallyEvaluateTrust + * The value must be of type NSNumber, encapsulating a BOOL value. + * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. + * Instead it will pause at the moment evaulation would typically occur, + * and allow us to handle the security evaluation however we see fit. + * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. + * + * Note that if you set this option, then all other configuration keys are ignored. + * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. + * + * For more information on trust evaluation see: + * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation + * https://developer.apple.com/library/ios/technotes/tn2232/_index.html + * + * If unspecified, the default value is NO. + * + * - GCDAsyncSocketUseCFStreamForTLS (iOS only) + * The value must be of type NSNumber, encapsulating a BOOL value. + * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. + * This gives us more control over the security protocol (many more configuration options), + * plus it allows us to optimize things like sys calls and buffer allocation. + * + * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption + * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket + * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property + * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. + * + * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, + * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. + * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. + * + * If unspecified, the default value is NO. + * + * ==== The available CONFIGURATION KEYS are: + * + * - kCFStreamSSLPeerName + * The value must be of type NSString. + * It should match the name in the X.509 certificate given by the remote party. + * See Apple's documentation for SSLSetPeerDomainName. + * + * - kCFStreamSSLCertificates + * The value must be of type NSArray. + * See Apple's documentation for SSLSetCertificate. + * + * - kCFStreamSSLIsServer + * The value must be of type NSNumber, encapsulationg a BOOL value. + * See Apple's documentation for SSLCreateContext for iOS. + * This is optional for iOS. If not supplied, a NO value is the default. + * This is not needed for Mac OS X, and the value is ignored. + * + * - GCDAsyncSocketSSLPeerID + * The value must be of type NSData. + * You must set this value if you want to use TLS session resumption. + * See Apple's documentation for SSLSetPeerID. + * + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. + * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. + * See also the SSLProtocol typedef. + * + * - GCDAsyncSocketSSLSessionOptionFalseStart + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionFalseStart. + * + * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. + * + * - GCDAsyncSocketSSLCipherSuites + * The values must be of type NSArray. + * Each item within the array must be a NSNumber, encapsulating an SSLCipherSuite. + * See Apple's documentation for SSLSetEnabledCiphers. + * See also the SSLCipherSuite typedef. + * + * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) + * The value must be of type NSData. + * See Apple's documentation for SSLSetDiffieHellmanParams. + * + * ==== The following UNAVAILABLE KEYS are: (with throw an exception) + * + * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsAnyRoot + * + * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredRoots + * + * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredCerts + * + * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetEnableCertVerify + * + * - kCFStreamSSLLevel (UNAVAILABLE) + * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. + * Corresponding deprecated method: SSLSetProtocolVersionEnabled + * + * + * Please refer to Apple's documentation for corresponding SSLFunctions. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * IMPORTANT SECURITY NOTE: + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * + * You can also perform additional validation in socketDidSecure. +**/ +- (void)startTLS:(nullable NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. +**/ +@property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket +**/ +- (nullable CFReadStreamRef)readStream; +- (nullable CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } +**/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. +**/ +- (nullable SSLContextRef)sslContext; + +#pragma mark Utilities + +/** + * The address lookup utility used by the class. + * This method is synchronous, so it's recommended you use it on a background thread/queue. + * + * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. + * + * @returns + * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. + * The addresses are specifically for TCP connections. + * You can filter the addresses, if needed, using the other utility methods provided by the class. +**/ ++ (nullable NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Extracting host and port information from raw address data. +**/ + ++ (nullable NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:( NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr fromAddress:(NSData *)address; + ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr family:(nullable sa_family_t *)afPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. +**/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; +**/ +- (nullable dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. +**/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. + **/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. +**/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * then an invocation of this delegate method will be enqueued on the delegateQueue + * before the disconnect method returns. + * + * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, + * and the delegate is not also deallocated, then this method will be invoked, + * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) + * This is a generally rare, but is possible if one writes code like this: + * + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * In this case it may preferrable to nil the delegate beforehand, like this: + * + * asyncSocket.delegate = nil; // Don't invoke my delegate method + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * Of course, this depends on how your state machine is configured. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. +**/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +/** + * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if startTLS is invoked with options that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * Thus this method uses a completionHandler block rather than a normal return value. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +@end +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/GCDAsyncUdpSocket.h b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/GCDAsyncUdpSocket.h new file mode 100644 index 0000000..af327e0 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Headers/GCDAsyncUdpSocket.h @@ -0,0 +1,1036 @@ +// +// GCDAsyncUdpSocket +// +// This class is in the public domain. +// Originally created by Robbie Hanson of Deusty LLC. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN +extern NSString *const GCDAsyncUdpSocketException; +extern NSString *const GCDAsyncUdpSocketErrorDomain; + +extern NSString *const GCDAsyncUdpSocketQueueName; +extern NSString *const GCDAsyncUdpSocketThreadName; + +typedef NS_ERROR_ENUM(GCDAsyncUdpSocketErrorDomain, GCDAsyncUdpSocketError) { + GCDAsyncUdpSocketNoError = 0, // Never used + GCDAsyncUdpSocketBadConfigError, // Invalid configuration + GCDAsyncUdpSocketBadParamError, // Invalid parameter was passed + GCDAsyncUdpSocketSendTimeoutError, // A send operation timed out + GCDAsyncUdpSocketClosedError, // The socket was closed + GCDAsyncUdpSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@class GCDAsyncUdpSocket; + +@protocol GCDAsyncUdpSocketDelegate +@optional + +/** + * By design, UDP is a connectionless protocol, and connecting is not needed. + * However, you may optionally choose to connect to a particular host for reasons + * outlined in the documentation for the various connect methods listed above. + * + * This method is called if one of the connect methods are invoked, and the connection is successful. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address; + +/** + * By design, UDP is a connectionless protocol, and connecting is not needed. + * However, you may optionally choose to connect to a particular host for reasons + * outlined in the documentation for the various connect methods listed above. + * + * This method is called if one of the connect methods are invoked, and the connection fails. + * This may happen, for example, if a domain name is given for the host and the domain name is unable to be resolved. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError * _Nullable)error; + +/** + * Called when the datagram with the given tag has been sent. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag; + +/** + * Called if an error occurs while trying to send a datagram. + * This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError * _Nullable)error; + +/** + * Called when the socket has received the requested datagram. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data + fromAddress:(NSData *)address + withFilterContext:(nullable id)filterContext; + +/** + * Called when the socket is closed. +**/ +- (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError * _Nullable)error; + +@end + +/** + * You may optionally set a receive filter for the socket. + * A filter can provide several useful features: + * + * 1. Many times udp packets need to be parsed. + * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. + * The end result is a parallel socket io, datagram parsing, and packet processing. + * + * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. + * The filter can prevent such packets from arriving at the delegate. + * And because the filter can run in its own independent queue, this doesn't slow down the delegate. + * + * - Since the udp protocol does not guarantee delivery, udp packets may be lost. + * Many protocols built atop udp thus provide various resend/re-request algorithms. + * This sometimes results in duplicate packets arriving. + * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. + * + * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. + * Such packets need to be ignored. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * @param data - The packet that was received. + * @param address - The address the data was received from. + * See utilities section for methods to extract info from address. + * @param context - Out parameter you may optionally set, which will then be passed to the delegate method. + * For example, filter block can parse the data and then, + * pass the parsed data to the delegate. + * + * @returns - YES if the received packet should be passed onto the delegate. + * NO if the received packet should be discarded, and not reported to the delegete. + * + * Example: + * + * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { + * + * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; + * + * *context = response; + * return (response != nil); + * }; + * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; + * +**/ +typedef BOOL (^GCDAsyncUdpSocketReceiveFilterBlock)(NSData *data, NSData *address, id __nullable * __nonnull context); + +/** + * You may optionally set a send filter for the socket. + * A filter can provide several interesting possibilities: + * + * 1. Optional caching of resolved addresses for domain names. + * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. + * + * 2. Reusable modules of code for bandwidth monitoring. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * @param data - The packet that was received. + * @param address - The address the data was received from. + * See utilities section for methods to extract info from address. + * @param tag - The tag that was passed in the send method. + * + * @returns - YES if the packet should actually be sent over the socket. + * NO if the packet should be silently dropped (not sent over the socket). + * + * Regardless of the return value, the delegate will be informed that the packet was successfully sent. + * +**/ +typedef BOOL (^GCDAsyncUdpSocketSendFilterBlock)(NSData *data, NSData *address, long tag); + + +@interface GCDAsyncUdpSocket : NSObject + +/** + * GCDAsyncUdpSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create its own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (instancetype)init; +- (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; +- (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq NS_DESIGNATED_INITIALIZER; + +#pragma mark Configuration + +- (nullable id)delegate; +- (void)setDelegate:(nullable id)delegate; +- (void)synchronouslySetDelegate:(nullable id)delegate; + +- (nullable dispatch_queue_t)delegateQueue; +- (void)setDelegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; + +- (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; +- (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * This means GCDAsyncUdpSocket automatically supports both protocols, + * and can send to IPv4 or IPv6 addresses, + * as well as receive over IPv4 and IPv6. + * + * For operations that require DNS resolution, GCDAsyncUdpSocket supports both IPv4 and IPv6. + * If a DNS lookup returns only IPv4 results, GCDAsyncUdpSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncUdpSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, then the protocol used depends on the configured preference. + * If IPv4 is preferred, then IPv4 is used. + * If IPv6 is preferred, then IPv6 is used. + * If neutral, then the first IP version in the resolved array will be used. + * + * Starting with Mac OS X 10.7 Lion and iOS 5, the default IP preference is neutral. + * On prior systems the default IP preference is IPv4. + **/ +- (BOOL)isIPv4Enabled; +- (void)setIPv4Enabled:(BOOL)flag; + +- (BOOL)isIPv6Enabled; +- (void)setIPv6Enabled:(BOOL)flag; + +- (BOOL)isIPv4Preferred; +- (BOOL)isIPv6Preferred; +- (BOOL)isIPVersionNeutral; + +- (void)setPreferIPv4; +- (void)setPreferIPv6; +- (void)setIPVersionNeutral; + +/** + * Gets/Sets the maximum size of the buffer that will be allocated for receive operations. + * The default maximum size is 65535 bytes. + * + * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. + * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. + * + * Since the OS/GCD notifies us of the size of each received UDP packet, + * the actual allocated buffer size for each packet is exact. + * And in practice the size of UDP packets is generally much smaller than the max. + * Indeed most protocols will send and receive packets of only a few bytes, + * or will set a limit on the size of packets to prevent fragmentation in the IP layer. + * + * If you set the buffer size too small, the sockets API in the OS will silently discard + * any extra data, and you will not be notified of the error. +**/ +- (uint16_t)maxReceiveIPv4BufferSize; +- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max; + +- (uint32_t)maxReceiveIPv6BufferSize; +- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max; + +/** + * Gets/Sets the maximum size of the buffer that will be allocated for send operations. + * The default maximum size is 65535 bytes. + * + * Given that a typical link MTU is 1500 bytes, a large UDP datagram will have to be + * fragmented, and that’s both expensive and risky (if one fragment goes missing, the + * entire datagram is lost). You are much better off sending a large number of smaller + * UDP datagrams, preferably using a path MTU algorithm to avoid fragmentation. + * + * You must set it before the sockt is created otherwise it won't work. + * + **/ +- (uint16_t)maxSendBufferSize; +- (void)setMaxSendBufferSize:(uint16_t)max; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally in any way. +**/ +- (nullable id)userData; +- (void)setUserData:(nullable id)arbitraryUserData; + +#pragma mark Diagnostics + +/** + * Returns the local address info for the socket. + * + * The localAddress method returns a sockaddr structure wrapped in a NSData object. + * The localHost method returns the human readable IP address as a string. + * + * Note: Address info may not be available until after the socket has been binded, connected + * or until after data has been sent. +**/ +- (nullable NSData *)localAddress; +- (nullable NSString *)localHost; +- (uint16_t)localPort; + +- (nullable NSData *)localAddress_IPv4; +- (nullable NSString *)localHost_IPv4; +- (uint16_t)localPort_IPv4; + +- (nullable NSData *)localAddress_IPv6; +- (nullable NSString *)localHost_IPv6; +- (uint16_t)localPort_IPv6; + +/** + * Returns the remote address info for the socket. + * + * The connectedAddress method returns a sockaddr structure wrapped in a NSData object. + * The connectedHost method returns the human readable IP address as a string. + * + * Note: Since UDP is connectionless by design, connected address info + * will not be available unless the socket is explicitly connected to a remote host/port. + * If the socket is not connected, these methods will return nil / 0. +**/ +- (nullable NSData *)connectedAddress; +- (nullable NSString *)connectedHost; +- (uint16_t)connectedPort; + +/** + * Returns whether or not this socket has been connected to a single host. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * If connected, the socket will only be able to send/receive data to/from the connected host. +**/ +- (BOOL)isConnected; + +/** + * Returns whether or not this socket has been closed. + * The only way a socket can be closed is if you explicitly call one of the close methods. +**/ +- (BOOL)isClosed; + +/** + * Returns whether or not this socket is IPv4. + * + * By default this will be true, unless: + * - IPv4 is disabled (via setIPv4Enabled:) + * - The socket is explicitly bound to an IPv6 address + * - The socket is connected to an IPv6 address +**/ +- (BOOL)isIPv4; + +/** + * Returns whether or not this socket is IPv6. + * + * By default this will be true, unless: + * - IPv6 is disabled (via setIPv6Enabled:) + * - The socket is explicitly bound to an IPv4 address + * _ The socket is connected to an IPv4 address + * + * This method will also return false on platforms that do not support IPv6. + * Note: The iPhone does not currently support IPv6. +**/ +- (BOOL)isIPv6; + +#pragma mark Binding + +/** + * Binds the UDP socket to the given port. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You may optionally pass a port number of zero to immediately bind the socket, + * yet still allow the OS to automatically assign an available port. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Binds the UDP socket to the given port and optional interface. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You may optionally pass a port number of zero to immediately bind the socket, + * yet still allow the OS to automatically assign an available port. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept packets from the local machine. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToPort:(uint16_t)port interface:(nullable NSString *)interface error:(NSError **)errPtr; + +/** + * Binds the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects the UDP socket to the given host and port. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific host/port has the following effect: + * - You will only be able to send data to the connected host/port. + * - You will only be able to receive data from the connected host/port. + * - You will receive ICMP messages that come from the connected host/port, such as "connection refused". + * + * The actual process of connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after it has been connected. + * You can only connect a socket once. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * + * This method is asynchronous as it requires a DNS lookup to resolve the given host name. + * If an obvious error is detected, this method immediately returns NO and sets errPtr. + * If you don't care about the error, you can pass nil for errPtr. + * Otherwise, this method returns YES and begins the asynchronous connection process. + * The result of the asynchronous connection process will be reported via the delegate methods. + **/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific address has the following effect: + * - You will only be able to send data to the connected address. + * - You will only be able to receive data from the connected address. + * - You will receive ICMP messages that come from the connected address, such as "connection refused". + * + * Connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after its been connected. + * You can only connect a socket once. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. + * + * Note: Unlike the connectToHost:onPort:error: method, this method does not require a DNS lookup. + * Thus when this method returns, the connection has either failed or fully completed. + * In other words, this method is synchronous, unlike the asynchronous connectToHost::: method. + * However, for compatibility and simplification of delegate code, if this method returns YES + * then the corresponding delegate method (udpSocket:didConnectToHost:port:) is still invoked. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +#pragma mark Multicast + +/** + * Join multicast group. + * Group should be an IP address (eg @"225.228.0.1"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr; + +/** + * Join multicast group. + * Group should be an IP address (eg @"225.228.0.1"). + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(nullable NSString *)interface error:(NSError **)errPtr; + +- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr; +- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(nullable NSString *)interface error:(NSError **)errPtr; + +/** + * Send multicast on a specified interface. + * For IPv4, interface should be the the IP address of the interface (eg @"192.168.10.1"). + * For IPv6, interface should be the a network interface name (eg @"en0"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ + +- (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr; +- (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr; + +#pragma mark Reuse Port + +/** + * By default, only one socket can be bound to a given IP address + port at a time. + * To enable multiple processes to simultaneously bind to the same address+port, + * you need to enable this functionality in the socket. All processes that wish to + * use the address+port simultaneously must all enable reuse port on the socket + * bound to that port. + **/ +- (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr; + +#pragma mark Broadcast + +/** + * By default, the underlying socket in the OS will not allow you to send broadcast messages. + * In order to send broadcast messages, you need to enable this functionality in the socket. + * + * A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is + * delivered to every host on the network. + * The reason this is generally disabled by default (by the OS) is to prevent + * accidental broadcast messages from flooding the network. +**/ +- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr; + +#pragma mark Sending + +/** + * Asynchronously sends the given data, with the given timeout and tag. + * + * This method may only be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given host and port. + * + * This method cannot be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param host + * The destination to send the udp packet to. + * May be specified as a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * You may also use the convenience strings of "loopback" or "localhost". + * + * @param port + * The port of the host to send to. + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data + toHost:(NSString *)host + port:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given address. + * + * This method cannot be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param remoteAddr + * The address to send the data to (specified as a sockaddr structure wrapped in a NSData object). + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * You may optionally set a send filter for the socket. + * A filter can provide several interesting possibilities: + * + * 1. Optional caching of resolved addresses for domain names. + * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. + * + * 2. Reusable modules of code for bandwidth monitoring. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * For more information about GCDAsyncUdpSocketSendFilterBlock, see the documentation for its typedef. + * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. + * + * Note: This method invokes setSendFilter:withQueue:isAsynchronous: (documented below), + * passing YES for the isAsynchronous parameter. +**/ +- (void)setSendFilter:(nullable GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue; + +/** + * The receive filter can be run via dispatch_async or dispatch_sync. + * Most typical situations call for asynchronous operation. + * + * However, there are a few situations in which synchronous operation is preferred. + * Such is the case when the filter is extremely minimal and fast. + * This is because dispatch_sync is faster than dispatch_async. + * + * If you choose synchronous operation, be aware of possible deadlock conditions. + * Since the socket queue is executing your block via dispatch_sync, + * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. + * For example, you can't query properties on the socket. +**/ +- (void)setSendFilter:(nullable GCDAsyncUdpSocketSendFilterBlock)filterBlock + withQueue:(nullable dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous; + +#pragma mark Receiving + +/** + * There are two modes of operation for receiving packets: one-at-a-time & continuous. + * + * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. + * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, + * where your state machine may not always be ready to process incoming packets. + * + * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. + * Receiving packets continuously is better suited to real-time streaming applications. + * + * You may switch back and forth between one-at-a-time mode and continuous mode. + * If the socket is currently in continuous mode, calling this method will switch it to one-at-a-time mode. + * + * When a packet is received (and not filtered by the optional receive filter), + * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. + * + * If the socket is able to begin receiving packets, this method returns YES. + * Otherwise it returns NO, and sets the errPtr with appropriate error information. + * + * An example error: + * You created a udp socket to act as a server, and immediately called receive. + * You forgot to first bind the socket to a port number, and received a error with a message like: + * "Must bind socket before you can receive data." +**/ +- (BOOL)receiveOnce:(NSError **)errPtr; + +/** + * There are two modes of operation for receiving packets: one-at-a-time & continuous. + * + * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. + * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, + * where your state machine may not always be ready to process incoming packets. + * + * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. + * Receiving packets continuously is better suited to real-time streaming applications. + * + * You may switch back and forth between one-at-a-time mode and continuous mode. + * If the socket is currently in one-at-a-time mode, calling this method will switch it to continuous mode. + * + * For every received packet (not filtered by the optional receive filter), + * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. + * + * If the socket is able to begin receiving packets, this method returns YES. + * Otherwise it returns NO, and sets the errPtr with appropriate error information. + * + * An example error: + * You created a udp socket to act as a server, and immediately called receive. + * You forgot to first bind the socket to a port number, and received a error with a message like: + * "Must bind socket before you can receive data." +**/ +- (BOOL)beginReceiving:(NSError **)errPtr; + +/** + * If the socket is currently receiving (beginReceiving has been called), this method pauses the receiving. + * That is, it won't read any more packets from the underlying OS socket until beginReceiving is called again. + * + * Important Note: + * GCDAsyncUdpSocket may be running in parallel with your code. + * That is, your delegate is likely running on a separate thread/dispatch_queue. + * When you invoke this method, GCDAsyncUdpSocket may have already dispatched delegate methods to be invoked. + * Thus, if those delegate methods have already been dispatch_async'd, + * your didReceive delegate method may still be invoked after this method has been called. + * You should be aware of this, and program defensively. +**/ +- (void)pauseReceiving; + +/** + * You may optionally set a receive filter for the socket. + * This receive filter may be set to run in its own queue (independent of delegate queue). + * + * A filter can provide several useful features. + * + * 1. Many times udp packets need to be parsed. + * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. + * The end result is a parallel socket io, datagram parsing, and packet processing. + * + * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. + * The filter can prevent such packets from arriving at the delegate. + * And because the filter can run in its own independent queue, this doesn't slow down the delegate. + * + * - Since the udp protocol does not guarantee delivery, udp packets may be lost. + * Many protocols built atop udp thus provide various resend/re-request algorithms. + * This sometimes results in duplicate packets arriving. + * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. + * + * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. + * Such packets need to be ignored. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * Example: + * + * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { + * + * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; + * + * *context = response; + * return (response != nil); + * }; + * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; + * + * For more information about GCDAsyncUdpSocketReceiveFilterBlock, see the documentation for its typedef. + * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. + * + * Note: This method invokes setReceiveFilter:withQueue:isAsynchronous: (documented below), + * passing YES for the isAsynchronous parameter. +**/ +- (void)setReceiveFilter:(nullable GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue; + +/** + * The receive filter can be run via dispatch_async or dispatch_sync. + * Most typical situations call for asynchronous operation. + * + * However, there are a few situations in which synchronous operation is preferred. + * Such is the case when the filter is extremely minimal and fast. + * This is because dispatch_sync is faster than dispatch_async. + * + * If you choose synchronous operation, be aware of possible deadlock conditions. + * Since the socket queue is executing your block via dispatch_sync, + * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. + * For example, you can't query properties on the socket. +**/ +- (void)setReceiveFilter:(nullable GCDAsyncUdpSocketReceiveFilterBlock)filterBlock + withQueue:(nullable dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous; + +#pragma mark Closing + +/** + * Immediately closes the underlying socket. + * Any pending send operations are discarded. + * + * The GCDAsyncUdpSocket instance may optionally be used again. + * (it will setup/configure/use another unnderlying BSD socket). +**/ +- (void)close; + +/** + * Closes the underlying socket after all pending send operations have been sent. + * + * The GCDAsyncUdpSocket instance may optionally be used again. + * (it will setup/configure/use another unnderlying BSD socket). +**/ +- (void)closeAfterSending; + +#pragma mark Advanced +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. + **/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket isn't connected, or explicity bound to a particular interface, + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Returns (creating if necessary) a CFReadStream/CFWriteStream for the internal socket. + * + * Generally GCDAsyncUdpSocket doesn't use CFStream. (It uses the faster GCD API's.) + * However, if you need one for any reason, + * these methods are a convenient way to get access to a safe instance of one. +**/ +- (nullable CFReadStreamRef)readStream; +- (nullable CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Example usage: + * + * [asyncUdpSocket performBlock:^{ + * [asyncUdpSocket enableBackgroundingOnSocket]; + * }]; + * + * + * NOTE : Apple doesn't currently support backgrounding UDP sockets. (Only TCP for now). +**/ +//- (BOOL)enableBackgroundingOnSockets; + +#endif + +#pragma mark Utilities + +/** + * Extracting host/port/family information from raw address data. +**/ + ++ (nullable NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; ++ (int)familyFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(uint16_t * __nullable)portPtr fromAddress:(NSData *)address; ++ (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(uint16_t * __nullable)portPtr family:(int * __nullable)afPtr fromAddress:(NSData *)address; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Info.plist b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Info.plist new file mode 100644 index 0000000..1f54b1b Binary files /dev/null and b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Info.plist differ diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Modules/module.modulemap b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Modules/module.modulemap new file mode 100644 index 0000000..6c90f59 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/Modules/module.modulemap @@ -0,0 +1,6 @@ +framework module CocoaAsyncSocket { + umbrella header "CocoaAsyncSocket.h" + + export * + module * { export * } +} diff --git a/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/_CodeSignature/CodeResources b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/_CodeSignature/CodeResources new file mode 100644 index 0000000..85eb14b --- /dev/null +++ b/ThirdPartyFrameworks/CocoaAsyncSocket.xcframework/ios-arm64_x86_64-simulator/CocoaAsyncSocket.framework/_CodeSignature/CodeResources @@ -0,0 +1,146 @@ + + + + + files + + Headers/CocoaAsyncSocket.h + + 19xueMkhcDCf6A2ihyiTCDjWjd4= + + Headers/GCDAsyncSocket.h + + JwDJxahaKup9fnB5MJuoxDHbdDs= + + Headers/GCDAsyncUdpSocket.h + + 9hL7D86xSUKQ1TBRDa+fDNkDlqI= + + Info.plist + + B1nAiJiqTAi+FlrYuC5SiWvuelU= + + Modules/module.modulemap + + +n94rYTWDjekX3imyh+PSyA9vgA= + + + files2 + + Headers/CocoaAsyncSocket.h + + hash2 + + VpE7gL1U1p/0urO77FEjPNjY06qrttQJnalOd+6VYDQ= + + + Headers/GCDAsyncSocket.h + + hash2 + + JL0b2lWPgVphz/ekZLsGMKrShDXTK2YY53aKtusc9hk= + + + Headers/GCDAsyncUdpSocket.h + + hash2 + + uNVm5yZ0jBhGDXZuAynPXvem1qcBvAVdWXAewQdJbh8= + + + Modules/module.modulemap + + hash2 + + RoVn8xMeEnU3Izg0DtYjYL/krI8V7qw0sa7Ggf+08Rs= + + + + rules + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^version.plist$ + + + rules2 + + .*\.dSYM($|/) + + weight + 11 + + ^(.*/)?\.DS_Store$ + + omit + + weight + 2000 + + ^.* + + ^.*\.lproj/ + + optional + + weight + 1000 + + ^.*\.lproj/locversion.plist$ + + omit + + weight + 1100 + + ^Base\.lproj/ + + weight + 1010 + + ^Info\.plist$ + + omit + + weight + 20 + + ^PkgInfo$ + + omit + + weight + 20 + + ^embedded\.provisionprofile$ + + weight + 20 + + ^version\.plist$ + + weight + 20 + + + + diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/Info.plist b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/Info.plist new file mode 100644 index 0000000..fab14c9 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/Info.plist @@ -0,0 +1,53 @@ + + + + + AvailableLibraries + + + LibraryIdentifier + ios-arm64_x86_64-simulator + LibraryPath + CocoaLumberjack.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + LibraryIdentifier + ios-arm64 + LibraryPath + CocoaLumberjack.framework + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + LibraryIdentifier + macos-arm64_x86_64 + LibraryPath + CocoaLumberjack.framework + SupportedArchitectures + + arm64 + x86_64 + + SupportedPlatform + macos + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/CocoaLumberjack b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/CocoaLumberjack new file mode 100755 index 0000000..6e0d33d Binary files /dev/null and b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/CocoaLumberjack differ diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/CLIColor.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/CLIColor.h new file mode 100644 index 0000000..e930566 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/CLIColor.h @@ -0,0 +1,54 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +#import + +#if TARGET_OS_OSX + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class represents an NSColor replacement for CLI projects that don't link with AppKit + **/ +@interface CLIColor : NSObject + +/** + * Convenience method for creating a `CLIColor` instance from RGBA params + * + * @param red red channel, between 0 and 1 + * @param green green channel, between 0 and 1 + * @param blue blue channel, between 0 and 1 + * @param alpha alpha channel, between 0 and 1 + */ ++ (instancetype)colorWithCalibratedRed:(CGFloat)red green:(CGFloat)green blue:(CGFloat)blue alpha:(CGFloat)alpha; + +/** + * Get the RGBA components from a `CLIColor` + * + * @param red red channel, between 0 and 1 + * @param green green channel, between 0 and 1 + * @param blue blue channel, between 0 and 1 + * @param alpha alpha channel, between 0 and 1 + */ +- (void)getRed:(nullable CGFloat *)red green:(nullable CGFloat *)green blue:(nullable CGFloat *)blue alpha:(nullable CGFloat *)alpha NS_SWIFT_NAME(get(red:green:blue:alpha:)); + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/CocoaLumberjack.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/CocoaLumberjack.h new file mode 100644 index 0000000..8082d6e --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/CocoaLumberjack.h @@ -0,0 +1,104 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +/** + * Welcome to CocoaLumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read "Getting Started" at: + * Documentation/GettingStarted.md + * + * Otherwise, here is a quick refresher. + * There are three steps to using the macros: + * + * Step 1: + * Import the header in your implementation or prefix file: + * + * #import + * + * Step 2: + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const DDLogLevel ddLogLevel = DDLogLevelVerbose; + * + * Step 2 [3rd party frameworks]: + * + * Define your LOG_LEVEL_DEF to a different variable/function than ddLogLevel: + * + * // #undef LOG_LEVEL_DEF // Undefine first only if needed + * #define LOG_LEVEL_DEF myLibLogLevel + * + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const DDLogLevel myLibLogLevel = DDLogLevelVerbose; + * + * Step 3: + * Replace your NSLog statements with DDLog statements according to the severity of the message. + * + * NSLog(@"Fatal error, no dohickey found!"); -> DDLogError(@"Fatal error, no dohickey found!"); + * + * DDLog works exactly the same as NSLog. + * This means you can pass it multiple variables just like NSLog. + **/ + +#import + +//! Project version number for CocoaLumberjack. +FOUNDATION_EXPORT double CocoaLumberjackVersionNumber; + +//! Project version string for CocoaLumberjack. +FOUNDATION_EXPORT const unsigned char CocoaLumberjackVersionString[]; + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +// Core +#import + +// Main macros +#import +#import + +// Capture ASL +#import + +// Loggers +#import + +#import +#import +#import +#import + +// Extensions +#import +#import +#import +#import +#import + +// CLI +#import + +// etc +#import +#import +#import diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDASLLogCapture.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDASLLogCapture.h new file mode 100644 index 0000000..7c5d8e3 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDASLLogCapture.h @@ -0,0 +1,46 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +#import + +@protocol DDLogger; + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class provides the ability to capture the ASL (Apple System Logs) + */ +API_DEPRECATED("Use DDOSLogger instead", macosx(10.4,10.12), ios(2.0,10.0), watchos(2.0,3.0), tvos(9.0,10.0)) +@interface DDASLLogCapture : NSObject + +/** + * Start capturing logs + */ ++ (void)start; + +/** + * Stop capturing logs + */ ++ (void)stop; + +/** + * The current capture level. + * @note Default log level: DDLogLevelVerbose (i.e. capture all ASL messages). + */ +@property (class) DDLogLevel captureLevel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDASLLogger.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDASLLogger.h new file mode 100644 index 0000000..65bae5c --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDASLLogger.h @@ -0,0 +1,63 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +#import + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import + +NS_ASSUME_NONNULL_BEGIN + +// Custom key set on messages sent to ASL +extern const char* const kDDASLKeyDDLog; + +// Value set for kDDASLKeyDDLog +extern const char* const kDDASLDDLogValue; + +/** + * This class provides a logger for the Apple System Log facility. + * + * As described in the "Getting Started" page, + * the traditional NSLog() function directs its output to two places: + * + * - Apple System Log + * - StdErr (if stderr is a TTY) so log statements show up in Xcode console + * + * To duplicate NSLog() functionality you can simply add this logger and a tty logger. + * However, if you instead choose to use file logging (for faster performance), + * you may choose to use a file logger and a tty logger. + **/ +API_DEPRECATED("Use DDOSLogger instead", macosx(10.4,10.12), ios(2.0,10.0), watchos(2.0,3.0), tvos(9.0,10.0)) +@interface DDASLLogger : DDAbstractLogger + +/** + * Singleton method + * + * @return the shared instance + */ +@property (nonatomic, class, readonly, strong) DDASLLogger *sharedInstance; + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDAbstractDatabaseLogger.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDAbstractDatabaseLogger.h new file mode 100644 index 0000000..281690e --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDAbstractDatabaseLogger.h @@ -0,0 +1,127 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class provides an abstract implementation of a database logger. + * + * That is, it provides the base implementation for a database logger to build atop of. + * All that is needed for a concrete database logger is to extend this class + * and override the methods in the implementation file that are prefixed with "db_". + **/ +@interface DDAbstractDatabaseLogger : DDAbstractLogger { + +@protected + NSUInteger _saveThreshold; + NSTimeInterval _saveInterval; + NSTimeInterval _maxAge; + NSTimeInterval _deleteInterval; + BOOL _deleteOnEverySave; + + NSInteger _saveTimerSuspended; + NSUInteger _unsavedCount; + dispatch_time_t _unsavedTime; + dispatch_source_t _saveTimer; + dispatch_time_t _lastDeleteTime; + dispatch_source_t _deleteTimer; +} + +/** + * Specifies how often to save the data to disk. + * Since saving is an expensive operation (disk io) it is not done after every log statement. + * These properties allow you to configure how/when the logger saves to disk. + * + * A save is done when either (whichever happens first): + * + * - The number of unsaved log entries reaches saveThreshold + * - The amount of time since the oldest unsaved log entry was created reaches saveInterval + * + * You can optionally disable the saveThreshold by setting it to zero. + * If you disable the saveThreshold you are entirely dependent on the saveInterval. + * + * You can optionally disable the saveInterval by setting it to zero (or a negative value). + * If you disable the saveInterval you are entirely dependent on the saveThreshold. + * + * It's not wise to disable both saveThreshold and saveInterval. + * + * The default saveThreshold is 500. + * The default saveInterval is 60 seconds. + **/ +@property (assign, readwrite) NSUInteger saveThreshold; + +/** + * See the description for the `saveThreshold` property + */ +@property (assign, readwrite) NSTimeInterval saveInterval; + +/** + * It is likely you don't want the log entries to persist forever. + * Doing so would allow the database to grow infinitely large over time. + * + * The maxAge property provides a way to specify how old a log statement can get + * before it should get deleted from the database. + * + * The deleteInterval specifies how often to sweep for old log entries. + * Since deleting is an expensive operation (disk io) is is done on a fixed interval. + * + * An alternative to the deleteInterval is the deleteOnEverySave option. + * This specifies that old log entries should be deleted during every save operation. + * + * You can optionally disable the maxAge by setting it to zero (or a negative value). + * If you disable the maxAge then old log statements are not deleted. + * + * You can optionally disable the deleteInterval by setting it to zero (or a negative value). + * + * If you disable both deleteInterval and deleteOnEverySave then old log statements are not deleted. + * + * It's not wise to enable both deleteInterval and deleteOnEverySave. + * + * The default maxAge is 7 days. + * The default deleteInterval is 5 minutes. + * The default deleteOnEverySave is NO. + **/ +@property (assign, readwrite) NSTimeInterval maxAge; + +/** + * See the description for the `maxAge` property + */ +@property (assign, readwrite) NSTimeInterval deleteInterval; + +/** + * See the description for the `maxAge` property + */ +@property (assign, readwrite) BOOL deleteOnEverySave; + +/** + * Forces a save of any pending log entries (flushes log entries to disk). + **/ +- (void)savePendingLogEntries; + +/** + * Removes any log entries that are older than maxAge. + **/ +- (void)deleteOldLogEntries; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDAssertMacros.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDAssertMacros.h new file mode 100644 index 0000000..f4b7d58 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDAssertMacros.h @@ -0,0 +1,30 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +/** + * NSAssert replacement that will output a log message even when assertions are disabled. + **/ +#define DDAssert(condition, frmt, ...) \ + if (!(condition)) { \ + NSString *description = [NSString stringWithFormat:frmt, ## __VA_ARGS__]; \ + DDLogError(@"%@", description); \ + NSAssert(NO, @"%@", description); \ + } +#define DDAssertCondition(condition) DDAssert(condition, @"Condition not satisfied: %s", #condition) + +/** + * Analog to `DDAssertionFailure` from DDAssert.swift for use in Objective C + */ +#define DDAssertionFailure(frmt, ...) DDAssert(NO, frmt, ##__VA_ARGS__) diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDContextFilterLogFormatter+Deprecated.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDContextFilterLogFormatter+Deprecated.h new file mode 100644 index 0000000..3dcc6af --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDContextFilterLogFormatter+Deprecated.h @@ -0,0 +1,119 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class provides a log formatter that filters log statements from a logging context not on the whitelist. + * @deprecated Use DDContextAllowlistFilterLogFormatter instead. + * + * A log formatter can be added to any logger to format and/or filter its output. + * You can learn more about log formatters here: + * Documentation/CustomFormatters.md + * + * You can learn more about logging context's here: + * Documentation/CustomContext.md + * + * But here's a quick overview / refresher: + * + * Every log statement has a logging context. + * These come from the underlying logging macros defined in DDLog.h. + * The default logging context is zero. + * You can define multiple logging context's for use in your application. + * For example, logically separate parts of your app each have a different logging context. + * Also 3rd party frameworks that make use of Lumberjack generally use their own dedicated logging context. + **/ +__attribute__((deprecated("Use DDContextAllowlistFilterLogFormatter instead"))) +typedef DDContextAllowlistFilterLogFormatter DDContextWhitelistFilterLogFormatter; + +@interface DDContextAllowlistFilterLogFormatter (Deprecated) + +/** + * Add a context to the whitelist + * @deprecated Use -addToAllowlist: instead. + * + * @param loggingContext the context + */ +- (void)addToWhitelist:(NSInteger)loggingContext __attribute__((deprecated("Use -addToAllowlist: instead"))); + +/** + * Remove context from whitelist + * @deprecated Use -removeFromAllowlist: instead. + * + * @param loggingContext the context + */ +- (void)removeFromWhitelist:(NSInteger)loggingContext __attribute__((deprecated("Use -removeFromAllowlist: instead"))); + +/** + * Return the whitelist + * @deprecated Use allowlist instead. + */ +@property (nonatomic, readonly, copy) NSArray *whitelist __attribute__((deprecated("Use allowlist instead"))); + +/** + * Check if a context is on the whitelist + * @deprecated Use -isOnAllowlist: instead. + * + * @param loggingContext the context + */ +- (BOOL)isOnWhitelist:(NSInteger)loggingContext __attribute__((deprecated("Use -isOnAllowlist: instead"))); + +@end + + +/** + * This class provides a log formatter that filters log statements from a logging context on the blacklist. + * @deprecated Use DDContextDenylistFilterLogFormatter instead. + **/ +__attribute__((deprecated("Use DDContextDenylistFilterLogFormatter instead"))) +typedef DDContextDenylistFilterLogFormatter DDContextBlacklistFilterLogFormatter; + +@interface DDContextDenylistFilterLogFormatter (Deprecated) + +/** + * Add a context to the blacklist + * @deprecated Use -addToDenylist: instead. + * + * @param loggingContext the context + */ +- (void)addToBlacklist:(NSInteger)loggingContext __attribute__((deprecated("Use -addToDenylist: instead"))); + +/** + * Remove context from blacklist + * @deprecated Use -removeFromDenylist: instead. + * + * @param loggingContext the context + */ +- (void)removeFromBlacklist:(NSInteger)loggingContext __attribute__((deprecated("Use -removeFromDenylist: instead"))); + +/** + * Return the blacklist + * @deprecated Use denylist instead. + */ +@property (readonly, copy) NSArray *blacklist __attribute__((deprecated("Use denylist instead"))); + +/** + * Check if a context is on the blacklist + * @deprecated Use -isOnDenylist: instead. + * + * @param loggingContext the context + */ +- (BOOL)isOnBlacklist:(NSInteger)loggingContext __attribute__((deprecated("Use -isOnDenylist: instead"))); + +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDContextFilterLogFormatter.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDContextFilterLogFormatter.h new file mode 100644 index 0000000..61d37aa --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDContextFilterLogFormatter.h @@ -0,0 +1,117 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +#import + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class provides a log formatter that filters log statements from a logging context not on the allowlist. + * + * A log formatter can be added to any logger to format and/or filter its output. + * You can learn more about log formatters here: + * Documentation/CustomFormatters.md + * + * You can learn more about logging context's here: + * Documentation/CustomContext.md + * + * But here's a quick overview / refresher: + * + * Every log statement has a logging context. + * These come from the underlying logging macros defined in DDLog.h. + * The default logging context is zero. + * You can define multiple logging context's for use in your application. + * For example, logically separate parts of your app each have a different logging context. + * Also 3rd party frameworks that make use of Lumberjack generally use their own dedicated logging context. + **/ +@interface DDContextAllowlistFilterLogFormatter : NSObject + +/** + * Designated default initializer + */ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Add a context to the allowlist + * + * @param loggingContext the context + */ +- (void)addToAllowlist:(NSInteger)loggingContext; + +/** + * Remove context from allowlist + * + * @param loggingContext the context + */ +- (void)removeFromAllowlist:(NSInteger)loggingContext; + +/** + * Return the allowlist + */ +@property (nonatomic, readonly, copy) NSArray *allowlist; + +/** + * Check if a context is on the allowlist + * + * @param loggingContext the context + */ +- (BOOL)isOnAllowlist:(NSInteger)loggingContext; + +@end + + +/** + * This class provides a log formatter that filters log statements from a logging context on the denylist. + **/ +@interface DDContextDenylistFilterLogFormatter : NSObject + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Add a context to the denylist + * + * @param loggingContext the context + */ +- (void)addToDenylist:(NSInteger)loggingContext; + +/** + * Remove context from denylist + * + * @param loggingContext the context + */ +- (void)removeFromDenylist:(NSInteger)loggingContext; + +/** + * Return the denylist + */ +@property (readonly, copy) NSArray *denylist; + +/** + * Check if a context is on the denylist + * + * @param loggingContext the context + */ +- (BOOL)isOnDenylist:(NSInteger)loggingContext; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDDispatchQueueLogFormatter.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDDispatchQueueLogFormatter.h new file mode 100644 index 0000000..618cc08 --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDDispatchQueueLogFormatter.h @@ -0,0 +1,223 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +#import + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Log formatter mode + */ +__attribute__((deprecated("DDDispatchQueueLogFormatter is always shareable"))) +typedef NS_ENUM(NSUInteger, DDDispatchQueueLogFormatterMode){ + /** + * This is the default option, means the formatter can be reused between multiple loggers and therefore is thread-safe. + * There is, of course, a performance cost for the thread-safety + */ + DDDispatchQueueLogFormatterModeShareble = 0, + /** + * If the formatter will only be used by a single logger, then the thread-safety can be removed + * @note: there is an assert checking if the formatter is added to multiple loggers and the mode is non-shareble + */ + DDDispatchQueueLogFormatterModeNonShareble, +}; + +/** + * Quality of Service names. + * + * Since macOS 10.10 and iOS 8.0, pthreads, dispatch queues and NSOperations express their + * scheduling priority by using an abstract classification called Quality of Service (QOS). + * + * This formatter will add a representation of this QOS in the log message by using those + * string constants. + * For example: + * + * `2011-10-17 20:21:45.435 AppName[19928:5207 (QOS:DF)] Your log message here` + * + * Where QOS is one of: + * `- UI = User Interactive` + * `- IN = User Initiated` + * `- DF = Default` + * `- UT = Utility` + * `- BG = Background` + * `- UN = Unspecified` + * + * Note: QOS will be absent in the log messages if running on OS versions that don't support it. + **/ +typedef NSString * DDQualityOfServiceName NS_STRING_ENUM; + +FOUNDATION_EXPORT DDQualityOfServiceName const DDQualityOfServiceUserInteractive NS_SWIFT_NAME(DDQualityOfServiceName.userInteractive) API_AVAILABLE(macos(10.10), ios(8.0)); +FOUNDATION_EXPORT DDQualityOfServiceName const DDQualityOfServiceUserInitiated NS_SWIFT_NAME(DDQualityOfServiceName.userInitiated) API_AVAILABLE(macos(10.10), ios(8.0)); +FOUNDATION_EXPORT DDQualityOfServiceName const DDQualityOfServiceDefault NS_SWIFT_NAME(DDQualityOfServiceName.default) API_AVAILABLE(macos(10.10), ios(8.0)); +FOUNDATION_EXPORT DDQualityOfServiceName const DDQualityOfServiceUtility NS_SWIFT_NAME(DDQualityOfServiceName.utility) API_AVAILABLE(macos(10.10), ios(8.0)); +FOUNDATION_EXPORT DDQualityOfServiceName const DDQualityOfServiceBackground NS_SWIFT_NAME(DDQualityOfServiceName.background) API_AVAILABLE(macos(10.10), ios(8.0)); +FOUNDATION_EXPORT DDQualityOfServiceName const DDQualityOfServiceUnspecified NS_SWIFT_NAME(DDQualityOfServiceName.unspecified) API_AVAILABLE(macos(10.10), ios(8.0)); + +/** + * This class provides a log formatter that prints the dispatch_queue label instead of the mach_thread_id. + * + * A log formatter can be added to any logger to format and/or filter its output. + * You can learn more about log formatters here: + * Documentation/CustomFormatters.md + * + * A typical `NSLog` (or `DDTTYLogger`) prints detailed info as `[:]`. + * For example: + * + * `2011-10-17 20:21:45.435 AppName[19928:5207] Your log message here` + * + * Where: + * `- 19928 = process id` + * `- 5207 = thread id (mach_thread_id printed in hex)` + * + * When using grand central dispatch (GCD), this information is less useful. + * This is because a single serial dispatch queue may be run on any thread from an internally managed thread pool. + * For example: + * + * `2011-10-17 20:32:31.111 AppName[19954:4d07] Message from my_serial_dispatch_queue` + * `2011-10-17 20:32:31.112 AppName[19954:5207] Message from my_serial_dispatch_queue` + * `2011-10-17 20:32:31.113 AppName[19954:2c55] Message from my_serial_dispatch_queue` + * + * This formatter allows you to replace the standard `[box:info]` with the dispatch_queue name. + * For example: + * + * `2011-10-17 20:32:31.111 AppName[img-scaling] Message from my_serial_dispatch_queue` + * `2011-10-17 20:32:31.112 AppName[img-scaling] Message from my_serial_dispatch_queue` + * `2011-10-17 20:32:31.113 AppName[img-scaling] Message from my_serial_dispatch_queue` + * + * If the dispatch_queue doesn't have a set name, then it falls back to the thread name. + * If the current thread doesn't have a set name, then it falls back to the mach_thread_id in hex (like normal). + * + * Note: If manually creating your own background threads (via `NSThread/alloc/init` or `NSThread/detachNeThread`), + * you can use `[[NSThread currentThread] setName:(NSString *)]`. + **/ +@interface DDDispatchQueueLogFormatter : NSObject + +/** + * Standard init method. + * Configure using properties as desired. + **/ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** + * Initializer with ability to set the queue mode + * + * @param mode choose between DDDispatchQueueLogFormatterModeShareble and DDDispatchQueueLogFormatterModeNonShareble, depending if the formatter is shared between several loggers or not + */ +- (instancetype)initWithMode:(DDDispatchQueueLogFormatterMode)mode __attribute__((deprecated("DDDispatchQueueLogFormatter is always shareable"))); + +/** + * The minQueueLength restricts the minimum size of the [detail box]. + * If the minQueueLength is set to 0, there is no restriction. + * + * For example, say a dispatch_queue has a label of "diskIO": + * + * If the minQueueLength is 0: [diskIO] + * If the minQueueLength is 4: [diskIO] + * If the minQueueLength is 5: [diskIO] + * If the minQueueLength is 6: [diskIO] + * If the minQueueLength is 7: [diskIO ] + * If the minQueueLength is 8: [diskIO ] + * + * The default minQueueLength is 0 (no minimum, so [detail box] won't be padded). + * + * If you want every [detail box] to have the exact same width, + * set both minQueueLength and maxQueueLength to the same value. + **/ +@property (assign, atomic) NSUInteger minQueueLength; + +/** + * The maxQueueLength restricts the number of characters that will be inside the [detail box]. + * If the maxQueueLength is 0, there is no restriction. + * + * For example, say a dispatch_queue has a label of "diskIO": + * + * If the maxQueueLength is 0: [diskIO] + * If the maxQueueLength is 4: [disk] + * If the maxQueueLength is 5: [diskI] + * If the maxQueueLength is 6: [diskIO] + * If the maxQueueLength is 7: [diskIO] + * If the maxQueueLength is 8: [diskIO] + * + * The default maxQueueLength is 0 (no maximum, so [detail box] won't be truncated). + * + * If you want every [detail box] to have the exact same width, + * set both minQueueLength and maxQueueLength to the same value. + **/ +@property (assign, atomic) NSUInteger maxQueueLength; + +/** + * Sometimes queue labels have long names like "com.apple.main-queue", + * but you'd prefer something shorter like simply "main". + * + * This method allows you to set such preferred replacements. + * The above example is set by default. + * + * To remove/undo a previous replacement, invoke this method with nil for the 'shortLabel' parameter. + **/ +- (nullable NSString *)replacementStringForQueueLabel:(NSString *)longLabel; + +/** + * See the `replacementStringForQueueLabel:` description + */ +- (void)setReplacementString:(nullable NSString *)shortLabel forQueueLabel:(NSString *)longLabel; + +@end + +/** + * Category on `DDDispatchQueueLogFormatter` to make method declarations easier to extend/modify + **/ +@interface DDDispatchQueueLogFormatter (OverridableMethods) + +/** + * Date formatter default configuration + */ +- (void)configureDateFormatter:(NSDateFormatter *)dateFormatter; + +/** + * Formatter method to transfrom from date to string + */ +- (NSString *)stringFromDate:(NSDate *)date; + +/** + * Method to compute the queue thread label + */ +- (NSString *)queueThreadLabelForLogMessage:(DDLogMessage *)logMessage; + +@end + +#pragma mark - DDAtomicCountable + +__attribute__((deprecated("DDAtomicCountable is useless since DDDispatchQueueLogFormatter is always shareable now"))) +@protocol DDAtomicCountable + +- (instancetype)initWithDefaultValue:(int32_t)defaultValue; +- (int32_t)increment; +- (int32_t)decrement; +- (int32_t)value; + +@end + +__attribute__((deprecated("DDAtomicCountable is deprecated"))) +@interface DDAtomicCounter: NSObject +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDFileLogger+Buffering.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDFileLogger+Buffering.h new file mode 100644 index 0000000..f4cfcfc --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDFileLogger+Buffering.h @@ -0,0 +1,27 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DDFileLogger (Buffering) + +- (instancetype)wrapWithBuffer; +- (instancetype)unwrapFromBuffer; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDFileLogger.h b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDFileLogger.h new file mode 100644 index 0000000..e18973b --- /dev/null +++ b/ThirdPartyFrameworks/CocoaLumberjack.xcframework/ios-arm64/CocoaLumberjack.framework/Headers/DDFileLogger.h @@ -0,0 +1,532 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2022, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software in source and binary forms, +// with or without modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// * 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. + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import + +@class DDLogFileInfo; + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class provides a logger to write log statements to a file. + **/ + + +// Default configuration and safety/sanity values. +// +// maximumFileSize -> kDDDefaultLogMaxFileSize +// rollingFrequency -> kDDDefaultLogRollingFrequency +// maximumNumberOfLogFiles -> kDDDefaultLogMaxNumLogFiles +// logFilesDiskQuota -> kDDDefaultLogFilesDiskQuota +// +// You should carefully consider the proper configuration values for your application. + +extern unsigned long long const kDDDefaultLogMaxFileSize; +extern NSTimeInterval const kDDDefaultLogRollingFrequency; +extern NSUInteger const kDDDefaultLogMaxNumLogFiles; +extern unsigned long long const kDDDefaultLogFilesDiskQuota; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The LogFileManager protocol is designed to allow you to control all aspects of your log files. + * + * The primary purpose of this is to allow you to do something with the log files after they have been rolled. + * Perhaps you want to compress them to save disk space. + * Perhaps you want to upload them to an FTP server. + * Perhaps you want to run some analytics on the file. + * + * A default LogFileManager is, of course, provided. + * The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. + * + * This protocol provides various methods to fetch the list of log files. + * + * There are two variants: sorted and unsorted. + * If sorting is not necessary, the unsorted variant is obviously faster. + * The sorted variant will return an array sorted by when the log files were created, + * with the most recently created log file at index 0, and the oldest log file at the end of the array. + * + * You can fetch only the log file paths (full path including name), log file names (name only), + * or an array of `DDLogFileInfo` objects. + * The `DDLogFileInfo` class is documented below, and provides a handy wrapper that + * gives you easy access to various file attributes such as the creation date or the file size. + */ +@protocol DDLogFileManager +@required + +// Public properties + +/** + * The maximum number of archived log files to keep on disk. + * For example, if this property is set to 3, + * then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk. + * Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted. + * + * You may optionally disable this option by setting it to zero. + **/ +@property (readwrite, assign, atomic) NSUInteger maximumNumberOfLogFiles; + +/** + * The maximum space that logs can take. On rolling logfile all old log files that exceed logFilesDiskQuota will + * be deleted. + * + * You may optionally disable this option by setting it to zero. + **/ +@property (readwrite, assign, atomic) unsigned long long logFilesDiskQuota; + +// Public methods + +/** + * Returns the logs directory (path) + */ +@property (nonatomic, readonly, copy) NSString *logsDirectory; + +/** + * Returns an array of `NSString` objects, + * each of which is the filePath to an existing log file on disk. + **/ +@property (nonatomic, readonly, strong) NSArray *unsortedLogFilePaths; + +/** + * Returns an array of `NSString` objects, + * each of which is the fileName of an existing log file on disk. + **/ +@property (nonatomic, readonly, strong) NSArray *unsortedLogFileNames; + +/** + * Returns an array of `DDLogFileInfo` objects, + * each representing an existing log file on disk, + * and containing important information about the log file such as it's modification date and size. + **/ +@property (nonatomic, readonly, strong) NSArray *unsortedLogFileInfos; + +/** + * Just like the `unsortedLogFilePaths` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +@property (nonatomic, readonly, strong) NSArray *sortedLogFilePaths; + +/** + * Just like the `unsortedLogFileNames` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +@property (nonatomic, readonly, strong) NSArray *sortedLogFileNames; + +/** + * Just like the `unsortedLogFileInfos` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +@property (nonatomic, readonly, strong) NSArray *sortedLogFileInfos; + +// Private methods (only to be used by DDFileLogger) + +/** + * Generates a new unique log file path, and creates the corresponding log file. + * This method is executed directly on the file logger's internal queue. + * The file has to exist by the time the method returns. + **/ +- (nullable NSString *)createNewLogFileWithError:(NSError **)error; + +@optional + +// Private methods (only to be used by DDFileLogger) +/** + * Creates a new log file ignoring any errors. Deprecated in favor of `-createNewLogFileWithError:`. + * Will only be called if `-createNewLogFileWithError:` is not implemented. + **/ +- (nullable NSString *)createNewLogFile __attribute__((deprecated("Use -createNewLogFileWithError:"))) NS_SWIFT_UNAVAILABLE("Use -createNewLogFileWithError:"); + +// Notifications from DDFileLogger + +/// Called when a log file was archived. Executed on global queue with default priority. +/// @param logFilePath The path to the log file that was archived. +/// @param wasRolled Whether or not the archiving happend after rolling the log file. +- (void)didArchiveLogFile:(NSString *)logFilePath wasRolled:(BOOL)wasRolled NS_SWIFT_NAME(didArchiveLogFile(atPath:wasRolled:)); + +// Deprecated APIs +/** + * Called when a log file was archived. Executed on global queue with default priority. + */ +- (void)didArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME(didArchiveLogFile(atPath:)) __attribute__((deprecated("Use -didArchiveLogFile:wasRolled:"))); + +/** + * Called when the roll action was executed and the log was archived. + * Executed on global queue with default priority. + */ +- (void)didRollAndArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME(didRollAndArchiveLogFile(atPath:)) __attribute__((deprecated("Use -didArchiveLogFile:wasRolled:"))); + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Default log file manager. + * + * All log files are placed inside the logsDirectory. + * If a specific logsDirectory isn't specified, the default directory is used. + * On Mac, this is in `~/Library/Logs/`. + * On iPhone, this is in `~/Library/Caches/Logs`. + * + * Log files are named `"