feat: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall.
`DefaultRecoverableJsonNetworkService` supports iOS 12.
This commit is contained in:
parent
0e0a8d733e
commit
5ca564476a
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
### 1.45.0
|
||||
|
||||
- **Added**: `SingleValueStorage` implementations + `AppInstallLifetimeSingleValueStorage` for automatically removing keychain items on app reinstall.
|
||||
- **Update**: `DefaultRecoverableJsonNetworkService` supports iOS 12.
|
||||
|
||||
### 1.44.0
|
||||
|
||||
- **Added**: HTTP status codes to `EndpointErrorResult.apiError` responses
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ 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")],
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@
|
|||
|
||||
import KeychainAccess
|
||||
import Foundation
|
||||
import TIFoundationUtils
|
||||
import LocalAuthentication
|
||||
|
||||
open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data> {
|
||||
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.Defaults {
|
||||
public static var encryptedTokenKeyStorageKey: String {
|
||||
keychainServiceIdentifier + ".encryptedTokenKey"
|
||||
public static var encryptedTokenKeyStorageKey: StorageKey<Data> {
|
||||
.init(rawValue: keychainServiceIdentifier + ".encryptedTokenKey")
|
||||
}
|
||||
|
||||
public static var reusableLAContext: LAContext {
|
||||
|
|
@ -40,11 +41,11 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data>
|
|||
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
|
||||
localAuthContext: LAContext = Defaults.reusableLAContext,
|
||||
settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(),
|
||||
encryptedTokenKeyStorageKey: String = Defaults.encryptedTokenKeyStorageKey) {
|
||||
encryptedTokenKeyStorageKey: StorageKey<Data> = 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)
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ open class DefaultEncryptedTokenKeyStorage: SingleValueAuthKeychainStorage<Data>
|
|||
|
||||
let setValueClosure: SetValueClosure = { 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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,22 +21,23 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import TIFoundationUtils
|
||||
import KeychainAccess
|
||||
|
||||
open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage<StringEncryptionResult> {
|
||||
open class Defaults: SingleValueAuthKeychainStorage<StringEncryptionResult>.Defaults {
|
||||
public static var encryptedTokenStorageKey: String {
|
||||
keychainServiceIdentifier + ".encryptedToken"
|
||||
public static var encryptedTokenStorageKey: StorageKey<StringEncryptionResult> {
|
||||
.init(rawValue: keychainServiceIdentifier + ".encryptedToken")
|
||||
}
|
||||
}
|
||||
|
||||
public init(keychain: Keychain = Keychain(service: Defaults.keychainServiceIdentifier),
|
||||
settingsStorage: AuthSettingsStorage = DefaultAuthSettingsStorage(),
|
||||
encryptedTokenStorageKey: String = Defaults.encryptedTokenStorageKey) {
|
||||
encryptedTokenStorageKey: StorageKey<StringEncryptionResult> = 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)
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +53,7 @@ open class DefaultEncryptedTokenStorage: SingleValueAuthKeychainStorage<StringEn
|
|||
|
||||
let setValueClosure: SetValueClosure = { keychain, value, storageKey in
|
||||
do {
|
||||
return .success(try keychain.set(value.asStorableData(), key: storageKey))
|
||||
return .success(try keychain.set(value.asStorableData(), key: storageKey.rawValue))
|
||||
} catch {
|
||||
return .failure(.unableToWriteData(underlyingError: error))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,52 +23,43 @@
|
|||
import TIFoundationUtils
|
||||
import KeychainAccess
|
||||
import Foundation
|
||||
import TIKeychainUtils
|
||||
|
||||
open class SingleValueAuthKeychainStorage<ValueType>: SingleValueStorage {
|
||||
open class SingleValueAuthKeychainStorage<ValueType>: BaseSingleValueKeychainStorage<ValueType> {
|
||||
open class Defaults {
|
||||
public static var keychainServiceIdentifier: String {
|
||||
Bundle.main.bundleIdentifier ?? "ru.touchin.TIAuth"
|
||||
}
|
||||
}
|
||||
|
||||
public typealias GetValueClosure = (Keychain, String) -> Result<ValueType, StorageError>
|
||||
public typealias SetValueClosure = (Keychain, ValueType, String) -> Result<Void, StorageError>
|
||||
|
||||
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<ValueType>,
|
||||
getValueClosure: @escaping GetValueClosure,
|
||||
setValueClosure: @escaping SetValueClosure) {
|
||||
|
||||
self.keychain = keychain
|
||||
self.settingsStorage = settingsStorage
|
||||
self.storageKey = storageKey
|
||||
self.getValueClosure = getValueClosure
|
||||
self.setValueClosure = setValueClosure
|
||||
|
||||
super.init(keychain: keychain,
|
||||
storageKey: storageKey,
|
||||
getValueClosure: getValueClosure,
|
||||
setValueClosure: setValueClosure)
|
||||
}
|
||||
|
||||
// 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<Void, StorageError> {
|
||||
return setValueClosure(keychain, value, storageKey)
|
||||
}
|
||||
|
||||
open func getValue() -> Result<ValueType, StorageError> {
|
||||
open override func getValue() -> Result<ValueType, StorageError> {
|
||||
guard !settingsStorage.shouldResetStoredAuthData else {
|
||||
let result: Result<ValueType, StorageError>
|
||||
|
||||
do {
|
||||
try keychain.remove(storageKey)
|
||||
try storage.remove(storageKey.rawValue)
|
||||
settingsStorage.shouldResetStoredAuthData = false
|
||||
|
||||
result = .failure(.valueNotFound)
|
||||
|
|
@ -79,6 +70,6 @@ open class SingleValueAuthKeychainStorage<ValueType>: SingleValueStorage {
|
|||
return result
|
||||
}
|
||||
|
||||
return getValueClosure(keychain, storageKey)
|
||||
return super.getValue()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ open class BaseCancellable: Cancellable {
|
|||
|
||||
public init() {}
|
||||
|
||||
deinit {
|
||||
cancel()
|
||||
}
|
||||
|
||||
open func cancel() {
|
||||
isCancelled = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ open class AppReinstallChecker {
|
|||
}
|
||||
|
||||
public init(defaultsStorage: UserDefaults = .standard,
|
||||
storageKey: String) {
|
||||
storageKey: StorageKey<Bool>) {
|
||||
|
||||
self.defaultsStorage = defaultsStorage
|
||||
self.storageKey = storageKey
|
||||
self.storageKey = storageKey.rawValue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ValueType>: BaseSingleValueStorage<ValueType, UserDefaults> {
|
||||
public init(defaults: UserDefaults,
|
||||
storageKey: StorageKey<ValueType>,
|
||||
getValueClosure: @escaping GetValueClosure,
|
||||
setValueClosure: @escaping SetValueClosure) {
|
||||
|
||||
super.init(storage: defaults,
|
||||
storageKey: storageKey,
|
||||
hasValueClosure: { .success($0.object(forKey: $1.rawValue) != nil) },
|
||||
deleteValueClosure: { .success($0.removeObject(forKey: $1.rawValue)) },
|
||||
getValueClosure: getValueClosure,
|
||||
setValueClosure: setValueClosure)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ValueType, StorageType>: SingleValueStorage {
|
||||
public typealias HasValueClosure = (StorageType, StorageKey<ValueType>) -> Result<Bool, StorageError>
|
||||
public typealias GetValueClosure = (StorageType, StorageKey<ValueType>) -> Result<ValueType, StorageError>
|
||||
public typealias SetValueClosure = (StorageType, ValueType, StorageKey<ValueType>) -> Result<Void, StorageError>
|
||||
public typealias DeleteValueClosure = (StorageType, StorageKey<ValueType>) -> Result<Void, StorageError>
|
||||
|
||||
public let storage: StorageType
|
||||
public let storageKey: StorageKey<ValueType>
|
||||
public let hasValueClosure: HasValueClosure
|
||||
public let deleteValueClosure: DeleteValueClosure
|
||||
public let getValueClosure: GetValueClosure
|
||||
public let setValueClosure: SetValueClosure
|
||||
|
||||
public init(storage: StorageType,
|
||||
storageKey: StorageKey<ValueType>,
|
||||
hasValueClosure: @escaping HasValueClosure,
|
||||
deleteValueClosure: @escaping DeleteValueClosure,
|
||||
getValueClosure: @escaping GetValueClosure,
|
||||
setValueClosure: @escaping SetValueClosure) {
|
||||
|
||||
self.storage = storage
|
||||
self.storageKey = storageKey
|
||||
self.hasValueClosure = hasValueClosure
|
||||
self.deleteValueClosure = deleteValueClosure
|
||||
self.getValueClosure = getValueClosure
|
||||
self.setValueClosure = setValueClosure
|
||||
}
|
||||
|
||||
// MARK: - SingleValueStorage
|
||||
|
||||
open func hasStoredValue() -> Bool {
|
||||
(try? hasValueClosure(storage, storageKey).get()) ?? false
|
||||
}
|
||||
|
||||
open func store(value: ValueType) -> Result<Void, StorageError> {
|
||||
setValueClosure(storage, value, storageKey)
|
||||
}
|
||||
|
||||
open func getValue() -> Result<ValueType, StorageError> {
|
||||
getValueClosure(storage, storageKey)
|
||||
}
|
||||
|
||||
public func deleteValue() -> Result<Void, StorageError> {
|
||||
deleteValueClosure(storage, storageKey)
|
||||
}
|
||||
}
|
||||
|
|
@ -27,4 +27,5 @@ public protocol SingleValueStorage {
|
|||
func hasStoredValue() -> Bool
|
||||
func store(value: ValueType) -> Result<Void, ErrorType>
|
||||
func getValue() -> Result<ValueType, ErrorType>
|
||||
func deleteValue() -> Result<Void, ErrorType>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
public init(defaults: UserDefaults, storageKey: StorageKey<String>) {
|
||||
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,
|
||||
setValueClosure: { .success($0.set($1, forKey: $2.rawValue)) })
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
//
|
||||
|
||||
import TIFoundationUtils
|
||||
|
||||
open class AppInstallLifetimeSingleValueStorage<Storage: SingleValueStorage>: 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<Storage.ValueType, Storage.ErrorType> {
|
||||
if appReinstallChecker.isAppFirstRun {
|
||||
let result = wrappedStorage.deleteValue()
|
||||
|
||||
if case .success = result {
|
||||
appReinstallChecker.isAppFirstRun = false
|
||||
}
|
||||
|
||||
return result.flatMap { .failure(StorageError.valueNotFound) }
|
||||
} else {
|
||||
return wrappedStorage.getValue()
|
||||
}
|
||||
}
|
||||
|
||||
open func store(value: Storage.ValueType) -> Result<Void, Storage.ErrorType> {
|
||||
let result = wrappedStorage.store(value: value)
|
||||
|
||||
if case .success = result {
|
||||
appReinstallChecker.isAppFirstRun = false
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func deleteValue() -> Result<Void, Storage.ErrorType> {
|
||||
wrappedStorage.deleteValue()
|
||||
}
|
||||
}
|
||||
|
||||
public extension SingleValueStorage {
|
||||
func appInstallLifetimeStorage(reinstallChecker: AppReinstallChecker) -> AppInstallLifetimeSingleValueStorage<Self>
|
||||
where Self.ErrorType == ErrorType {
|
||||
|
||||
AppInstallLifetimeSingleValueStorage(storage: self,
|
||||
appReinstallChecker: reinstallChecker)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// 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<ValueType>: BaseSingleValueStorage<ValueType, Keychain> {
|
||||
public init(keychain: Keychain,
|
||||
storageKey: StorageKey<ValueType>,
|
||||
getValueClosure: @escaping GetValueClosure,
|
||||
setValueClosure: @escaping SetValueClosure) {
|
||||
|
||||
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,
|
||||
setValueClosure: setValueClosure)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> {
|
||||
public init(keychain: Keychain, storageKey: StorageKey<String>) {
|
||||
let getValueClosure: BaseSingleValueKeychainStorage<String>.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 setValueClosure: BaseSingleValueKeychainStorage<String>.SetValueClosure = { 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,
|
||||
setValueClosure: setValueClosure)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,55 +20,110 @@
|
|||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Moya
|
||||
import TINetworking
|
||||
import TISwiftUtils
|
||||
import TIFoundationUtils
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
open class DefaultRecoverableJsonNetworkService<ApiError: Decodable & Error>: DefaultJsonNetworkService {
|
||||
public typealias EndpointResponse<S: Decodable> = EndpointRecoverableRequestResult<S, ApiError, MoyaError>
|
||||
public typealias ErrorType = EndpointErrorResult<ApiError, MoyaError>
|
||||
public typealias EndpointResponse<S: Decodable> = EndpointRecoverableRequestResult<S, ApiError, NetworkError>
|
||||
public typealias ErrorType = EndpointErrorResult<ApiError, NetworkError>
|
||||
public typealias RequestRetrier = AnyEndpointRequestRetrier<ErrorType>
|
||||
|
||||
public private(set) var defaultRequestRetriers: [RequestRetrier] = []
|
||||
|
||||
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
|
||||
prependRequestRetriers: [RequestRetrier] = [],
|
||||
appendRequestRetriers: [RequestRetrier] = [],
|
||||
completion: @escaping ParameterClosure<EndpointResponse<S>>) -> Cancellable
|
||||
{
|
||||
|
||||
process(recoverableRequest: recoverableRequest,
|
||||
errorHandlers: prependRequestRetriers + defaultRequestRetriers + appendRequestRetriers,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
@available(iOS 13.0.0, *)
|
||||
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
|
||||
prependRequestRetriers: [RequestRetrier] = [],
|
||||
appendRequestRetriers: [RequestRetrier] = []) async ->
|
||||
EndpointResponse<S> {
|
||||
|
||||
await process(recoverableRequest: recoverableRequest,
|
||||
errorHandlers: prependRequestRetriers + defaultRequestRetriers + appendRequestRetriers)
|
||||
await withTaskCancellableClosure {
|
||||
process(recoverableRequest: recoverableRequest,
|
||||
prependRequestRetriers: prependRequestRetriers,
|
||||
appendRequestRetriers: appendRequestRetriers,
|
||||
completion: $0)
|
||||
}
|
||||
}
|
||||
|
||||
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
|
||||
errorHandlers: [RequestRetrier]) async -> EndpointResponse<S> {
|
||||
errorHandlers: [RequestRetrier],
|
||||
completion: @escaping ParameterClosure<EndpointResponse<S>>) -> Cancellable {
|
||||
|
||||
let result: RequestResult<S, ApiError> = await process(request: recoverableRequest)
|
||||
|
||||
if case let .failure(errorResponse) = result {
|
||||
var failures = [errorResponse]
|
||||
|
||||
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)
|
||||
Cancellables.scoped { cancellableBag in
|
||||
process(request: recoverableRequest) { (result: RequestResult<S, ApiError>) in
|
||||
if case let .failure(errorResponse) = result {
|
||||
Self.validateAndRepair(recoverableRequest: recoverableRequest,
|
||||
errors: [errorResponse],
|
||||
retriers: errorHandlers,
|
||||
cancellableBag: cancellableBag,
|
||||
completion: completion)
|
||||
} else {
|
||||
completion(result.mapError { .init(failures: [$0]) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .failure(.init(failures: failures))
|
||||
@available(iOS 13.0.0, *)
|
||||
open func process<B: Encodable, S>(recoverableRequest: EndpointRequest<B, S>,
|
||||
errorHandlers: [RequestRetrier]) async -> EndpointResponse<S> {
|
||||
|
||||
await withTaskCancellableClosure {
|
||||
process(recoverableRequest: recoverableRequest,
|
||||
errorHandlers: errorHandlers,
|
||||
completion: $0)
|
||||
}
|
||||
}
|
||||
|
||||
private static func validateAndRepair<B, S, R: Collection>(recoverableRequest: EndpointRequest<B, S>,
|
||||
errors: [ErrorType],
|
||||
retriers: R,
|
||||
cancellableBag: BaseCancellableBag,
|
||||
completion: @escaping ParameterClosure<EndpointResponse<S>>)
|
||||
where R.Element == RequestRetrier {
|
||||
|
||||
guard let retrier = retriers.first, !cancellableBag.isCancelled else {
|
||||
completion(.failure(.init(failures: errors)))
|
||||
return
|
||||
}
|
||||
|
||||
return result.mapError { .init(failures: [$0]) }
|
||||
retrier.validateAndRepair(errorResults: errors) { handlerResult in
|
||||
switch handlerResult {
|
||||
case let .success(retryResult):
|
||||
switch retryResult {
|
||||
case .retry, .retryWithDelay:
|
||||
validateAndRepair(recoverableRequest: recoverableRequest,
|
||||
errors: errors,
|
||||
retriers: retriers,
|
||||
cancellableBag: cancellableBag,
|
||||
completion: completion)
|
||||
case .doNotRetry, .doNotRetryWithError:
|
||||
validateAndRepair(recoverableRequest: recoverableRequest,
|
||||
errors: errors,
|
||||
retriers: retriers.dropFirst(),
|
||||
cancellableBag: cancellableBag,
|
||||
completion: completion)
|
||||
}
|
||||
case let .failure(error):
|
||||
validateAndRepair(recoverableRequest: recoverableRequest,
|
||||
errors: errors + [error],
|
||||
retriers: retriers.dropFirst(),
|
||||
cancellableBag: cancellableBag,
|
||||
completion: completion)
|
||||
}
|
||||
}
|
||||
.add(to: cancellableBag)
|
||||
}
|
||||
|
||||
public func register<RequestRetrier: EndpointRequestRetrier>(defaultRequestRetrier: RequestRetrier)
|
||||
|
|
|
|||
|
|
@ -36,13 +36,24 @@ public protocol ApiInteractor {
|
|||
completion: @escaping ParameterClosure<R>) -> Cancellable
|
||||
}
|
||||
|
||||
public extension ApiInteractor {
|
||||
func process<B: Encodable, S, F>(request: EndpointRequest<B, S>,
|
||||
completion: @escaping ParameterClosure<RequestResult<S, F>>) -> 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<B: Encodable, S, F>(request: EndpointRequest<B, S>) async -> RequestResult<S, F> {
|
||||
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<B: Encodable, S: Decodable, AE: Decodable, R>(request: EndpointRequest<B, S>,
|
||||
|
|
@ -50,13 +61,11 @@ public extension ApiInteractor {
|
|||
mapFailure: @escaping Closure<FailureMappingInput<AE>, R>,
|
||||
mapNetworkError: @escaping Closure<NetworkError, R>) async -> R {
|
||||
|
||||
await withTaskCancellableClosure { completion in
|
||||
await withTaskCancellableClosure {
|
||||
process(request: request,
|
||||
mapSuccess: mapSuccess,
|
||||
mapFailure: mapFailure,
|
||||
mapNetworkError: mapNetworkError) {
|
||||
completion($0)
|
||||
}
|
||||
mapNetworkError: mapNetworkError, completion: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,31 @@ import TIFoundationUtils
|
|||
open class DefaultSecuritySchemePreprocessor: SecuritySchemePreprocessor {
|
||||
struct ValueNotProvidedError: Error {}
|
||||
|
||||
public typealias ResultProvider = (@escaping (Result<String, Error>) -> 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 +59,53 @@ open class DefaultSecuritySchemePreprocessor: SecuritySchemePreprocessor {
|
|||
public func preprocess<B, S>(request: EndpointRequest<B, S>,
|
||||
using security: SecurityScheme,
|
||||
completion: @escaping (Result<EndpointRequest<B, S>, 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<B, S>(request: EndpointRequest<B, S>,
|
||||
using security: SecurityScheme,
|
||||
securityValue: String) -> EndpointRequest<B, S> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue