diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e1a4d5..98b84531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +### 1.19.0 + +- **Add**: Add presenter protocols to `TISwiftUICore` and `TIUIKitCore` modules +- **Add**: `CodeConfirmPresenter` protocol and `DefaultCodeConfirmPresenter` implementation in `TIAuth` module + ### 1.18.0 - **Add**: add MapManagers for routine maps configuration diff --git a/LeadKit.podspec b/LeadKit.podspec index 8f04e3a9..39d4ae97 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKit" - s.version = "1.18.0" + s.version = "1.19.0" s.summary = "iOS framework with a bunch of tools for rapid development" s.homepage = "https://github.com/TouchInstinct/LeadKit" s.license = "Apache License, Version 2.0" diff --git a/Package.swift b/Package.swift index 7741cb14..0b91e79d 100644 --- a/Package.swift +++ b/Package.swift @@ -11,6 +11,9 @@ let package = Package( // MARK: - UIKit .library(name: "TIUIKitCore", targets: ["TIUIKitCore"]), .library(name: "TIUIElements", targets: ["TIUIElements"]), + + // MARK: - SwiftUI + .library(name: "TISwiftUICore", targets: ["TISwiftUICore"]), // MARK: - Utils .library(name: "TISwiftUtils", targets: ["TISwiftUtils"]), @@ -33,6 +36,7 @@ let package = Package( .library(name: "OTPSwiftView", targets: ["OTPSwiftView"]), .library(name: "TITransitions", targets: ["TITransitions"]), .library(name: "TIPagination", targets: ["TIPagination"]), + .library(name: "TIAuth", targets: ["TIAuth"]), ], dependencies: [ .package(url: "https://github.com/maxsokolov/TableKit.git", .upToNextMajor(from: "2.11.0")), @@ -47,6 +51,10 @@ let package = Package( // MARK: - UIKit .target(name: "TIUIKitCore", path: "TIUIKitCore/Sources"), .target(name: "TIUIElements", dependencies: ["TIUIKitCore", "TISwiftUtils"], path: "TIUIElements/Sources"), + + // MARK: - SwiftUI + + .target(name: "TISwiftUICore", path: "TISwiftUICore/Sources"), // MARK: - Utils .target(name: "TISwiftUtils", path: "TISwiftUtils/Sources"), @@ -70,6 +78,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"], path: "TIAuth/Sources"), // MARK: - Tests diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index bf74c4c3..4a1ecd33 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIAppleMapUtils' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIAuth/README.md b/TIAuth/README.md new file mode 100644 index 00000000..5205e23c --- /dev/null +++ b/TIAuth/README.md @@ -0,0 +1,14 @@ +# TIAuth + +Login, registration, confirmation and other related actions + +## CodeConfirmPresenter + +### Features + +- Code confirm and code refresh actions +- Code refresh countdown in foreground and background +- Additional 2FA auth handling +- Remaining attempts handling +- Code autofill from custom sources (push, etc) +- UIKit and SwiftUI compatible \ No newline at end of file diff --git a/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmPresenter.swift b/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmPresenter.swift new file mode 100644 index 00000000..156b0e51 --- /dev/null +++ b/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmPresenter.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2022 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 + +@MainActor +protocol CodeConfirmPresenter { + // MARK: - User actions handling + + func inputChanged(newInput: String?) + func refreshCode() + + // MARK: - View lifecycle handling + + func viewDidPresented() + + // MARK: - Autofill + + func autofill(code: String, with codeId: String?) +} diff --git a/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmStateStorage.swift b/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmStateStorage.swift new file mode 100644 index 00000000..c13a9dc7 --- /dev/null +++ b/TIAuth/Sources/CodeConfirmPresenter/CodeConfirmStateStorage.swift @@ -0,0 +1,32 @@ +// +// Copyright (c) 2022 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 + +@MainActor +public protocol CodeConfirmStateStorage: AnyObject { + var currentUserInput: String? { get set } + var canRefreshCodeAfter: Int? { get set } + var remainingAttempts: Int? { get set } + var isExecutingRequest: Bool { get set } + var canRequestNewCode: Bool { get set } +} diff --git a/TIAuth/Sources/CodeConfirmPresenter/DefaultCodeConfirmPresenter.swift b/TIAuth/Sources/CodeConfirmPresenter/DefaultCodeConfirmPresenter.swift new file mode 100644 index 00000000..bad77a05 --- /dev/null +++ b/TIAuth/Sources/CodeConfirmPresenter/DefaultCodeConfirmPresenter.swift @@ -0,0 +1,295 @@ +// +// Copyright (c) 2022 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 +import TIFoundationUtils + +@available(iOS 13.0, *) +open class DefaultCodeConfirmPresenter: CodeConfirmPresenter { + + open class Output { + public typealias OnConfirmSuccessClosure = (ConfirmResponse) -> Void + + public var onConfirmSuccess: OnConfirmSuccessClosure + + public init(onConfirmSuccess: @escaping OnConfirmSuccessClosure) { + self.onConfirmSuccess = onConfirmSuccess + } + } + + open class Requests { + public typealias ConfirmRequestClosure = (String) async -> ConfirmResponse + public typealias RefreshRequestClosure = () async -> RefreshResponse + + public var confirmRequest: ConfirmRequestClosure + public var refreshRequest: RefreshRequestClosure + + public init(confirmRequest: @escaping ConfirmRequestClosure, + refreshRequest: @escaping RefreshRequestClosure) { + + self.confirmRequest = confirmRequest + self.refreshRequest = refreshRequest + } + } + + public struct Config { + public enum Defaults { + public static var codeLength: Int { + 6 + } + + public static var autoRefresh: Bool { + true + } + } + + public var codeLength: Int + public var autoRefresh: Bool + + public init(codeLength: Int = Defaults.codeLength, + autoRefresh: Bool = Defaults.autoRefresh) { + + self.codeLength = codeLength + self.autoRefresh = autoRefresh + } + } + + private let codeRefreshTimer = TITimer(mode: .activeAndBackground) + private let codeLifetimeTimer = TITimer(mode: .activeAndBackground) + + public var output: Output + public var requests: Requests + public weak var stateStorage: CodeConfirmStateStorage? + public var config = Config() + public var currentCodeResponse: CodeResponse + + public init(input: Input, + output: Output, + requests: Requests, + stateStorage: CodeConfirmStateStorage? = nil) { + + self.currentCodeResponse = input + self.output = output + self.requests = requests + self.stateStorage = stateStorage + } + + // MARK: - Requests + + open func confirm(code: String) async { + stateStorage?.isExecutingRequest = true + + let confirmResponse = await requests.confirmRequest(code) + + isSuccessConfirm(response: confirmResponse) + ? handle(successConfirmResponse: confirmResponse) + : handle(failureConfirmResponse: confirmResponse) + + stateStorage?.isExecutingRequest = false + } + + open func refreshCode() async { + stateStorage?.isExecutingRequest = true + + let refreshResponse = await requests.refreshRequest() + + if isSuccessRefresh(response: refreshResponse) { + handle(successRefreshResponse: refreshResponse) + } else { + handle(failureRefreshResponse: refreshResponse) + } + + stateStorage?.isExecutingRequest = false + } + + // MARK: - Response handling + + open func handle(successConfirmResponse response: ConfirmResponse) { + if let additionalAuth = response.requiredAdditionalAuth { + handle(additionalAuth: additionalAuth) + } else { + output.onConfirmSuccess(response) + } + } + + open func handle(failureConfirmResponse: ConfirmResponse) { + stateStorage?.currentUserInput = nil + + if let remainingAttempts = failureConfirmResponse.remainingAttempts, remainingAttempts <= 0 { + onConfirmAttemptsExhausted() + } + + // show error message, etc. + } + + open func handle(additionalAuth auth: String) { + // custom subclass handling + } + + open func onConfirmAttemptsExhausted() { + // custom subclass handling + } + + open func handle(successRefreshResponse response: RefreshResponse) { + updateStateStorage(from: response) + + start(codeLifetimeTimer: codeLifetimeTimer, + codeRefreshTimer: codeRefreshTimer, + for: response) + } + + func handle(failureRefreshResponse response: RefreshResponse) { + updateStateStorage(from: response) + + // show error message, etc. + } + + // MARK: - Response processing + + open func lifetimeDuration(of code: CodeResponse) -> Int? { + code.validUntil?.timeIntervalSinceNow.intValue + } + + open func nonRefreshableInterval(of code: CodeResponse) -> Int? { + code.refreshableAfter?.timeIntervalSinceNow.intValue ?? lifetimeDuration(of: code) + } + + open func isSuccessConfirm(response: ConfirmResponse) -> Bool { + true + } + + open func isSuccessRefresh(response: RefreshResponse) -> Bool { + true + } + + // MARK: - User actions handling + + open func inputChanged(newInput: String?) { + stateStorage?.currentUserInput = newInput + + if let code = newInput, code.count >= config.codeLength { + stateStorage?.isExecutingRequest = true + + Task { + await confirm(code: code) + } + } + } + + open func refreshCode() { + stateStorage?.canRequestNewCode = false + stateStorage?.canRefreshCodeAfter = nil + + Task { + await refreshCode() + } + } + + // MARK: - View lifecycle handling + + open func viewDidPresented() { + start(codeLifetimeTimer: codeLifetimeTimer, + codeRefreshTimer: codeRefreshTimer, + for: currentCodeResponse) + } + + // MARK: - Autofill + + open func autofill(code: String, with codeId: String? = nil) { + guard currentCodeResponse.codeId == codeId else { + return + } + + inputChanged(newInput: code) + } + + // MARK: - Subclass customization + + open func start(codeLifetimeTimer: TITimer, + codeRefreshTimer: TITimer, + for code: CodeResponse) { + + start(codeRefreshTimer: codeRefreshTimer, for: code) + start(codeLifetimeTimer: codeLifetimeTimer, for: code) + } + + open func start(codeRefreshTimer: TITimer, for code: CodeResponse) { + guard let nonRefreshableInterval = nonRefreshableInterval(of: code) else { + return + } + + codeRefreshTimer.eventHandler = { [weak self] in + self?.updateRemaining(nonRefreshableInterval: nonRefreshableInterval, + elapsedInterval: $0) + } + codeRefreshTimer.start() + } + + open func updateRemaining(nonRefreshableInterval: Int, elapsedInterval: TimeInterval) { + let secondsLeft = nonRefreshableInterval - elapsedInterval.intValue + + stateStorage?.canRefreshCodeAfter = secondsLeft + stateStorage?.canRequestNewCode = secondsLeft <= 0 + + if secondsLeft < 0 { + codeRefreshTimer.pause() + } + } + + open func start(codeLifetimeTimer: TITimer, for code: CodeResponse) { + guard let lifetimeInterval = lifetimeDuration(of: code) else { + return + } + + codeLifetimeTimer.eventHandler = { [weak self] in + self?.updateRemaining(lifetimeInterval: lifetimeInterval, + elapsedInterval: $0) + } + codeLifetimeTimer.start() + } + + open func updateRemaining(lifetimeInterval: Int, elapsedInterval: TimeInterval) { + let secondsLeft = lifetimeInterval - elapsedInterval.intValue + + if secondsLeft < 0 { + codeLifetimeTimer.pause() + + if config.autoRefresh { + refreshCode() + } + } + } + + open func updateStateStorage(from response: CodeResponse) { + stateStorage?.remainingAttempts = response.remainingAttempts + stateStorage?.currentUserInput = nil + stateStorage?.canRefreshCodeAfter = nonRefreshableInterval(of: response) + stateStorage?.canRequestNewCode = (stateStorage?.canRefreshCodeAfter ?? 0) <= 0 + } +} + +private extension TimeInterval { + var intValue: Int { + Int(self) + } +} diff --git a/TIAuth/Sources/RequestResponseProtocols/CodeConfirmResponse.swift b/TIAuth/Sources/RequestResponseProtocols/CodeConfirmResponse.swift new file mode 100644 index 00000000..38b207fe --- /dev/null +++ b/TIAuth/Sources/RequestResponseProtocols/CodeConfirmResponse.swift @@ -0,0 +1,25 @@ +// +// Copyright (c) 2022 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 protocol CodeConfirmResponse: CodeResponse { + var requiredAdditionalAuth: String? { get } +} diff --git a/TIAuth/Sources/RequestResponseProtocols/CodeRefreshResponse.swift b/TIAuth/Sources/RequestResponseProtocols/CodeRefreshResponse.swift new file mode 100644 index 00000000..2c39ecfb --- /dev/null +++ b/TIAuth/Sources/RequestResponseProtocols/CodeRefreshResponse.swift @@ -0,0 +1,24 @@ +// +// Copyright (c) 2022 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 protocol CodeRefreshResponse: CodeResponse { +} diff --git a/TIAuth/Sources/RequestResponseProtocols/CodeResponse.swift b/TIAuth/Sources/RequestResponseProtocols/CodeResponse.swift new file mode 100644 index 00000000..a567d6c9 --- /dev/null +++ b/TIAuth/Sources/RequestResponseProtocols/CodeResponse.swift @@ -0,0 +1,31 @@ +// +// Copyright (c) 2022 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 protocol CodeResponse { + var validUntil: Date? { get } + var codeId: String? { get } + var refreshableAfter: Date? { get } + var confirmationId: String? { get } + var remainingAttempts: Int? { get } +} diff --git a/TIAuth/Sources/RequestResponseProtocols/Extensions/Result+CodeResponse.swift b/TIAuth/Sources/RequestResponseProtocols/Extensions/Result+CodeResponse.swift new file mode 100644 index 00000000..971d7d0d --- /dev/null +++ b/TIAuth/Sources/RequestResponseProtocols/Extensions/Result+CodeResponse.swift @@ -0,0 +1,68 @@ +// +// Copyright (c) 2022 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 extension Result { + var success: Success? { + if case let .success(wrapped) = self { + return wrapped + } + + return nil + } +} + +extension Result: CodeResponse where Success: CodeResponse { + public var validUntil: Date? { + success?.validUntil + } + + public var codeId: String? { + success?.codeId + } + + public var refreshableAfter: Date? { + success?.refreshableAfter + } + + public var confirmationId: String? { + success?.confirmationId + } + + public var remainingAttempts: Int? { + success?.remainingAttempts + } +} + +extension Result: CodeRefreshResponse where Success: CodeRefreshResponse { +} + +extension Result: CodeConfirmResponse where Success: CodeConfirmResponse { + public var remainingAttempts: Int? { + success?.remainingAttempts + } + + public var requiredAdditionalAuth: String? { + success?.requiredAdditionalAuth + } +} diff --git a/TIAuth/TIAuth.podspec b/TIAuth/TIAuth.podspec new file mode 100644 index 00000000..74df774f --- /dev/null +++ b/TIAuth/TIAuth.podspec @@ -0,0 +1,16 @@ +Pod::Spec.new do |s| + s.name = 'TIAuth' + s.version = '1.19.0' + s.summary = 'Login, registration, confirmation and other related actions' + s.homepage = 'https://github.com/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://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '13.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + + s.dependency 'TIFoundationUtils', s.version.to_s +end diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 4056fd0b..073f1d77 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Set of helpers for Foundation framework classes.' s.homepage = 'https://github.com/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 7a549391..b0f14e84 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIGoogleMapUtils' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 2aa1588d..07aec6a0 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Set of helpers for Keychain classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIMapUtils/Sources/Managers/BaseMapManager.swift b/TIMapUtils/Sources/Managers/BaseMapManager.swift index bcc0a8df..a8dc5b37 100644 --- a/TIMapUtils/Sources/Managers/BaseMapManager.swift +++ b/TIMapUtils/Sources/Managers/BaseMapManager.swift @@ -20,6 +20,9 @@ // THE SOFTWARE. // +import Foundation +import UIKit.UIGeometry + open class BaseMapManager 'MIT', :file => 'LICENSE' } diff --git a/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift b/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift index 4f2ef79d..a0336ea0 100644 --- a/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift +++ b/TIMoyaNetworking/Sources/NetworkService/DefaultJsonNetworkService.swift @@ -28,6 +28,8 @@ import Foundation import TIFoundationUtils open class DefaultJsonNetworkService { + public typealias RequestResult = EndpointRequestResult + public var session: Session public var serializationQueue: DispatchQueue = .global(qos: .default) @@ -65,7 +67,7 @@ open class DefaultJsonNetworkService { } @available(iOS 13.0.0, *) - open func process(request: EndpointRequest) async -> EndpointRequestResult { + open func process(request: EndpointRequest) async -> RequestResult { await process(request: request, mapSuccess: Result.success, mapFailure: { .failure(.apiError($0)) }, diff --git a/TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult.swift b/TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult.swift index b7c4a65c..05cc5f82 100644 --- a/TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult.swift +++ b/TIMoyaNetworking/Sources/NetworkService/EndpointErrorResult.swift @@ -23,12 +23,12 @@ import Moya import Foundation -public enum EndpointErrorResult: Error { - case apiError(E) - case networkError(MoyaError) +public enum EndpointErrorResult: Error { + case apiError(ApiError) + case networkError(NetworkError) } -public extension EndpointErrorResult { +public extension EndpointErrorResult where NetworkError == MoyaError { var isNetworkConnectionProblem: Bool { guard case let .networkError(moyaError) = self, case let .underlying(error, _) = moyaError, diff --git a/TIMoyaNetworking/Sources/NetworkService/EndpointRequestResult.swift b/TIMoyaNetworking/Sources/NetworkService/EndpointRequestResult.swift index d9a318ac..4895c37a 100644 --- a/TIMoyaNetworking/Sources/NetworkService/EndpointRequestResult.swift +++ b/TIMoyaNetworking/Sources/NetworkService/EndpointRequestResult.swift @@ -20,4 +20,4 @@ // THE SOFTWARE. // -public typealias EndpointRequestResult = Result> +public typealias EndpointRequestResult = Result> diff --git a/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift b/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift index 82a15c1b..ee4585c8 100644 --- a/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift +++ b/TIMoyaNetworking/Sources/RecoverableNetworkService/DefaultRecoverableJsonNetworkService.swift @@ -26,7 +26,7 @@ import TISwiftUtils @available(iOS 13.0.0, *) open class DefaultRecoverableJsonNetworkService: DefaultJsonNetworkService { - public typealias ErrorType = EndpointErrorResult + public typealias ErrorType = EndpointErrorResult public typealias ErrorHandlerResultType = RecoverableErrorHandlerResult public typealias ErrorHandlerType = AnyAsyncEventHandler @@ -35,16 +35,16 @@ open class DefaultRecoverableJsonNetworkService: De open func process(recoverableRequest: EndpointRequest, prependErrorHandlers: [ErrorHandlerType] = [], appendErrorHandlers: [ErrorHandlerType] = []) async -> - EndpointRequestResult { + RequestResult { await process(recoverableRequest: recoverableRequest, errorHandlers: prependErrorHandlers + defaultErrorHandlers + appendErrorHandlers) } open func process(recoverableRequest: EndpointRequest, - errorHandlers: [ErrorHandlerType]) async -> EndpointRequestResult { + errorHandlers: [ErrorHandlerType]) async -> RequestResult { - let result: EndpointRequestResult = await process(request: recoverableRequest) + let result: RequestResult = await process(request: recoverableRequest) if case let .failure(errorResponse) = result { for handler in errorHandlers { diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 04994f84..7e8f09f2 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Moya + Swagger network service.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 1dbbedd7..79ca472c 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Swagger-frendly networking layer helpers.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 6c19b5f8..4c9b9fb6 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Caching results of EndpointRequests.' s.homepage = 'https://github.com/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 d2773640..9b9e3e94 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Generic pagination component.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TISwiftUICore/README.md b/TISwiftUICore/README.md new file mode 100644 index 00000000..75092272 --- /dev/null +++ b/TISwiftUICore/README.md @@ -0,0 +1,3 @@ +# TISwiftUICore + +Core UI elements: protocols, views and helpers. \ No newline at end of file diff --git a/TISwiftUICore/Sources/Presenter/SwiftUIPresenter.swift b/TISwiftUICore/Sources/Presenter/SwiftUIPresenter.swift new file mode 100644 index 00000000..a7d93fc0 --- /dev/null +++ b/TISwiftUICore/Sources/Presenter/SwiftUIPresenter.swift @@ -0,0 +1,36 @@ +// +// Copyright (c) 2022 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 SwiftUI +import Combine + +@MainActor +@available(iOS 13.0, *) +public protocol SwiftUIPresenter: ObservableObject { + associatedtype View: SwiftUI.View + + func createView() -> View + +#if DEBUG + static func assembleForPreview() -> Self +#endif +} diff --git a/TISwiftUICore/TISwiftUICore.podspec b/TISwiftUICore/TISwiftUICore.podspec new file mode 100644 index 00000000..4fd2f120 --- /dev/null +++ b/TISwiftUICore/TISwiftUICore.podspec @@ -0,0 +1,15 @@ +Pod::Spec.new do |s| + s.name = 'TISwiftUICore' + s.version = '1.19.0' + s.summary = 'Core UI elements: protocols, views and helpers..' + s.homepage = 'https://github.com/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://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '13.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + +end diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index 7c168fab..8302bf30 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Bunch of useful helpers for Swift development.' s.homepage = 'https://github.com/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 df5be06b..3030291c 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Set of helpers for TableKit classes.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TITransitions/TITransitions.podspec b/TITransitions/TITransitions.podspec index c3e50798..6766926a 100644 --- a/TITransitions/TITransitions.podspec +++ b/TITransitions/TITransitions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITransitions' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Set of custom transitions to present controller. ' s.homepage = 'https://github.com/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 bd98018b..0c928223 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Bunch of useful protocols and views.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/TIUIKitCore/Sources/Presenter/UIViewControllerPresenter.swift b/TIUIKitCore/Sources/Presenter/UIViewControllerPresenter.swift new file mode 100644 index 00000000..a9b547ce --- /dev/null +++ b/TIUIKitCore/Sources/Presenter/UIViewControllerPresenter.swift @@ -0,0 +1,30 @@ +// +// Copyright (c) 2022 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 UIKit.UIViewController + +@MainActor +public protocol UIViewControllerPresenter { + associatedtype ViewController: UIViewController + + func createViewController() -> ViewController +} diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index b4d89a37..4a16d06e 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Core UI elements: protocols, views and helpers.' s.homepage = 'https://github.com/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 fd8ed43a..ab16c2d4 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIYandexMapUtils' - s.version = '1.18.0' + s.version = '1.19.0' s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.' s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name s.license = { :type => 'MIT', :file => 'LICENSE' } diff --git a/project-scripts/push_to_podspecs.sh b/project-scripts/push_to_podspecs.sh index 2cf66e80..9196b6fc 100755 --- a/project-scripts/push_to_podspecs.sh +++ b/project-scripts/push_to_podspecs.sh @@ -10,7 +10,9 @@ ORDERED_PODSPECS="../TISwiftUtils/TISwiftUtils.podspec ../TIFoundationUtils/TIFoundationUtils.podspec ../TIKeychainUtils/TIKeychainUtils.podspec ../TIUIKitCore/TIUIKitCore.podspec +../TISwiftUICore/TISwiftUICore.podspec ../TIUIElements/TIUIElements.podspec +../TIAuth/TIAuth.podspec ../TITableKitUtils/TITableKitUtils.podspec ../TINetworking/TINetworking.podspec ../TINetworkingCache/TINetworkingCache.podspec