Edit account

Edit account now has its own model
Refactored account creation and updated errors into separate enum
Renamed some structs
This commit is contained in:
Stuart Breckenridge
2020-07-14 16:25:37 +08:00
parent 06af59fb2b
commit aca43090f8
13 changed files with 455 additions and 128 deletions

View File

@@ -16,9 +16,10 @@ class AccountsPreferencesModel: ObservableObject {
case add, credentials, none
}
// Selected Account
public private(set) var account: Account?
// Configured Accounts
// All Accounts
@Published var sortedAccounts: [Account] = []
@Published var selectedConfiguredAccountID: String? = AccountManager.shared.defaultAccount.accountID {
didSet {
@@ -62,8 +63,6 @@ class AccountsPreferencesModel: ObservableObject {
}
@Published var showDeleteConfirmation: Bool = false
// Subscriptions
var notificationSubscriptions = Set<AnyCancellable>()

View File

@@ -20,7 +20,7 @@ struct AccountsPreferencesView: View {
HStack(alignment: .top, spacing: 10) {
listOfAccounts
EditAccountView(viewModel: viewModel)
AccountDetailView(viewModel: viewModel)
.frame(height: 300, alignment: .leading)
}
Spacer()
@@ -32,7 +32,7 @@ struct AccountsPreferencesView: View {
case .add:
AddAccountView(preferencesModel: viewModel)
case .credentials:
EditAccountCredentials(viewModel: viewModel)
EditAccountCredentialsView(viewModel: viewModel)
case .none:
EmptyView()
}
@@ -48,7 +48,6 @@ struct AccountsPreferencesView: View {
viewModel.showDeleteConfirmation = false
}))
})
}
var listOfAccounts: some View {

View File

@@ -13,35 +13,7 @@ import Secrets
class AddAccountModel: ObservableObject {
enum AddAccountErrors: CustomStringConvertible {
case invalidUsernamePassword, invalidUsernamePasswordAPI, networkError, keyChainError, other(error: Error) , none
var description: String {
switch self {
case .invalidUsernamePassword:
return NSLocalizedString("Invalid email or password combination.", comment: "Invalid email/password combination.")
case .invalidUsernamePasswordAPI:
return NSLocalizedString("Invalid email, password, or API URL combination.", comment: "Invalid email/password/API combination.")
case .networkError:
return NSLocalizedString("Network Error. Please try later.", comment: "Network Error. Please try later.")
case .keyChainError:
return NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
case .other(let error):
return NSLocalizedString(error.localizedDescription, comment: "Other add account error")
default:
return NSLocalizedString("N/A", comment: "N/A")
}
}
static func ==(lhs: AddAccountErrors, rhs: AddAccountErrors) -> Bool {
switch (lhs, rhs) {
case (.other(let lhsError), .other(let rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
default:
return false
}
}
}
#if DEBUG
let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .feedWrangler, .freshRSS, .cloudKit, .newsBlur]
@@ -56,7 +28,7 @@ class AddAccountModel: ObservableObject {
@Published var apiUrl: String = ""
@Published var newLocalAccountName: String = ""
@Published var accountIsAuthenticating: Bool = false
@Published var addAccountError: AddAccountErrors = .none {
@Published var addAccountError: AccountUpdateErrors = .none {
didSet {
if addAccountError == .none {
showError = false
@@ -261,7 +233,7 @@ extension AddAccountModel {
return
}
let account = AccountManager.shared.createAccount(type: .newsBlur)
let account = AccountManager.shared.createAccount(type: .freshRSS)
do {
try account.removeCredentials(type: .readerBasic)

View File

@@ -50,7 +50,7 @@ struct AddAccountView: View {
Spacer()
HStack {
if viewModel.accountIsAuthenticating {
ProgressView()
ProgressView("Adding Account")
}
Spacer()
Button(action: { presentationMode.wrappedValue.dismiss() }, label: {
@@ -121,12 +121,9 @@ struct AddAccountView: View {
var userNamePasswordAndAPIUrlView: some View {
Group {
TextField("Email", text: $viewModel.userName)
.textFieldStyle(RoundedBorderTextFieldStyle())
SecureField("Password", text: $viewModel.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("API URL", text: $viewModel.apiUrl)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}.textFieldStyle(RoundedBorderTextFieldStyle())
}

View File

@@ -1,5 +1,5 @@
//
// EditAccountView.swift
// AccountDetailView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 14/7/20.
@@ -10,7 +10,7 @@ import SwiftUI
import Account
import Combine
struct EditAccountView: View {
struct AccountDetailView: View {
@ObservedObject var viewModel: AccountsPreferencesModel
@@ -76,8 +76,8 @@ struct EditAccountView: View {
}
struct EditAccountView_Previews: PreviewProvider {
struct AccountDetailView_Previews: PreviewProvider {
static var previews: some View {
EditAccountView(viewModel: AccountsPreferencesModel())
AccountDetailView(viewModel: AccountsPreferencesModel())
}
}

View File

@@ -0,0 +1,262 @@
//
// EditAccountCredentialsModel.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 14/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Account
import Secrets
class EditAccountCredentialsModel: ObservableObject {
@Published var userName: String = ""
@Published var password: String = ""
@Published var apiUrl: String = ""
@Published var accountIsUpdatingCredentials: Bool = false
@Published var accountCredentialsWereUpdated: Bool = false
@Published var error: AccountUpdateErrors = .none {
didSet {
if error == .none {
showError = false
} else {
showError = true
}
}
}
@Published var showError: Bool = false
func updateAccountCredentials(_ account: Account) {
switch account.type {
case .onMyMac:
return
case .feedbin:
updateFeedbin(account)
case .cloudKit:
return
case .feedWrangler:
updateFeedWrangler(account)
case .feedly:
updateFeedly(account)
case .freshRSS:
updateFreshRSS(account)
case .newsBlur:
updateNewsblur(account)
}
}
func retrieveCredentials(_ account: Account) {
switch account.type {
case .feedbin:
let credentials = try? account.retrieveCredentials(type: .basic)
userName = credentials?.username ?? ""
password = credentials?.secret ?? ""
case .feedWrangler:
let credentials = try? account.retrieveCredentials(type: .feedWranglerBasic)
userName = credentials?.username ?? ""
password = credentials?.secret ?? ""
case .freshRSS:
let credentials = try? account.retrieveCredentials(type: .readerBasic)
userName = credentials?.username ?? ""
password = credentials?.secret ?? ""
case .newsBlur:
let credentials = try? account.retrieveCredentials(type: .newsBlurBasic)
userName = credentials?.username ?? ""
password = credentials?.secret ?? ""
default:
return
}
}
}
// MARK:- Update API
extension EditAccountCredentialsModel {
func updateFeedbin(_ account: Account) {
accountIsUpdatingCredentials = true
let credentials = Credentials(type: .basic, username: userName, secret: password)
Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in
guard let self = self else { return }
self.accountIsUpdatingCredentials = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.error = .invalidUsernamePassword
return
}
do {
try account.removeCredentials(type: .basic)
try account.storeCredentials(validatedCredentials)
self.accountCredentialsWereUpdated = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.error = .other(error: error)
}
})
} catch {
self.error = .keyChainError
}
case .failure:
self.error = .networkError
}
}
}
func updateFeedWrangler(_ account: Account) {
accountIsUpdatingCredentials = true
let credentials = Credentials(type: .feedWranglerBasic, username: userName, secret: password)
Account.validateCredentials(type: .feedWrangler, credentials: credentials) { [weak self] result in
guard let self = self else { return }
self.accountIsUpdatingCredentials = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.error = .invalidUsernamePassword
return
}
do {
try account.removeCredentials(type: .feedWranglerBasic)
try account.removeCredentials(type: .feedWranglerToken)
try account.storeCredentials(credentials)
try account.storeCredentials(validatedCredentials)
self.accountCredentialsWereUpdated = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.error = .other(error: error)
}
})
} catch {
self.error = .keyChainError
}
case .failure:
self.error = .networkError
}
}
}
func updateFeedly(_ account: Account) {
}
func updateFreshRSS(_ account: Account) {
accountIsUpdatingCredentials = true
let credentials = Credentials(type: .readerBasic, username: userName, secret: password)
Account.validateCredentials(type: .freshRSS, credentials: credentials) { [weak self] result in
guard let self = self else { return }
self.accountIsUpdatingCredentials = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.error = .invalidUsernamePassword
return
}
do {
try account.removeCredentials(type: .readerBasic)
try account.removeCredentials(type: .readerAPIKey)
try account.storeCredentials(credentials)
try account.storeCredentials(validatedCredentials)
self.accountCredentialsWereUpdated = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.error = .other(error: error)
}
})
} catch {
self.error = .keyChainError
}
case .failure:
self.error = .networkError
}
}
}
func updateNewsblur(_ account: Account) {
accountIsUpdatingCredentials = true
let credentials = Credentials(type: .newsBlurBasic, username: userName, secret: password)
Account.validateCredentials(type: .newsBlur, credentials: credentials) { [weak self] result in
guard let self = self else { return }
self.accountIsUpdatingCredentials = false
switch result {
case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else {
self.error = .invalidUsernamePassword
return
}
do {
try account.removeCredentials(type: .newsBlurBasic)
try account.removeCredentials(type: .newsBlurSessionId)
try account.storeCredentials(credentials)
try account.storeCredentials(validatedCredentials)
self.accountCredentialsWereUpdated = true
account.refreshAll(completion: { result in
switch result {
case .success:
break
case .failure(let error):
self.error = .other(error: error)
}
})
} catch {
self.error = .keyChainError
}
case .failure:
self.error = .networkError
}
}
}
}
// MARK:- Retrieve Credentials
extension EditAccountCredentialsModel {
}

View File

@@ -0,0 +1,94 @@
//
// EditAccountCredentialsView.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 14/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Secrets
struct EditAccountCredentialsView: View {
@Environment(\.presentationMode) var presentationMode
@StateObject private var editModel = EditAccountCredentialsModel()
@ObservedObject var viewModel: AccountsPreferencesModel
var body: some View {
Form {
HStack {
Spacer()
Image(rsImage: viewModel.account!.smallIcon!.image)
.resizable()
.frame(width: 30, height: 30)
Text(viewModel.account?.nameForDisplay ?? "")
Spacer()
}.padding()
HStack(alignment: .center) {
VStack(alignment: .trailing, spacing: 12) {
Text("Username: ")
Text("Password: ")
if viewModel.account?.type == .freshRSS {
Text("API URL: ")
}
}.frame(width: 75)
VStack(alignment: .leading, spacing: 12) {
TextField("Username", text: $editModel.userName)
SecureField("Password", text: $editModel.password)
if viewModel.account?.type == .freshRSS {
TextField("API URL", text: $editModel.apiUrl)
}
}
}.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
HStack{
if editModel.accountIsUpdatingCredentials {
ProgressView("Updating")
}
Spacer()
Button("Cancel", action: {
presentationMode.wrappedValue.dismiss()
})
if viewModel.account?.type != .freshRSS {
Button("Update", action: {
editModel.updateAccountCredentials(viewModel.account!)
}).disabled(editModel.userName.count == 0 || editModel.password.count == 0)
} else {
Button("Update", action: {
editModel.updateAccountCredentials(viewModel.account!)
}).disabled(editModel.userName.count == 0 || editModel.password.count == 0 || editModel.apiUrl.count == 0)
}
}
}.onAppear {
editModel.retrieveCredentials(viewModel.account!)
}
.onChange(of: editModel.accountCredentialsWereUpdated) { value in
if value == true {
viewModel.sheetToShow = .none
}
}
.alert(isPresented: $editModel.showError) {
Alert(title: Text("Error Adding Account"),
message: Text(editModel.error.description),
dismissButton: .default(Text("Dismiss"),
action: {
editModel.error = .none
}))
}
.frame(idealWidth: 300, idealHeight: 200, alignment: .top)
.padding()
}
}
struct EditAccountCredentials_Previews: PreviewProvider {
static var previews: some View {
EditAccountCredentialsView(viewModel: AccountsPreferencesModel())
}
}

View File

@@ -0,0 +1,18 @@
//
// AccountManagement.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 14/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
protocol AccountManagement {
}

View File

@@ -0,0 +1,39 @@
//
// AccountUpdateErrors.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 14/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
enum AccountUpdateErrors: CustomStringConvertible {
case invalidUsernamePassword, invalidUsernamePasswordAPI, networkError, keyChainError, other(error: Error) , none
var description: String {
switch self {
case .invalidUsernamePassword:
return NSLocalizedString("Invalid email or password combination.", comment: "Invalid email/password combination.")
case .invalidUsernamePasswordAPI:
return NSLocalizedString("Invalid email, password, or API URL combination.", comment: "Invalid email/password/API combination.")
case .networkError:
return NSLocalizedString("Network Error. Please try later.", comment: "Network Error. Please try later.")
case .keyChainError:
return NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
case .other(let error):
return NSLocalizedString(error.localizedDescription, comment: "Other add account error")
default:
return NSLocalizedString("N/A", comment: "N/A")
}
}
static func ==(lhs: AccountUpdateErrors, rhs: AccountUpdateErrors) -> Bool {
switch (lhs, rhs) {
case (.other(let lhsError), .other(let rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
default:
return false
}
}
}

View File

@@ -1,69 +0,0 @@
//
// EditAccountCredentials.swift
// Multiplatform macOS
//
// Created by Stuart Breckenridge on 14/7/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
import Secrets
struct EditAccountCredentials: View {
@ObservedObject var viewModel: AccountsPreferencesModel
@Environment(\.presentationMode) var presentationMode
@State private var userName: String = ""
@State private var password: String = ""
@State private var apiUrl: String?
var body: some View {
Form {
HStack {
Spacer()
Image(rsImage: viewModel.account!.smallIcon!.image)
.resizable()
.frame(width: 30, height: 30)
Text(viewModel.account?.nameForDisplay ?? "")
Spacer()
}.padding()
HStack(alignment: .center) {
VStack(alignment: .trailing, spacing: 12) {
Text("Username: ")
Text("Password: ")
}.frame(width: 75)
VStack(alignment: .leading, spacing: 12) {
TextField("Username", text: $userName)
SecureField("Password", text: $password)
}
}.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
HStack{
Spacer()
Button("Dismiss", action: {
presentationMode.wrappedValue.dismiss()
})
Button("Update", action: {
presentationMode.wrappedValue.dismiss()
})
}
}.onAppear {
let credentials = try? viewModel.account?.retrieveCredentials(type: .basic)
userName = credentials?.username ?? ""
password = credentials?.secret ?? ""
}
.frame(idealWidth: 300, idealHeight: 200, alignment: .top)
.padding()
}
}
struct EditAccountCredentials_Previews: PreviewProvider {
static var previews: some View {
EditAccountCredentials(viewModel: AccountsPreferencesModel())
}
}