diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift index 462286d..a45d300 100644 --- a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift +++ b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift @@ -148,11 +148,12 @@ public class Client { /// Use the olympus session endpoint to see if the existing session is still valid public func validateSession() -> AnyPublisher { return Current.network.dataTask(with: URLRequest.olympusSession) - .tryMap { (data, response) in - guard - let jsonObject = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any], - jsonObject["provider"] != nil - else { throw AuthenticationError.invalidSession } + .map(\.data) + .decode(type: AppleSession.self, decoder: JSONDecoder()) + .tryMap { session in + if session.provider == nil { + throw AuthenticationError.notDeveloperAppleId + } } .eraseToAnyPublisher() } @@ -174,6 +175,7 @@ public enum AuthenticationState: Equatable { case unauthenticated case waitingForSecondFactor(TwoFactorOption, AuthOptionsResponse, AppleSessionData) case authenticated + case notAppleDeveloper } public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { @@ -186,7 +188,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { case accountUsesUnknownAuthenticationKind(String?) case accountLocked(String) case badStatusCode(statusCode: Int, data: Data, response: HTTPURLResponse) - + case notDeveloperAppleId + public var errorDescription: String? { switch self { case .invalidSession: @@ -212,6 +215,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { return message case let .badStatusCode(statusCode, _, _): return "Received an unexpected status code: \(statusCode). If you continue to have problems, please submit a bug report in the Help menu." + case .notDeveloperAppleId: + return "You are not registered as an Apple Developer. Please visit Apple Developer Registration. https://developer.apple.com/register/" } } } @@ -362,3 +367,20 @@ public enum SecurityCode { } } } + +/// Object returned from olympus/v1/session +/// Used to check Provider, and show name +/// If Provider is nil, we can assume the Apple User is NOT an Apple Developer and can't download Xcode. +public struct AppleSession: Decodable, Equatable { + public let user: AppleUser + public let provider: AppleProvider? +} + +public struct AppleProvider: Decodable, Equatable { + public let providerId: Int + public let name: String +} + +public struct AppleUser: Decodable, Equatable { + public let fullName: String +} diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index d295d68..28449ee 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -40,9 +40,15 @@ extension AppState { } private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher { + // We need to check if the Apple ID that is logged in is an Apple Developer + // Since users can use xcodereleases, we don't check for Apple ID on a xcode list refresh + // If the Apple Id is not a developer, the download action will try and download a xip that is invalid, causing a `xcode13.0.xip is damaged and can't be expanded.` Logger.appState.info("Using \(downloader) downloader") - return getXcodeArchive(installationType, downloader: downloader) + return validateSession() + .flatMap { _ in + self.getXcodeArchive(installationType, downloader: downloader) + } .flatMap { xcode, url -> AnyPublisher in self.installArchivedXcode(xcode, at: url) } diff --git a/Xcodes/Backend/AppState+Update.swift b/Xcodes/Backend/AppState+Update.swift index 62a327d..ed998af 100644 --- a/Xcodes/Backend/AppState+Update.swift +++ b/Xcodes/Backend/AppState+Update.swift @@ -71,7 +71,12 @@ extension AppState { private func updateAvailableXcodes(from dataSource: DataSource) -> AnyPublisher<[AvailableXcode], Error> { switch dataSource { case .apple: - return signInIfNeeded() + return signInIfNeeded() + .flatMap { [unowned self] in + // this will check to see if the Apple ID is a valid Apple Developer or not. + // If it's not, we can't use the Apple source to get xcode info. + self.validateSession() + } .flatMap { [unowned self] in self.releasedXcodes().combineLatest(self.prereleaseXcodes()) } .receive(on: DispatchQueue.main) .map { releasedXcodes, prereleaseXcodes in diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 6c3c48b..c14ebf4 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -116,8 +116,9 @@ class AppState: ObservableObject { .receive(on: DispatchQueue.main) .handleEvents(receiveCompletion: { completion in if case .failure = completion { - self.authenticationState = .unauthenticated - self.presentedSheet = .signIn + // this is causing some awkwardness with showing an alert with the error and also popping up the sign in view + // self.authenticationState = .unauthenticated + // self.presentedSheet = .signIn } }) .eraseToAnyPublisher() @@ -227,7 +228,7 @@ class AppState: ObservableObject { self.authError = error case .finished: switch self.authenticationState { - case .authenticated, .unauthenticated: + case .authenticated, .unauthenticated, .notAppleDeveloper: self.presentedSheet = nil self.secondFactorData = nil case let .waitingForSecondFactor(option, authOptions, sessionData): @@ -315,7 +316,7 @@ class AppState: ObservableObject { self.$authenticationState .filter { state in switch state { - case .authenticated, .unauthenticated: return true + case .authenticated, .unauthenticated, .notAppleDeveloper: return true case .waitingForSecondFactor: return false } } @@ -324,6 +325,9 @@ class AppState: ObservableObject { if state == .unauthenticated { throw AuthenticationError.invalidSession } + if state == .notAppleDeveloper { + throw AuthenticationError.notDeveloperAppleId + } return Void() } }