diff --git a/Example/DemoApp/ContentView.swift b/Example/DemoApp/ContentView.swift index 590e435..1175205 100644 --- a/Example/DemoApp/ContentView.swift +++ b/Example/DemoApp/ContentView.swift @@ -42,6 +42,14 @@ struct ContentView: View { .foregroundColor(.black) .cornerRadius(10) + Button("Print Available Builds") { + UpdateUtil.getAllAvailableBuils() + } + .padding() + .background(.red) + .foregroundColor(.black) + .cornerRadius(10) + Button("Clear Tokens") { UpdateUtil.clearTokens() } diff --git a/Example/DemoApp/UpdateUtil.swift b/Example/DemoApp/UpdateUtil.swift index 05a9621..d45a5ec 100644 --- a/Example/DemoApp/UpdateUtil.swift +++ b/Example/DemoApp/UpdateUtil.swift @@ -27,6 +27,15 @@ struct UpdateUtil { } } + @MainActor + static func getAllAvailableBuils() { + let params = GetAllReleasesParams(apiKey: Constants.apiKey, + requiresLogin: false) + ETDistribution.shared.getAvailableBuilds(params: params) { result in + print(result) + } + } + @MainActor static func handleUpdateResult(result: Result) { guard case let .success(releaseInfo) = result else { diff --git a/Sources/ETDistribution.swift b/Sources/ETDistribution.swift index 7384b6b..93f293e 100644 --- a/Sources/ETDistribution.swift +++ b/Sources/ETDistribution.swift @@ -92,24 +92,36 @@ public final class ETDistribution: NSObject { } public func getReleaseInfo(releaseId: String, completion: @escaping (@MainActor (Result) -> Void)) { + let params = GetReleaseParams(apiKey: self.apiKey, releaseId: releaseId) + getReleaseInfo(params: params, completion: completion) + } + + public func getReleaseInfo(params: GetReleaseParams, completion: @escaping (@MainActor (Result) -> Void)) { + let loginSettings = params.loginSetting ?? self.loginSettings + let loginLevel = params.loginLevel ?? self.loginLevel + if let loginSettings = loginSettings, (loginLevel?.rawValue ?? 0) > LoginLevel.noLogin.rawValue { Auth.getAccessToken(settings: loginSettings) { [weak self] result in switch result { case .success(let accessToken): - self?.getReleaseInfo(releaseId: releaseId, accessToken: accessToken, completion: completion) + self?.getReleaseInfo(releaseId: params.releaseId, accessToken: accessToken, completion: completion) case .failure(let error): completion(.failure(error)) } } } else { - getReleaseInfo(releaseId: releaseId, accessToken: nil) { [weak self] result in + getReleaseInfo(releaseId: params.releaseId, accessToken: nil) { [weak self] result in if case .failure(let error) = result, case RequestError.loginRequired = error { // Attempt login if backend returns "Login Required" - self?.loginSettings = LoginSetting.default - self?.loginLevel = .onlyForDownload - self?.getReleaseInfo(releaseId: releaseId, completion: completion) + let params = GetReleaseParams(apiKey: params.apiKey, + releaseId: params.releaseId, + loginSetting: LoginSetting.default, + loginLevel: .onlyForDownload) + self?.loginSettings = params.loginSetting + self?.loginLevel = params.loginLevel + self?.getReleaseInfo(params: params, completion: completion) return } completion(result) @@ -150,6 +162,45 @@ public final class ETDistribution: NSObject { actions: actions) } } + + /// Obtain all available builds + /// - Parameters: + /// - params: A `GetAllReleasesParams` object. + /// - completion: A closure that is called with the result of all builds. + public func getAvailableBuilds(params: GetAllReleasesParams, completion: @escaping (@MainActor (Result) -> Void)) { + let loginSettings = params.loginSetting ?? self.loginSettings + let loginLevel = params.loginLevel ?? self.loginLevel + + if let loginSettings = loginSettings, + (loginLevel?.rawValue ?? 0) > LoginLevel.noLogin.rawValue { + Auth.getAccessToken(settings: loginSettings) { [weak self] result in + switch result { + case .success(let accessToken): + self?.getAllBuilds(params: params, accessToken: accessToken, completion: completion) + case .failure(let error): + completion(.failure(error)) + } + } + } else { + getAllBuilds(params: params, accessToken: nil) { [weak self] result in + if case .failure(let error) = result, + case RequestError.loginRequired = error { + // Attempt login if backend returns "Login Required" + let params = GetAllReleasesParams(apiKey: params.apiKey, + loginSetting: LoginSetting.default, + loginLevel: .onlyForDownload, + binaryIdentifierOverride: params.binaryIdentifierOverride, + appIdOverride: params.appIdOverride + ) + self?.loginSettings = params.loginSetting + self?.loginLevel = params.loginLevel + self?.getAvailableBuilds(params: params, completion: completion) + return + } + completion(result) + } + } + } // MARK: - Private private lazy var session = URLSession(configuration: URLSessionConfiguration.ephemeral) @@ -157,6 +208,7 @@ public final class ETDistribution: NSObject { private var loginSettings: LoginSetting? private var loginLevel: LoginLevel? private var apiKey: String = "" + private static let baseUrl = "https://api.emergetools.com" override private init() { super.init() @@ -195,28 +247,19 @@ public final class ETDistribution: NSObject { private func getUpdatesFromBackend(params: CheckForUpdateParams, accessToken: String? = nil, completion: (@MainActor (Result) -> Void)? = nil) { - guard var components = URLComponents(string: "https://api.emergetools.com/distribution/checkForUpdates") else { - fatalError("Invalid URL") - } - - components.queryItems = [ - URLQueryItem(name: "apiKey", value: params.apiKey), - URLQueryItem(name: "binaryIdentifier", value: params.binaryIdentifierOverride ?? uuid), - URLQueryItem(name: "appId", value: params.appIdOverride ?? Bundle.main.bundleIdentifier), - URLQueryItem(name: "platform", value: "ios") + var queryItems: [String: String?] = [ + "apiKey": params.apiKey, + "binaryIdentifier": params.binaryIdentifierOverride ?? uuid, + "appId": params.appIdOverride ?? Bundle.main.bundleIdentifier, + "platform": "ios" ] if let tagName = params.tagName { - components.queryItems?.append(URLQueryItem(name: "tag", value: tagName)) - } - - guard let url = components.url else { - fatalError("Invalid URL") - } - var request = URLRequest(url: url) - request.httpMethod = "GET" - if let accessToken = accessToken { - request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + queryItems["tag"] = tagName } + + let request = buildRequest(path: "/distribution/checkForUpdates", + accessToken: accessToken, + queryItems: queryItems) session.checkForUpdate(request) { [weak self] result in let mappedResult = result.map { $0.updateInfo } @@ -231,24 +274,14 @@ public final class ETDistribution: NSObject { private func getReleaseInfo(releaseId: String, accessToken: String? = nil, completion: @escaping @MainActor (Result) -> Void) { - guard var components = URLComponents(string: "https://api.emergetools.com/distribution/getRelease") else { - fatalError("Invalid URL") - } - - components.queryItems = [ - URLQueryItem(name: "apiKey", value: apiKey), - URLQueryItem(name: "uploadId", value: releaseId), - URLQueryItem(name: "platform", value: "ios") + let queryItems: [String: String?] = [ + "apiKey": apiKey, + "uploadId": releaseId, + "platform": "ios" ] - - guard let url = components.url else { - fatalError("Invalid URL") - } - var request = URLRequest(url: url) - request.httpMethod = "GET" - if let accessToken = accessToken { - request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") - } + let request = buildRequest(path: "/distribution/getRelease", + accessToken: accessToken, + queryItems: queryItems) session.getReleaseInfo(request, completion: completion) } @@ -288,4 +321,42 @@ public final class ETDistribution: NSObject { private func handlePostponeRelease() { UserDefaults.postponeTimeout = Date(timeIntervalSinceNow: 60 * 60 * 24) } + + private func getAllBuilds(params: GetAllReleasesParams, + accessToken: String? = nil, + completion: @escaping @MainActor (Result) -> Void) { + let queryItems: [String: String?] = [ + "apiKey": params.apiKey, + "binaryIdentifier": params.binaryIdentifierOverride ?? uuid, + "appId": params.appIdOverride ?? Bundle.main.bundleIdentifier, + "platform": "ios", + "page": "\(params.page)" + ] + let request = buildRequest(path: "/distribution/allUpdates", + accessToken: accessToken, + queryItems: queryItems) + + session.getAvailableReleases(request, completion: completion) + } + + private func buildRequest(path: String, + accessToken: String?, + queryItems: [String: String?]) -> URLRequest { + guard var components = URLComponents(string: "\(ETDistribution.baseUrl)\(path)") else { + fatalError("Invalid URL") + } + + components.queryItems = queryItems.map { URLQueryItem(name: $0.key, value: $0.value) } + + guard let url = components.url else { + fatalError("Invalid URL") + } + var request = URLRequest(url: url) + request.httpMethod = "GET" + if let accessToken = accessToken { + request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") + } + + return request + } } diff --git a/Sources/Models/DistributionAvailableBuildsResponse.swift b/Sources/Models/DistributionAvailableBuildsResponse.swift new file mode 100644 index 0000000..a147915 --- /dev/null +++ b/Sources/Models/DistributionAvailableBuildsResponse.swift @@ -0,0 +1,13 @@ +// +// DistributionAvailableBuildsResponse.swift +// ETDistribution +// +// Created by Itay Brenner on 17/2/25. +// + +public struct DistributionAvailableBuildsResponse: Decodable, Sendable { + let page: Int + let totalPages: Int + let totalBuilds: Int + let builds: [DistributionReleaseBasicInfo] +} diff --git a/Sources/Models/DistributionReleaseBasicInfo.swift b/Sources/Models/DistributionReleaseBasicInfo.swift new file mode 100644 index 0000000..bb0dbca --- /dev/null +++ b/Sources/Models/DistributionReleaseBasicInfo.swift @@ -0,0 +1,24 @@ +// +// DistributionReleaseBasicInfo.swift +// ETDistribution +// +// Created by Itay Brenner on 18/2/25. +// + +import Foundation + +@objc +public final class DistributionReleaseBasicInfo: NSObject, Decodable, Sendable { + public let id: String + public let tag: String + public let version: String + public let build: String + public let appId: String + public let iconUrl: String? + public let appName: String + private let createdDate: String + + public var created: Date? { + Date.fromString(createdDate) + } +} diff --git a/Sources/Network/URLSession+Distribute.swift b/Sources/Network/URLSession+Distribute.swift index 143cbea..a5907ef 100644 --- a/Sources/Network/URLSession+Distribute.swift +++ b/Sources/Network/URLSession+Distribute.swift @@ -39,6 +39,12 @@ extension URLSession { } } + func getAvailableReleases(_ request: URLRequest, completion: @escaping @MainActor (Result) -> Void) { + self.perform(request, decode: DistributionAvailableBuildsResponse.self, useCamelCase: true, completion: completion) { [weak self] data, statusCode in + return self?.getErrorFrom(data: data, statusCode: statusCode) ?? RequestError.badRequest("") + } + } + private func perform(_ request: URLRequest, decode decodable: T.Type, useCamelCase: Bool = true, diff --git a/Sources/Models/CheckForUpdateParams.swift b/Sources/Params/CheckForUpdateParams.swift similarity index 75% rename from Sources/Models/CheckForUpdateParams.swift rename to Sources/Params/CheckForUpdateParams.swift index ae64fca..f6079a9 100644 --- a/Sources/Models/CheckForUpdateParams.swift +++ b/Sources/Params/CheckForUpdateParams.swift @@ -7,30 +7,11 @@ import Foundation -/// Type of authenticated access to required. The default case shows the Emerge Tools login page. -/// A custom connection can be used to automatically redirect to an SSO page. -public enum LoginSetting: Sendable { - case `default` - case connection(String) -} - -/// Level of login required. By default no login is required -/// Available levels: -/// - none: No login is requiried -/// - onlyForDownload: login is required only when downloading the app -/// - everything: login is always required when doing API calls. -@objc -public enum LoginLevel: Int, Sendable { - case noLogin - case onlyForDownload - case everything -} - /// A model for configuring parameters needed to check for app updates. /// /// Note: `tagName` is generally not needed, the SDK will identify the tag automatically. @objc -public final class CheckForUpdateParams: NSObject { +public final class CheckForUpdateParams: CommonParams { /// Create a new CheckForUpdateParams object. /// @@ -46,12 +27,12 @@ public final class CheckForUpdateParams: NSObject { requiresLogin: Bool = false, binaryIdentifierOverride: String? = nil, appIdOverride: String? = nil) { - self.apiKey = apiKey self.tagName = tagName - self.loginSetting = requiresLogin ? .default : nil - self.loginLevel = requiresLogin ? .everything : .noLogin self.binaryIdentifierOverride = binaryIdentifierOverride self.appIdOverride = appIdOverride + super.init(apiKey: apiKey, + loginSetting: requiresLogin ? .default : nil, + loginLevel: requiresLogin ? .everything : .noLogin) } /// Create a new CheckForUpdateParams object with a connection name. @@ -70,12 +51,12 @@ public final class CheckForUpdateParams: NSObject { loginLevel: LoginLevel = .everything, binaryIdentifierOverride: String? = nil, appIdOverride: String? = nil) { - self.apiKey = apiKey self.tagName = tagName - self.loginSetting = .connection(connection) - self.loginLevel = loginLevel self.binaryIdentifierOverride = binaryIdentifierOverride self.appIdOverride = appIdOverride + super.init(apiKey: apiKey, + loginSetting: .connection(connection), + loginLevel: loginLevel) } /// Create a new CheckForUpdateParams object with a login setting. @@ -93,18 +74,15 @@ public final class CheckForUpdateParams: NSObject { loginLevel: LoginLevel = .everything, binaryIdentifierOverride: String? = nil, appIdOverride: String? = nil) { - self.apiKey = apiKey self.tagName = tagName - self.loginSetting = loginSetting - self.loginLevel = loginLevel self.binaryIdentifierOverride = binaryIdentifierOverride self.appIdOverride = appIdOverride + super.init(apiKey: apiKey, + loginSetting: loginSetting, + loginLevel: loginLevel) } - let apiKey: String let tagName: String? - let loginSetting: LoginSetting? - let loginLevel: LoginLevel? let binaryIdentifierOverride: String? let appIdOverride: String? } diff --git a/Sources/Params/CommonParams.swift b/Sources/Params/CommonParams.swift new file mode 100644 index 0000000..68ce2fc --- /dev/null +++ b/Sources/Params/CommonParams.swift @@ -0,0 +1,23 @@ +// +// CommonParams.swift +// ETDistribution +// +// Created by Itay Brenner on 18/2/25. +// + +import Foundation + +@objc +public class CommonParams: NSObject { + public init(apiKey: String, + loginSetting: LoginSetting?, + loginLevel: LoginLevel?) { + self.apiKey = apiKey + self.loginSetting = loginSetting + self.loginLevel = loginLevel + } + + let apiKey: String + let loginSetting: LoginSetting? + let loginLevel: LoginLevel? +} diff --git a/Sources/Params/GetAllReleasesParams.swift b/Sources/Params/GetAllReleasesParams.swift new file mode 100644 index 0000000..31857ad --- /dev/null +++ b/Sources/Params/GetAllReleasesParams.swift @@ -0,0 +1,87 @@ +// +// GetAllReleasesParams.swift +// ETDistribution +// +// Created by Itay Brenner on 18/2/25. +// + +import Foundation + +/// A model for configuring parameters needed to get an update information. +/// +@objc +public final class GetAllReleasesParams: CommonParams { + + /// Create a new GetAllReleasesParams object. + /// + /// - Parameters: + /// - apiKey: A `String` API key used for authentication. + /// - requiresLogin: A `Bool` indicating if user login is required before checking for updates. Defaults to `false`. + /// - page: Page Number, pages start from 1 + /// - binaryIdentifierOverride: Override the binary identifier for local debugging + /// - appIdOverride: Override the app identifier (Bundle Id) for local debugging + @objc + public init(apiKey: String, + requiresLogin: Bool = false, + page: NSNumber? = 1, + binaryIdentifierOverride: String? = nil, + appIdOverride: String? = nil) { + self.page = page ?? 1 + self.binaryIdentifierOverride = binaryIdentifierOverride + self.appIdOverride = appIdOverride + super.init(apiKey: apiKey, + loginSetting: requiresLogin ? .default : nil, + loginLevel: requiresLogin ? .everything : .noLogin) + } + + /// Create a new GetAllReleasesParams object with a connection name. + /// + /// - Parameters: + /// - apiKey: A `String` API key used for authentication. + /// - connection: A `String` connection name for a company. Will automatically redirect login to the company’s SSO page. + /// - loginLevel: An optional `LoginLevel` to set whether a login is required for downloading updates, checking for updates or never + /// - page: Page Number, pages start from 1 + /// - binaryIdentifierOverride: Override the binary identifier for local debugging + /// - appIdOverride: Override the app identifier (Bundle Id) for local debugging + @objc + public init(apiKey: String, + connection: String, + loginLevel: LoginLevel = .everything, + page: NSNumber? = 1, + binaryIdentifierOverride: String? = nil, + appIdOverride: String? = nil) { + self.page = page ?? 1 + self.binaryIdentifierOverride = binaryIdentifierOverride + self.appIdOverride = appIdOverride + super.init(apiKey: apiKey, + loginSetting: .connection(connection), + loginLevel: loginLevel) + } + + /// Create a new GetAllReleasesParams object with a login setting. + /// + /// - Parameters: + /// - apiKey: A `String` API key used for authentication. + /// - loginSetting: A `LoginSetting` to require authenticated access to updates. + /// - loginLevel: An optional `LoginLevel` to set whether a login is required for downloading updates, checking for updates or never + /// - page: Page Number, pages start from 1 + /// - binaryIdentifierOverride: Override the binary identifier for local debugging + /// - appIdOverride: Override the app identifier (Bundle Id) for local debugging + public init(apiKey: String, + loginSetting: LoginSetting, + loginLevel: LoginLevel = .everything, + page: NSNumber? = 1, + binaryIdentifierOverride: String? = nil, + appIdOverride: String? = nil) { + self.page = page ?? 1 + self.binaryIdentifierOverride = binaryIdentifierOverride + self.appIdOverride = appIdOverride + super.init(apiKey: apiKey, + loginSetting: loginSetting, + loginLevel: loginLevel) + } + + let page: NSNumber + let binaryIdentifierOverride: String? + let appIdOverride: String? +} diff --git a/Sources/Params/GetReleaseParams.swift b/Sources/Params/GetReleaseParams.swift new file mode 100644 index 0000000..58c191a --- /dev/null +++ b/Sources/Params/GetReleaseParams.swift @@ -0,0 +1,67 @@ +// +// GetReleaseParams.swift +// ETDistribution +// +// Created by Itay Brenner on 18/2/25. +// + +import Foundation + +/// A model for configuring parameters needed to get an update information. +/// +@objc +public final class GetReleaseParams: CommonParams { + + /// Create a new GetReleaseParams object. + /// + /// - Parameters: + /// - apiKey: A `String` API key used for authentication. + /// - releaseId: A `String` identifying the relase. + /// - requiresLogin: A `Bool` indicating if user login is required before checking for updates. Defaults to `false`. + @objc + public init(apiKey: String, + releaseId: String, + requiresLogin: Bool = false) { + self.releaseId = releaseId + super.init(apiKey: apiKey, + loginSetting: requiresLogin ? .default : nil, + loginLevel: requiresLogin ? .everything : .noLogin) + } + + /// Create a new GetReleaseParams object with a connection name. + /// + /// - Parameters: + /// - apiKey: A `String` API key used for authentication. + /// - releaseId: A `String` identifying the relase. + /// - connection: A `String` connection name for a company. Will automatically redirect login to the company’s SSO page. + /// - loginLevel: An optional `LoginLevel` to set whether a login is required for downloading updates, checking for updates or never + @objc + public init(apiKey: String, + releaseId: String, + connection: String, + loginLevel: LoginLevel = .everything) { + self.releaseId = releaseId + super.init(apiKey: apiKey, + loginSetting: .connection(connection), + loginLevel: loginLevel) + } + + /// Create a new GetReleaseParams object with a login setting. + /// + /// - Parameters: + /// - apiKey: A `String` API key used for authentication. + /// - releaseId: A `String` identifying the relase. + /// - loginSetting: A `LoginSetting` to require authenticated access to updates. + /// - loginLevel: An optional `LoginLevel` to set whether a login is required for downloading updates, checking for updates or never + public init(apiKey: String, + releaseId: String, + loginSetting: LoginSetting, + loginLevel: LoginLevel = .everything) { + self.releaseId = releaseId + super.init(apiKey: apiKey, + loginSetting: loginSetting, + loginLevel: loginLevel) + } + + let releaseId: String +} diff --git a/Sources/Params/LoginModels.swift b/Sources/Params/LoginModels.swift new file mode 100644 index 0000000..3e0733e --- /dev/null +++ b/Sources/Params/LoginModels.swift @@ -0,0 +1,25 @@ +// +// LoginModels.swift +// ETDistribution +// +// Created by Itay Brenner on 18/2/25. +// + +/// Type of authenticated access to required. The default case shows the Emerge Tools login page. +/// A custom connection can be used to automatically redirect to an SSO page. +public enum LoginSetting: Sendable { + case `default` + case connection(String) +} + +/// Level of login required. By default no login is required +/// Available levels: +/// - none: No login is requiried +/// - onlyForDownload: login is required only when downloading the app +/// - everything: login is always required when doing API calls. +@objc +public enum LoginLevel: Int, Sendable { + case noLogin + case onlyForDownload + case everything +}