present sign in when credentials are required

This commit is contained in:
Andrew Erickson 2021-04-30 14:43:54 -06:00
parent d43aac3346
commit 287b5500fe
7 changed files with 105 additions and 34 deletions

View file

@ -7,6 +7,8 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* 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 */; }; 63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EAA4EA259944450046AB8F /* ProgressButton.swift */; };
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; }; CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; };
CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */; }; CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */; };
@ -156,6 +158,8 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference 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>"; }; 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>"; }; 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>"; }; CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateUpdateTests.swift; sourceTree = "<group>"; };
@ -307,6 +311,7 @@
CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */, CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */,
63EAA4EA259944450046AB8F /* ProgressButton.swift */, 63EAA4EA259944450046AB8F /* ProgressButton.swift */,
CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */, CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */,
536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */,
); );
path = Common; path = Common;
sourceTree = "<group>"; sourceTree = "<group>";
@ -382,6 +387,7 @@
CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */, CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */,
CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */, CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */,
CAA1CB4C255A5CFD003FD669 /* SignInPhoneListView.swift */, CAA1CB4C255A5CFD003FD669 /* SignInPhoneListView.swift */,
536CFDD1263C94DE00026CE0 /* SignedInView.swift */,
); );
path = SignIn; path = SignIn;
sourceTree = "<group>"; sourceTree = "<group>";
@ -755,6 +761,7 @@
CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */, CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */,
CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */, CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */,
CAA8589B25A2B83000ACF8C0 /* Aria2CError.swift in Sources */, CAA8589B25A2B83000ACF8C0 /* Aria2CError.swift in Sources */,
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */,
CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */, CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */,
CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */, CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */,
CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */, CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */,
@ -814,6 +821,7 @@
CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */, CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */,
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */, CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */, 63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */,
536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */,
CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */, CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */,
CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */, CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */,
CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */, CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */,

View file

