From 287b5500fecfbdf23c9e0366d5bae3b3e8b7f2a3 Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Fri, 30 Apr 2021 14:43:54 -0600 Subject: [PATCH 1/8] present sign in when credentials are required --- Xcodes.xcodeproj/project.pbxproj | 8 +++++ Xcodes/Backend/AppState.swift | 30 ++++++++++------ Xcodes/Frontend/Common/XcodesSheet.swift | 8 +++++ Xcodes/Frontend/MainWindow.swift | 30 ++++++++++++++-- .../Preferences/GeneralPreferencePane.swift | 34 +++++++++---------- .../SignIn/SignInCredentialsView.swift | 5 ++- Xcodes/Frontend/SignIn/SignedInView.swift | 24 +++++++++++++ 7 files changed, 105 insertions(+), 34 deletions(-) create mode 100644 Xcodes/Frontend/Common/XcodesSheet.swift create mode 100644 Xcodes/Frontend/SignIn/SignedInView.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 39e22a7..ca0cfce 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; }; + 536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */; }; 63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EAA4EA259944450046AB8F /* ProgressButton.swift */; }; CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; }; CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */; }; @@ -156,6 +158,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = ""; }; + 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesSheet.swift; sourceTree = ""; }; 63EAA4EA259944450046AB8F /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = ""; }; CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = ""; }; CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateUpdateTests.swift; sourceTree = ""; }; @@ -307,6 +311,7 @@ CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */, 63EAA4EA259944450046AB8F /* ProgressButton.swift */, CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */, + 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */, ); path = Common; sourceTree = ""; @@ -382,6 +387,7 @@ CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */, CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */, CAA1CB4C255A5CFD003FD669 /* SignInPhoneListView.swift */, + 536CFDD1263C94DE00026CE0 /* SignedInView.swift */, ); path = SignIn; sourceTree = ""; @@ -755,6 +761,7 @@ CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */, CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */, CAA8589B25A2B83000ACF8C0 /* Aria2CError.swift in Sources */, + 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */, CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */, CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */, CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */, @@ -814,6 +821,7 @@ CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */, CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */, 63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */, + 536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */, CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */, CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */, CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */, diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 6e4d622..22c2c97 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -38,7 +38,7 @@ class AppState: ObservableObject { } @Published var updatePublisher: AnyCancellable? var isUpdating: Bool { updatePublisher != nil } - @Published var presentingSignInAlert = false + @Published var presentedSheet: XcodesSheet? = nil @Published var isProcessingAuthRequest = false @Published var secondFactorData: SecondFactorData? @Published var xcodeBeingConfirmedForUninstallation: Xcode? @@ -65,6 +65,14 @@ class AppState: ObservableObject { var dataSource: DataSource { Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default } + + var savedUsername: String? { + Current.defaults.string(forKey: "username") + } + + var hasSavedUsername: Bool { + savedUsername != nil + } // MARK: - Init @@ -94,7 +102,7 @@ class AppState: ObservableObject { .handleEvents(receiveCompletion: { completion in if case .failure = completion { self.authenticationState = .unauthenticated - self.presentingSignInAlert = true + self.presentedSheet = .signIn } }) .eraseToAnyPublisher() @@ -104,7 +112,7 @@ class AppState: ObservableObject { validateSession() .catch { (error) -> AnyPublisher in guard - let username = Current.defaults.string(forKey: "username"), + let username = savedUsername, let password = try? Current.keychain.getString(username) else { return Fail(error: error) @@ -147,7 +155,7 @@ class AppState: ObservableObject { } func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) { - self.presentingSignInAlert = false + self.presentedSheet = .twoFactor self.secondFactorData = SecondFactorData( option: option, authOptions: authOptions, @@ -198,7 +206,7 @@ class AppState: ObservableObject { switch completion { case let .failure(error): if case .invalidUsernameOrPassword = error as? AuthenticationError, - let username = Current.defaults.string(forKey: "username") { + let username = savedUsername { // remove any keychain password if we fail to log with an invalid username or password so it doesn't try again. try? Current.keychain.remove(username) Current.defaults.removeObject(forKey: "username") @@ -209,7 +217,7 @@ class AppState: ObservableObject { case .finished: switch self.authenticationState { case .authenticated, .unauthenticated: - self.presentingSignInAlert = false + self.presentedSheet = nil self.secondFactorData = nil case let .waitingForSecondFactor(option, authOptions, sessionData): self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt) @@ -218,11 +226,10 @@ class AppState: ObservableObject { } func signOut() { - let username = Current.defaults.string(forKey: "username") - Current.defaults.removeObject(forKey: "username") - if let username = username { + if let username = savedUsername { try? Current.keychain.remove(username) } + Current.defaults.removeObject(forKey: "username") AppleAPI.Current.network.session.configuration.httpCookieStorage?.removeCookies(since: .distantPast) authenticationState = .unauthenticated } @@ -327,7 +334,10 @@ class AppState: ObservableObject { receiveCompletion: { [unowned self] completion in self.installationPublishers[id] = nil if case let .failure(error) = completion { - self.error = error + // Prevent setting the app state error if it is an invalid session, we will present the sign in view instead + if error as? AuthenticationError != .invalidSession { + self.error = error + } if let index = self.allXcodes.firstIndex(where: { $0.id == id }) { self.allXcodes[index].installState = .notInstalled } diff --git a/Xcodes/Frontend/Common/XcodesSheet.swift b/Xcodes/Frontend/Common/XcodesSheet.swift new file mode 100644 index 0000000..0a39add --- /dev/null +++ b/Xcodes/Frontend/Common/XcodesSheet.swift @@ -0,0 +1,8 @@ +import Foundation + +enum XcodesSheet: Identifiable { + case signIn + case twoFactor + + var id: Int { hashValue } +} diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index d003227..1089ecf 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -38,9 +38,15 @@ struct MainWindow: View { .navigationSubtitle(subtitleText) .frame(minWidth: 600, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity) .emittingError($appState.error, recoveryHandler: { _ in }) - .sheet(isPresented: $appState.secondFactorData.isNotNil) { - secondFactorView(appState.secondFactorData!) - .environmentObject(appState) + .sheet(item: $appState.presentedSheet) { sheet in + switch sheet { + case .signIn: + signInView() + .environmentObject(appState) + case .twoFactor: + secondFactorView(appState.secondFactorData!) + .environmentObject(appState) + } } // This overlay is only here to work around the one-alert-per-view limitation .overlay( @@ -111,6 +117,24 @@ struct MainWindow: View { SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) } } + + @ViewBuilder + private func signInView() -> some View { + if appState.hasSavedUsername { + VStack { + SignedInView() + .padding(32) + HStack { + Spacer() + Button("Close") { appState.presentedSheet = nil } + .keyboardShortcut(.cancelAction) + } + } + .padding() + } else { + SignInCredentialsView() + } + } } struct MainWindow_Previews: PreviewProvider { diff --git a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift index a251bfd..8cd710b 100644 --- a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift @@ -7,27 +7,25 @@ struct GeneralPreferencePane: View { var body: some View { VStack(alignment: .leading) { GroupBox(label: Text("Apple ID")) { - // If we have saved a username then we will show it here, - // even if we don't have a valid session right now, - // because we should be able to get a valid session if needed with the password in the keychain - // and a 2FA code from the user. - // Note that AppState.authenticationState is not necessarily .authenticated in this case, though. - if let username = Current.defaults.string(forKey: "username") { - HStack(alignment:.top, spacing: 10) { - Text(username) - Button("Sign Out", action: appState.signOut) - } - .frame(maxWidth: .infinity, alignment: .leading) - } else { - Button("Sign In", action: { self.appState.presentingSignInAlert = true }) - .frame(maxWidth: .infinity, alignment: .leading) - } + // If we have saved a username then we will show it here, + // even if we don't have a valid session right now, + // because we should be able to get a valid session if needed with the password in the keychain + // and a 2FA code from the user. + // Note that AppState.authenticationState is not necessarily .authenticated in this case, though. + if appState.hasSavedUsername { + SignedInView() + } else { + Button("Sign In", action: { self.appState.presentedSheet = .signIn }) + } } .groupBoxStyle(PreferencesGroupBoxStyle()) - .sheet(isPresented: $appState.presentingSignInAlert) { - SignInCredentialsView(isPresented: $appState.presentingSignInAlert) - .environmentObject(appState) + + Divider() + + GroupBox(label: Text("Notifications")) { + NotificationsView().environmentObject(appState) } + .groupBoxStyle(PreferencesGroupBoxStyle()) } .frame(width: 400) } diff --git a/Xcodes/Frontend/SignIn/SignInCredentialsView.swift b/Xcodes/Frontend/SignIn/SignInCredentialsView.swift index accc54a..06c28ae 100644 --- a/Xcodes/Frontend/SignIn/SignInCredentialsView.swift +++ b/Xcodes/Frontend/SignIn/SignInCredentialsView.swift @@ -2,7 +2,6 @@ import SwiftUI struct SignInCredentialsView: View { @EnvironmentObject var appState: AppState - @Binding var isPresented: Bool @State private var username: String = "" @State private var password: String = "" @@ -26,7 +25,7 @@ struct SignInCredentialsView: View { HStack { Spacer() - Button("Cancel") { isPresented = false } + Button("Cancel") { appState.presentedSheet = nil } .keyboardShortcut(.cancelAction) ProgressButton(isInProgress: appState.isProcessingAuthRequest, action: { appState.signIn(username: username, password: password) }) { @@ -44,7 +43,7 @@ struct SignInCredentialsView: View { struct SignInCredentialsView_Previews: PreviewProvider { static var previews: some View { - SignInCredentialsView(isPresented: .constant(true)) + SignInCredentialsView() .environmentObject(AppState()) } } diff --git a/Xcodes/Frontend/SignIn/SignedInView.swift b/Xcodes/Frontend/SignIn/SignedInView.swift new file mode 100644 index 0000000..1cf5e5d --- /dev/null +++ b/Xcodes/Frontend/SignIn/SignedInView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct SignedInView: View { + @EnvironmentObject var appState: AppState + + private var username: String { + appState.savedUsername ?? "" + } + + var body: some View { + HStack(alignment:.top, spacing: 10) { + Text(username) + Button("Sign Out", action: appState.signOut) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct SignedInView_Previews: PreviewProvider { + static var previews: some View { + SignedInView() + .previewLayout(.sizeThatFits) + } +} From c300a75b221f65f53b7b180934975db41a24d08e Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Fri, 30 Apr 2021 14:44:14 -0600 Subject: [PATCH 2/8] add login button in main toolbar --- Xcodes/Frontend/XcodeList/MainToolbar.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Xcodes/Frontend/XcodeList/MainToolbar.swift b/Xcodes/Frontend/XcodeList/MainToolbar.swift index 392ae32..bedf3be 100644 --- a/Xcodes/Frontend/XcodeList/MainToolbar.swift +++ b/Xcodes/Frontend/XcodeList/MainToolbar.swift @@ -13,6 +13,11 @@ struct MainToolbarModifier: ViewModifier { private var toolbar: some ToolbarContent { ToolbarItemGroup(placement: .status) { + Button(action: { appState.presentedSheet = .signIn }, label: { + Label("Login", systemImage: "person.circle") + }) + .help("Login") + ProgressButton( isInProgress: appState.isUpdating, action: appState.update From cafa7baf1f28bf2263a00bf70b84910475ea01bd Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Fri, 30 Apr 2021 14:56:40 -0600 Subject: [PATCH 3/8] present two factor sheet after 2fa data is set --- Xcodes/Backend/AppState.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 22c2c97..c5641a6 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -155,12 +155,12 @@ class AppState: ObservableObject { } func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) { - self.presentedSheet = .twoFactor self.secondFactorData = SecondFactorData( option: option, authOptions: authOptions, sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) ) + self.presentedSheet = .twoFactor } func requestSMS(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) { From 357121bad56058941b1f70327a2acbb4e53d5aa6 Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Fri, 30 Apr 2021 20:15:54 -0600 Subject: [PATCH 4/8] fix branch conflicts --- Xcodes/Backend/AppState.swift | 2 +- Xcodes/Frontend/Preferences/GeneralPreferencePane.swift | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index c5641a6..a9483c9 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -112,7 +112,7 @@ class AppState: ObservableObject { validateSession() .catch { (error) -> AnyPublisher in guard - let username = savedUsername, + let username = self.savedUsername, let password = try? Current.keychain.getString(username) else { return Fail(error: error) diff --git a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift index 8cd710b..19af99a 100644 --- a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift @@ -19,13 +19,6 @@ struct GeneralPreferencePane: View { } } .groupBoxStyle(PreferencesGroupBoxStyle()) - - Divider() - - GroupBox(label: Text("Notifications")) { - NotificationsView().environmentObject(appState) - } - .groupBoxStyle(PreferencesGroupBoxStyle()) } .frame(width: 400) } From 499d33f8e01e09e9ab984162106976b8e158016e Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Sat, 1 May 2021 08:20:54 -0600 Subject: [PATCH 5/8] only show signed in view if authenticated --- Xcodes/Frontend/MainWindow.swift | 2 +- Xcodes/Frontend/Preferences/GeneralPreferencePane.swift | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index 1089ecf..4c5aa69 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -120,7 +120,7 @@ struct MainWindow: View { @ViewBuilder private func signInView() -> some View { - if appState.hasSavedUsername { + if appState.authenticationState == .authenticated { VStack { SignedInView() .padding(32) diff --git a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift index 19af99a..2a9843a 100644 --- a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift @@ -7,12 +7,7 @@ struct GeneralPreferencePane: View { var body: some View { VStack(alignment: .leading) { GroupBox(label: Text("Apple ID")) { - // If we have saved a username then we will show it here, - // even if we don't have a valid session right now, - // because we should be able to get a valid session if needed with the password in the keychain - // and a 2FA code from the user. - // Note that AppState.authenticationState is not necessarily .authenticated in this case, though. - if appState.hasSavedUsername { + if appState.authenticationState == .authenticated { SignedInView() } else { Button("Sign In", action: { self.appState.presentedSheet = .signIn }) From 228969242239fd9705ee64eb404259c5794bcfac Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Sat, 1 May 2021 09:12:36 -0600 Subject: [PATCH 6/8] show auth errors inline on sign in view --- Xcodes/Backend/AppState.swift | 1 + Xcodes/Frontend/MainWindow.swift | 1 + .../SignIn/SignInCredentialsView.swift | 29 ++++++++++++++----- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index a9483c9..6833c2e 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -127,6 +127,7 @@ class AppState: ObservableObject { } func signIn(username: String, password: String) { + authError = nil signIn(username: username, password: password) .sink( receiveCompletion: { _ in }, diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index 4c5aa69..954b221 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -133,6 +133,7 @@ struct MainWindow: View { .padding() } else { SignInCredentialsView() + .frame(width: 400) } } } diff --git a/Xcodes/Frontend/SignIn/SignInCredentialsView.swift b/Xcodes/Frontend/SignIn/SignInCredentialsView.swift index 06c28ae..593f735 100644 --- a/Xcodes/Frontend/SignIn/SignInCredentialsView.swift +++ b/Xcodes/Frontend/SignIn/SignInCredentialsView.swift @@ -14,30 +14,42 @@ struct SignInCredentialsView: View { Text("Apple ID:") .frame(minWidth: 100, alignment: .trailing) TextField("example@icloud.com", text: $username) - .frame(width: 250) } HStack { Text("Password:") .frame(minWidth: 100, alignment: .trailing) SecureField("Required", text: $password) - .frame(width: 250) + } + if appState.authError != nil { + HStack { + Text("") + .frame(minWidth: 100) + Text(appState.authError?.legibleLocalizedDescription ?? "") + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.red) + } } HStack { Spacer() - Button("Cancel") { appState.presentedSheet = nil } - .keyboardShortcut(.cancelAction) - ProgressButton(isInProgress: appState.isProcessingAuthRequest, - action: { appState.signIn(username: username, password: password) }) { - Text("Next") + Button("Cancel") { + appState.authError = nil + appState.presentedSheet = nil } + .keyboardShortcut(.cancelAction) + ProgressButton( + isInProgress: appState.isProcessingAuthRequest, + action: { appState.signIn(username: username, password: password) }, + label: { + Text("Next") + } + ) .disabled(username.isEmpty || password.isEmpty) .keyboardShortcut(.defaultAction) } .frame(height: 25) } .padding() - .emittingError($appState.authError, recoveryHandler: { _ in }) } } @@ -45,5 +57,6 @@ struct SignInCredentialsView_Previews: PreviewProvider { static var previews: some View { SignInCredentialsView() .environmentObject(AppState()) + .previewLayout(.sizeThatFits) } } From 761b2bd8cb6ef8ebe3a692cb930cae9fd9ff93ca Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Sat, 1 May 2021 09:13:26 -0600 Subject: [PATCH 7/8] handle account locked auth error --- Xcodes/AppleAPI/Sources/AppleAPI/Client.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift index f04f069..462286d 100644 --- a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift +++ b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift @@ -36,6 +36,10 @@ public class Client { case 401: return Fail(error: AuthenticationError.invalidUsernameOrPassword(username: accountName)) .eraseToAnyPublisher() + case 403: + let errorMessage = responseBody.serviceErrors?.first?.description.replacingOccurrences(of: "-20209: ", with: "") ?? "" + return Fail(error: AuthenticationError.accountLocked(errorMessage)) + .eraseToAnyPublisher() case 409: return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey) case 412 where Client.authTypes.contains(responseBody.authType ?? ""): @@ -180,6 +184,7 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { case appleIDAndPrivacyAcknowledgementRequired case accountUsesTwoStepAuthentication case accountUsesUnknownAuthenticationKind(String?) + case accountLocked(String) case badStatusCode(statusCode: Int, data: Data, response: HTTPURLResponse) public var errorDescription: String? { @@ -203,6 +208,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { return "Received a response from Apple that indicates this account has two-step authentication enabled. xcodes currently only supports the newer two-factor authentication, though. Please consider upgrading to two-factor authentication, or explain why this isn't an option for you by making a new feature request in the Help menu." case .accountUsesUnknownAuthenticationKind: return "Received a response from Apple that indicates this account has two-step or two-factor authentication enabled, but xcodes is unsure how to handle this response. If you continue to have problems, please submit a bug report in the Help menu." + case let .accountLocked(message): + 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." } From 89f978684ab278e0286d00466d948debc5ee643d Mon Sep 17 00:00:00 2001 From: Andrew Erickson Date: Sat, 1 May 2021 12:51:26 -0600 Subject: [PATCH 8/8] include updates to sparkle license --- Xcodes/Resources/Licenses.rtf | 67 +++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index 2abb775..58f4a3d 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -363,6 +363,73 @@ SOFTWARE.\ \ \ +\fs34 Sparkle\ +\ + +\fs26 Copyright (c) 2006-2013 Andy Matuschak.\ +Copyright (c) 2009-2013 Elgato Systems GmbH.\ +Copyright (c) 2011-2014 Kornel Lesi\uc0\u324 ski.\ +Copyright (c) 2015-2017 Mayur Pawashe.\ +Copyright (c) 2014 C.W. Betts.\ +Copyright (c) 2014 Petroules Corporation.\ +Copyright (c) 2014 Big Nerd Ranch.\ +All rights reserved.\ +\ +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.\ +\ +=================\ +EXTERNAL LICENSES\ +=================\ +\ +bspatch.c and bsdiff.c, from bsdiff 4.3 :\ + Copyright (c) 2003-2005 Colin Percival.\ +\ +sais.c and sais.c, from sais-lite (2010/08/07) :\ + Copyright (c) 2008-2010 Yuta Mori.\ +\ +SUDSAVerifier.m:\ + Copyright (c) 2011 Mark Hamlin.\ +\ +All rights reserved.\ +\ +Redistribution and use in source and binary forms, with or without\ +modification, are permitted providing that the following conditions\ +are met:\ +1. Redistributions of source code must retain the above copyright\ + notice, this list of conditions and the following disclaimer.\ +2. Redistributions in binary form must reproduce the above copyright\ + notice, this list of conditions and the following disclaimer in the\ + documentation and/or other materials provided with the distribution.\ +\ +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\ +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\ +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\ +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\ +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\ +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\ +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\ +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING\ +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\ +POSSIBILITY OF SUCH DAMAGE.\ +\ +\ + \fs34 LegibleError\ \