Merge branch 'main' into Notifications

This commit is contained in:
Matt Kiazyk 2021-05-02 09:38:29 -05:00
commit ecfb49a216
No known key found for this signature in database
GPG key ID: 33D9938D5D45EFE2
11 changed files with 195 additions and 131 deletions

View file

@ -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 */; };
@ -158,6 +160,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = "<group>"; };
536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesSheet.swift; sourceTree = "<group>"; };
63EAA4EA259944450046AB8F /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = "<group>"; };
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = "<group>"; };
CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateUpdateTests.swift; sourceTree = "<group>"; };
@ -311,6 +315,7 @@
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
63EAA4EA259944450046AB8F /* ProgressButton.swift */,
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */,
);
path = Common;
sourceTree = "<group>";
@ -386,6 +391,7 @@
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
CAA1CB4C255A5CFD003FD669 /* SignInPhoneListView.swift */,
536CFDD1263C94DE00026CE0 /* SignedInView.swift */,
);
path = SignIn;
sourceTree = "<group>";
@ -761,6 +767,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 */,
@ -821,6 +828,7 @@
CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */,
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */,
536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */,
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */,
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */,
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */,

View file

@ -1,88 +0,0 @@
{
"object": {
"pins": [
{
"package": "CombineExpectations",
"repositoryURL": "https://github.com/groue/CombineExpectations",
"state": {
"branch": null,
"revision": "989a92221899929ab8347a5878aa2b16db8b81ca",
"version": "0.6.0"
}
},
{
"package": "XcodeReleases",
"repositoryURL": "https://github.com/xcodereleases/data",
"state": {
"branch": null,
"revision": "b47228c688b608e34b3b84079ab6052a24c7a981",
"version": null
}
},
{
"package": "ErrorHandling",
"repositoryURL": "https://github.com/RobotsAndPencils/ErrorHandling",
"state": {
"branch": null,
"revision": "7be837fcb515447c0776805c3288fb7d5181ec68",
"version": "0.1.0"
}
},
{
"package": "KeychainAccess",
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess",
"state": {
"branch": null,
"revision": "8d33ffd6f74b3bcfc99af759d4204c6395a3f918",
"version": "3.2.1"
}
},
{
"package": "LegibleError",
"repositoryURL": "https://github.com/mxcl/LegibleError",
"state": {
"branch": null,
"revision": "909e9bab3ded97350b28a5ab41dd745dd8aa9710",
"version": "1.0.4"
}
},
{
"package": "Path.swift",
"repositoryURL": "https://github.com/mxcl/Path.swift",
"state": {
"branch": null,
"revision": "dac007e907a4f4c565cfdc55a9ce148a761a11d5",
"version": "0.16.3"
}
},
{
"package": "Sparkle",
"repositoryURL": "https://github.com/sparkle-project/Sparkle/",
"state": {
"branch": null,
"revision": "891afd44c7075e699924ed9b81d8dc94a5111dfd",
"version": "1.24.0-spm"
}
},
{
"package": "SwiftSoup",
"repositoryURL": "https://github.com/scinfu/SwiftSoup",
"state": {
"branch": null,
"revision": "aeb5b4249c273d1783a5299e05be1b26e061ea81",
"version": "2.0.0"
}
},
{
"package": "Version",
"repositoryURL": "https://github.com/mxcl/Version",
"state": {
"branch": null,
"revision": "087c91fedc110f9f833b14ef4c32745dabca8913",
"version": "1.0.3"
}
}
]
},
"version": 1
}

View file

@ -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."
}

View file

@ -41,7 +41,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?
@ -68,6 +68,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
@ -97,7 +105,7 @@ class AppState: ObservableObject {
.handleEvents(receiveCompletion: { completion in
if case .failure = completion {
self.authenticationState = .unauthenticated
self.presentingSignInAlert = true
self.presentedSheet = .signIn
}
})
.eraseToAnyPublisher()
@ -107,7 +115,7 @@ class AppState: ObservableObject {
validateSession()
.catch { (error) -> AnyPublisher<Void, Error> in
guard
let username = Current.defaults.string(forKey: "username"),
let username = self.savedUsername,
let password = try? Current.keychain.getString(username)
else {
return Fail(error: error)
@ -122,6 +130,7 @@ class AppState: ObservableObject {
}
func signIn(username: String, password: String) {
authError = nil
signIn(username: username, password: password)
.sink(
receiveCompletion: { _ in },
@ -150,12 +159,12 @@ class AppState: ObservableObject {
}
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
self.presentingSignInAlert = false
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) {
@ -201,7 +210,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")
@ -212,7 +221,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)
@ -221,11 +230,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
}
@ -331,7 +339,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
}

View file

@ -0,0 +1,8 @@
import Foundation
enum XcodesSheet: Identifiable {
case signIn
case twoFactor
var id: Int { hashValue }
}

View file

@ -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,25 @@ struct MainWindow: View {
SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
}
}
@ViewBuilder
private func signInView() -> some View {
if appState.authenticationState == .authenticated {
VStack {
SignedInView()
.padding(32)
HStack {
Spacer()
Button("Close") { appState.presentedSheet = nil }
.keyboardShortcut(.cancelAction)
}
}
.padding()
} else {
SignInCredentialsView()
.frame(width: 400)
}
}
}
struct MainWindow_Previews: PreviewProvider {

View file

@ -7,28 +7,13 @@ 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 appState.authenticationState == .authenticated {
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")) {

View file

@ -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 = ""
@ -15,36 +14,49 @@ 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") { isPresented = false }
.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 })
}
}
struct SignInCredentialsView_Previews: PreviewProvider {
static var previews: some View {
SignInCredentialsView(isPresented: .constant(true))
SignInCredentialsView()
.environmentObject(AppState())
.previewLayout(.sizeThatFits)
}
}

View file

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

View file

@ -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

View file

@ -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 <http://www.daemonology.net/bsdiff/>:\
Copyright (c) 2003-2005 Colin Percival.\
\
sais.c and sais.c, from sais-lite (2010/08/07) <https://sites.google.com/site/yuta256/sais>:\
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\
\