mirror of
https://github.com/XcodesOrg/XcodesApp.git
synced 2026-03-25 08:55:46 +00:00
Store credentials in the keychain
Like xcodes, storing the username in defaults so we know which item to look up in the keychain later. This also fixes the Xcode list update logic to not only validate the session but login with saved credentials if it fails.
This commit is contained in:
parent
867ad8ab4b
commit
5149e817da
4 changed files with 120 additions and 58 deletions
|
|
@ -4,6 +4,7 @@ import Combine
|
|||
import Path
|
||||
import PromiseKit
|
||||
import LegibleError
|
||||
import KeychainAccess
|
||||
|
||||
class AppState: ObservableObject {
|
||||
private let list = XcodeList()
|
||||
|
|
@ -20,6 +21,7 @@ class AppState: ObservableObject {
|
|||
|
||||
func validateSession() -> AnyPublisher<Void, Error> {
|
||||
return client.validateSession()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(receiveCompletion: { completion in
|
||||
if case .failure = completion {
|
||||
self.authenticationState = .unauthenticated
|
||||
|
|
@ -29,20 +31,50 @@ class AppState: ObservableObject {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func login(username: String, password: String) {
|
||||
client.login(accountName: username, password: password)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(
|
||||
receiveCompletion: { completion in
|
||||
self.handleAuthenticationFlowCompletion(completion)
|
||||
},
|
||||
receiveValue: { authenticationState in
|
||||
self.authenticationState = authenticationState
|
||||
func loginIfNeeded() -> AnyPublisher<Void, Error> {
|
||||
validateSession()
|
||||
.catch { (error) -> AnyPublisher<Void, Error> in
|
||||
guard
|
||||
let username = Current.defaults.string(forKey: "username"),
|
||||
let password = try? Current.keychain.getString(username)
|
||||
else {
|
||||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return self.login(username: username, password: password)
|
||||
.map { _ in Void() }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func login(username: String, password: String) {
|
||||
login(username: username, password: password)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func login(username: String, password: String) -> AnyPublisher<AuthenticationState, Error> {
|
||||
try? Current.keychain.set(password, key: username)
|
||||
Current.defaults.set(username, forKey: "username")
|
||||
|
||||
return client.login(accountName: username, password: password)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(
|
||||
receiveOutput: { authenticationState in
|
||||
self.authenticationState = authenticationState
|
||||
},
|
||||
receiveCompletion: { completion in
|
||||
self.handleAuthenticationFlowCompletion(completion)
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
|
||||
self.presentingSignInAlert = false
|
||||
self.secondFactorData = SecondFactorData(
|
||||
|
|
@ -89,6 +121,12 @@ class AppState: ObservableObject {
|
|||
private func handleAuthenticationFlowCompletion(_ completion: Subscribers.Completion<Error>) {
|
||||
switch completion {
|
||||
case let .failure(error):
|
||||
if case .invalidUsernameOrPassword = error as? AuthenticationError,
|
||||
let username = Current.defaults.string(forKey: "username") {
|
||||
// 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)
|
||||
}
|
||||
|
||||
self.error = AlertContent(title: "Error signing in", message: error.legibleLocalizedDescription)
|
||||
case .finished:
|
||||
switch self.authenticationState {
|
||||
|
|
@ -101,30 +139,55 @@ class AppState: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func logOut() {
|
||||
let username = Current.defaults.string(forKey: "username")
|
||||
Current.defaults.removeObject(forKey: "username")
|
||||
if let username = username {
|
||||
try? Current.keychain.remove(username)
|
||||
}
|
||||
AppleAPI.Current.network.session.configuration.httpCookieStorage?.removeCookies(since: .distantPast)
|
||||
authenticationState = .unauthenticated
|
||||
}
|
||||
|
||||
// MARK: - Load Xcode Versions
|
||||
|
||||
func update() {
|
||||
// Treat this implementation as a placeholder that can be thrown away.
|
||||
// It's only here to make it easy to see that auth works.
|
||||
update()
|
||||
.sink(receiveCompletion: { _ in },
|
||||
receiveValue: { _ in })
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { _ in }
|
||||
)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
public func update() -> AnyPublisher<[Xcode], Error> {
|
||||
// Wrap the Promise API in a Publisher for now
|
||||
return Deferred {
|
||||
Future { promise in
|
||||
self.list.update()
|
||||
.done { promise(.success($0)) }
|
||||
.catch { promise(.failure($0)) }
|
||||
public func update() -> AnyPublisher<[Xcode], Never> {
|
||||
loginIfNeeded()
|
||||
.flatMap {
|
||||
// Wrap the Promise API in a Publisher for now
|
||||
Deferred {
|
||||
Future { promise in
|
||||
self.list.update()
|
||||
.done { promise(.success($0)) }
|
||||
.catch { promise(.failure($0)) }
|
||||
}
|
||||
}
|
||||
.handleEvents(
|
||||
receiveCompletion: { completion in
|
||||
if case let .failure(error) = completion {
|
||||
self.error = AlertContent(title: "Update Error", message: error.legibleLocalizedDescription)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
.handleEvents(receiveOutput: { [unowned self] xcodes in
|
||||
self.updateAllVersions(xcodes)
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
.catch { _ in
|
||||
Just(self.list.availableXcodes)
|
||||
}
|
||||
.handleEvents(
|
||||
receiveOutput: { [unowned self] xcodes in
|
||||
self.updateAllVersions(xcodes)
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
private func updateAllVersions(_ xcodes: [Xcode]) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public struct Environment {
|
|||
public var network = Network()
|
||||
public var logging = Logging()
|
||||
public var keychain = Keychain()
|
||||
public var defaults = Defaults()
|
||||
}
|
||||
|
||||
public var Current = Environment()
|
||||
|
|
@ -152,3 +153,20 @@ public struct Keychain {
|
|||
try remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Defaults {
|
||||
public var string: (String) -> String? = { UserDefaults.standard.string(forKey: $0) }
|
||||
public func string(forKey key: String) -> String? {
|
||||
string(key)
|
||||
}
|
||||
|
||||
public var set: (Any?, String) -> Void = { UserDefaults.standard.set($0, forKey: $1) }
|
||||
public func set(_ value: Any?, forKey key: String) {
|
||||
set(value, key)
|
||||
}
|
||||
|
||||
public var removeObject: (String) -> Void = { UserDefaults.standard.removeObject(forKey: $0) }
|
||||
public func removeObject(forKey key: String) {
|
||||
removeObject(key)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,12 +95,15 @@ struct XcodeListView: View {
|
|||
message: Text(verbatim: error.message),
|
||||
dismissButton: .default(Text("OK")))
|
||||
}
|
||||
.alert(item: self.$rowBeingConfirmedForUninstallation) { row in
|
||||
Alert(title: Text("Uninstall Xcode \(row.title)?"),
|
||||
message: Text("It will be moved to the Trash, but won't be emptied."),
|
||||
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: row.id) }),
|
||||
secondaryButton: .cancel(Text("Cancel")))
|
||||
}
|
||||
/*
|
||||
Removing this for now, because it's overriding the error alert that's being worked on above.
|
||||
.alert(item: self.$rowBeingConfirmedForUninstallation) { row in
|
||||
Alert(title: Text("Uninstall Xcode \(row.title)?"),
|
||||
message: Text("It will be moved to the Trash, but won't be emptied."),
|
||||
primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: row.id) }),
|
||||
secondaryButton: .cancel(Text("Cancel")))
|
||||
}
|
||||
**/
|
||||
.sheet(isPresented: $appState.secondFactorData.isNotNil) {
|
||||
secondFactorView(appState.secondFactorData!)
|
||||
.environmentObject(appState)
|
||||
|
|
|
|||
|
|
@ -8,21 +8,15 @@ struct SettingsView: View {
|
|||
VStack(alignment: .leading) {
|
||||
GroupBox(label: Text("Apple ID")) {
|
||||
VStack(alignment: .leading) {
|
||||
switch appState.authenticationState {
|
||||
case .authenticated:
|
||||
Text("Signed in")
|
||||
Button("Sign Out", action: {})
|
||||
|
||||
case .unauthenticated:
|
||||
if let username = Current.defaults.string(forKey: "username") {
|
||||
Text(username)
|
||||
Button("Sign Out", action: appState.logOut)
|
||||
} else {
|
||||
Button("Sign In", action: { self.appState.presentingSignInAlert = true })
|
||||
.sheet(isPresented: $appState.presentingSignInAlert) {
|
||||
SignInCredentialsView(isPresented: $appState.presentingSignInAlert)
|
||||
.environmentObject(appState)
|
||||
}
|
||||
|
||||
case .waitingForSecondFactor:
|
||||
Button("Signing In...", action: {})
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
|
@ -40,23 +34,7 @@ struct SettingsView_Previews: PreviewProvider {
|
|||
static var previews: some View {
|
||||
Group {
|
||||
SettingsView()
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.authenticationState = .authenticated
|
||||
})
|
||||
|
||||
SettingsView()
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.authenticationState = .unauthenticated
|
||||
})
|
||||
|
||||
SettingsView()
|
||||
.environmentObject(configure(AppState()) {
|
||||
$0.authenticationState = .waitingForSecondFactor(
|
||||
TwoFactorOption.codeSent,
|
||||
AuthOptionsResponse(trustedPhoneNumbers: nil, trustedDevices: nil, securityCode: .init(length: 6)),
|
||||
AppleSessionData(serviceKey: "", sessionID: "", scnt: "")
|
||||
)
|
||||
})
|
||||
.environmentObject(AppState())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue