Skip to content
Merged
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
81 changes: 51 additions & 30 deletions Example/DemoApp/UpdateUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,57 @@ import ETDistribution
struct UpdateUtil {
static func checkForUpdates() {
ETDistribution.shared.checkForUpdate(params: CheckForUpdateParams(apiKey: Constants.apiKey)) { result in
switch result {
case .success(let releaseInfo):
if let releaseInfo {
print("Update found: \(releaseInfo)")
guard let url = ETDistribution.shared.buildUrlForInstall(releaseInfo.downloadUrl) else {
return
}
DispatchQueue.main.async {
UIApplication.shared.open(url) { _ in
exit(0)
}
guard case let .success(releaseInfo) = result else {
if case let .failure(error) = result {
print("Error checking for update: \(error)")
}
return
}

guard let releaseInfo = releaseInfo else {
print("Already up to date")
return
}

print("Update found: \(releaseInfo), requires login: \(releaseInfo.loginRequiredForDownload)")
if releaseInfo.loginRequiredForDownload {
// Get new release info, with login
ETDistribution.shared.getReleaseInfo(releaseId: releaseInfo.id) { newReleaseInfo in
if case let .success(newReleaseInfo) = newReleaseInfo {
UpdateUtil.installRelease(releaseInfo: newReleaseInfo)
}
} else {
print("Already up to date")
}
case .failure(let error):
print("Error checking for update: \(error)")
} else {
UpdateUtil.installRelease(releaseInfo: releaseInfo)
}
}
}

static func checkForUpdatesWithLogin() {
let params = CheckForUpdateParams(apiKey: Constants.apiKey, requiresLogin: true)
ETDistribution.shared.checkForUpdate(params: params) { result in
switch result {
case .success(let releaseInfo):
if let releaseInfo {
print("Update found: \(releaseInfo)")
guard let url = ETDistribution.shared.buildUrlForInstall(releaseInfo.downloadUrl) else {
return
}
DispatchQueue.main.async {
UIApplication.shared.open(url) { _ in
exit(0)
}
guard case let .success(releaseInfo) = result else {
if case let .failure(error) = result {
print("Error checking for update: \(error)")
}
return
}

guard let releaseInfo = releaseInfo else {
print("Already up to date")
return
}

print("Update found: \(releaseInfo), requires login: \(releaseInfo.loginRequiredForDownload)")
if releaseInfo.loginRequiredForDownload {
// Get new release info, with login
ETDistribution.shared.getReleaseInfo(releaseId: releaseInfo.id) { newReleaseInfo in
if case let .success(newReleaseInfo) = newReleaseInfo {
UpdateUtil.installRelease(releaseInfo: newReleaseInfo)
}
} else {
print("Already up to date")
}
case .failure(let error):
print("Error checking for update: \(error)")
} else {
UpdateUtil.installRelease(releaseInfo: releaseInfo)
}
}
}
Expand All @@ -80,4 +90,15 @@ struct UpdateUtil {
completion()
}
}

private static func installRelease(releaseInfo: DistributionReleaseInfo) {
guard let url = ETDistribution.shared.buildUrlForInstall(releaseInfo.downloadUrl) else {
return
}
DispatchQueue.main.async {
UIApplication.shared.open(url) { _ in
exit(0)
}
}
}
}
6 changes: 3 additions & 3 deletions Sources/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ enum Auth {
static let refreshTokenKey = "refreshToken"
}

static func getAccessToken(settings: CheckForUpdateParams.LoginSetting, completion: @escaping (Result<String, Error>) -> Void) {
static func getAccessToken(settings: LoginSetting, completion: @escaping (Result<String, Error>) -> Void) {
KeychainHelper.getToken(key: Constants.accessTokenKey) { token in
if let token = token,
JWTHelper.isValid(token: token) {
Expand Down Expand Up @@ -54,7 +54,7 @@ enum Auth {
}
}

private static func requestLogin(_ settings: CheckForUpdateParams.LoginSetting, _ completion: @escaping (Result<String, Error>) -> Void) {
private static func requestLogin(_ settings: LoginSetting, _ completion: @escaping (Result<String, Error>) -> Void) {
login(settings: settings) { result in
switch result {
case .success(let response):
Expand Down Expand Up @@ -110,7 +110,7 @@ enum Auth {
}

private static func login(
settings: CheckForUpdateParams.LoginSetting,
settings: LoginSetting,
completion: @escaping (Result<AuthCodeResponse, Error>) -> Void)
{
let verifier = getVerifier()!
Expand Down
89 changes: 83 additions & 6 deletions Sources/ETDistribution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public final class ETDistribution: NSObject {
}

public func buildUrlForInstall(_ plistUrl: String) -> URL? {
guard var components = URLComponents(string: "itms-services://") else {
guard plistUrl != "REQUIRES_LOGIN",
var components = URLComponents(string: "itms-services://") else {
return nil
}
components.queryItems = [
Expand All @@ -86,10 +87,39 @@ public final class ETDistribution: NSObject {
]
return components.url
}

public func getReleaseInfo(releaseId: String, completion: @escaping ((Result<DistributionReleaseInfo, Error>) -> Void)) {
if let loginSettings = loginSettings,
(loginLevel?.rawValue ?? 0) > LoginLevel.none.rawValue {
Auth.getAccessToken(settings: loginSettings) { [weak self] result in
switch result {
case .success(let accessToken):
self?.getReleaseInfo(releaseId: releaseId, accessToken: accessToken, completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
} else {
getReleaseInfo(releaseId: 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)
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 = ""

override private init() {
super.init()
Expand All @@ -105,18 +135,22 @@ public final class ETDistribution: NSObject {
// Not checking for updates when the debugger is attached
return
}
apiKey = params.apiKey
loginLevel = params.loginLevel
loginSettings = params.loginSetting

if let loginSettings = params.loginSetting {
if let loginSettings = params.loginSetting,
params.loginLevel == .everything {
Auth.getAccessToken(settings: loginSettings) { [weak self] result in
switch result {
case .success(let accessToken):
self?.performRequest(params: params, accessToken: accessToken, completion: completion)
self?.getUpdatesFromBackend(params: params, accessToken: accessToken, completion: completion)
case .failure(let error):
completion?(.failure(error))
}
}
} else {
performRequest(params: params, accessToken: nil) { [weak self] result in
getUpdatesFromBackend(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"
Expand All @@ -130,7 +164,7 @@ public final class ETDistribution: NSObject {
#endif
}

private func performRequest(params: CheckForUpdateParams,
private func getUpdatesFromBackend(params: CheckForUpdateParams,
accessToken: String? = nil,
completion: ((Result<DistributionReleaseInfo?, Error>) -> Void)? = nil) {
guard var components = URLComponents(string: "https://api.emergetools.com/distribution/checkForUpdates") else {
Expand Down Expand Up @@ -166,6 +200,31 @@ public final class ETDistribution: NSObject {
}
}

private func getReleaseInfo(releaseId: String,
accessToken: String? = nil,
completion: @escaping ((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")
]

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")
}

session.getReleaseInfo(request, completion: completion)
}

private func handleResponse(response: DistributionReleaseInfo) {
guard response.id != UserDefaults.skippedRelease,
(UserDefaults.postponeTimeout == nil || UserDefaults.postponeTimeout! < Date() ) else {
Expand Down Expand Up @@ -208,7 +267,25 @@ public final class ETDistribution: NSObject {
}

private func handleInstallRelease(_ release: DistributionReleaseInfo) {
guard let url = self.buildUrlForInstall(release.downloadUrl) else {
if release.loginRequiredForDownload, let loginSettings = loginSettings {
Auth.getAccessToken(settings: loginSettings) { [weak self] result in
guard case let .success(accessToken) = result else {
return
}
self?.getReleaseInfo(releaseId: release.id, accessToken: accessToken) { [weak self] result in
guard case .success(let release) = result else {
return
}
self?.installAppWithDownloadString(release.downloadUrl)
}
}
} else {
installAppWithDownloadString(release.downloadUrl)
}
}

private func installAppWithDownloadString(_ urlString: String) {
guard let url = self.buildUrlForInstall(urlString) else {
return
}
UIApplication.shared.open(url) { _ in
Expand Down
36 changes: 27 additions & 9 deletions Sources/Models/CheckForUpdateParams.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@

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 {
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 {
case none
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.
Expand All @@ -26,6 +45,7 @@ public final class CheckForUpdateParams: NSObject {
self.apiKey = apiKey
self.tagName = tagName
self.loginSetting = requiresLogin ? .default : nil
self.loginLevel = requiresLogin ? .everything : .none
}

/// Create a new CheckForUpdateParams object with a connection name.
Expand All @@ -37,10 +57,12 @@ public final class CheckForUpdateParams: NSObject {
@objc
public init(apiKey: String,
tagName: String? = nil,
connection: String) {
connection: String,
loginLevel: LoginLevel = .everything) {
self.apiKey = apiKey
self.tagName = tagName
self.loginSetting = .connection(connection)
self.loginLevel = loginLevel
}

/// Create a new CheckForUpdateParams object with a login setting.
Expand All @@ -51,20 +73,16 @@ public final class CheckForUpdateParams: NSObject {
/// - loginSetting: A `LoginSetting` to require authenticated access to updates.
public init(apiKey: String,
tagName: String? = nil,
loginSetting: LoginSetting) {
loginSetting: LoginSetting,
loginLevel: LoginLevel = .everything) {
self.apiKey = apiKey
self.tagName = tagName
self.loginSetting = loginSetting
}

/// 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 {
case `default`
case connection(String)
self.loginLevel = loginLevel
}

let apiKey: String
let tagName: String?
let loginSetting: LoginSetting?
let loginLevel: LoginLevel?
}
1 change: 1 addition & 0 deletions Sources/Models/DistributionReleaseInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ public final class DistributionReleaseInfo: NSObject, Decodable {
public let version: String
public let appId: String
public let downloadUrl: String
public let loginRequiredForDownload: Bool
}
Loading