Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Example/DemoApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
9 changes: 9 additions & 0 deletions Example/DemoApp/UpdateUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<DistributionReleaseInfo?, Error>) {
guard case let .success(releaseInfo) = result else {
Expand Down
153 changes: 112 additions & 41 deletions Sources/ETDistribution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,36 @@ public final class ETDistribution: NSObject {
}

public func getReleaseInfo(releaseId: String, completion: @escaping (@MainActor (Result<DistributionReleaseInfo, Error>) -> Void)) {
let params = GetReleaseParams(apiKey: self.apiKey, releaseId: releaseId)
getReleaseInfo(params: params, completion: completion)
}

public func getReleaseInfo(params: GetReleaseParams, completion: @escaping (@MainActor (Result<DistributionReleaseInfo, Error>) -> 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)
Expand Down Expand Up @@ -150,13 +162,53 @@ 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<DistributionAvailableBuildsResponse, Error>) -> 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)
private lazy var uuid = BinaryParser.getMainBinaryUUID()
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()
Expand Down Expand Up @@ -195,28 +247,19 @@ public final class ETDistribution: NSObject {
private func getUpdatesFromBackend(params: CheckForUpdateParams,
accessToken: String? = nil,
completion: (@MainActor (Result<DistributionReleaseInfo?, Error>) -> 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 }
Expand All @@ -231,24 +274,14 @@ public final class ETDistribution: NSObject {
private func getReleaseInfo(releaseId: String,
accessToken: String? = nil,
completion: @escaping @MainActor (Result<DistributionReleaseInfo, Error>) -> 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)
}
Expand Down Expand Up @@ -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<DistributionAvailableBuildsResponse, Error>) -> 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
}
}
13 changes: 13 additions & 0 deletions Sources/Models/DistributionAvailableBuildsResponse.swift
Original file line number Diff line number Diff line change
@@ -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]
}
24 changes: 24 additions & 0 deletions Sources/Models/DistributionReleaseBasicInfo.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 6 additions & 0 deletions Sources/Network/URLSession+Distribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ extension URLSession {
}
}

func getAvailableReleases(_ request: URLRequest, completion: @escaping @MainActor (Result<DistributionAvailableBuildsResponse, Error>) -> 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<T: Sendable & Decodable>(_ request: URLRequest,
decode decodable: T.Type,
useCamelCase: Bool = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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?
}
Loading