@ -38,7 +38,7 @@ class AppState: ObservableObject {
} }
@Published var updatePublisher: AnyCancellable? @Published var updatePublisher: AnyCancellable?
var isUpdating: Bool { updatePublisher != nil } var isUpdating: Bool { updatePublisher != nil }
@Published var presentingSignInAlert = false @Published var presentedSheet: XcodesSheet? = nil
@Published var isProcessingAuthRequest = false @Published var isProcessingAuthRequest = false
@Published var secondFactorData: SecondFactorData? @Published var secondFactorData: SecondFactorData?
@Published var xcodeBeingConfirmedForUninstallation: Xcode? @Published var xcodeBeingConfirmedForUninstallation: Xcode?
@ -65,6 +65,14 @@ class AppState: ObservableObject {
var dataSource: DataSource { var dataSource: DataSource {
Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default 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 // MARK: - Init
@ -94,7 +102,7 @@ class AppState: ObservableObject {
.handleEvents(receiveCompletion: { completion in .handleEvents(receiveCompletion: { completion in
if case .failure = completion { if case .failure = completion {
self.authenticationState = .unauthenticated self.authenticationState = .unauthenticated
self.presentingSignInAlert = true self.presentedSheet = .signIn
} }
}) })
.eraseToAnyPublisher() .eraseToAnyPublisher()
@ -104,7 +112,7 @@ class AppState: ObservableObject {
validateSession() validateSession()
.catch { (error) -> AnyPublisher<Void, Error> in .catch { (error) -> AnyPublisher<Void, Error> in
guard guard
let username = Current.defaults.string(forKey: "username"), let username = savedUsername,
let password = try? Current.keychain.getString(username) let password = try? Current.keychain.getString(username)
else { else {
return Fail(error: error) return Fail(error: error)
@ -147,7 +155,7 @@ class AppState: ObservableObject {
} }
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) { func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
self.presentingSignInAlert = false self.presentedSheet = .twoFactor
self.secondFactorData = SecondFactorData( self.secondFactorData = SecondFactorData(
option: option, option: option,
authOptions: authOptions, authOptions: authOptions,
@ -198,7 +206,7 @@ class AppState: ObservableObject {
switch completion { switch completion {
case let .failure(error): case let .failure(error):
if case .invalidUsernameOrPassword = error as? AuthenticationError, 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. // 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) try? Current.keychain.remove(username)
Current.defaults.removeObject(forKey: "username") Current.defaults.removeObject(forKey: "username")
@ -209,7 +217,7 @@ class AppState: ObservableObject {
case .finished: case .finished:
switch self.authenticationState { switch self.authenticationState {
case .authenticated, .unauthenticated: case .authenticated, .unauthenticated:
self.presentingSignInAlert = false self.presentedSheet = nil
self.secondFactorData = nil self.secondFactorData = nil
case let .waitingForSecondFactor(option, authOptions, sessionData): case let .waitingForSecondFactor(option, authOptions, sessionData):
self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt) self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
@ -218,11 +226,10 @@ class AppState: ObservableObject {
} }
func signOut() { func signOut() {
let username = Current.defaults.string(forKey: "username") if let username = savedUsername {
Current.defaults.removeObject(forKey: "username")
if let username = username {
try? Current.keychain.remove(username) try? Current.keychain.remove(username)
} }
Current.defaults.removeObject(forKey: "username")
AppleAPI.Current.network.session.configuration.httpCookieStorage?.removeCookies(since: .distantPast) AppleAPI.Current.network.session.configuration.httpCookieStorage?.removeCookies(since: .distantPast)
authenticationState = .unauthenticated authenticationState = .unauthenticated
} }
@ -327,7 +334,10 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in receiveCompletion: { [unowned self] completion in
self.installationPublishers[id] = nil self.installationPublishers[id] = nil
if case let .failure(error) = completion { 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 }) { if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
self.allXcodes[index].installState = .notInstalled 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) .navigationSubtitle(subtitleText)
.frame(minWidth: 600, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity) .frame(minWidth: 600, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.emittingError($appState.error, recoveryHandler: { _ in }) .emittingError($appState.error, recoveryHandler: { _ in })
.sheet(isPresented: $appState.secondFactorData.isNotNil) { .sheet(item: $appState.presentedSheet) { sheet in
secondFactorView(appState.secondFactorData!) switch sheet {
.environmentObject(appState) 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 // This overlay is only here to work around the one-alert-per-view limitation
.overlay( .overlay(
@ -111,6 +117,24 @@ struct MainWindow: View {
SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) 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 { struct MainWindow_Previews: PreviewProvider {

View file

@ -7,27 +7,25 @@ struct GeneralPreferencePane: View {
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
GroupBox(label: Text("Apple ID")) { GroupBox(label: Text("Apple ID")) {
// If we have saved a username then we will show it here, // If we have saved a username then we will show it here,
// even if we don't have a valid session right now, // 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 // 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. // and a 2FA code from the user.
// Note that AppState.authenticationState is not necessarily .authenticated in this case, though. // Note that AppState.authenticationState is not necessarily .authenticated in this case, though.
if let username = Current.defaults.string(forKey: "username") { if appState.hasSavedUsername {
HStack(alignment:.top, spacing: 10) { SignedInView()
Text(username) } else {
Button("Sign Out", action: appState.signOut) Button("Sign In", action: { self.appState.presentedSheet = .signIn })
} }
.frame(maxWidth: .infinity, alignment: .leading)
} else {
Button("Sign In", action: { self.appState.presentingSignInAlert = true })
.frame(maxWidth: .infinity, alignment: .leading)
}
} }
.groupBoxStyle(PreferencesGroupBoxStyle()) .groupBoxStyle(PreferencesGroupBoxStyle())
.sheet(isPresented: $appState.presentingSignInAlert) {
SignInCredentialsView(isPresented: $appState.presentingSignInAlert) Divider()
.environmentObject(appState)
GroupBox(label: Text("Notifications")) {
NotificationsView().environmentObject(appState)
} }
.groupBoxStyle(PreferencesGroupBoxStyle())
} }
.frame(width: 400) .frame(width: 400)
} }

View file

@ -2,7 +2,6 @@ import SwiftUI
struct SignInCredentialsView: View { struct SignInCredentialsView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Binding var isPresented: Bool
@State private var username: String = "" @State private var username: String = ""
@State private var password: String = "" @State private var password: String = ""
@ -26,7 +25,7 @@ struct SignInCredentialsView: View {
HStack { HStack {
Spacer() Spacer()
Button("Cancel") { isPresented = false } Button("Cancel") { appState.presentedSheet = nil }
.keyboardShortcut(.cancelAction) .keyboardShortcut(.cancelAction)
ProgressButton(isInProgress: appState.isProcessingAuthRequest, ProgressButton(isInProgress: appState.isProcessingAuthRequest,
action: { appState.signIn(username: username, password: password) }) { action: { appState.signIn(username: username, password: password) }) {
@ -44,7 +43,7 @@ struct SignInCredentialsView: View {
struct SignInCredentialsView_Previews: PreviewProvider { struct SignInCredentialsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SignInCredentialsView(isPresented: .constant(true)) SignInCredentialsView()
.environmentObject(AppState()) .environmentObject(AppState())
} }
} }

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