diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7864ab..91b67ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 1.45.0 + +- **Added**: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall. +- **Added**: `TILogging` with error logging types +- **Update**: `DefaultRecoverableJsonNetworkService` supports iOS 12. +- **Update**: `DefaultFingerprintsProvider` now uses `SingleValueStorage` + ### 1.44.0 - **Added**: HTTP status codes to `EndpointErrorResult.apiError` responses diff --git a/Package.swift b/Package.swift index 663448ce..18d25962 100644 --- a/Package.swift +++ b/Package.swift @@ -66,21 +66,40 @@ let package = Package( .target(name: "TISwiftUICore", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TISwiftUICore/Sources"), // MARK: - Utils - .target(name: "TISwiftUtils", path: "TISwiftUtils/Sources"), - .target(name: "TIFoundationUtils", dependencies: ["TISwiftUtils"], path: "TIFoundationUtils", exclude: ["TIFoundationUtils.app"]), - .target(name: "TIKeychainUtils", dependencies: ["TIFoundationUtils", "KeychainAccess"], path: "TIKeychainUtils/Sources"), + + .target(name: "TISwiftUtils", + path: "TISwiftUtils/Sources", + plugins: [.plugin(name: "TISwiftLintPlugin")]), + + .target(name: "TIFoundationUtils", + dependencies: ["TISwiftUtils"], + path: "TIFoundationUtils", + exclude: ["TIFoundationUtils.app"], + plugins: [.plugin(name: "TISwiftLintPlugin")]), + + .target(name: "TIKeychainUtils", + dependencies: ["TIFoundationUtils", "KeychainAccess"], + path: "TIKeychainUtils/Sources", + exclude: ["../TIKeychainUtils.app"], + plugins: [.plugin(name: "TISwiftLintPlugin")]), + .target(name: "TITableKitUtils", dependencies: ["TIUIElements", "TableKit"], path: "TITableKitUtils/Sources"), .target(name: "TIDeeplink", dependencies: ["TIFoundationUtils"], path: "TIDeeplink", exclude: ["TIDeeplink.app"]), .target(name: "TIDeveloperUtils", dependencies: ["TISwiftUtils", "TIUIKitCore", "TIUIElements"], path: "TIDeveloperUtils/Sources"), + .target(name: "TILogging", path: "TILogging/Sources", plugins: ["TISwiftLintPlugin"]), // MARK: - Networking .target(name: "TINetworking", - dependencies: ["TIFoundationUtils", "Alamofire"], + dependencies: ["TIFoundationUtils", "Alamofire", "TILogging"], path: "TINetworking/Sources", plugins: [.plugin(name: "TISwiftLintPlugin")]), - .target(name: "TIMoyaNetworking", dependencies: ["TINetworking", "TIFoundationUtils", "Moya"], path: "TIMoyaNetworking"), + .target(name: "TIMoyaNetworking", + dependencies: ["TINetworking", "TIFoundationUtils", "Moya"], + path: "TIMoyaNetworking/Sources", + plugins: [.plugin(name: "TISwiftLintPlugin")]), + .target(name: "TINetworkingCache", dependencies: ["TIFoundationUtils", "TINetworking", "Cache"], path: "TINetworkingCache/Sources"), // MARK: - Maps @@ -91,12 +110,12 @@ let package = Package( .target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"), .target(name: "TITransitions", path: "TITransitions/Sources"), .target(name: "TIPagination", dependencies: ["Cursors", "TISwiftUtils"], path: "TIPagination/Sources"), - .target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "KeychainAccess"], path: "TIAuth/Sources"), + .target(name: "TIAuth", dependencies: ["TIFoundationUtils", "TIUIKitCore", "TIKeychainUtils"], path: "TIAuth/Sources"), .target(name: "TIEcommerce", dependencies: ["TIFoundationUtils", "TISwiftUtils", "TINetworking", "TIUIKitCore", "TIUIElements"], path: "TIEcommerce/Sources"), .target(name: "TITextProcessing", dependencies: [.product(name: "Antlr4", package: "antlr4")], path: "TITextProcessing/Sources", - exclude: ["TITextProcessing.app"]), + exclude: ["../TITextProcessing.app"]), .binaryTarget(name: "SwiftLintBinary", url: "https://github.com/realm/SwiftLint/releases/download/0.52.2/SwiftLintBinary-macos.artifactbundle.zip", diff --git a/Plugins/TISwiftLintPlugin/plugin.swift b/Plugins/TISwiftLintPlugin/plugin.swift index 5a2d4188..dad082b2 100644 --- a/Plugins/TISwiftLintPlugin/plugin.swift +++ b/Plugins/TISwiftLintPlugin/plugin.swift @@ -21,6 +21,7 @@ // import PackagePlugin +import Foundation @main struct SwiftLintPlugin: BuildToolPlugin { @@ -29,8 +30,14 @@ struct SwiftLintPlugin: BuildToolPlugin { let swiftlintExecutablePath = try context.tool(named: "swiftlint").path + let srcRoot = context.package.directory.string + let targetDir = target.directory.string + + let relativeTargetDir = targetDir.replacingOccurrences(of: srcRoot, with: "") + let clearRelativeTargetDir = relativeTargetDir[relativeTargetDir.index(after: relativeTargetDir.startIndex)...] // trim leading / + return [ - .prebuildCommand(displayName: "SwiftLint linting...", + .prebuildCommand(displayName: "SwiftLint linting \(target.name)...", executable: swiftlintScriptPath, arguments: [ swiftlintExecutablePath, @@ -38,9 +45,9 @@ struct SwiftLintPlugin: BuildToolPlugin { ], environment: [ "SCRIPT_DIR": swiftlintScriptPath.removingLastComponent().string, - "SRCROOT": context.package.directory.string, + "SRCROOT": srcRoot, "SCRIPT_INPUT_FILE_COUNT": "1", - "SCRIPT_INPUT_FILE_0": target.directory.removingLastComponent().lastComponent, + "SCRIPT_INPUT_FILE_0": clearRelativeTargetDir, // "FORCE_LINT": "1", // Lint all files in target (not only modified) // "AUTOCORRECT": "1" ], diff --git a/README.md b/README.md index a83e9cf2..9b4f48f6 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ LICENSE - [TIFoundationUtils](docs/tifoundationutils) * [AsyncOperation](docs/tifoundationutils/asyncoperation.md) +- [TIKeychainUtils](docs/tikeychainutils) + * [SingleValueStorage](docs/tikeychainutils/singlevaluestorage.md) - [TIUIElements](docs/tiuielements) * [Skeletons](docs/tiuielements/skeletons.md) * [Placeholders](docs/tiuielements/placeholder.md) diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 207b515e..6b1c6fc1 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIAuth/Sources/TokenStorage/DefaultAuthSettingsStorage.swift b/TIAuth/Sources/TokenStorage/DefaultAuthSettingsStorage.swift index ce72a5ea..a12f2a56 100644 --- a/TIAuth/Sources/TokenStorage/DefaultAuthSettingsStorage.swift +++ b/TIAuth/Sources/TokenStorage/DefaultAuthSettingsStorage.swift @@ -25,8 +25,8 @@ import Foundation open class DefaultAuthSettingsStorage: AuthSettingsStorage { public enum Defaults { - public static var shouldResetAuthDataKey: String { - "shouldResetAuthData" + public static var shouldResetAuthDataKey: StorageKey { + .init(rawValue: "shouldResetAuthData") } } @@ -44,7 +44,7 @@ open class DefaultAuthSettingsStorage: AuthSettingsStorage { } public init(defaultsStorage: UserDefaults = .standard, - storageKey: String = Defaults.shouldResetAuthDataKey) { + storageKey: StorageKey = Defaults.shouldResetAuthDataKey) { self.reinstallChecker = AppReinstallChecker(defaultsStorage: defaultsStorage, storageKey: storageKey) diff --git a/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenKeyStorage.swift b/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenKeyStorage.swift index 70fd0800..9b23e6bd 100644 --- a/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenKeyStorage.swift +++ b/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenKeyStorage.swift @@ -22,12 +22,13 @@ import KeychainAccess import Foundation +import TIFoundationUtils import LocalAuthentication open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage { open class Defaults: SingleValueAuthKeychainStorage.Defaults { - public static var encryptedTokenKeyStorageKey: String { - keychainServiceIdentifier + ".encryptedTokenKey" + public static var encryptedTokenKeyStorageKey: StorageKey { + .init(rawValue: keychainServiceIdentifier + ".encryptedTokenKey") } public static var reusableLAContext: LAContext { @@ -40,11 +41,11 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier), localAuthContext: LAContext = Defaults.reusableLAContext, settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(), - encryptedTokenKeyStorageKey: String = Defaults.encryptedTokenKeyStorageKey) { + encryptedTokenKeyStorageKey: StorageKey = Defaults.encryptedTokenKeyStorageKey) { let getValueClosure: GetValueClosure = { keychain, storageKey in do { - guard let value = try keychain.getData(storageKey) else { + guard let value = try keychain.getData(storageKey.rawValue) else { return .failure(.valueNotFound) } @@ -54,9 +55,9 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage } } - let setValueClosure: SetValueClosure = { keychain, value, storageKey in + let storeValueClosure: StoreValueClosure = { keychain, value, storageKey in do { - return .success(try keychain.set(value, key: storageKey)) + return .success(try keychain.set(value, key: storageKey.rawValue)) } catch { return .failure(.unableToWriteData(underlyingError: error)) } @@ -66,6 +67,6 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage settingsStorage: settingsStorage, storageKey: encryptedTokenKeyStorageKey, getValueClosure: getValueClosure, - setValueClosure: setValueClosure) + storeValueClosure: storeValueClosure) } } diff --git a/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenStorage.swift b/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenStorage.swift index fc928fc3..95a90e16 100644 --- a/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenStorage.swift +++ b/TIAuth/Sources/TokenStorage/DefaultEncryptedTokenStorage.swift @@ -21,22 +21,23 @@ // import Foundation +import TIFoundationUtils import KeychainAccess open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage { open class Defaults: SingleValueAuthKeychainStorage.Defaults { - public static var encryptedTokenStorageKey: String { - keychainServiceIdentifier + ".encryptedToken" + public static var encryptedTokenStorageKey: StorageKey { + .init(rawValue: keychainServiceIdentifier + ".encryptedToken") } } public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier), settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(), - encryptedTokenStorageKey: String = Defaults.encryptedTokenStorageKey) { + encryptedTokenStorageKey: StorageKey = Defaults.encryptedTokenStorageKey) { let getValueClosure: GetValueClosure = { keychain, storageKey in do { - guard let value = try keychain.getData(storageKey) else { + guard let value = try keychain.getData(storageKey.rawValue) else { return .failure(.valueNotFound) } @@ -50,9 +51,9 @@ open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage: SingleValueStorage { +open class SingleValueAuthKeychainStorage: BaseSingleValueKeychainStorage { open class Defaults { public static var keychainServiceIdentifier: String { Bundle.main.bundleIdentifier ?? "ru.touchin.TIAuth" } } - public typealias GetValueClosure = (Keychain, String) -> Result - public typealias SetValueClosure = (Keychain, ValueType, String) -> Result - - public let keychain: Keychain public let settingsStorage: AuthSettingsStorage - public let storageKey: String - public let getValueClosure: GetValueClosure - public let setValueClosure: SetValueClosure public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier), settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(), - storageKey: String, + storageKey: StorageKey, getValueClosure: @escaping GetValueClosure, - setValueClosure: @escaping SetValueClosure) { + storeValueClosure: @escaping StoreValueClosure) { - self.keychain = keychain self.settingsStorage = settingsStorage - self.storageKey = storageKey - self.getValueClosure = getValueClosure - self.setValueClosure = setValueClosure + + super.init(keychain: keychain, + storageKey: storageKey, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) } // MARK: - SingleValueStorage - open func hasStoredValue() -> Bool { - !settingsStorage.shouldResetStoredAuthData && ((try? keychain.contains(storageKey)) ?? false) + open override func hasStoredValue() -> Bool { + !settingsStorage.shouldResetStoredAuthData && super.hasStoredValue() } - open func store(value: ValueType) -> Result { - return setValueClosure(keychain, value, storageKey) - } - - open func getValue() -> Result { + open override func getValue() -> Result { guard !settingsStorage.shouldResetStoredAuthData else { let result: Result do { - try keychain.remove(storageKey) + try storage.remove(storageKey.rawValue) settingsStorage.shouldResetStoredAuthData = false result = .failure(.valueNotFound) @@ -79,6 +70,6 @@ open class SingleValueAuthKeychainStorage: SingleValueStorage { return result } - return getValueClosure(keychain, storageKey) + return super.getValue() } } diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec index 051f92ec..d9783051 100644 --- a/TIAuth/TIAuth.podspec +++ b/TIAuth/TIAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAuth' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Login, registration, confirmation and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -13,6 +13,6 @@ Pod::Spec.new do |s| s.source_files = s.name + '/Sources/**/*' s.dependency 'TIFoundationUtils', s.version.to_s + s.dependency 'TIKeychainUtils', s.version.to_s s.dependency 'TIUIKitCore', s.version.to_s - s.dependency 'KeychainAccess', "~> 4.2" end diff --git a/TIDeeplink/TIDeeplink.podspec b/TIDeeplink/TIDeeplink.podspec index a1e237e2..2584a307 100644 --- a/TIDeeplink/TIDeeplink.podspec +++ b/TIDeeplink/TIDeeplink.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeeplink' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Deeplink service API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIDeveloperUtils/TIDeveloperUtils.podspec b/TIDeveloperUtils/TIDeveloperUtils.podspec index 7731de9d..688d67f9 100644 --- a/TIDeveloperUtils/TIDeveloperUtils.podspec +++ b/TIDeveloperUtils/TIDeveloperUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIDeveloperUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIEcommerce/TIEcommerce.podspec b/TIEcommerce/TIEcommerce.podspec index 5cfa9c42..d843966c 100644 --- a/TIEcommerce/TIEcommerce.podspec +++ b/TIEcommerce/TIEcommerce.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIEcommerce' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Cart, products, promocodes, bonuses and other related actions' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift b/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift index 850fa217..69f9a365 100644 --- a/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift +++ b/TIFoundationUtils/Cancellables/Sources/BaseCancellable.swift @@ -25,6 +25,10 @@ open class BaseCancellable: Cancellable { public init() {} + deinit { + cancel() + } + open func cancel() { isCancelled = true } diff --git a/TIFoundationUtils/DataStorage/Sources/AppReinstallChecker.swift b/TIFoundationUtils/DataStorage/Sources/AppReinstallChecker.swift index bb2dcee7..8fc4c023 100644 --- a/TIFoundationUtils/DataStorage/Sources/AppReinstallChecker.swift +++ b/TIFoundationUtils/DataStorage/Sources/AppReinstallChecker.swift @@ -40,9 +40,9 @@ open class AppReinstallChecker { } public init(defaultsStorage: UserDefaults = .standard, - storageKey: String) { + storageKey: StorageKey) { self.defaultsStorage = defaultsStorage - self.storageKey = storageKey + self.storageKey = storageKey.rawValue } } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AnySingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AnySingleValueStorage.swift new file mode 100644 index 00000000..81a2a197 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AnySingleValueStorage.swift @@ -0,0 +1,64 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +public struct AnySingleValueStorage: SingleValueStorage { + public typealias HasValueClosure = () -> Bool + public typealias GetValueClosure = () -> Result + public typealias StoreValueClosure = (ValueType) -> Result + public typealias DeleteValueClosure = () -> Result + + private let hasValueClosure: HasValueClosure + private let deleteValueClosure: DeleteValueClosure + private let getValueClosure: GetValueClosure + private let storeValueClosure: StoreValueClosure + + public init(storage: Storage) + where Storage.ValueType == ValueType, Storage.ErrorType == ErrorType { + + self.hasValueClosure = storage.hasStoredValue + self.deleteValueClosure = storage.deleteValue + self.getValueClosure = storage.getValue + self.storeValueClosure = storage.store + } + + public func hasStoredValue() -> Bool { + hasValueClosure() + } + + public func store(value: ValueType) -> Result { + storeValueClosure(value) + } + + public func getValue() -> Result { + getValueClosure() + } + + public func deleteValue() -> Result { + deleteValueClosure() + } +} + +public extension SingleValueStorage { + func eraseToAnySingleValueStorate() -> AnySingleValueStorage { + AnySingleValueStorage(storage: self) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AppInstallLifetimeSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AppInstallLifetimeSingleValueStorage.swift new file mode 100644 index 00000000..0ea24379 --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/AppInstallLifetimeSingleValueStorage.swift @@ -0,0 +1,82 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +open class AppInstallLifetimeSingleValueStorage: SingleValueStorage +where Storage.ErrorType == StorageError { + + public let appReinstallChecker: AppReinstallChecker + public let wrappedStorage: Storage + + public init(storage: Storage, + appReinstallChecker: AppReinstallChecker) { + + self.wrappedStorage = storage + self.appReinstallChecker = appReinstallChecker + } + + // MARK: - SingleValueStorage + + open func hasStoredValue() -> Bool { + if appReinstallChecker.isAppFirstRun { + return false + } + + return wrappedStorage.hasStoredValue() + } + + open func getValue() -> Result { + guard appReinstallChecker.isAppFirstRun else { + return wrappedStorage.getValue() + } + + let result = wrappedStorage.deleteValue() + + if case .success = result { + appReinstallChecker.isAppFirstRun = false + } + + return result.flatMap { .failure(.valueNotFound) } + } + + open func store(value: Storage.ValueType) -> Result { + let result = wrappedStorage.store(value: value) + + if case .success = result { + appReinstallChecker.isAppFirstRun = false + } + + return result + } + + public func deleteValue() -> Result { + wrappedStorage.deleteValue() + } +} + +public extension SingleValueStorage { + func appInstallLifetimeStorage(reinstallChecker: AppReinstallChecker) -> AppInstallLifetimeSingleValueStorage + where Self.ErrorType == ErrorType { + + AppInstallLifetimeSingleValueStorage(storage: self, + appReinstallChecker: reinstallChecker) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueDefaultsStorage.swift new file mode 100644 index 00000000..08e1dccd --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueDefaultsStorage.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +open class BaseSingleValueDefaultsStorage: BaseSingleValueStorage { + public init(defaults: UserDefaults, + storageKey: StorageKey, + getValueClosure: @escaping GetValueClosure, + storeValueClosure: @escaping StoreValueClosure) { + + super.init(storage: defaults, + storageKey: storageKey, + hasValueClosure: { .success($0.object(forKey: $1.rawValue) != nil) }, + deleteValueClosure: { .success($0.removeObject(forKey: $1.rawValue)) }, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueStorage.swift new file mode 100644 index 00000000..73a2119d --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/BaseSingleValueStorage.swift @@ -0,0 +1,68 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +open class BaseSingleValueStorage: SingleValueStorage { + public typealias HasValueClosure = (StorageType, StorageKey) -> Result + public typealias GetValueClosure = (StorageType, StorageKey) -> Result + public typealias StoreValueClosure = (StorageType, ValueType, StorageKey) -> Result + public typealias DeleteValueClosure = (StorageType, StorageKey) -> Result + + public let storage: StorageType + public let storageKey: StorageKey + public let hasValueClosure: HasValueClosure + public let deleteValueClosure: DeleteValueClosure + public let getValueClosure: GetValueClosure + public let storeValueClosure: StoreValueClosure + + public init(storage: StorageType, + storageKey: StorageKey, + hasValueClosure: @escaping HasValueClosure, + deleteValueClosure: @escaping DeleteValueClosure, + getValueClosure: @escaping GetValueClosure, + storeValueClosure: @escaping StoreValueClosure) { + + self.storage = storage + self.storageKey = storageKey + self.hasValueClosure = hasValueClosure + self.deleteValueClosure = deleteValueClosure + self.getValueClosure = getValueClosure + self.storeValueClosure = storeValueClosure + } + + // MARK: - SingleValueStorage + + open func hasStoredValue() -> Bool { + (try? hasValueClosure(storage, storageKey).get()) ?? false + } + + open func store(value: ValueType) -> Result { + storeValueClosure(storage, value, storageKey) + } + + open func getValue() -> Result { + getValueClosure(storage, storageKey) + } + + open func deleteValue() -> Result { + deleteValueClosure(storage, storageKey) + } +} diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/SingleValueStorage.swift similarity index 96% rename from TIFoundationUtils/DataStorage/Sources/SingleValueStorage.swift rename to TIFoundationUtils/DataStorage/Sources/SingleValueStorage/SingleValueStorage.swift index 4c832e6a..d1a440b8 100644 --- a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage.swift +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/SingleValueStorage.swift @@ -27,4 +27,5 @@ public protocol SingleValueStorage { func hasStoredValue() -> Bool func store(value: ValueType) -> Result func getValue() -> Result + func deleteValue() -> Result } diff --git a/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/StringValueDefaultsStorage.swift b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/StringValueDefaultsStorage.swift new file mode 100644 index 00000000..9bc2c4dc --- /dev/null +++ b/TIFoundationUtils/DataStorage/Sources/SingleValueStorage/StringValueDefaultsStorage.swift @@ -0,0 +1,40 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import Foundation + +public final class StringValueDefaultsStorage: BaseSingleValueDefaultsStorage { + public init(defaults: UserDefaults, storageKey: StorageKey) { + let getValueClosure: GetValueClosure = { defaults, storageKey in + guard let value = defaults.string(forKey: storageKey.rawValue) else { + return .failure(.valueNotFound) + } + + return .success(value) + } + + super.init(defaults: defaults, + storageKey: storageKey, + getValueClosure: getValueClosure, + storeValueClosure: { .success($0.set($1, forKey: $2.rawValue)) }) + } +} diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index b585a5eb..3be78475 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index 4e9fe29f..a5e1af69 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIKeychainUtils/PlaygroundPodfile b/TIKeychainUtils/PlaygroundPodfile new file mode 100644 index 00000000..a39cbfee --- /dev/null +++ b/TIKeychainUtils/PlaygroundPodfile @@ -0,0 +1,11 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TIModuleName' do + platform :ios, 11 + use_frameworks! + + pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIKeychainUtils', :path => '../../../../TIKeychainUtils/TIKeychainUtils.podspec' + pod 'KeychainAccess' +end diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/BaseSingleValueKeychainStorage.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/BaseSingleValueKeychainStorage.swift new file mode 100644 index 00000000..0641a4df --- /dev/null +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/BaseSingleValueKeychainStorage.swift @@ -0,0 +1,49 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import TIFoundationUtils +import KeychainAccess + +open class BaseSingleValueKeychainStorage: BaseSingleValueStorage { + public init(keychain: Keychain, + storageKey: StorageKey, + getValueClosure: @escaping GetValueClosure, + storeValueClosure: @escaping StoreValueClosure) { + + let hasValueClosure: HasValueClosure = { keychain, storageKey in + Result { try keychain.contains(storageKey.rawValue) } + .mapError { StorageError.unableToExtractData(underlyingError: $0) } + } + + let deleteValueClosure: DeleteValueClosure = { keychain, storageKey in + Result { try keychain.remove(storageKey.rawValue) } + .mapError { StorageError.unableToWriteData(underlyingError: $0) } + } + + super.init(storage: keychain, + storageKey: storageKey, + hasValueClosure: hasValueClosure, + deleteValueClosure: deleteValueClosure, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} diff --git a/TIKeychainUtils/Sources/SingleValueKeychainStorage/StringValueKeychainStorage.swift b/TIKeychainUtils/Sources/SingleValueKeychainStorage/StringValueKeychainStorage.swift new file mode 100644 index 00000000..85b5cf7a --- /dev/null +++ b/TIKeychainUtils/Sources/SingleValueKeychainStorage/StringValueKeychainStorage.swift @@ -0,0 +1,53 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import KeychainAccess +import TIFoundationUtils + +public final class StringValueKeychainStorage: BaseSingleValueKeychainStorage { + public init(keychain: Keychain, storageKey: StorageKey) { + let getValueClosure: GetValueClosure = { keychain, storageKey in + do { + guard let value = try keychain.get(storageKey.rawValue) else { + return .failure(.valueNotFound) + } + + return .success(value) + } catch { + return .failure(.unableToExtractData(underlyingError: error)) + } + } + + let storeValueClosure: StoreValueClosure = { keychain, value, storageKey in + do { + return .success(try keychain.set(value, key: storageKey.rawValue)) + } catch { + return .failure(.unableToWriteData(underlyingError: error)) + } + } + + super.init(keychain: keychain, + storageKey: storageKey, + getValueClosure: getValueClosure, + storeValueClosure: storeValueClosure) + } +} diff --git a/TIKeychainUtils/TIKeychainUtils.app/.gitignore b/TIKeychainUtils/TIKeychainUtils.app/.gitignore new file mode 100644 index 00000000..b7fe13ce --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/.gitignore @@ -0,0 +1,4 @@ +# gitignore nef files +**/build/ +**/nef/ +LICENSE \ No newline at end of file diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/Info.plist b/TIKeychainUtils/TIKeychainUtils.app/Contents/Info.plist new file mode 100644 index 00000000..831ea97a --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/Info.plist @@ -0,0 +1,28 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + launcher + CFBundleIconFile + AppIcon + CFBundleIconName + AppIcon + CFBundleIdentifier + com.fortysevendeg.nef + CFBundleInfoDictionaryVersion + 6.0 + CFBundleSupportedPlatforms + + MacOSX + + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 10.14 + NSHumanReadableCopyright + Copyright © 2019 The nef Authors. All rights reserved. + + diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/.gitignore b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/.gitignore new file mode 100644 index 00000000..18bd1f3b --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/.gitignore @@ -0,0 +1,26 @@ +## gitignore nef files +**/build/ +**/nef/ +LICENSE + +## User data +**/xcuserdata/ +podfile.lock +**.DS_Store + +## Obj-C/Swift specific +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +## CocoaPods +**Pods** + +## Carthage +**Carthage** + +## SPM +.build +.swiftpm +swiftpm diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/Podfile b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/Podfile new file mode 100644 index 00000000..f0a332cf --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/Podfile @@ -0,0 +1,11 @@ +ENV["DEVELOPMENT_INSTALL"] = "true" + +target 'TIKeychainUtils' do + platform :ios, 11 + use_frameworks! + + pod 'TIFoundationUtils', :path => '../../../../TIFoundationUtils/TIFoundationUtils.podspec' + pod 'TISwiftUtils', :path => '../../../../TISwiftUtils/TISwiftUtils.podspec' + pod 'TIKeychainUtils', :path => '../../../../TIKeychainUtils/TIKeychainUtils.podspec' + pod 'KeychainAccess' +end diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..8808b79f --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Pages/SingleValueStorage.xcplaygroundpage/Contents.swift @@ -0,0 +1,73 @@ +/*: + # `SingleValueStorage` - протокол для доступа к значению которое может храниться в Keychain, UserDefaults или ещё где-то. + +Позволяет: + +- инкапсулировать внутри себя логику получения, записи и удаления значения +- добавлять дополнительную логику для получения или изменения значений через композицию или наследование +- ограничить доступ к данным в UserDefaults или Keychain в разных частях приложения + + */ + +/*: + ### `StringValueKeychainStorage` + + Класс для работы со строковым значением нахоящимся в keychain (самый частый кейс) +*/ + +import TIKeychainUtils +import TIFoundationUtils +import KeychainAccess + +extension StorageKey { + static var apiToken: StorageKey { + .init(rawValue: "apiToken") + } + + static var deleteApiToken: StorageKey { + .init(rawValue: "deleteApiToken") + } +} + +let keychain = Keychain() + +let apiTokenKeychainStorage = StringValueKeychainStorage(keychain: keychain, storageKey: .apiToken) + +if apiTokenKeychainStorage.hasStoredValue() { + // open auth user flow, perform requests +} else { + // show login screen + // ... + + // login + +// switch await userService.login() { +// case .success: +// // open auth user flow, perform requests +// case .failure: +// // show login screen +// } +} + +/*: + ### `AppInstallLifetimeSingleValueStorage` + + Класс позволяющий добавить дополнительную функциональность очистки значения по конкретному ключу в keychain + после переустановки приложения +*/ + +import Foundation + +let defaults = UserDefaults.standard // or AppGroup defaults + +let appReinstallChecker = AppReinstallChecker(defaultsStorage: defaults, + storageKey: .deleteApiToken) + +let appInstallAwareTokenStorage = apiTokenKeychainStorage.appInstallLifetimeStorage(reinstallChecker: appReinstallChecker) + +if appInstallAwareTokenStorage.hasStoredValue() { + // app wasn't reinstalled, token is exist +} else { + // app was reinstalled or token is empty + // ... +} diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Sources/NefPlaygroundSupport.swift b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Sources/NefPlaygroundSupport.swift new file mode 100644 index 00000000..2ffea80a --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/Sources/NefPlaygroundSupport.swift @@ -0,0 +1,30 @@ +import UIKit + +public protocol NefPlaygroundLiveViewable {} +extension UIView: NefPlaygroundLiveViewable {} +extension UIViewController: NefPlaygroundLiveViewable {} + +#if NOT_IN_PLAYGROUND +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) {} + public static func needsIndefiniteExecution(_ state: Bool) {} + } +} + +#else +import PlaygroundSupport + +public enum Nef { + public enum Playground { + public static func liveView(_ view: NefPlaygroundLiveViewable) { + PlaygroundPage.current.liveView = (view as! PlaygroundLiveViewable) + } + + public static func needsIndefiniteExecution(_ state: Bool) { + PlaygroundPage.current.needsIndefiniteExecution = state + } + } +} + +#endif diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/contents.xcplayground b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/contents.xcplayground new file mode 100644 index 00000000..00daa653 --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground/contents.xcplayground @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/project.pbxproj b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/project.pbxproj new file mode 100644 index 00000000..838e90d3 --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/project.pbxproj @@ -0,0 +1,396 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 419F8E81EC23E596305C14C1 /* Pods_TIKeychainUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A9558DC3B75CF88D5A98670 /* Pods_TIKeychainUtils.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 02F7E6D0F4B151F4585D0961 /* Pods-TIKeychainUtils.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIKeychainUtils.debug.xcconfig"; path = "Target Support Files/Pods-TIKeychainUtils/Pods-TIKeychainUtils.debug.xcconfig"; sourceTree = ""; }; + 1A9558DC3B75CF88D5A98670 /* Pods_TIKeychainUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TIKeychainUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7D43492919D876D7B5F60316 /* Pods-TIKeychainUtils.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TIKeychainUtils.release.xcconfig"; path = "Target Support Files/Pods-TIKeychainUtils/Pods-TIKeychainUtils.release.xcconfig"; sourceTree = ""; }; + 8BACBE8322576CAD00266845 /* TIKeychainUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TIKeychainUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8BACBE8622576CAD00266845 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8BACBE8022576CAD00266845 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 419F8E81EC23E596305C14C1 /* Pods_TIKeychainUtils.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 75FB9D9E19767EA711BCA3E2 /* Pods */ = { + isa = PBXGroup; + children = ( + 02F7E6D0F4B151F4585D0961 /* Pods-TIKeychainUtils.debug.xcconfig */, + 7D43492919D876D7B5F60316 /* Pods-TIKeychainUtils.release.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 8B39A26221D40F8700DE2643 = { + isa = PBXGroup; + children = ( + 8BACBE8422576CAD00266845 /* TIKeychainUtils */, + 8B39A26C21D40F8700DE2643 /* Products */, + 75FB9D9E19767EA711BCA3E2 /* Pods */, + B96CA711C514498357DF4109 /* Frameworks */, + ); + sourceTree = ""; + }; + 8B39A26C21D40F8700DE2643 /* Products */ = { + isa = PBXGroup; + children = ( + 8BACBE8322576CAD00266845 /* TIKeychainUtils.framework */, + ); + name = Products; + sourceTree = ""; + }; + 8BACBE8422576CAD00266845 /* TIKeychainUtils */ = { + isa = PBXGroup; + children = ( + 8BACBE8622576CAD00266845 /* Info.plist */, + ); + path = TIKeychainUtils; + sourceTree = ""; + }; + B96CA711C514498357DF4109 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1A9558DC3B75CF88D5A98670 /* Pods_TIKeychainUtils.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8BACBE7E22576CAD00266845 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8BACBE8222576CAD00266845 /* TIKeychainUtils */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIKeychainUtils" */; + buildPhases = ( + FB020BFF1242B09050D0D379 /* [CP] Check Pods Manifest.lock */, + 8BACBE7E22576CAD00266845 /* Headers */, + 8BACBE7F22576CAD00266845 /* Sources */, + 8BACBE8022576CAD00266845 /* Frameworks */, + 8BACBE8122576CAD00266845 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TIKeychainUtils; + productName = TIKeychainUtils2; + productReference = 8BACBE8322576CAD00266845 /* TIKeychainUtils.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8B39A26321D40F8700DE2643 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1010; + LastUpgradeCheck = 1200; + ORGANIZATIONNAME = "47 Degrees"; + TargetAttributes = { + 8BACBE8222576CAD00266845 = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIKeychainUtils" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8B39A26221D40F8700DE2643; + productRefGroup = 8B39A26C21D40F8700DE2643 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8BACBE8222576CAD00266845 /* TIKeychainUtils */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8BACBE8122576CAD00266845 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + FB020BFF1242B09050D0D379 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-TIKeychainUtils-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8BACBE7F22576CAD00266845 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8B39A27721D40F8800DE2643 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 8B39A27821D40F8800DE2643 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 8BACBE8822576CAD00266845 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 02F7E6D0F4B151F4585D0961 /* Pods-TIKeychainUtils.debug.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIKeychainUtils_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIKeychainUtils/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIKeychainUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8BACBE8922576CAD00266845 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7D43492919D876D7B5F60316 /* Pods-TIKeychainUtils.release.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + CURRENT_TIKeychainUtils_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "$(SRCROOT)/TIKeychainUtils/Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.47deg.ios.TIKeychainUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8B39A26621D40F8700DE2643 /* Build configuration list for PBXProject "TIKeychainUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8B39A27721D40F8800DE2643 /* Debug */, + 8B39A27821D40F8800DE2643 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8BACBE8A22576CAD00266845 /* Build configuration list for PBXNativeTarget "TIKeychainUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8BACBE8822576CAD00266845 /* Debug */, + 8BACBE8922576CAD00266845 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8B39A26321D40F8700DE2643 /* Project object */; +} diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..423fc503 --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/xcshareddata/xcschemes/TIKeychainUtils.xcscheme b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/xcshareddata/xcschemes/TIKeychainUtils.xcscheme new file mode 100644 index 00000000..065c0e82 --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcodeproj/xcshareddata/xcschemes/TIKeychainUtils.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcworkspace/contents.xcworkspacedata b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..530d439c --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils/Info.plist b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils/Info.plist new file mode 100644 index 00000000..98d14f60 --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1.0 + NSHumanReadableCopyright + Copyright © 2019. The nef authors. + + diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/launcher b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/launcher new file mode 100755 index 00000000..fd9aba4b --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.app/Contents/MacOS/launcher @@ -0,0 +1,6 @@ +#!/bin/bash + +workspace="TIKeychainUtils.xcworkspace" +workspacePath=$(echo "$0" | rev | cut -f2- -d '/' | rev) + +open "`pwd`/$workspacePath/$workspace" diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/Resources/AppIcon.icns b/TIKeychainUtils/TIKeychainUtils.app/Contents/Resources/AppIcon.icns new file mode 100644 index 00000000..32814f1c Binary files /dev/null and b/TIKeychainUtils/TIKeychainUtils.app/Contents/Resources/AppIcon.icns differ diff --git a/TIKeychainUtils/TIKeychainUtils.app/Contents/Resources/Assets.car b/TIKeychainUtils/TIKeychainUtils.app/Contents/Resources/Assets.car new file mode 100644 index 00000000..79d9ea89 Binary files /dev/null and b/TIKeychainUtils/TIKeychainUtils.app/Contents/Resources/Assets.car differ diff --git a/TIKeychainUtils/TIKeychainUtils.playground b/TIKeychainUtils/TIKeychainUtils.playground new file mode 120000 index 00000000..c1284cc1 --- /dev/null +++ b/TIKeychainUtils/TIKeychainUtils.playground @@ -0,0 +1 @@ +TIKeychainUtils.app/Contents/MacOS/TIKeychainUtils.playground \ No newline at end of file diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index eaad9f74..0b37df22 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TILogging/Sources/DefaultOSLogErrorLogger.swift b/TILogging/Sources/DefaultOSLogErrorLogger.swift new file mode 100644 index 00000000..5d90c94d --- /dev/null +++ b/TILogging/Sources/DefaultOSLogErrorLogger.swift @@ -0,0 +1,44 @@ +// +// Copyright (c) 2023 Touch Instinct +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +import os + +open class DefaultOSLogErrorLogger: ErrorLogger { + public var log: OSLog + + public init(log: OSLog) { + self.log = log + } + + public convenience init(subsystem: String, category: String) { + self.init(log: OSLog(subsystem: subsystem, category: category)) + } + + open func log(error: Error, file: StaticString, line: Int) { + os_log("%{public}s:%{public}d %{public}s", + log: log, + type: .error, + file.debugDescription, + line, + error.localizedDescription) + } +} diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSecureStorage.swift b/TILogging/Sources/ErrorLogger.swift similarity index 91% rename from TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSecureStorage.swift rename to TILogging/Sources/ErrorLogger.swift index 99fa9e69..44f99e3f 100644 --- a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSecureStorage.swift +++ b/TILogging/Sources/ErrorLogger.swift @@ -20,6 +20,8 @@ // THE SOFTWARE. // -public protocol FingerprintsSecureStorage { - var knownPins: [String: Set] { get set } +import Foundation + +public protocol ErrorLogger { + func log(error: Error, file: StaticString, line: Int) } diff --git a/TILogging/TILogging.podspec b/TILogging/TILogging.podspec new file mode 100644 index 00000000..d47e273a --- /dev/null +++ b/TILogging/TILogging.podspec @@ -0,0 +1,15 @@ +Pod::Spec.new do |s| + s.name = 'TILogging' + s.version = '1.45.0' + s.summary = 'Logging for TI libraries.' + s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } + s.source = { :git => 'https://git.svc.touchin.ru/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '11.0' + s.swift_versions = ['5.7'] + + s.source_files = s.name + '/Sources/**/*' + +end diff --git a/TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift index 49200b0f..3ea07ff9 100644 --- a/TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift +++ b/TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift @@ -45,6 +45,6 @@ public struct CALayerDrawingOperation: DrawingOperation { context.concatenate(offsetTransform) layer.render(in: context) - offsetTransform.concatenating(offsetTransform.inverted()) + context.concatenate(offsetTransform.inverted()) } } diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index be12acde..2ce197c2 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMapUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Set of helpers for map objects clustering and interacting.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift b/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift index f7da59ff..dca3a6cb 100644 --- a/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift +++ b/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift @@ -69,10 +69,10 @@ open class DefaultJsonNetworkService: ApiInteractor { } open func process(request: EndpointRequest, - mapSuccess: @escaping Closure, - mapFailure: @escaping Closure, R>, - mapNetworkError: @escaping Closure, - completion: @escaping ParameterClosure) -> TIFoundationUtils.Cancellable { + mapSuccess: @escaping Closure, + mapFailure: @escaping Closure, R>, + mapNetworkError: @escaping Closure, + completion: @escaping ParameterClosure) -> TIFoundationUtils.Cancellable { let cancellableBag = BaseCancellableBag() @@ -94,7 +94,7 @@ open class DefaultJsonNetworkService: ApiInteractor { mapFailure: mapFailure, mapNetworkError: mapNetworkError, completion: completion) - .add(to: cancellableBag) + .add(to: cancellableBag) } catch { callbackQueue.async { completion(mapNetworkError(.encodableMapping(error))) @@ -116,10 +116,10 @@ open class DefaultJsonNetworkService: ApiInteractor { } open func process(request: SerializedRequest, - mapSuccess: @escaping Closure, - mapFailure: @escaping Closure, R>, - mapNetworkError: @escaping Closure, - completion: @escaping ParameterClosure) -> TIFoundationUtils.Cancellable { + mapSuccess: @escaping Closure, + mapFailure: @escaping Closure, R>, + mapNetworkError: @escaping Closure, + completion: @escaping ParameterClosure) -> TIFoundationUtils.Cancellable { createProvider().request(request) { [jsonDecoder, callbackQueue, @@ -195,12 +195,12 @@ open class DefaultJsonNetworkService: ApiInteractor { } private static func preprocess(request: EndpointRequest, - preprocessors: P, - cancellableBag: BaseCancellableBag, - completion: @escaping (Result, Error>) -> Void) + preprocessors: P, + cancellableBag: BaseCancellableBag, + completion: @escaping (Result, Error>) -> Void) where P.Element == EndpointRequestPreprocessor { - guard let preprocessor = preprocessors.first else { + guard let preprocessor = preprocessors.first, !cancellableBag.isCancelled else { completion(.success(request)) return } diff --git a/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift b/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift index eaaa8988..96338ebd 100644 --- a/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift +++ b/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift @@ -20,66 +20,147 @@ // THE SOFTWARE. // -import Moya import TINetworking import TISwiftUtils +import TIFoundationUtils +import Alamofire -@available(iOS 13.0.0, *) open class DefaultRecoverableJsonNetworkService: DefaultJsonNetworkService { - public typealias EndpointResponse = EndpointRecoverableRequestResult - public typealias ErrorType = EndpointErrorResult + public typealias EndpointResponse = EndpointRecoverableRequestResult + public typealias ErrorType = EndpointErrorResult + public typealias RecoverableErrorType = ErrorCollection public typealias RequestRetrier = AnyEndpointRequestRetrier public private(set) var defaultRequestRetriers: [RequestRetrier] = [] + open func process(recoverableRequest: EndpointRequest, + prependRequestRetriers: [RequestRetrier] = [], + appendRequestRetriers: [RequestRetrier] = [], + completion: @escaping ParameterClosure>) -> Cancellable { + + process(recoverableRequest: recoverableRequest, + errorHandlers: prependRequestRetriers + defaultRequestRetriers + appendRequestRetriers, + completion: completion) + } + + @available(iOS 13.0.0, *) open func process(recoverableRequest: EndpointRequest, prependRequestRetriers: [RequestRetrier] = [], appendRequestRetriers: [RequestRetrier] = []) async -> EndpointResponse { - await process(recoverableRequest: recoverableRequest, - errorHandlers: prependRequestRetriers + defaultRequestRetriers + appendRequestRetriers) + await withTaskCancellableClosure { + process(recoverableRequest: recoverableRequest, + prependRequestRetriers: prependRequestRetriers, + appendRequestRetriers: appendRequestRetriers, + completion: $0) + } } + open func process(recoverableRequest: EndpointRequest, + errorHandlers: [RequestRetrier], + completion: @escaping ParameterClosure>) -> Cancellable { + + Cancellables.scoped { cancellableBag in + process(request: recoverableRequest) { [weak self] in + self?.handle(recoverableResponse: $0, + request: recoverableRequest, + errorHandlers: errorHandlers, + cancellableBag: cancellableBag, + completion: completion) + } + } + } + + @available(iOS 13.0.0, *) open func process(recoverableRequest: EndpointRequest, errorHandlers: [RequestRetrier]) async -> EndpointResponse { - let result: RequestResult = await process(request: recoverableRequest) + await withTaskCancellableClosure { + process(recoverableRequest: recoverableRequest, + errorHandlers: errorHandlers, + completion: $0) + } + } - if case let .failure(errorResponse) = result { - var failures = [errorResponse] + open func handle(recoverableResponse: RequestResult, + request: EndpointRequest, + errorHandlers: [RequestRetrier], + cancellableBag: BaseCancellableBag, + completion: @escaping ParameterClosure>) { - for handler in errorHandlers { - let handlerResult = await handler.validateAndRepair(errorResults: failures) - - switch handlerResult { - case let .success(retryResult): - switch retryResult { - case .retry, .retryWithDelay: - return await process(recoverableRequest: recoverableRequest, errorHandlers: errorHandlers) - case .doNotRetry, .doNotRetryWithError: - break - } - case let .failure(error): - failures.append(error) - } + if case let .failure(errorResponse) = recoverableResponse { + guard !cancellableBag.isCancelled else { + return } - return .failure(.init(failures: failures)) - } + Self.validateAndRepair(request: request, + errors: [errorResponse], + retriers: errorHandlers, + cancellableBag: cancellableBag) { + switch $0 { + case .retry, .retryWithDelay: + self.process(request: request) { + completion($0.mapError { .init(failures: [$0]) }) + } + .add(to: cancellableBag) - return result.mapError { .init(failures: [$0]) } + case .doNotRetry, .doNotRetryWithError: + completion(recoverableResponse.mapError { .init(failures: [$0]) }) + } + } + } else { + completion(recoverableResponse.mapError { .init(failures: [$0]) }) + } } public func register(defaultRequestRetrier: RequestRetrier) - where RequestRetrier.ErrorResult == ErrorType { + where RequestRetrier.ErrorResult == ErrorType { defaultRequestRetriers.append(defaultRequestRetrier.asAnyEndpointRequestRetrier()) } public func set(defaultRequestRetriers: RequestRetrier...) - where RequestRetrier.ErrorResult == ErrorType { + where RequestRetrier.ErrorResult == ErrorType { self.defaultRequestRetriers = defaultRequestRetriers.map { $0.asAnyEndpointRequestRetrier() } } + + private static func validateAndRepair(request: EndpointRequest, + errors: [ErrorType], + retriers: R, + cancellableBag: BaseCancellableBag, + completion: @escaping ParameterClosure) + where R.Element == RequestRetrier { + + guard let retrier = retriers.first, !cancellableBag.isCancelled else { + completion(.doNotRetry) + return + } + + retrier.validateAndRepair(errorResults: errors) { handlerResult in + switch handlerResult { + case let .success(retryResult): + switch retryResult { + case .retry, .retryWithDelay: + completion(.retry) + + case .doNotRetry, .doNotRetryWithError: + validateAndRepair(request: request, + errors: errors, + retriers: retriers.dropFirst(), + cancellableBag: cancellableBag, + completion: completion) + } + + case let .failure(error): + validateAndRepair(request: request, + errors: errors + [error], + retriers: retriers.dropFirst(), + cancellableBag: cancellableBag, + completion: completion) + } + } + .add(to: cancellableBag) + } } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 281c1fc4..7c885bdd 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift index e5877e29..cf21bda6 100644 --- a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsProvider.swift @@ -20,29 +20,64 @@ // THE SOFTWARE. // +import TIFoundationUtils +import TILogging + open class DefaultFingerprintsProvider: FingerprintsProvider { - public var secureStorage: FingerprintsSecureStorage - public var settingsStorage: FingerprintsSettingsStorage + public typealias FingerprintsMapping = [String: Set] - public init(secureStorage: FingerprintsSecureStorage, - settingsStorage: FingerprintsSettingsStorage, - bundledFingerprints: [String: Set]) { + public var secureStorage: AnySingleValueStorage + public var bundledFingerprints: FingerprintsMapping + public var errorLogger: ErrorLogger - self.secureStorage = secureStorage - self.settingsStorage = settingsStorage + public init(secureStorage: Storage, + bundledFingerprints: FingerprintsMapping, + errorLogger: ErrorLogger = TINetworkingLogger(category: "Fingerprints")) + where Storage.ValueType == FingerprintsMapping, Storage.ErrorType == StorageError { - if settingsStorage.shouldResetFingerprints { - self.secureStorage.knownPins = bundledFingerprints - self.settingsStorage.shouldResetFingerprints = false + self.secureStorage = secureStorage.eraseToAnySingleValueStorate() + self.bundledFingerprints = bundledFingerprints + self.errorLogger = errorLogger + + let fingerprintsUpdateResult = secureStorage + .getValue() + .map { + $0.merging(bundledFingerprints) { storedFingerprints, bundleFingerprints in + storedFingerprints.union(bundleFingerprints) + } + } + .flatMap { + secureStorage.store(value: $0) + } + + if case let .failure(storageError) = fingerprintsUpdateResult { + errorLogger.log(error: storageError, file: #file, line: #line) } } - public func fingerprints(forHost host: String) -> Set { - secureStorage.knownPins[host] ?? [] + open func fingerprints(forHost host: String) -> Set { + (try? secureStorage + .getValue() + .flatMapError { _ -> Result in + .success(bundledFingerprints) + } + .get())?[host] ?? [] } - public func add(fingerprints: [String], forHost host: String) { - let pinsForHost = (secureStorage.knownPins[host] ?? []).union(fingerprints) - secureStorage.knownPins.updateValue(pinsForHost, forKey: host) + open func add(fingerprints: Set, forHost host: String) { + let fingerprintsUpdateResult = secureStorage + .getValue() + .map { + $0.merging([host: fingerprints]) { storedFingerprints, addedFingerprints in + storedFingerprints.union(addedFingerprints) + } + } + .flatMap { + secureStorage.store(value: $0) + } + + if case let .failure(storageError) = fingerprintsUpdateResult { + errorLogger.log(error: storageError, file: #file, line: #line) + } } } diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsSettingsStorage.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsSettingsStorage.swift index 6926f1eb..72f61591 100644 --- a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsSettingsStorage.swift +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/DefaultFingerprintsSettingsStorage.swift @@ -23,29 +23,16 @@ import TIFoundationUtils import Foundation -open class DefaultFingerprintsSettingsStorage: FingerprintsSettingsStorage { +open class FingerprintsReinstallChecker: AppReinstallChecker { public enum Defaults { - public static var shouldResetFingerprintsKey: String { - "shouldResetFingerprints" + public static var shouldResetFingerprintsKey: StorageKey { + .init(rawValue: "shouldResetFingerprints") } } - private let reinstallChecker: AppReinstallChecker + public override init(defaultsStorage: UserDefaults = .standard, + storageKey: StorageKey = Defaults.shouldResetFingerprintsKey) { - // MARK: - PinCodeSettingsStorage - - open var shouldResetFingerprints: Bool { - get { - reinstallChecker.isAppFirstRun - } - set { - reinstallChecker.isAppFirstRun = newValue - } - } - - public init(defaultsStorage: UserDefaults = .standard, - storageKey: String = Defaults.shouldResetFingerprintsKey) { - - self.reinstallChecker = AppReinstallChecker(defaultsStorage: defaultsStorage, storageKey: storageKey) + super.init(defaultsStorage: defaultsStorage, storageKey: storageKey) } } diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsProvider.swift b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsProvider.swift index 871f46de..906025cd 100644 --- a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsProvider.swift +++ b/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsProvider.swift @@ -22,5 +22,5 @@ public protocol FingerprintsProvider { func fingerprints(forHost host: String) -> Set - func add(fingerprints: [String], forHost host: String) + func add(fingerprints: Set, forHost host: String) } diff --git a/TINetworking/Sources/ApiInteractor/ApiInteractor.swift b/TINetworking/Sources/ApiInteractor/ApiInteractor.swift index 93a5e148..ffe1c485 100644 --- a/TINetworking/Sources/ApiInteractor/ApiInteractor.swift +++ b/TINetworking/Sources/ApiInteractor/ApiInteractor.swift @@ -36,13 +36,24 @@ public protocol ApiInteractor { completion: @escaping ParameterClosure) -> Cancellable } +public extension ApiInteractor { + func process(request: EndpointRequest, + completion: @escaping ParameterClosure>) -> Cancellable { + + process(request: request, + mapSuccess: Result.success, + mapFailure: { .failure(.apiError($0.apiError, $0.statusCode)) }, + mapNetworkError: { .failure(.networkError($0)) }, + completion: completion) + } +} + @available(iOS 13.0.0, *) public extension ApiInteractor { func process(request: EndpointRequest) async -> RequestResult { - await process(request: request, - mapSuccess: Result.success, - mapFailure: { .failure(.apiError($0.apiError, $0.statusCode)) }, - mapNetworkError: { .failure(.networkError($0)) }) + await withTaskCancellableClosure { + process(request: request, completion: $0) + } } func process(request: EndpointRequest, @@ -50,13 +61,12 @@ public extension ApiInteractor { mapFailure: @escaping Closure, R>, mapNetworkError: @escaping Closure) async -> R { - await withTaskCancellableClosure { completion in + await withTaskCancellableClosure { process(request: request, mapSuccess: mapSuccess, mapFailure: mapFailure, - mapNetworkError: mapNetworkError) { - completion($0) - } + mapNetworkError: mapNetworkError, + completion: $0) } } } diff --git a/TINetworking/Sources/Mapping/ResponseContent/ApplicationJsonResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/ApplicationJsonResponseContent.swift index 37a6497f..f01834f7 100644 --- a/TINetworking/Sources/Mapping/ResponseContent/ApplicationJsonResponseContent.swift +++ b/TINetworking/Sources/Mapping/ResponseContent/ApplicationJsonResponseContent.swift @@ -33,8 +33,16 @@ open class ApplicationJsonResponseContent: BaseContent, Respon // MARK: - ResponseContent - public func decodeResponse(data: Data) throws -> Model { - try jsonDecoder.decode(Model.self, from: data) + public func decodeResponse(data: Data) -> Result { + do { + return .success(try jsonDecoder.decode(Model.self, from: data)) + } catch let decodingError as DecodingError { + return .failure(decodingError) + } catch { + return .failure(.dataCorrupted(.init(codingPath: [], + debugDescription: .init(), + underlyingError: error))) + } } } diff --git a/TINetworking/Sources/Mapping/ResponseContent/EmptyResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/EmptyResponseContent.swift index 301b7358..beefa435 100644 --- a/TINetworking/Sources/Mapping/ResponseContent/EmptyResponseContent.swift +++ b/TINetworking/Sources/Mapping/ResponseContent/EmptyResponseContent.swift @@ -23,7 +23,7 @@ import Foundation public final class EmptyResponseContent: BaseContent, ResponseContent { - public func decodeResponse(data: Data) throws { - () + public func decodeResponse(data: Data) -> Result { + .success(()) } } diff --git a/TINetworking/Sources/Mapping/ResponseContent/MapResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/MapResponseContent.swift index 7e38fabb..0a8f6d1e 100644 --- a/TINetworking/Sources/Mapping/ResponseContent/MapResponseContent.swift +++ b/TINetworking/Sources/Mapping/ResponseContent/MapResponseContent.swift @@ -24,11 +24,11 @@ import Foundation import TISwiftUtils public final class MapResponseContent: BaseContent, ResponseContent { - private let decodeClosure: ThrowableClosure + private let decodeClosure: Closure> public init(responseContent: C, transform: @escaping Closure) { decodeClosure = { - transform(try responseContent.decodeResponse(data: $0)) + responseContent.decodeResponse(data: $0).map(transform) } super.init(mediaTypeName: responseContent.mediaTypeName) @@ -36,8 +36,8 @@ public final class MapResponseContent: BaseContent, ResponseContent { // MARK: - ResponseContent - public func decodeResponse(data: Data) throws -> Model { - try decodeClosure(data) + public func decodeResponse(data: Data) -> Result { + decodeClosure(data) } } @@ -54,7 +54,7 @@ public extension JSONDecoder { responseContent().map(tranfsorm) } - func decoding(to tranfsorm: @escaping Closure) -> ThrowableClosure { + func decoding(to tranfsorm: @escaping Closure) -> Closure> { responseContent(tranfsorm).decodeResponse } } diff --git a/TINetworking/Sources/Mapping/ResponseContent/ResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/ResponseContent.swift index 91cdd16c..999e4d1a 100644 --- a/TINetworking/Sources/Mapping/ResponseContent/ResponseContent.swift +++ b/TINetworking/Sources/Mapping/ResponseContent/ResponseContent.swift @@ -25,5 +25,5 @@ import Foundation public protocol ResponseContent: Content { associatedtype Model - func decodeResponse(data: Data) throws -> Model + func decodeResponse(data: Data) -> Result } diff --git a/TINetworking/Sources/Mapping/ResponseContent/TextPlainResponseContent.swift b/TINetworking/Sources/Mapping/ResponseContent/TextPlainResponseContent.swift index c26cd2ff..f1a2143f 100644 --- a/TINetworking/Sources/Mapping/ResponseContent/TextPlainResponseContent.swift +++ b/TINetworking/Sources/Mapping/ResponseContent/TextPlainResponseContent.swift @@ -38,11 +38,14 @@ public final class TextPlainResponseContent: BaseContent, ResponseContent { // MARK: - ResponseContent - public func decodeResponse(data: Data) throws -> String { + public func decodeResponse(data: Data) -> Result { guard let plainText = String(data: data, encoding: encoding) else { - throw StringDecodingError(data: data, encoding: encoding) + let context = DecodingError.Context(codingPath: [], + debugDescription: .init(), + underlyingError: StringDecodingError(data: data, encoding: encoding)) + return .failure(.typeMismatch(String.self, context)) } - return plainText + return .success(plainText) } } diff --git a/TINetworking/Sources/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift b/TINetworking/Sources/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift index c0bf2676..cabd5c80 100644 --- a/TINetworking/Sources/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift +++ b/TINetworking/Sources/RequestPreprocessors/DefaultEndpointSecurityRequestPreprocessor.swift @@ -25,17 +25,32 @@ import TIFoundationUtils open class DefaultSecuritySchemePreprocessor: SecuritySchemePreprocessor { struct ValueNotProvidedError: Error {} + public typealias ResultProvider = (@escaping (Result) -> Void) -> Cancellable public typealias ValueProvider = (@escaping (String?) -> Void) -> Cancellable - private let valueProvider: ValueProvider + private let resultProvider: ResultProvider - public init(valueProvider: @escaping ValueProvider) { - self.valueProvider = valueProvider + public init(resultProvider: @escaping ResultProvider) { + self.resultProvider = resultProvider } - public init(staticValue: String?) { - self.valueProvider = { + public init(valueProvider: @escaping ValueProvider) { + self.resultProvider = { completion in + valueProvider { + guard let value = $0 else { + completion(.failure(ValueNotProvidedError())) + return + } + + completion(.success(value)) + } + } + } + + public convenience init(staticValue: String?) { + self.init { $0(staticValue) + return Cancellables.nonCancellable() } } @@ -45,44 +60,53 @@ open class DefaultSecuritySchemePreprocessor: SecuritySchemePreprocessor { public func preprocess(request: EndpointRequest, using security: SecurityScheme, completion: @escaping (Result, Error>) -> Void) -> Cancellable { + resultProvider { + switch $0 { + case let .success(value): + completion(.success(Self.modify(request: request, + using: security, + securityValue: value))) + + case let .failure(error): + completion(.failure(error)) + } + } + } + + private static func modify(request: EndpointRequest, + using security: SecurityScheme, + securityValue: String) -> EndpointRequest { var modifiedRequest = request - return valueProvider { - guard let value = $0 else { - completion(.failure(ValueNotProvidedError())) - return - } + switch security { + case let .http(authenticationScheme): + let headerValue = "\(authenticationScheme.rawValue) \(securityValue)" + var headerParameters = modifiedRequest.headerParameters ?? [:] + headerParameters.updateValue(.init(value: headerValue), + forKey: "Authorization") - switch security { - case let .http(authenticationScheme): - let headerValue = "\(authenticationScheme.rawValue) \(value)" + modifiedRequest.headerParameters = headerParameters + + case let .apiKey(parameterLocation, parameterName): + switch parameterLocation { + case .header: var headerParameters = modifiedRequest.headerParameters ?? [:] - headerParameters.updateValue(.init(value: headerValue), - forKey: "Authorization") + headerParameters.updateValue(.init(value: securityValue), + forKey: parameterName) modifiedRequest.headerParameters = headerParameters - case let .apiKey(parameterLocation, parameterName): - switch parameterLocation { - case .header: - var headerParameters = modifiedRequest.headerParameters ?? [:] - headerParameters.updateValue(.init(value: value), - forKey: parameterName) + case .query: + modifiedRequest.queryParameters.updateValue(.init(value: securityValue), + forKey: parameterName) - modifiedRequest.headerParameters = headerParameters - - case .query: - modifiedRequest.queryParameters.updateValue(.init(value: value), - forKey: parameterName) - - case .cookie: - modifiedRequest.cookieParameters.updateValue(.init(value: value), - forKey: parameterName) - } + case .cookie: + modifiedRequest.cookieParameters.updateValue(.init(value: securityValue), + forKey: parameterName) } - - completion(.success(modifiedRequest)) } + + return modifiedRequest } } diff --git a/TINetworking/Sources/Response/ResponseType+Decoding.swift b/TINetworking/Sources/Response/ResponseType+Decoding.swift index da78f1ac..d63e6532 100644 --- a/TINetworking/Sources/Response/ResponseType+Decoding.swift +++ b/TINetworking/Sources/Response/ResponseType+Decoding.swift @@ -24,18 +24,26 @@ import Foundation import TISwiftUtils public extension ResponseType { - typealias DecodingClosure = ThrowableClosure + typealias DecodingClosure = Closure> func decode(mapping: [KeyValueTuple>]) -> Result { + var decodingErrors: [DecodingError] = [] + for (statusCodesMimeType, decodeClosure) in mapping where statusCodesMimeType.statusCode == statusCode && statusCodesMimeType.mimeType == mimeType { - do { - return .success(try decodeClosure(data)) - } catch { - return .failure(objectMappingError(underlyingError: error)) + switch decodeClosure(data) { + case let .success(result): + return .success(result) + + case let .failure(decodingError): + decodingErrors.append(decodingError) } } + if let firstDecodingError = decodingErrors.first { + return .failure(objectMappingError(underlyingError: firstDecodingError)) + } + guard mapping.contains(where: { $0.key.statusCode == statusCode }) else { return .failure(unsupportedStatusCodeError(statusCode: statusCode)) } diff --git a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSettingsStorage.swift b/TINetworking/Sources/TINetworkingLogger.swift similarity index 81% rename from TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSettingsStorage.swift rename to TINetworking/Sources/TINetworkingLogger.swift index daf4909a..96366b48 100644 --- a/TINetworking/Sources/Alamofire/FingerprintsTrustEvaluation/FingerprintsSettingsStorage.swift +++ b/TINetworking/Sources/TINetworkingLogger.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022 Touch Instinct +// Copyright (c) 2023 Touch Instinct // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the Software), to deal @@ -20,7 +20,11 @@ // THE SOFTWARE. // -public protocol FingerprintsSettingsStorage { - /// Should be true by default (on app first run) - var shouldResetFingerprints: Bool { get set } +import TILogging +import os + +public final class TINetworkingLogger: DefaultOSLogErrorLogger { + public init(category: String) { + super.init(log: OSLog(subsystem: "TINetworking", category: category)) + } } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 271cfd64..b00f90fd 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } @@ -13,5 +13,6 @@ Pod::Spec.new do |s| s.source_files = s.name + '/Sources/**/*' s.dependency 'TIFoundationUtils', s.version.to_s + s.dependency 'TILogging', s.version.to_s s.dependency 'Alamofire', "~> 5.4" end diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 6fbf7bec..a66652cc 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index deafca04..180d8c99 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Generic pagination component.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec index 743a1454..32478aa0 100644 --- a/TISwiftUICore/TISwiftUICore.podspec +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUICore' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index 4ba217e8..a8c15a3c 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index b7c058b6..8b3eee32 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITextProcessing/TITextProcessing.podspec b/TITextProcessing/TITextProcessing.podspec index 420cf3d0..2ed02fb1 100644 --- a/TITextProcessing/TITextProcessing.podspec +++ b/TITextProcessing/TITextProcessing.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITextProcessing' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'A text processing service helping to get a text mask and a placeholder from incoming regex.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index 4615979b..7866a8e6 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/Sources/Alerts/Protocols/AlertPresentationContext.swift b/TIUIKitCore/Sources/Alerts/Protocols/AlertPresentationContext.swift index 377a4086..2406f6c8 100644 --- a/TIUIKitCore/Sources/Alerts/Protocols/AlertPresentationContext.swift +++ b/TIUIKitCore/Sources/Alerts/Protocols/AlertPresentationContext.swift @@ -24,6 +24,7 @@ import TISwiftUtils import UIKit /// A context from where the alert can be presented. +@MainActor public protocol AlertPresentationContext { func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: VoidClosure?) } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index 085e975d..dbe86d0c 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIWebView/TIWebView.podspec b/TIWebView/TIWebView.podspec index 05d8491f..c6a1fb90 100644 --- a/TIWebView/TIWebView.podspec +++ b/TIWebView/TIWebView.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIWebView' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Universal web view API' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index 927375f9..757a8b3c 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.44.0' + s.version = '1.45.0' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' s.homepage = 'https://git.svc.touchin.ru/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/build-scripts b/build-scripts index 39109c6e..318e0ce0 160000 --- a/build-scripts +++ b/build-scripts @@ -1 +1 @@ -Subproject commit 39109c6e6032b2a59f4cdd7b80ac06c4dc8b33c0 +Subproject commit 318e0ce0215da8c790f9a4ea945d1773cb35687f diff --git a/docs/tikeychainutils/singlevaluestorage.md b/docs/tikeychainutils/singlevaluestorage.md new file mode 100644 index 00000000..8ccfebe9 --- /dev/null +++ b/docs/tikeychainutils/singlevaluestorage.md @@ -0,0 +1,72 @@ + +# `SingleValueStorage` - протокол для доступа к значению которое может храниться в Keychain, UserDefaults или ещё где-то. + +Позволяет: + +- инкапсулировать внутри себя логику получения, записи и удаления значения +- добавлять дополнительную логику для получения или изменения значений через композицию или наследование +- ограничить доступ к данным в UserDefaults или Keychain в разных частях приложения + + +### `StringValueKeychainStorage` + + Класс для работы со строковым значением нахоящимся в keychain (самый частый кейс) + +```swift +import TIKeychainUtils +import TIFoundationUtils +import KeychainAccess + +extension StorageKey { + static var apiToken: StorageKey { + .init(rawValue: "apiToken") + } + + static var deleteApiToken: StorageKey { + .init(rawValue: "deleteApiToken") + } +} + +let keychain = Keychain() + +let apiTokenKeychainStorage = StringValueKeychainStorage(keychain: keychain, storageKey: .apiToken) + +if apiTokenKeychainStorage.hasStoredValue() { + // open auth user flow, perform requests +} else { + // show login screen + // ... + + // login + +// switch await userService.login() { +// case .success: +// // open auth user flow, perform requests +// case .failure: +// // show login screen +// } +} +``` + +### `AppInstallLifetimeSingleValueStorage` + + Класс позволяющий добавить дополнительную функциональность очистки значения по конкретному ключу в keychain + после переустановки приложения + +```swift +import Foundation + +let defaults = UserDefaults.standard // or AppGroup defaults + +let appReinstallChecker = AppReinstallChecker(defaultsStorage: defaults, + storageKey: .deleteApiToken) + +let appInstallAwareTokenStorage = apiTokenKeychainStorage.appInstallLifetimeStorage(reinstallChecker: appReinstallChecker) + +if appInstallAwareTokenStorage.hasStoredValue() { + // app wasn't reinstalled, token is exist +} else { + // app was reinstalled or token is empty + // ... +} +``` diff --git a/project-scripts/ordered_modules_list.txt b/project-scripts/ordered_modules_list.txt index e426fddf..49d3f59c 100644 --- a/project-scripts/ordered_modules_list.txt +++ b/project-scripts/ordered_modules_list.txt @@ -1,3 +1,4 @@ +TILogging TISwiftUtils TIPagination TIFoundationUtils