diff --git a/CHANGELOG.md b/CHANGELOG.md index dd145896f..5870bb8ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## 2.12.2 - 2024-04-24 + +[IMPROVED] Support for pending payments +[IMPROVED] Upgraded payments to StoreKit 2 +[IMPROVED] Upgraded OpenVPN and OpenSSL libraries +[NOTE] Removed support for iOS 14 + ## 2.12.1 - 2024-02-24 [FIXED] In-app payments for legacy accounts diff --git a/IVPNClient.xcodeproj/project.pbxproj b/IVPNClient.xcodeproj/project.pbxproj index 5cd4e16c3..623991b6c 100644 --- a/IVPNClient.xcodeproj/project.pbxproj +++ b/IVPNClient.xcodeproj/project.pbxproj @@ -60,6 +60,7 @@ 8223C54F22EAEC7000CD283D /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8223C54B22E9E93A00CD283D /* Session.swift */; }; 8223C55022EAEC7100CD283D /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8223C54B22E9E93A00CD283D /* Session.swift */; }; 822563922431E03A00AE7F8D /* AccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 822563912431E03A00AE7F8D /* AccountView.swift */; }; + 8228C8D22B1DE906005977D3 /* PurchaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8228C8D12B1DE906005977D3 /* PurchaseManager.swift */; }; 8229196E2182EB1C00978BBA /* String+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8229196D2182EB1B00978BBA /* String+Ext.swift */; }; 822919712182EB1C00978BBA /* String+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8229196D2182EB1B00978BBA /* String+Ext.swift */; }; 822920A02480FA3600476FC1 /* ServersSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8229209F2480FA3600476FC1 /* ServersSort.swift */; }; @@ -93,8 +94,7 @@ 824777EA21A6BC3A001EEFAF /* Network+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824777E621A6BC3A001EEFAF /* Network+CoreDataProperties.swift */; }; 8247A5ED215D037600E8D680 /* UserDefaults+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825A43FC215CCFE70076131F /* UserDefaults+Ext.swift */; }; 8247C0602A7CF54300A7C02F /* V2RayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8247C05F2A7CF54300A7C02F /* V2RayConfig.swift */; }; - 8247E1DA22686217006C0C08 /* IAPManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8247E1D922686217006C0C08 /* IAPManager.swift */; }; - 8247E1DE22687C28006C0C08 /* ProductIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8247E1DD22687C28006C0C08 /* ProductIdentifier.swift */; }; + 8247E1DE22687C28006C0C08 /* ProductId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8247E1DD22687C28006C0C08 /* ProductId.swift */; }; 82486FAD2A277058009B53F4 /* liboqs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 82486FAC2A277058009B53F4 /* liboqs.a */; }; 824B141C2609D5E700766B05 /* DNSProtocolTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824B141B2609D5E700766B05 /* DNSProtocolTypeTests.swift */; }; 824B86B926D3D16100D0101A /* FileManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824B86AD26D3D16100D0101A /* FileManager+Extension.swift */; }; @@ -108,6 +108,7 @@ 824BC466240906ED00A61B29 /* VPNStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 824BC465240906ED00A61B29 /* VPNStatusViewModel.swift */; }; 82526BEF24123D2900E00880 /* NetworkViewTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82526BEE24123D2900E00880 /* NetworkViewTableCell.swift */; }; 8252747E21F1F80400D4B8B5 /* ServerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8252747D21F1F80400D4B8B5 /* ServerViewController.swift */; }; + 825443982B2A1B8F00D77095 /* Store.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 825443972B2A1B8F00D77095 /* Store.storekit */; }; 82555005220ACAAF004763A7 /* VPNServersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82555004220ACAAF004763A7 /* VPNServersTests.swift */; }; 82589A2B21FB5A580009CC6C /* UIImage+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82589A2A21FB5A580009CC6C /* UIImage+Ext.swift */; }; 825E834F25A327EB00938240 /* CaptchaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825E834E25A327EB00938240 /* CaptchaViewController.swift */; }; @@ -154,6 +155,7 @@ 828E9C95231E5780001E1FCF /* Data+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 828E9C94231E5780001E1FCF /* Data+Ext.swift */; }; 828E9C96231E5780001E1FCF /* Data+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 828E9C94231E5780001E1FCF /* Data+Ext.swift */; }; 8290195F243CB27500777B6E /* ControlPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8290195E243CB27500777B6E /* ControlPanelView.swift */; }; + 8292D0FC2B45AEE8001EA123 /* TunnelKit+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8292D0FB2B45AEE8001EA123 /* TunnelKit+Ext.swift */; }; 8292E19B21748B0500123538 /* UserDefaults+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 825A43FC215CCFE70076131F /* UserDefaults+Ext.swift */; }; 8292E1A72174C10700123538 /* Peer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8292E1A62174C10700123538 /* Peer.swift */; }; 8292E1A92174C11600123538 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8292E1A82174C11600123538 /* Interface.swift */; }; @@ -194,7 +196,6 @@ 82A6D74A24A3780B00D6C0E1 /* ConnectToServerPopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82A6D74924A3780B00D6C0E1 /* ConnectToServerPopupView.swift */; }; 82A7F10523C8661B0015A357 /* ServiceStatusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82A7F10423C8661B0015A357 /* ServiceStatusTests.swift */; }; 82AA8818231E330A00E18ECB /* SessionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AA8817231E330A00E18ECB /* SessionStatus.swift */; }; - 82AAF0E92253A4A8005E792F /* StaticWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AAF0E82253A4A8005E792F /* StaticWebViewController.swift */; }; 82AB0875291A6B5F0084625A /* AddCustomPortViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB0874291A6B5F0084625A /* AddCustomPortViewController.swift */; }; 82AB0877291A6B9C0084625A /* CustomPort+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB0876291A6B9C0084625A /* CustomPort+CoreDataClass.swift */; }; 82AB0879291A6BB90084625A /* CustomPort+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82AB0878291A6BB90084625A /* CustomPort+CoreDataProperties.swift */; }; @@ -498,6 +499,7 @@ 8223C54B22E9E93A00CD283D /* Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; 8223C54D22EAE93F00CD283D /* SessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManager.swift; sourceTree = ""; }; 822563912431E03A00AE7F8D /* AccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountView.swift; sourceTree = ""; }; + 8228C8D12B1DE906005977D3 /* PurchaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseManager.swift; sourceTree = ""; }; 8229196D2182EB1B00978BBA /* String+Ext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Ext.swift"; sourceTree = ""; }; 8229209F2480FA3600476FC1 /* ServersSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSort.swift; sourceTree = ""; }; 822B85D821B941A200715691 /* NotificationName+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Ext.swift"; sourceTree = ""; }; @@ -523,8 +525,7 @@ 824777E521A6BC3A001EEFAF /* Network+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Network+CoreDataClass.swift"; sourceTree = ""; }; 824777E621A6BC3A001EEFAF /* Network+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Network+CoreDataProperties.swift"; sourceTree = ""; }; 8247C05F2A7CF54300A7C02F /* V2RayConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = V2RayConfig.swift; sourceTree = ""; }; - 8247E1D922686217006C0C08 /* IAPManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPManager.swift; sourceTree = ""; }; - 8247E1DD22687C28006C0C08 /* ProductIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductIdentifier.swift; sourceTree = ""; }; + 8247E1DD22687C28006C0C08 /* ProductId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductId.swift; sourceTree = ""; }; 82486FAC2A277058009B53F4 /* liboqs.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = liboqs.a; sourceTree = ""; }; 82486FB02A27705F009B53F4 /* sha3x4.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = sha3x4.h; sourceTree = ""; }; 82486FB12A27705F009B53F4 /* oqsconfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = oqsconfig.h; sourceTree = ""; }; @@ -547,6 +548,7 @@ 824F56072233FE6F00BCDD5C /* libwg-go.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libwg-go.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 82526BEE24123D2900E00880 /* NetworkViewTableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkViewTableCell.swift; sourceTree = ""; }; 8252747D21F1F80400D4B8B5 /* ServerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerViewController.swift; sourceTree = ""; }; + 825443972B2A1B8F00D77095 /* Store.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Store.storekit; sourceTree = ""; }; 82555004220ACAAF004763A7 /* VPNServersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNServersTests.swift; sourceTree = ""; }; 8258649C2237A0830081DC4B /* SDCAlertView.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDCAlertView.framework; path = ../Carthage/Build/iOS/SDCAlertView.framework; sourceTree = ""; }; 825864A02237B1060081DC4B /* Bamboo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Bamboo.framework; path = ../Carthage/Build/iOS/Bamboo.framework; sourceTree = ""; }; @@ -588,6 +590,7 @@ 828D8A6C258245AD00CB0E5B /* TwoFactorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoFactorViewController.swift; sourceTree = ""; }; 828E9C94231E5780001E1FCF /* Data+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Ext.swift"; sourceTree = ""; }; 8290195E243CB27500777B6E /* ControlPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlPanelView.swift; sourceTree = ""; }; + 8292D0FB2B45AEE8001EA123 /* TunnelKit+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TunnelKit+Ext.swift"; sourceTree = ""; }; 8292E1A62174C10700123538 /* Peer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Peer.swift; sourceTree = ""; }; 8292E1A82174C11600123538 /* Interface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = ""; }; 8292E1AA2174C12200123538 /* Tunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = ""; }; @@ -618,7 +621,6 @@ 82A9E8C323471EBE007BCA7E /* release.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = release.xcconfig; sourceTree = ""; }; 82A9E8C423471EBE007BCA7E /* staging.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = staging.xcconfig; sourceTree = ""; }; 82AA8817231E330A00E18ECB /* SessionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionStatus.swift; sourceTree = ""; }; - 82AAF0E82253A4A8005E792F /* StaticWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticWebViewController.swift; sourceTree = ""; }; 82AB0874291A6B5F0084625A /* AddCustomPortViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCustomPortViewController.swift; sourceTree = ""; }; 82AB0876291A6B9C0084625A /* CustomPort+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomPort+CoreDataClass.swift"; sourceTree = ""; }; 82AB0878291A6BB90084625A /* CustomPort+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomPort+CoreDataProperties.swift"; sourceTree = ""; }; @@ -1000,12 +1002,12 @@ 82D598BF21A6991B000FABDE /* NetworkManager.swift */, 9CB2CE371DB0F860007A4D2D /* FileSystemManager.swift */, 9C69422D1DD20FC200F9A801 /* KeyChain.swift */, - 8247E1D922686217006C0C08 /* IAPManager.swift */, 820535272302B9D7007BDD58 /* APIAccessManager.swift */, 8223C54D22EAE93F00CD283D /* SessionManager.swift */, 8206F32224367A240056B465 /* VPNErrorObserver.swift */, 825E836225A4834200938240 /* APIPublicKeyPin.swift */, 826C1F8325DBEF1800314C4B /* DNSManager.swift */, + 8228C8D12B1DE906005977D3 /* PurchaseManager.swift */, ); path = Managers; sourceTree = ""; @@ -1021,7 +1023,7 @@ 82C9739F217DFA9C00CE06D4 /* Host.swift */, 9C6942361DD218A900F9A801 /* AccessDetails.swift */, 9C3031341DB42EF900C38B0C /* Application.swift */, - 8247E1DD22687C28006C0C08 /* ProductIdentifier.swift */, + 8247E1DD22687C28006C0C08 /* ProductId.swift */, 9CB2CE1E1DAA5258007A4D2D /* Authentication.swift */, 9CBFF02F2102254800FE1757 /* Settings.swift */, 826C56D122FD4F2600D2B76A /* ServiceStatus.swift */, @@ -1156,6 +1158,7 @@ 82CE598F25ED3C7A0078099D /* URL+Ext.swift */, 82B329CA29F7C9F400F3ED9B /* UIWindow+Ext.swift */, 822BC6892A7CF3A700C733DF /* Decodable+Ext.swift */, + 8292D0FB2B45AEE8001EA123 /* TunnelKit+Ext.swift */, ); path = Extensions; sourceTree = ""; @@ -1260,7 +1263,6 @@ 8252747D21F1F80400D4B8B5 /* ServerViewController.swift */, 828772F8221C01C300D5E330 /* ServersConfigurationTableViewController.swift */, 8221377A2227E75E001E1BF5 /* CustomDNSViewController.swift */, - 82AAF0E82253A4A8005E792F /* StaticWebViewController.swift */, 8269CAC22264962F00CF488A /* AntiTrackerViewController.swift */, 82F917382344861A0025ED3A /* TermsOfServiceViewController.swift */, 8201A5032356536B008C83DB /* UpgradePlanViewController.swift */, @@ -1654,6 +1656,7 @@ 82FF0D4123153D1000440E5D /* Colors.xcassets */, 9CB2CE261DAA6C1B007A4D2D /* IVPNClient.entitlements */, 9CB2CE321DAF9283007A4D2D /* Model.xcdatamodeld */, + 825443972B2A1B8F00D77095 /* Store.storekit */, ); path = IVPNClient; sourceTree = ""; @@ -1993,6 +1996,7 @@ 9CDDD5B41D9D2F9F00D39924 /* Main.storyboard in Resources */, 9C2833741D9D3EB60024C553 /* Initial.storyboard in Resources */, 826470C42446F67100403A14 /* Signup.storyboard in Resources */, + 825443982B2A1B8F00D77095 /* Store.storekit in Resources */, 82FF0D4223153D1000440E5D /* Colors.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -2259,6 +2263,7 @@ 8277F1CD22118D08007C6F15 /* ProofsViewModel.swift in Sources */, 8269CAC32264962F00CF488A /* AntiTrackerViewController.swift in Sources */, 82E5449224EE584E006DEF8D /* UIImageView+Ext.swift in Sources */, + 8292D0FC2B45AEE8001EA123 /* TunnelKit+Ext.swift in Sources */, 82E81AE72449C44F00D81FB7 /* PaymentComponentView.swift in Sources */, 8232FBF62240E40F006B81D2 /* Error+Ext.swift in Sources */, 820203932186EE0E00D756AA /* WireGuardSettingsViewController.swift in Sources */, @@ -2270,7 +2275,6 @@ 8243584425DAA7BD005FDEBB /* SecureDNSViewController.swift in Sources */, 82BBF26D21AE95C000589766 /* UIApplication+Ext.swift in Sources */, 82B329CB29F7C9F400F3ED9B /* UIWindow+Ext.swift in Sources */, - 8247E1DA22686217006C0C08 /* IAPManager.swift in Sources */, 82C973A0217DFA9C00CE06D4 /* Host.swift in Sources */, 82A160BA221C4E2000730577 /* Server+CoreDataClass.swift in Sources */, 9CB2CE381DB0F860007A4D2D /* FileSystemManager.swift in Sources */, @@ -2319,7 +2323,6 @@ 823FFB072338DF1800F91A5D /* Capability.swift in Sources */, 82061F65238D2730009DDF4D /* Ping.swift in Sources */, 82AB0879291A6BB90084625A /* CustomPort+CoreDataProperties.swift in Sources */, - 82AAF0E92253A4A8005E792F /* StaticWebViewController.swift in Sources */, 8282482A225C7312001314F8 /* WireGuardRegenerationRateCell.swift in Sources */, 824777EA21A6BC3A001EEFAF /* Network+CoreDataProperties.swift in Sources */, 82C34D6E26FB02F900F06016 /* WireGuardEndpoint.swift in Sources */, @@ -2357,6 +2360,7 @@ 8206E5D022967E37003119AF /* UserActivityType.swift in Sources */, 82A6D74A24A3780B00D6C0E1 /* ConnectToServerPopupView.swift in Sources */, 828772FB221C28E000D5E330 /* FlagImageView.swift in Sources */, + 8228C8D22B1DE906005977D3 /* PurchaseManager.swift in Sources */, 82E7880C22B0DA0D00A98D76 /* NETunnelProviderProtocol+Ext.swift in Sources */, 82968A35298A98C300077E0A /* KeyChain.swift in Sources */, 82F638C2217DA89000410318 /* AddressType.swift in Sources */, @@ -2375,7 +2379,7 @@ 821CA2D7287C5AB20067F70D /* PortViewController.swift in Sources */, 82061F66238D2730009DDF4D /* ICMPHeader.swift in Sources */, 824BC466240906ED00A61B29 /* VPNStatusViewModel.swift in Sources */, - 8247E1DE22687C28006C0C08 /* ProductIdentifier.swift in Sources */, + 8247E1DE22687C28006C0C08 /* ProductId.swift in Sources */, 829DF2822497953C000DC2DB /* UIButton+Ext.swift in Sources */, 82234B6721BA7F3500B082DE /* Logger.swift in Sources */, 82DB75EC239E75EB0073E846 /* NEVPNStatus+Ext.swift in Sources */, @@ -2525,7 +2529,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = UnitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2556,7 +2560,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = UnitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2592,7 +2596,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "wireguard-tunnel-provider/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2632,7 +2636,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "wireguard-tunnel-provider/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2664,7 +2668,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = UITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2693,7 +2697,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = UITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2721,7 +2725,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = UITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2788,7 +2792,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = ""; @@ -2808,7 +2812,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = IVPNClient/IVPNClient.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = WQXXM75BYN; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -2817,7 +2821,7 @@ ); HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "$(SRCROOT)/IVPNClient/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2826,7 +2830,7 @@ "$(inherited)", "$(PROJECT_DIR)/IVPNClient/liboqs", ); - MARKETING_VERSION = 2.12.1; + MARKETING_VERSION = 2.12.2; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "-D DEBUG"; @@ -2853,7 +2857,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = UnitTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2899,7 +2903,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "openvpn-tunnel-provider/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -2942,7 +2946,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "wireguard-tunnel-provider/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", @@ -2981,7 +2985,7 @@ INFOPLIST_FILE = IVPNWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IVPNWidget; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 IVPN. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -3016,7 +3020,7 @@ INFOPLIST_FILE = IVPNWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IVPNWidget; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 IVPN. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; @@ -3050,7 +3054,7 @@ INFOPLIST_FILE = IVPNWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = IVPNWidget; INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 IVPN. All rights reserved."; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited)"; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; @@ -3094,7 +3098,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "openvpn-tunnel-provider/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3146,7 +3150,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "openvpn-tunnel-provider/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3220,7 +3224,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = ""; @@ -3277,7 +3281,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_FILE = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_NAME = ""; SDKROOT = iphoneos; @@ -3297,7 +3301,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = IVPNClient/IVPNClient.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = WQXXM75BYN; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -3306,7 +3310,7 @@ ); HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "$(SRCROOT)/IVPNClient/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3315,7 +3319,7 @@ "$(inherited)", "$(PROJECT_DIR)/IVPNClient/liboqs", ); - MARKETING_VERSION = 2.12.1; + MARKETING_VERSION = 2.12.2; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "-D DEBUG"; @@ -3336,7 +3340,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = IVPNClient/IVPNClient.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = WQXXM75BYN; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -3345,7 +3349,7 @@ ); HEADER_SEARCH_PATHS = "\"$(SRCROOT)/IVPNClient/liboqs/include\""; INFOPLIST_FILE = "$(SRCROOT)/IVPNClient/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -3354,7 +3358,7 @@ "$(inherited)", "$(PROJECT_DIR)/IVPNClient/liboqs", ); - MARKETING_VERSION = 2.12.1; + MARKETING_VERSION = 2.12.2; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "-D RELEASE"; @@ -3464,7 +3468,7 @@ repositoryURL = "https://github.com/passepartoutvpn/tunnelkit"; requirement = { kind = exactVersion; - version = 4.1.0; + version = 6.2.0; }; }; 829F5FC529A13CAE009E1AD3 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { diff --git a/IVPNClient.xcodeproj/xcshareddata/xcschemes/IVPNClient.xcscheme b/IVPNClient.xcodeproj/xcshareddata/xcschemes/IVPNClient.xcscheme index b86e17588..2d24c5fc7 100644 --- a/IVPNClient.xcodeproj/xcshareddata/xcschemes/IVPNClient.xcscheme +++ b/IVPNClient.xcodeproj/xcshareddata/xcschemes/IVPNClient.xcscheme @@ -109,6 +109,9 @@ isEnabled = "YES"> + + Bool { evaluateUITests() registerUserDefaults() - finishIncompletePurchases() createLogFiles() resetLastPingTimestamp() clearURLCache() + startPurchaseObserver() DNSManager.shared.loadProfile { _ in } return true @@ -402,3 +393,49 @@ extension AppDelegate: UIApplicationDelegate { } } + +// MARK: - PurchaseManagerDelegate - + +extension AppDelegate: PurchaseManagerDelegate { + + func purchaseStart() { + + } + + func purchasePending() { + DispatchQueue.main.async { + guard let viewController = UIApplication.topViewController() else { + return + } + + viewController.showAlert(title: "Pending payment", message: "Payment is pending for approval. We will complete the transaction as soon as payment is approved.") + } + } + + func purchaseSuccess(activeUntil: String, extended: Bool) { + guard extended else { + return + } + + DispatchQueue.main.async { + guard let viewController = UIApplication.topViewController() else { + return + } + + viewController.showSubscriptionActivatedAlert(activeUntil: activeUntil) + } + } + + func purchaseError(error: Any?) { + DispatchQueue.main.async { + guard let viewController = UIApplication.topViewController() else { + return + } + + if let error = error as? ErrorResult { + viewController.showErrorAlert(title: "Error", message: error.message) + } + } + } + +} diff --git a/IVPNClient/Config/Config.swift b/IVPNClient/Config/Config.swift index 59d11fb73..df789cff6 100644 --- a/IVPNClient/Config/Config.swift +++ b/IVPNClient/Config/Config.swift @@ -37,10 +37,10 @@ struct Config { static let apiSessionDelete = "/v4/session/delete" static let apiSessionWGKeySet = "/v4/session/wg/set" static let apiAccountNew = "/v4/account/new" - static let apiPaymentInitial = "/v4/account/payment/ios/initial" - static let apiPaymentAdd = "/v4/account/payment/ios/add" - static let apiPaymentAddLegacy = "/v2/mobile/ios/subscription-purchased" - static let apiPaymentRestore = "/v4/account/payment/ios/restore" + static let apiPaymentInitial = "/v5/account/payment/ios/initial" + static let apiPaymentAdd = "/v5/account/payment/ios/add" + static let apiPaymentRestore = "/v5/account/payment/ios/restore" + static let apiPaymentAddLegacy = "/v2/mobile/ios/subscription-purchased-v2" static let urlTypeLogin = "login" static let urlTypeConnect = "connect" @@ -109,4 +109,8 @@ struct Config { return value } + // MARK: Log files + + static let maxBytes = 100000 + } diff --git a/IVPNClient/Enums/ApiResults/SessionStatus.swift b/IVPNClient/Enums/ApiResults/SessionStatus.swift index 6af241eca..37a1057ae 100644 --- a/IVPNClient/Enums/ApiResults/SessionStatus.swift +++ b/IVPNClient/Enums/ApiResults/SessionStatus.swift @@ -26,6 +26,7 @@ import Foundation struct SessionStatus: Decodable { let status: Int let deviceName: String? + let extended: Bool? let serviceStatus: ServiceStatus var serviceActive: Bool { diff --git a/IVPNClient/Managers/IAPManager.swift b/IVPNClient/Managers/IAPManager.swift deleted file mode 100644 index ce9225166..000000000 --- a/IVPNClient/Managers/IAPManager.swift +++ /dev/null @@ -1,292 +0,0 @@ -// -// IAPManager.swift -// IVPN iOS app -// https://github.com/ivpn/ios-app -// -// Created by Juraj Hilje on 2019-04-18. -// Copyright (c) 2023 IVPN Limited. -// -// This file is part of the IVPN iOS app. -// -// The IVPN iOS app is free software: you can redistribute it and/or -// modify it under the terms of the GNU General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any later version. -// -// The IVPN iOS app is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -// details. -// -// You should have received a copy of the GNU General Public License -// along with the IVPN iOS app. If not, see . -// - -import StoreKit -import SwiftyStoreKit - -class IAPManager { - - // MARK: - Properties - - - static let shared = IAPManager() - var products: [SKProduct] = [] - - var canMakePurchases: Bool { - return SKPaymentQueue.canMakePayments() - } - - private var apiEndpoint: String { - if KeyChain.sessionToken != nil { - if !Application.shared.serviceStatus.isNewStyleAccount() { - return Config.apiPaymentAddLegacy - } - - return Config.apiPaymentAdd - } - - return Config.apiPaymentInitial - } - - // MARK: - Methods - - - func fetchProducts(completion: @escaping ([SKProduct]?, String?) -> Void) { - SwiftyStoreKit.retrieveProductsInfo(ProductIdentifier.all) { result in - if !result.retrievedProducts.isEmpty { - self.products = Array(result.retrievedProducts) - completion(Array(result.retrievedProducts), nil) - log(.info, message: "Products successfully fetched from App Store.") - } else if !result.invalidProductIDs.isEmpty { - completion(nil, "Invalid product identifier") - log(.info, message: "Invalid App Store product identifier.") - } else { - completion(nil, String(describing: result.error)) - log(.info, message: "There was an error with fetching products from App Store.") - } - } - } - - func purchaseProduct(identifier: String, completion: @escaping (PurchaseDetails?, String?) -> Void) { - SwiftyStoreKit.purchaseProduct(identifier, quantity: 1, atomically: false) { result in - switch result { - case .success(let purchase): - completion(purchase, nil) - log(.info, message: "Product was successfully purchased.") - case .error(let error): - switch error.code { - case .unknown: completion(nil, "Unknown error. Please contact support") - case .clientInvalid: completion(nil, "Not allowed to make the payment") - case .paymentCancelled: completion(nil, "Payment cancelled") - case .paymentInvalid: completion(nil, "The purchase identifier was invalid") - case .paymentNotAllowed: completion(nil, "The device is not allowed to make the payment") - case .storeProductNotAvailable: completion(nil, "The product is not available in the current storefront") - case .cloudServicePermissionDenied: completion(nil, "Access to cloud service information is not allowed") - case .cloudServiceNetworkConnectionFailed: completion(nil, "Could not connect to the network") - case .cloudServiceRevoked: completion(nil, "User has revoked permission to use this cloud service") - default: completion(nil, (error as NSError).localizedDescription) - } - log(.error, message: "There was an error with purchase.") - } - } - } - - func finishIncompletePurchases(completion: @escaping (ServiceStatus?, ErrorResult?) -> Void) { - SwiftyStoreKit.completeTransactions(atomically: false) { products in - self.completePurchases(products: products, endpoint: self.apiEndpoint) { serviceStatus, error in - completion(serviceStatus, error) - } - } - } - - func restorePurchases(completion: @escaping (Account?, ErrorResult?) -> Void) { - SwiftyStoreKit.restorePurchases(atomically: false) { results in - if results.restoreFailedPurchases.count > 0 { - if let restoreError = results.restoreFailedPurchases.first { - let error = ErrorResult(status: 500, message: restoreError.0.localizedDescription) - completion(nil, error) - log(.error, message: restoreError.0.localizedDescription) - return - } - - let error = ErrorResult(status: 500, message: "Unknown error") - completion(nil, error) - log(.error, message: "Unknown error") - } else if results.restoredPurchases.count > 0 { - var purchases = results.restoredPurchases - purchases.sort { $0.transaction.transactionDate! > $1.transaction.transactionDate! } - self.completeRestoredPurchase(purchase: purchases.first!) { account, error in - completion(account, error) - log(.info, message: "Purchases are restored.") - } - } else { - let error = ErrorResult(status: 500, message: "There are no purchases to restore.") - completion(nil, error) - log(.error, message: "There are no purchases to restore.") - } - } - } - - func completePurchase(purchase: PurchaseDetails, completion: @escaping (ServiceStatus?, ErrorResult?) -> Void) { - let endpoint = apiEndpoint - let params = purchaseParams(purchase: purchase, endpoint: endpoint) - let request = ApiRequestDI(method: .post, endpoint: endpoint, params: params) - - ApiService.shared.requestCustomError(request) { (result: ResultCustomError) in - switch result { - case .success(let sessionStatus): - if purchase.needsFinishTransaction { - SwiftyStoreKit.finishTransaction(purchase.transaction) - } - Application.shared.serviceStatus = sessionStatus.serviceStatus - completion(sessionStatus.serviceStatus, nil) - log(.info, message: "Purchase was successfully finished.") - case .failure(let error): - let defaultErrorResult = ErrorResult(status: 500, message: "Purchase was completed but service cannot be activated. Restart application to retry.") - completion(nil, error ?? defaultErrorResult) - log(.error, message: "There was an error with purchase completion: \(error?.message ?? "")") - } - } - } - - func completePurchases(products: [Purchase], endpoint: String, completion: @escaping (ServiceStatus?, ErrorResult?) -> Void) { - if let product = products.last { - log(.info, message: "Found incomplete purchase. Completing purchase...") - - switch product.transaction.transactionState { - case .purchased, .restored: - if product.needsFinishTransaction { - let params = finishPurchaseParams(product: product, endpoint: endpoint) - let request = ApiRequestDI(method: .post, endpoint: endpoint, params: params) - - ApiService.shared.requestCustomError(request) { (result: ResultCustomError) in - switch result { - case .success(let sessionStatus): - SwiftyStoreKit.finishTransaction(product.transaction) - Application.shared.serviceStatus = sessionStatus.serviceStatus - completion(sessionStatus.serviceStatus, nil) - log(.info, message: "Purchase was successfully finished.") - case .failure(let error): - let defaultErrorResult = ErrorResult(status: 500, message: "Purchase was completed but service cannot be activated. Restart application to retry.") - completion(nil, error ?? defaultErrorResult) - log(.error, message: "There was an error with purchase completion: \(error?.message ?? "")") - } - } - } - case .failed, .purchasing, .deferred: - break - @unknown default: - break - } - } - } - - func completeRestoredPurchase(purchase: Purchase, completion: @escaping (Account?, ErrorResult?) -> Void) { - let params = restorePurchaseParams() - let request = ApiRequestDI(method: .post, endpoint: Config.apiPaymentRestore, params: params) - - ApiService.shared.requestCustomError(request) { (result: ResultCustomError) in - switch result { - case .success(let account): - if purchase.needsFinishTransaction { - SwiftyStoreKit.finishTransaction(purchase.transaction) - } - KeyChain.username = account.accountId - completion(account, nil) - log(.info, message: "Purchase was successfully finished.") - case .failure(let error): - let defaultErrorResult = ErrorResult(status: 500, message: "Purchase was restored but service cannot be activated. Restart application to retry.") - completion(nil, error ?? defaultErrorResult) - log(.error, message: "There was an error with purchase completion: \(error?.message ?? "")") - } - } - } - - func getProduct(identifier: String) -> SKProduct? { - for product in products where product.productIdentifier == identifier { - return product - } - - return nil - } - - func productPrice(product: SKProduct) -> String { - let formatter = NumberFormatter() - formatter.numberStyle = .currency - formatter.locale = product.priceLocale - - return formatter.string(from: product.price) ?? "" - } - - func completeTransactions() { - SwiftyStoreKit.completeTransactions(atomically: true) { _ in } - } - - // MARK: - Private methods - - - private func purchaseParams(purchase: PurchaseDetails, endpoint: String) -> [URLQueryItem] { - let transactionId = purchase.transaction.transactionIdentifier ?? "Unknown transaction ID" - let base64receipt = SwiftyStoreKit.localReceiptData?.base64EncodedString(options: []) ?? "" - - switch endpoint { - case Config.apiPaymentInitial: - return [ - URLQueryItem(name: "account_id", value: KeyChain.tempUsername ?? ""), - URLQueryItem(name: "product_id", value: purchase.product.productIdentifier), - URLQueryItem(name: "transaction_id", value: transactionId), - URLQueryItem(name: "receipt", value: base64receipt) - ] - case Config.apiPaymentAdd: - return [ - URLQueryItem(name: "session_token", value: KeyChain.sessionToken ?? ""), - URLQueryItem(name: "product_id", value: purchase.product.productIdentifier), - URLQueryItem(name: "transaction_id", value: transactionId), - URLQueryItem(name: "receipt", value: base64receipt) - ] - case Config.apiPaymentAddLegacy: - return [ - URLQueryItem(name: "username", value: KeyChain.username ?? ""), - URLQueryItem(name: "productId", value: purchase.product.productIdentifier), - URLQueryItem(name: "transactionId", value: transactionId), - URLQueryItem(name: "receiptData", value: base64receipt) - ] - default: - return [] - } - } - - private func finishPurchaseParams(product: Purchase, endpoint: String) -> [URLQueryItem] { - let transactionId = product.transaction.transactionIdentifier ?? "" - let base64receipt = SwiftyStoreKit.localReceiptData?.base64EncodedString(options: []) ?? "" - - switch endpoint { - case Config.apiPaymentInitial: - return [ - URLQueryItem(name: "account_id", value: KeyChain.tempUsername ?? ""), - URLQueryItem(name: "product_id", value: product.productId), - URLQueryItem(name: "transaction_id", value: transactionId), - URLQueryItem(name: "receipt", value: base64receipt) - ] - case Config.apiPaymentAdd: - return [ - URLQueryItem(name: "session_token", value: KeyChain.sessionToken ?? ""), - URLQueryItem(name: "product_id", value: product.productId), - URLQueryItem(name: "transaction_id", value: transactionId), - URLQueryItem(name: "receipt", value: base64receipt) - ] - case Config.apiPaymentAddLegacy: - return [ - URLQueryItem(name: "username", value: KeyChain.username ?? ""), - URLQueryItem(name: "productId", value: product.productId), - URLQueryItem(name: "transactionId", value: transactionId), - URLQueryItem(name: "receiptData", value: base64receipt) - ] - default: - return [] - } - } - - private func restorePurchaseParams() -> [URLQueryItem] { - let base64receipt = SwiftyStoreKit.localReceiptData?.base64EncodedString(options: []) ?? "" - return [URLQueryItem(name: "receipt", value: base64receipt)] - } - -} diff --git a/IVPNClient/Managers/NavigationManager.swift b/IVPNClient/Managers/NavigationManager.swift index 932a03a8c..20d16820c 100644 --- a/IVPNClient/Managers/NavigationManager.swift +++ b/IVPNClient/Managers/NavigationManager.swift @@ -87,15 +87,6 @@ class NavigationManager { return navController! } - static func getStaticWebViewController(resourceName: String, screenTitle: String) -> UIViewController { - let storyBoard = UIStoryboard(name: "Main", bundle: nil) - let viewController = storyBoard.instantiateViewController(withIdentifier: "staticWebView") as! StaticWebViewController - viewController.resourceName = resourceName - viewController.screenTitle = screenTitle - - return viewController - } - static func getTermsOfServiceViewController() -> UIViewController { let storyBoard = UIStoryboard(name: "Initial", bundle: nil) diff --git a/IVPNClient/Managers/PurchaseManager.swift b/IVPNClient/Managers/PurchaseManager.swift new file mode 100644 index 000000000..f0fe61c43 --- /dev/null +++ b/IVPNClient/Managers/PurchaseManager.swift @@ -0,0 +1,246 @@ +// +// PurchaseManager.swift +// IVPN iOS app +// https://github.com/ivpn/ios-app +// +// Created by Juraj Hilje on 2023-12-04. +// Copyright (c) 2023 IVPN Limited. +// +// This file is part of the IVPN iOS app. +// +// The IVPN iOS app is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// The IVPN iOS app is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License +// along with the IVPN iOS app. If not, see . +// + +import StoreKit + +@objc protocol PurchaseManagerDelegate: AnyObject { + func purchaseStart() + func purchasePending() + func purchaseSuccess(activeUntil: String, extended: Bool) + func purchaseError(error: Any?) +} + +class PurchaseManager: NSObject { + + // MARK: - Properties - + + static let shared = PurchaseManager() + + weak var delegate: PurchaseManagerDelegate? + + var canMakePurchases: Bool { + return SKPaymentQueue.canMakePayments() + } + + var observerTask: Task? = nil + + private(set) var products: [Product] = [] + + private var apiEndpoint: String { + guard let _ = KeyChain.sessionToken else { + return Config.apiPaymentInitial + } + + return Application.shared.serviceStatus.isNewStyleAccount() ? Config.apiPaymentAdd : Config.apiPaymentAddLegacy + } + + deinit { + stopObserver() + } + + // MARK: - Methods - + + func loadProducts() async throws { + products = try await Product.products(for: ProductId.all) + } + + func getProduct(id: String) -> Product? { + for product in products where product.id == id { + return product + } + + return nil + } + + func purchase(_ productId: String) async throws -> Product.PurchaseResult? { + guard let product = getProduct(id: productId) else { + return nil + } + + delegate?.purchaseStart() + let result = try await product.purchase() + + switch result { + case let .success(.verified(transaction)): + // Successful purchase + log(.info, message: "[Store] Completing successful in-app purchase \(productId)") + self.complete(transaction) + break + case .success(.unverified(_, _)): + // Successful purchase but transaction/receipt can't be verified + // Could be a jailbroken phone + log(.info, message: "[Store] Purchase \(productId): success, unverified") + delegate?.purchaseError(error: ErrorResult(status: 500, message: "Purchase is unverified.")) + break + case .pending: + // Transaction waiting on SCA (Strong Customer Authentication) or + // approval from Ask to Buy + log(.info, message: "[Store] Purchase \(productId): pending") + delegate?.purchasePending() + break + case .userCancelled: + // ^^^ + log(.info, message: "[Store] Purchase \(productId): userCancelled") + delegate?.purchaseError(error: ErrorResult(status: 500, message: "User canelled the purchase.")) + break + @unknown default: + break + } + + return result + } + + func startObserver() { + observerTask = Task(priority: .background) { + for await result in Transaction.updates { + guard case .verified(let transaction) = result else { + continue + } + + if transaction.revocationDate == nil { + log(.info, message: "[Store] Completing unfinished purchase \(transaction.productID)") + complete(transaction) + } + } + } + } + + func stopObserver() { + observerTask?.cancel() + } + + func restorePurchases(completion: @escaping (Account?, ErrorResult?) -> Void) { + Task { + for await result in Transaction.currentEntitlements { + guard case .verified(let transaction) = result else { + continue + } + + if transaction.revocationDate == nil { + self.getAccountFor(transaction: transaction) { account, error in + completion(account, error) + } + return + } + } + + let error = ErrorResult(status: 500, message: "There are no purchases to restore.") + log(.error, message: "[Store] There are no purchases to restore") + completion(nil, error) + } + } + + func complete(_ transaction: Transaction) { + let defaultError = ErrorResult(status: 500, message: "Purchase was completed but service cannot be activated. Restart application to retry.") + let endpoint = apiEndpoint + + guard let params = purchaseParams(transaction: transaction, endpoint: endpoint) else { + delegate?.purchaseError(error: defaultError) + return + } + + let request = ApiRequestDI(method: .post, endpoint: endpoint, params: params) + + ApiService.shared.requestCustomError(request) { (result: ResultCustomError) in + switch result { + case .success(let sessionStatus): + Application.shared.serviceStatus = sessionStatus.serviceStatus + Task { + await transaction.finish() + self.delegate?.purchaseSuccess(activeUntil: sessionStatus.serviceStatus.activeUntilString(), extended: sessionStatus.extended ?? !sessionStatus.serviceStatus.isNewStyleAccount()) + log(.info, message: "[Store] Purchase \(transaction.productID) completed successfully") + } + case .failure(let error): + self.delegate?.purchaseError(error: error ?? defaultError) + log(.error, message: "[Store] There was an error with purchase completion: \(error?.message ?? "")") + } + } + } + + // MARK: - Private methods - + + private func getAccountFor(transaction: Transaction, completion: @escaping (Account?, ErrorResult?) -> Void) { + let defaultError = ErrorResult(status: 500, message: "Purchase was restored but service cannot be activated. Restart application to retry.") + guard let params = restorePurchaseParams(transaction) else { + completion(nil, defaultError) + return + } + + let request = ApiRequestDI(method: .post, endpoint: Config.apiPaymentRestore, params: params) + + ApiService.shared.requestCustomError(request) { (result: ResultCustomError) in + switch result { + case .success(let account): + KeyChain.username = account.accountId + completion(account, nil) + log(.info, message: "[Store] Purchase restored successfully") + case .failure(let error): + completion(nil, error ?? defaultError) + log(.error, message: "[Store] There was an error with restoring purchase: \(error?.message ?? "")") + } + } + } + + private func purchaseParams(transaction: Transaction, endpoint: String) -> [URLQueryItem]? { + let productId = transaction.productID + let transactionId = String(transaction.id) + + switch endpoint { + case Config.apiPaymentInitial: + guard let tempUsername = KeyChain.tempUsername else { + return nil + } + return [ + URLQueryItem(name: "account_id", value: tempUsername), + URLQueryItem(name: "product_id", value: productId), + URLQueryItem(name: "transaction_id", value: transactionId) + ] + case Config.apiPaymentAdd: + guard let sessionToken = KeyChain.sessionToken else { + return nil + } + return [ + URLQueryItem(name: "session_token", value: sessionToken), + URLQueryItem(name: "product_id", value: productId), + URLQueryItem(name: "transaction_id", value: transactionId) + ] + case Config.apiPaymentAddLegacy: + guard let username = KeyChain.username else { + return nil + } + return [ + URLQueryItem(name: "username", value: username), + URLQueryItem(name: "productId", value: productId), + URLQueryItem(name: "transactionId", value: transactionId) + ] + default: + return nil + } + } + + private func restorePurchaseParams(_ transaction: Transaction) -> [URLQueryItem]? { + let transactionId = String(transaction.id) + return [URLQueryItem(name: "transaction_id", value: transactionId)] + } + +} diff --git a/IVPNClient/Managers/VPNManager.swift b/IVPNClient/Managers/VPNManager.swift index 08d39beaa..a4609ffb8 100644 --- a/IVPNClient/Managers/VPNManager.swift +++ b/IVPNClient/Managers/VPNManager.swift @@ -130,10 +130,8 @@ class VPNManager { manager.saveToPreferences { error in if let error = error, error.code == 5 { manager.isOnDemandEnabled = false - if #available(iOS 15.1, *) { - if #available(iOS 16, *) { } else { - manager.protocolConfiguration?.includeAllNetworks = false - } + if #available(iOS 16, *) { } else { + manager.protocolConfiguration?.includeAllNetworks = false } NotificationCenter.default.post(name: Notification.Name.VPNConfigurationDisabled, object: nil) return @@ -271,10 +269,8 @@ class VPNManager { manager.loadFromPreferences { _ in manager.onDemandRules = [NEOnDemandRule]() manager.isOnDemandEnabled = false - if #available(iOS 15.1, *) { - if #available(iOS 16, *) { } else { - manager.protocolConfiguration?.includeAllNetworks = false - } + if #available(iOS 16, *) { } else { + manager.protocolConfiguration?.includeAllNetworks = false } manager.saveToPreferences { _ in } } @@ -326,10 +322,8 @@ class VPNManager { manager.loadFromPreferences { _ in manager.onDemandRules = [NEOnDemandRule]() manager.isOnDemandEnabled = false - if #available(iOS 15.1, *) { - if #available(iOS 16, *) { } else { - manager.protocolConfiguration?.includeAllNetworks = false - } + if #available(iOS 16, *) { } else { + manager.protocolConfiguration?.includeAllNetworks = false } manager.saveToPreferences(completionHandler: completion) } @@ -398,32 +392,15 @@ class VPNManager { } func getOpenVPNLog(completion: @escaping (String?) -> Void) { - guard let session = openvpnManager?.connection as? NETunnelProviderSession else { - completion(nil) - return - } + let maxBytes = UInt64(Config.maxBytes) - do { - try session.sendProviderMessage(OpenVPNProvider.Message.requestLog.data) { data in - guard let data = data, !data.isEmpty else { - completion(nil) - return - } - - guard let newestLog = String(data: data, encoding: .utf8), !newestLog.isEmpty else { - completion(nil) - return - } - - completion(newestLog) - return - } - } catch { + guard let url = FileManager.openvpnLogTextFileURL else { completion(nil) return } - completion(nil) + let lines = url.trailingLines(bytes: maxBytes) + completion(lines.joined(separator: "\n")) } func getWireGuardLog(completion: @escaping (String?) -> Void) { diff --git a/IVPNClient/Models/ProductIdentifier.swift b/IVPNClient/Models/ProductId.swift similarity index 90% rename from IVPNClient/Models/ProductIdentifier.swift rename to IVPNClient/Models/ProductId.swift index 730185db6..f7e38871a 100644 --- a/IVPNClient/Models/ProductIdentifier.swift +++ b/IVPNClient/Models/ProductId.swift @@ -1,13 +1,5 @@ // -// ProductIdentifier.swift -// IVPNClient -// -// Created by Juraj Hilje on 18/04/2019. -// Copyright © 2019 IVPN. All rights reserved. -// - -// -// ProductIdentifier.swift +// ProductId.swift // IVPN iOS app // https://github.com/ivpn/ios-app // @@ -31,7 +23,7 @@ import Foundation -struct ProductIdentifier { +struct ProductId { static let standardWeek = "net.ivpn.subscriptions.standard.1week" static let standardMonth = "net.ivpn.subscriptions.standard.1month" diff --git a/IVPNClient/Models/Service.swift b/IVPNClient/Models/Service.swift index 347361fd8..1bdd91635 100644 --- a/IVPNClient/Models/Service.swift +++ b/IVPNClient/Models/Service.swift @@ -33,9 +33,11 @@ struct Service { // MARK: - Computed properties - var priceText: String { - guard !IAPManager.shared.products.isEmpty else { return "" } - guard let product = IAPManager.shared.getProduct(identifier: productId) else { return "" } - return IAPManager.shared.productPrice(product: product) + guard let product = PurchaseManager.shared.getProduct(id: productId) else { + return "" + } + + return product.displayPrice } var durationText: String { @@ -89,28 +91,28 @@ struct Service { case .standard: switch duration { case .week: - return ProductIdentifier.standardWeek + return ProductId.standardWeek case .month: - return ProductIdentifier.standardMonth + return ProductId.standardMonth case .year: - return ProductIdentifier.standardYear + return ProductId.standardYear case .twoYears: - return ProductIdentifier.standardTwoYears + return ProductId.standardTwoYears case .threeYears: - return ProductIdentifier.standardThreeYears + return ProductId.standardThreeYears } case .pro: switch duration { case .week: - return ProductIdentifier.proWeek + return ProductId.proWeek case .month: - return ProductIdentifier.proMonth + return ProductId.proMonth case .year: - return ProductIdentifier.proYear + return ProductId.proYear case .twoYears: - return ProductIdentifier.proTwoYears + return ProductId.proTwoYears case .threeYears: - return ProductIdentifier.proThreeYears + return ProductId.proThreeYears } } } diff --git a/IVPNClient/Scenes/Base.lproj/Main.storyboard b/IVPNClient/Scenes/Base.lproj/Main.storyboard index a6b3b0372..d360ed775 100644 --- a/IVPNClient/Scenes/Base.lproj/Main.storyboard +++ b/IVPNClient/Scenes/Base.lproj/Main.storyboard @@ -681,7 +681,7 @@ - + @@ -2021,14 +2021,14 @@ - + - + - + @@ -2725,40 +2725,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/IVPNClient/Scenes/Signup.storyboard b/IVPNClient/Scenes/Signup.storyboard index d233c683d..c2f2a4d20 100644 --- a/IVPNClient/Scenes/Signup.storyboard +++ b/IVPNClient/Scenes/Signup.storyboard @@ -1,9 +1,9 @@ - + - + @@ -293,7 +293,7 @@ - + @@ -670,7 +670,7 @@ - + @@ -920,7 +920,7 @@ - + diff --git a/IVPNClient/Scenes/Signup/LoginViewController.swift b/IVPNClient/Scenes/Signup/LoginViewController.swift index 80c75fb43..c0df4a380 100644 --- a/IVPNClient/Scenes/Signup/LoginViewController.swift +++ b/IVPNClient/Scenes/Signup/LoginViewController.swift @@ -104,17 +104,19 @@ class LoginViewController: UIViewController { hud.detailTextLabel.text = "Restoring purchases..." hud.show(in: (navigationController?.view)!) - IAPManager.shared.restorePurchases { account, error in - self.hud.dismiss() - - if let error = error { - self.showErrorAlert(title: "Restore failed", message: error.message) - return - } - - if account != nil { - self.userName.text = account?.accountId - self.sessionManager.createSession() + PurchaseManager.shared.restorePurchases { account, error in + DispatchQueue.main.async { + self.hud.dismiss() + + if let error = error { + self.showErrorAlert(title: "Restore failed", message: error.message) + return + } + + if let account = account { + self.userName.text = account.accountId + self.sessionManager.createSession() + } } } } diff --git a/IVPNClient/Scenes/Signup/Payment/PaymentViewController.swift b/IVPNClient/Scenes/Signup/Payment/PaymentViewController.swift index ca64c8302..396f0dd8f 100644 --- a/IVPNClient/Scenes/Signup/Payment/PaymentViewController.swift +++ b/IVPNClient/Scenes/Signup/Payment/PaymentViewController.swift @@ -22,7 +22,7 @@ // import UIKit -import SwiftyStoreKit +import StoreKit import SnapKit import JGProgressHUD @@ -54,7 +54,7 @@ class PaymentViewController: UITableViewController { lazy var retryButton: UIButton = { let button = UIButton(type: .system) - button.addTarget(self, action: #selector(fetchProducts), for: .touchUpInside) + button.addTarget(self, action: #selector(load), for: .touchUpInside) button.setTitle("Retry", for: .normal) button.sizeToFit() button.isHidden = true @@ -101,7 +101,9 @@ class PaymentViewController: UITableViewController { } @IBAction func purchase(_ sender: UIButton) { - purchaseProduct(identifier: service.productId) + Task { + await purchaseProduct(identifier: service.productId) + } } @IBAction func close() { @@ -119,16 +121,24 @@ class PaymentViewController: UITableViewController { setupView() } + override func viewDidDisappear(_ animated: Bool) { + let appDelegate = UIApplication.shared.delegate as! AppDelegate + PurchaseManager.shared.delegate = appDelegate + super.viewDidDisappear(animated) + } + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + PurchaseManager.shared.delegate = self + if extendingService { if Application.shared.authentication.isLoggedIn && !Application.shared.serviceStatus.isNewStyleAccount() { let serviceType = ServiceType.getType(currentPlan: Application.shared.serviceStatus.currentPlan) service = Service(type: serviceType, duration: .year) } - fetchProducts() + load() } } @@ -175,75 +185,33 @@ class PaymentViewController: UITableViewController { } } - @objc private func fetchProducts() { - displayMode = .loading - - IAPManager.shared.fetchProducts { [weak self] products, error in - guard let self = self else { return } - - if error != nil { - self.showAlert(title: "iTunes Store error", message: "Cannot connect to iTunes Store") - self.displayMode = .error - return - } - - if products != nil { - self.displayMode = .content - } + @objc private func load() { + Task { + await loadProducts() } } - private func purchaseProduct(identifier: String) { - guard deviceCanMakePurchases() else { return } - - hud.indicatorView = JGProgressHUDIndeterminateIndicatorView() - hud.detailTextLabel.text = "Processing payment..." - hud.show(in: (navigationController?.view)!) + private func loadProducts() async { + displayMode = .loading - IAPManager.shared.purchaseProduct(identifier: identifier) { [weak self] purchase, error in - guard let self = self else { return } - - if let error = error { - self.showErrorAlert(title: "Error", message: error) - self.hud.dismiss() - return - } - - if let purchase = purchase { - self.completePurchase(purchase: purchase) - } + do { + try await PurchaseManager.shared.loadProducts() + displayMode = .content + } catch { + showAlert(title: "iTunes Store error", message: "Cannot connect to iTunes Store") + displayMode = .error } } - private func completePurchase(purchase: PurchaseDetails) { - IAPManager.shared.completePurchase(purchase: purchase) { [weak self] serviceStatus, error in - guard let self = self else { return } - - self.hud.dismiss() - - if let error = error { - self.showErrorAlert(title: "Error", message: error.message) { _ in - if error.status == 400 { - self.navigationController?.dismiss(animated: true, completion: nil) - } - } - return - } - - if let serviceStatus = serviceStatus { - self.showSubscriptionActivatedAlert(serviceStatus: serviceStatus) { - if KeyChain.sessionToken == nil { - KeyChain.username = KeyChain.tempUsername - KeyChain.tempUsername = nil - self.sessionManager.createSession() - return - } - - self.navigationController?.dismiss(animated: true) { - NotificationCenter.default.post(name: Notification.Name.SubscriptionActivated, object: nil) - } - } - } + private func purchaseProduct(identifier: String) async { + guard deviceCanMakePurchases() else { + return + } + + do { + _ = try await PurchaseManager.shared.purchase(identifier) + } catch { + showErrorAlert(title: "Error", message: error.localizedDescription) } } @@ -299,6 +267,67 @@ extension PaymentViewController { } +// MARK: - PurchaseManagerDelegate - + +extension PaymentViewController: PurchaseManagerDelegate { + + func purchaseStart() { + DispatchQueue.main.async { [self] in + hud.indicatorView = JGProgressHUDIndeterminateIndicatorView() + hud.detailTextLabel.text = "Processing payment..." + hud.show(in: (navigationController?.view)!) + } + } + + func purchasePending() { + DispatchQueue.main.async { [self] in + hud.dismiss() + showAlert(title: "Pending payment", message: "Payment is pending for approval. We will complete the transaction as soon as payment is approved.") + } + } + + func purchaseSuccess(activeUntil: String, extended: Bool) { + guard extended else { + hud.dismiss() + return + } + + DispatchQueue.main.async { [self] in + hud.dismiss() + + showSubscriptionActivatedAlert(activeUntil: activeUntil) { + if KeyChain.sessionToken == nil { + KeyChain.username = KeyChain.tempUsername + KeyChain.tempUsername = nil + self.sessionManager.createSession() + return + } + + self.navigationController?.dismiss(animated: true) { + NotificationCenter.default.post(name: Notification.Name.SubscriptionActivated, object: nil) + } + } + } + } + + func purchaseError(error: Any?) { + DispatchQueue.main.async { [self] in + hud.dismiss() + + guard let error = error as? ErrorResult else { + return + } + + showErrorAlert(title: "Error", message: error.message) { _ in + if error.status == 400 { + self.navigationController?.dismiss(animated: true, completion: nil) + } + } + } + } + +} + // MARK: - SessionManagerDelegate - extension PaymentViewController { diff --git a/IVPNClient/Scenes/Signup/SelectPlan/SelectPlanViewController.swift b/IVPNClient/Scenes/Signup/SelectPlan/SelectPlanViewController.swift index 77c7aec6c..668dc9ddd 100644 --- a/IVPNClient/Scenes/Signup/SelectPlan/SelectPlanViewController.swift +++ b/IVPNClient/Scenes/Signup/SelectPlan/SelectPlanViewController.swift @@ -59,7 +59,7 @@ class SelectPlanViewController: UITableViewController { lazy var retryButton: UIButton = { let button = UIButton(type: .system) - button.addTarget(self, action: #selector(fetchProducts), for: .touchUpInside) + button.addTarget(self, action: #selector(load), for: .touchUpInside) button.setTitle("Retry", for: .normal) button.sizeToFit() button.isHidden = true @@ -133,7 +133,7 @@ class SelectPlanViewController: UITableViewController { super.viewDidAppear(animated) if displayMode == .loading { - fetchProducts() + load() } segueStarted = false @@ -197,22 +197,22 @@ class SelectPlanViewController: UITableViewController { } } - @objc private func fetchProducts() { + @objc private func load() { + Task { + await loadProducts() + } + } + + private func loadProducts() async { displayMode = .loading - IAPManager.shared.fetchProducts { [weak self] products, error in - guard let self = self else { return } - - if error != nil { - self.showAlert(title: "iTunes Store error", message: "Cannot connect to iTunes Store") - self.displayMode = .error - return - } - - if products != nil { - self.updateSubscriptions() - self.displayMode = .content - } + do { + try await PurchaseManager.shared.loadProducts() + updateSubscriptions() + displayMode = .content + } catch { + showAlert(title: "iTunes Store error", message: "Cannot connect to iTunes Store") + displayMode = .error } } diff --git a/IVPNClient/Scenes/ViewControllers/SettingsViewController.swift b/IVPNClient/Scenes/ViewControllers/SettingsViewController.swift index 3a910a421..7205075ed 100644 --- a/IVPNClient/Scenes/ViewControllers/SettingsViewController.swift +++ b/IVPNClient/Scenes/ViewControllers/SettingsViewController.swift @@ -359,11 +359,10 @@ extension SettingsViewController { // Kill Switch if indexPath.section == 3 && indexPath.row == 4 { - if #available(iOS 15.1, *) { - if #available(iOS 16, *) { } else { - return UITableView.automaticDimension - } + if #available(iOS 16, *) { } else { + return UITableView.automaticDimension } + return 0 } diff --git a/IVPNClient/Scenes/ViewControllers/StaticWebViewController.swift b/IVPNClient/Scenes/ViewControllers/StaticWebViewController.swift deleted file mode 100644 index 0f734d1f9..000000000 --- a/IVPNClient/Scenes/ViewControllers/StaticWebViewController.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// StaticWebViewController.swift -// IVPN iOS app -// https://github.com/ivpn/ios-app -// -// Created by Juraj Hilje on 2019-04-02. -// Copyright (c) 2023 IVPN Limited. -// -// This file is part of the IVPN iOS app. -// -// The IVPN iOS app is free software: you can redistribute it and/or -// modify it under the terms of the GNU General Public License as published by the Free -// Software Foundation, either version 3 of the License, or (at your option) any later version. -// -// The IVPN iOS app is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -// details. -// -// You should have received a copy of the GNU General Public License -// along with the IVPN iOS app. If not, see . -// - -import UIKit -import WebKit - -class StaticWebViewController: UIViewController { - - @IBOutlet weak var webView: WKWebView! - - var resourceName = "" - var screenTitle = "" - - override func viewDidLoad() { - super.viewDidLoad() - title = screenTitle - - if let data = FileSystemManager.loadDataFromResource( - resourceName: resourceName, - resourceType: "html", - bundle: Bundle.main) { - - if let content = String(data: data, encoding: .utf8) { - webView.loadHTMLString(content, baseURL: nil) - } - } - } - -} diff --git a/IVPNClient/Store.storekit b/IVPNClient/Store.storekit new file mode 100644 index 000000000..f90319fbe --- /dev/null +++ b/IVPNClient/Store.storekit @@ -0,0 +1,385 @@ +{ + "identifier" : "57E4D561", + "nonRenewingSubscriptions" : [ + { + "displayPrice" : "9.99", + "familyShareable" : false, + "internalID" : "1193681555", + "localizations" : [ + { + "description" : "This In-App purchase will extend VPN subscription for 1 month. \nSubscription can be purchased multiple times by customer. Each purchase will add additional month to the account.", + "displayName" : "1 Month", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.1month", + "referenceName" : "1 Month", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "9.99", + "familyShareable" : false, + "internalID" : "1515476683", + "localizations" : [ + { + "description" : "IVPN Pro service 1 month non-renewing", + "displayName" : "IVPN Pro 1 month", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.pro.1month", + "referenceName" : "1 month non-renewing IVPN Pro subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "5.99", + "familyShareable" : false, + "internalID" : "1515475932", + "localizations" : [ + { + "description" : "IVPN Standard service 1 month non-renewing", + "displayName" : "IVPN Standard 1 month", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.standard.1month", + "referenceName" : "1 month non-renewing IVPN Standard subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "3.99", + "familyShareable" : false, + "internalID" : "1515476549", + "localizations" : [ + { + "description" : "IVPN Pro service 1 week non-renewing", + "displayName" : "IVPN Pro 1 week", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.pro.1week", + "referenceName" : "1 week non-renewing IVPN Pro subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "1.99", + "familyShareable" : false, + "internalID" : "1515475821", + "localizations" : [ + { + "description" : "IVPN Standard service 1 week non-renewing", + "displayName" : "IVPN Standard 1 week", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.standard.1week", + "referenceName" : "1 week non-renewing IVPN Standard subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "99.99", + "familyShareable" : false, + "internalID" : "1515476688", + "localizations" : [ + { + "description" : "IVPN Pro service 1 year non-renewing", + "displayName" : "IVPN Pro 1 year", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.pro.1year", + "referenceName" : "1 year non-renewing IVPN Pro subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "59.99", + "familyShareable" : false, + "internalID" : "1515475940", + "localizations" : [ + { + "description" : "IVPN Standard service 1 year non-renewing", + "displayName" : "IVPN Standard 1 year", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.standard.1year", + "referenceName" : "1 year non-renewing IVPN Standard subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "99.99", + "familyShareable" : false, + "internalID" : "1193684808", + "localizations" : [ + { + "description" : "This In-App purchase will extend VPN subscription for 12 months. \nSubscription can be purchased multiple times by customer. Each purchase will add additional month to the account.", + "displayName" : "12 Months", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.12month", + "referenceName" : "12 Months", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "159.99", + "familyShareable" : false, + "internalID" : "1515476948", + "localizations" : [ + { + "description" : "IVPN Pro service 2 years non-renewing", + "displayName" : "IVPN Pro 2 years", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.pro.2year", + "referenceName" : "2 years non-renewing IVPN Pro subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "99.99", + "familyShareable" : false, + "internalID" : "1515476287", + "localizations" : [ + { + "description" : "IVPN Standard service 2 years non-renewing", + "displayName" : "IVPN Standard 2 years", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.standard.2year", + "referenceName" : "2 years non-renewing IVPN Standard subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "219.99", + "familyShareable" : false, + "internalID" : "1515477187", + "localizations" : [ + { + "description" : "IVPN Pro service 3 years non-renewing", + "displayName" : "IVPN Pro 3 years", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.pro.3year", + "referenceName" : "3 years non-renewing IVPN Pro subscription", + "type" : "NonRenewingSubscription" + }, + { + "displayPrice" : "139.99", + "familyShareable" : false, + "internalID" : "1515476242", + "localizations" : [ + { + "description" : "IVPN Standard service 3 years non-renewing", + "displayName" : "IVPN Standard 3 years", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.subscriptions.standard.3year", + "referenceName" : "3 years non-renewing IVPN Standard subscription", + "type" : "NonRenewingSubscription" + } + ], + "products" : [ + + ], + "settings" : { + "_applicationInternalID" : "1193122683", + "_askToBuyEnabled" : false, + "_developerTeamID" : "WQXXM75BYN", + "_failTransactionsEnabled" : false, + "_lastSynchronizedDate" : 724332316.402367, + "_locale" : "en_US", + "_storefront" : "USA", + "_storeKitErrors" : [ + { + "current" : { + "index" : 2, + "type" : "generic" + }, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : { + "index" : 1, + "type" : "purchase" + }, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] + }, + "subscriptionGroups" : [ + { + "id" : "20519080", + "localizations" : [ + + ], + "name" : "IVPN Service", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "9.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "1459483527", + "introductoryOffer" : { + "internalID" : "C2DE8327", + "numberOfPeriods" : 1, + "paymentMode" : "free", + "subscriptionPeriod" : "P3D" + }, + "localizations" : [ + { + "description" : "1 month of IVPN Pro service subscription", + "displayName" : "1 month IVPN Pro service", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.autosubscriptions.1month", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "1 month auto-renewable IVPN Pro subscription", + "subscriptionGroupID" : "20519080", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "5.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "1460132096", + "introductoryOffer" : { + "internalID" : "ADA414D2", + "numberOfPeriods" : 1, + "paymentMode" : "free", + "subscriptionPeriod" : "P3D" + }, + "localizations" : [ + { + "description" : "1 month IVPN Standard service subscription", + "displayName" : "1 month IVPN Standard service", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.autosubscriptions.standard.1month", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "1 month auto-renewable IVPN Standard subscription", + "subscriptionGroupID" : "20519080", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "99.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "1459482414", + "introductoryOffer" : { + "internalID" : "50C20442", + "numberOfPeriods" : 1, + "paymentMode" : "free", + "subscriptionPeriod" : "P3D" + }, + "localizations" : [ + { + "description" : "1 year of IVPN Pro service subscription", + "displayName" : "1 year IVPN Pro service", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.autosubscriptions.12month", + "recurringSubscriptionPeriod" : "P1Y", + "referenceName" : "12 months auto-renewable IVPN Pro subscription", + "subscriptionGroupID" : "20519080", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "59.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "1460131621", + "introductoryOffer" : { + "internalID" : "6D20BF20", + "numberOfPeriods" : 1, + "paymentMode" : "free", + "subscriptionPeriod" : "P3D" + }, + "localizations" : [ + { + "description" : "1 year IVPN Standard service subscription", + "displayName" : "1 year IVPN Standard service", + "locale" : "en_US" + } + ], + "productID" : "net.ivpn.autosubscriptions.standard.12month", + "recurringSubscriptionPeriod" : "P1Y", + "referenceName" : "12 months auto-renewable IVPN Standard subscription", + "subscriptionGroupID" : "20519080", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 3, + "minor" : 0 + } +} diff --git a/IVPNClient/Utilities/Extensions/FileManager+Extension.swift b/IVPNClient/Utilities/Extensions/FileManager+Extension.swift index ff6d18e06..4ce46bf51 100644 --- a/IVPNClient/Utilities/Extensions/FileManager+Extension.swift +++ b/IVPNClient/Utilities/Extensions/FileManager+Extension.swift @@ -54,10 +54,15 @@ extension FileManager { return sharedFolderURL?.appendingPathComponent("WireGuard.log") } + static var openvpnLogTextFileURL: URL? { + return sharedFolderURL?.appendingPathComponent("Tunnel.log") + } + static func deleteFile(at url: URL) -> Bool { do { try FileManager.default.removeItem(at: url) } catch { + log(.error, message: error.localizedDescription) return false } return true diff --git a/IVPNClient/Utilities/Extensions/NETunnelProviderProtocol+Ext.swift b/IVPNClient/Utilities/Extensions/NETunnelProviderProtocol+Ext.swift index 11a60eca9..1ac89c33a 100644 --- a/IVPNClient/Utilities/Extensions/NETunnelProviderProtocol+Ext.swift +++ b/IVPNClient/Utilities/Extensions/NETunnelProviderProtocol+Ext.swift @@ -33,67 +33,53 @@ extension NETunnelProviderProtocol { // MARK: OpenVPN static func makeOpenVPNProtocol(settings: ConnectionSettings, accessDetails: AccessDetails) -> NETunnelProviderProtocol { - guard let host = getHost() else { - return NETunnelProviderProtocol() - } + var proto = NETunnelProviderProtocol() - let username = accessDetails.username - let socketType: SocketType = settings.protocolType() == "TCP" ? .tcp : .udp - let credentials = OpenVPN.Credentials(username, KeyChain.vpnPassword ?? "") - let staticKey = OpenVPN.StaticKey.init(file: OpenVPNConf.tlsAuth, direction: OpenVPN.StaticKey.Direction.client) - let port = UInt16(getPort(settings: settings)) - - var sessionBuilder = OpenVPN.ConfigurationBuilder() - sessionBuilder.ca = OpenVPN.CryptoContainer(pem: OpenVPNConf.caCert) - sessionBuilder.cipher = .aes256cbc - sessionBuilder.compressionFraming = .disabled - sessionBuilder.endpointProtocols = [EndpointProtocol(socketType, port)] - sessionBuilder.hostname = host.host - sessionBuilder.tlsWrap = OpenVPN.TLSWrap.init(strategy: .auth, key: staticKey!) - - if let dnsServers = openVPNdnsServers(), !dnsServers.isEmpty, dnsServers != [""] { - sessionBuilder.dnsServers = dnsServers - log(.info, message: "DNS server: \(dnsServers)") - - switch DNSProtocolType.preferred() { - case .doh: - sessionBuilder.dnsProtocol = .https - sessionBuilder.dnsHTTPSURL = URL.init(string: DNSProtocolType.getServerURL(address: UserDefaults.shared.customDNS)) - case .dot: - sessionBuilder.dnsProtocol = .tls - sessionBuilder.dnsTLSServerName = DNSProtocolType.getServerName(address: UserDefaults.shared.customDNS) - default: - sessionBuilder.dnsProtocol = .plain - } + guard let host = getHost() else { + return proto } - var builder = OpenVPNProvider.ConfigurationBuilder(sessionConfiguration: sessionBuilder.build()) - builder.shouldDebug = true - builder.debugLogFormat = "$Dyyyy-MM-dd HH:mm:ss$d $L $M" - builder.masksPrivateData = true + let credentials = OpenVPN.Credentials(accessDetails.username, KeyChain.vpnPassword ?? "") - let configuration = builder.build() - let keychain = Keychain(group: Config.appGroup) - _ = try? keychain.set(password: credentials.password, for: credentials.username, context: Config.openvpnTunnelProvider) - let proto = try! configuration.generatedTunnelProtocol( - withBundleIdentifier: Config.openvpnTunnelProvider, + let params = OpenVPN.Config.Parameters( + title: Config.openvpnTunnelTitle, appGroup: Config.appGroup, - context: Config.openvpnTunnelProvider, - credentials: credentials + hostname: host.host, + port: UInt16(getPort(settings: settings)), + socketType: settings.protocolType() == "TCP" ? .tcp : .udp, + dnsServers: openVPNdnsServers(), + dnsProtocol: DNSProtocolType.preferred(), + customDNS: UserDefaults.shared.customDNS ) - proto.disconnectOnSleep = !UserDefaults.shared.keepAlive - if #available(iOS 15.1, *) { - if #available(iOS 16, *) { } else { - proto.includeAllNetworks = UserDefaults.shared.killSwitch - } + var cfg = OpenVPN.Config.make(params: params) + cfg.username = credentials.username + + let keychain = Keychain(group: Config.appGroup) + var passwordReference = Data() + do { + passwordReference = try keychain.set(password: credentials.password, for: credentials.username, context: Config.openvpnTunnelProvider) + } catch { + log(.error, message: "Keychain failure: \(error)") } - if #available(iOS 14.2, *) { - proto.includeAllNetworks = disableLanAccess() - proto.excludeLocalNetworks = !disableLanAccess() + var extra = NetworkExtensionExtra() + extra.passwordReference = passwordReference + + do { + proto = try cfg.asTunnelProtocol(withBundleIdentifier: Config.openvpnTunnelProvider, extra: extra) + } catch { + log(.error, message: "Keychain failure: \(error)") + } + + if #available(iOS 16, *) { } else { + proto.includeAllNetworks = UserDefaults.shared.killSwitch } + proto.includeAllNetworks = disableLanAccess() + proto.excludeLocalNetworks = !disableLanAccess() + proto.disconnectOnSleep = !UserDefaults.shared.keepAlive + return proto } @@ -183,16 +169,12 @@ extension NETunnelProviderProtocol { configuration.providerConfiguration = tunnel.generateProviderConfiguration() configuration.disconnectOnSleep = !UserDefaults.shared.keepAlive - if #available(iOS 15.1, *) { - if #available(iOS 16, *) { } else { - configuration.includeAllNetworks = UserDefaults.shared.killSwitch - } - } - - if #available(iOS 14.2, *) { - configuration.includeAllNetworks = disableLanAccess() - configuration.excludeLocalNetworks = !disableLanAccess() + if #available(iOS 16, *) { } else { + configuration.includeAllNetworks = UserDefaults.shared.killSwitch } + + configuration.includeAllNetworks = disableLanAccess() + configuration.excludeLocalNetworks = !disableLanAccess() return configuration } diff --git a/IVPNClient/Utilities/Extensions/TunnelKit+Ext.swift b/IVPNClient/Utilities/Extensions/TunnelKit+Ext.swift new file mode 100644 index 000000000..bd54592a2 --- /dev/null +++ b/IVPNClient/Utilities/Extensions/TunnelKit+Ext.swift @@ -0,0 +1,82 @@ +// +// TunnelKit+Ext.swift +// IVPN iOS app +// https://github.com/ivpn/ios-app +// +// Created by Juraj Hilje on 2024-01-03. +// Copyright (c) 2024 IVPN Limited. +// +// This file is part of the IVPN iOS app. +// +// The IVPN iOS app is free software: you can redistribute it and/or +// modify it under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) any later version. +// +// The IVPN iOS app is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License +// along with the IVPN iOS app. If not, see . +// + +import Foundation +import TunnelKitCore +import TunnelKitOpenVPN + +extension OpenVPN { + + struct Config { + + struct Parameters { + let title: String + let appGroup: String + let hostname: String + let port: UInt16 + let socketType: SocketType + let dnsServers: [String]? + let dnsProtocol: DNSProtocolType + let customDNS: String + } + + static func make(params: Parameters) -> OpenVPN.ProviderConfiguration { + let tlsKey = OpenVPN.StaticKey(file: OpenVPNConf.tlsAuth, direction: .client)! + + var builder = OpenVPN.ConfigurationBuilder() + builder.ca = OpenVPN.CryptoContainer(pem: OpenVPNConf.caCert) + builder.cipher = .aes256cbc + builder.compressionFraming = .disabled + builder.remotes = [Endpoint(params.hostname, EndpointProtocol(params.socketType, params.port))] + builder.tlsWrap = TLSWrap(strategy: .auth, key: tlsKey) + builder.routingPolicies = [.IPv4, .IPv6] + + if let dnsServers = params.dnsServers, !dnsServers.isEmpty, dnsServers != [""] { + builder.dnsServers = dnsServers + + switch params.dnsProtocol { + case .doh: + builder.dnsProtocol = .https + builder.dnsHTTPSURL = URL.init(string: DNSProtocolType.getServerURL(address: params.customDNS)) + case .dot: + builder.dnsProtocol = .tls + builder.dnsTLSServerName = DNSProtocolType.getServerName(address: params.customDNS) + default: + builder.dnsProtocol = .plain + } + } + + let cfg = builder.build() + + var configuration = OpenVPN.ProviderConfiguration(params.title, appGroup: params.appGroup, configuration: cfg) + configuration.shouldDebug = true + configuration.debugLogFormat = "$Dyyyy-MM-dd HH:mm:ss$d $L $M" + configuration.masksPrivateData = true + configuration.debugLogPath = FileManager.openvpnLogTextFileURL?.lastPathComponent + + return configuration + } + + } + +} diff --git a/IVPNClient/Utilities/Extensions/UIApplication+Ext.swift b/IVPNClient/Utilities/Extensions/UIApplication+Ext.swift index 420732efe..f78fff72f 100644 --- a/IVPNClient/Utilities/Extensions/UIApplication+Ext.swift +++ b/IVPNClient/Utilities/Extensions/UIApplication+Ext.swift @@ -25,8 +25,16 @@ import UIKit extension UIApplication { + var keyWindow: UIWindow? { + return self.connectedScenes + .filter { $0.activationState == .foregroundActive } + .first(where: { $0 is UIWindowScene }) + .flatMap({ $0 as? UIWindowScene })?.windows + .first(where: \.isKeyWindow) + } + public var isSplitOrSlideOver: Bool { - guard let window = self.windows.filter({ $0.isKeyWindow }).first else { + guard let window = keyWindow else { return false } diff --git a/IVPNClient/Utilities/Extensions/UIViewController+Ext.swift b/IVPNClient/Utilities/Extensions/UIViewController+Ext.swift index 71ca4b474..362af3e8c 100644 --- a/IVPNClient/Utilities/Extensions/UIViewController+Ext.swift +++ b/IVPNClient/Utilities/Extensions/UIViewController+Ext.swift @@ -134,10 +134,10 @@ extension UIViewController { } } - func showSubscriptionActivatedAlert(serviceStatus: ServiceStatus, completion: (() -> Void)? = nil) { + func showSubscriptionActivatedAlert(activeUntil: String, completion: (() -> Void)? = nil) { showAlert( title: "Thank you!", - message: "The payment was successfully processed.\nService is active until: " + serviceStatus.activeUntilString(), + message: "The payment was successfully processed.\nService is active until: " + activeUntil, handler: { _ in if let completion = completion { completion() @@ -262,7 +262,7 @@ extension UIViewController { } func deviceCanMakePurchases() -> Bool { - guard IAPManager.shared.canMakePurchases else { + guard PurchaseManager.shared.canMakePurchases else { showAlert(title: "Error", message: "In-App Purchases are not available on your device.") return false } diff --git a/IVPNClient/Utilities/Extensions/URL+Ext.swift b/IVPNClient/Utilities/Extensions/URL+Ext.swift index e49218958..f2afee3dc 100644 --- a/IVPNClient/Utilities/Extensions/URL+Ext.swift +++ b/IVPNClient/Utilities/Extensions/URL+Ext.swift @@ -43,4 +43,53 @@ extension URL { return "" } + func trailingContent(bytes: UInt64) -> String { + var file: FileHandle? + + defer { + try? file?.close() + } + + do { + file = try FileHandle(forReadingFrom: self) + + guard let size = try file?.seekToEnd() else { + log(.error, message: "Cannot seek") + return "" + } + + var offset: UInt64 + if bytes < size { + offset = size - bytes + } else { + offset = 0 + } + + try file?.seek(toOffset: offset) + + guard let data = try file?.readToEnd() else { + log(.error, message: "No data") + return "" + } + + guard let string = String(data: data, encoding: .utf8) else { + log(.error, message: "Cannot encode string") + return "" + } + + return string + } catch { + log(.error, message: "Error while reading file: \(error)") + return "" + } + } + + func trailingLines(bytes: UInt64) -> [String] { + return trailingContent(bytes: bytes) + .components(separatedBy: "\n") + .filter { + !$0.isEmpty + } + } + } diff --git a/V2RayControl/go.mod b/V2RayControl/go.mod index 97dc39a93..5090db538 100644 --- a/V2RayControl/go.mod +++ b/V2RayControl/go.mod @@ -10,7 +10,6 @@ require ( github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 // indirect github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/gorilla/websocket v1.5.0 // indirect @@ -22,7 +21,7 @@ require ( github.com/pires/go-proxyproto v0.6.2 // indirect github.com/quic-go/qtls-go1-19 v0.3.2 // indirect github.com/quic-go/qtls-go1-20 v0.3.1 // indirect - github.com/quic-go/quic-go v0.37.7 // indirect + github.com/quic-go/quic-go v0.42.0 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect @@ -31,17 +30,18 @@ require ( github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect github.com/xtaci/smux v1.5.15 // indirect go.starlark.net v0.0.0-20220817180228-f738f5508c12 // indirect + go.uber.org/mock v0.4.0 // indirect go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d // indirect - golang.org/x/crypto v0.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.13.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.56.3 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/V2RayControl/go.sum b/V2RayControl/go.sum index 67f9271b9..9f3152431 100644 --- a/V2RayControl/go.sum +++ b/V2RayControl/go.sum @@ -81,7 +81,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -226,8 +225,8 @@ github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc8 github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg= github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.37.7 h1:AgKsQLZ1+YCwZd2GYhBUsJDYZwEkA5gENtAjb+MxONU= -github.com/quic-go/quic-go v0.37.7/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU= +github.com/quic-go/quic-go v0.42.0 h1:uSfdap0eveIl8KXnipv9K7nlwZ5IqLlYOpJ58u5utpM= +github.com/quic-go/quic-go v0.42.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= @@ -279,13 +278,14 @@ github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2Rer github.com/xtaci/smux v1.5.15 h1:6hMiXswcleXj5oNfcJc+DXS8Vj36XX2LaX98udog6Kc= github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.starlark.net v0.0.0-20220817180228-f738f5508c12 h1:xOBJXWGEDwU5xSDxH6macxO11Us0AH2fTa9rmsbbF7g= go.starlark.net v0.0.0-20220817180228-f738f5508c12/go.mod h1:VZcBMdr3cT3PnBoWunTabuSEXwVAH+ZJ5zxfs3AdASk= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go4.org/netipx v0.0.0-20220812043211-3cc044ffd68d h1:ggxwEf5eu0l8v+87VhX1czFh8zJul3hK16Gmruxn7hw= @@ -298,8 +298,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -321,7 +321,6 @@ golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCc golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -341,8 +340,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -352,7 +351,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -372,13 +370,11 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -388,6 +384,7 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -408,7 +405,6 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -453,8 +449,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=