diff --git a/iOS/Account/Account.strings b/iOS/Account/Account.strings index 7d9b15040..385293f21 100644 --- a/iOS/Account/Account.strings +++ b/iOS/Account/Account.strings @@ -12,7 +12,7 @@ "LOCAL_ACCOUNT_NAME_PHONE" = "On My iPhone"; "LOCAL_ACCOUNT_NAME_PAD" = "On My iPad"; "ACCOUNT_EMAIL_ADDRESS_PROMPT" = "Email Address"; -"ACCOUNT_USERNAME_PROMPT" = "Username"; +"ACCOUNT_USERNAME_OR_EMAIL_PROMPT" = "Username or Email"; "ACCOUNT_PASSWORD_PROMPT" = "Password"; diff --git a/iOS/Account/Views/CloudKitAddAccountView.swift b/iOS/Account/Views/CloudKitAddAccountView.swift index 95bae4208..5b4873286 100644 --- a/iOS/Account/Views/CloudKitAddAccountView.swift +++ b/iOS/Account/Views/CloudKitAddAccountView.swift @@ -12,7 +12,7 @@ import Account struct CloudKitAddAccountView: View { @Environment(\.dismiss) private var dismiss - @State private var addAccountError: (LocalizedError?, Bool) = (nil, false) + @State private var accountError: (Error?, Bool) = (nil, false) var body: some View { NavigationView { @@ -28,10 +28,10 @@ struct CloudKitAddAccountView: View { Button(action: { dismiss() }, label: { Text("CANCEL_BUTTON_TITLE", tableName: "Buttons") }) } } - .alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $addAccountError.1) { + .alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $accountError.1) { Button(action: {}, label: { Text("DISMISS_BUTTON_TITLE", tableName: "Buttons") }) } message: { - Text(addAccountError.0?.localizedDescription ?? "Unknown Error") + Text(accountError.0?.localizedDescription ?? "Unknown Error") } .dismissOnExternalContextLaunch() .dismissOnAccountAdd() @@ -41,7 +41,7 @@ struct CloudKitAddAccountView: View { var createCloudKitAccount: some View { Button { guard FileManager.default.ubiquityIdentityToken != nil else { - addAccountError = (LocalizedNetNewsWireError.iCloudDriveMissing, true) + accountError = (LocalizedNetNewsWireError.iCloudDriveMissing, true) return } let _ = AccountManager.shared.createAccount(type: .cloudKit) diff --git a/iOS/Account/Views/FeedbinAddAccountView.swift b/iOS/Account/Views/FeedbinAddAccountView.swift index bf79dfc0b..e6fa61f40 100644 --- a/iOS/Account/Views/FeedbinAddAccountView.swift +++ b/iOS/Account/Views/FeedbinAddAccountView.swift @@ -54,6 +54,7 @@ struct FeedbinAddAccountView: View { } .navigationTitle(Text(account?.type.localizedAccountName() ?? "")) .navigationBarTitleDisplayMode(.inline) + .interactiveDismissDisabled(showProgressIndicator) .dismissOnExternalContextLaunch() .dismissOnAccountAdd() } @@ -73,8 +74,14 @@ struct FeedbinAddAccountView: View { Button { Task { do { - try await executeAccountCredentials() - dismiss() + if account == nil { + // Create a new account + try await executeAccountCredentials() + } else { + // Updating account credentials + try await executeAccountCredentials() + dismiss() + } } catch { accountError = (error, true) } diff --git a/iOS/Account/Views/NewsBlurAddAccountView.swift b/iOS/Account/Views/NewsBlurAddAccountView.swift index 3a8612c23..b1a208125 100644 --- a/iOS/Account/Views/NewsBlurAddAccountView.swift +++ b/iOS/Account/Views/NewsBlurAddAccountView.swift @@ -12,7 +12,7 @@ import Secrets import RSWeb import RSCore -struct NewsBlurAddAccountView: View { +struct NewsBlurAddAccountView: View, Logging { @Environment(\.dismiss) private var dismiss @State var account: Account? = nil @@ -52,6 +52,7 @@ struct NewsBlurAddAccountView: View { } message: { Text(accountError.0?.localizedDescription ?? "") } + .interactiveDismissDisabled(showProgressIndicator) .dismissOnExternalContextLaunch() .dismissOnAccountAdd() } @@ -78,7 +79,7 @@ struct NewsBlurAddAccountView: View { var accountDetails: some View { Section { - TextField("Email", text: $accountUserName, prompt: Text("ACCOUNT_USERNAME_PROMPT", tableName: "Account")) + TextField("Email", text: $accountUserName, prompt: Text("ACCOUNT_USERNAME_OR_EMAIL_PROMPT", tableName: "Account")) .autocorrectionDisabled() .autocapitalization(.none) SecureField("Password", text: $accountPassword, prompt: Text("ACCOUNT_PASSWORD_PROMPT", tableName: "Account")) @@ -90,8 +91,14 @@ struct NewsBlurAddAccountView: View { Button { Task { do { - try await executeAccountCredentials() - dismiss() + if account == nil { + // Create a new account + try await executeAccountCredentials() + } else { + // Updating account credentials + try await executeAccountCredentials() + dismiss() + } } catch { accountError = (error, true) } @@ -115,27 +122,33 @@ struct NewsBlurAddAccountView: View { } private func executeAccountCredentials() async throws { - let trimmedEmailAddress = accountUserName.trimmingWhitespace + let trimmedUsername = accountUserName.trimmingWhitespace - guard (account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: trimmedEmailAddress)) else { + guard (account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: trimmedUsername)) else { throw LocalizedNetNewsWireError.duplicateAccount } showProgressIndicator = true - let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedEmailAddress, secret: accountPassword) + let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: accountPassword) return try await withCheckedThrowingContinuation { continuation in Account.validateCredentials(type: .newsBlur, credentials: basicCredentials) { result in switch result { case .success(let credentials): if let sessionsCredentials = credentials { + if self.account == nil { self.account = AccountManager.shared.createAccount(type: .newsBlur) } do { - try self.account?.removeCredentials(type: .newsBlurBasic) - try self.account?.removeCredentials(type: .newsBlurSessionId) + do { + try self.account?.removeCredentials(type: .newsBlurBasic) + try self.account?.removeCredentials(type: .newsBlurSessionId) + } catch { + NewsBlurAddAccountView.logger.error("\(error.localizedDescription)") + } + try self.account?.storeCredentials(basicCredentials) try self.account?.storeCredentials(sessionsCredentials) @@ -144,22 +157,27 @@ struct NewsBlurAddAccountView: View { case .success(_): showProgressIndicator = false continuation.resume() + return case .failure(let failure): showProgressIndicator = false continuation.resume(throwing: failure) + return } }) } catch { showProgressIndicator = false continuation.resume(throwing: LocalizedNetNewsWireError.keychainError) + return } } else { showProgressIndicator = false continuation.resume(throwing: LocalizedNetNewsWireError.invalidUsernameOrPassword) + return } case .failure(let failure): showProgressIndicator = false continuation.resume(throwing: failure) + return } } } diff --git a/iOS/Account/Views/ReaderAPIAddAccountView.swift b/iOS/Account/Views/ReaderAPIAddAccountView.swift index 09344a347..6f6d6e0d7 100644 --- a/iOS/Account/Views/ReaderAPIAddAccountView.swift +++ b/iOS/Account/Views/ReaderAPIAddAccountView.swift @@ -24,7 +24,7 @@ struct ReaderAPIAddAccountView: View { @State private var accountSecret: String = "" @State private var accountAPIUrl: String = "" @State private var showProgressIndicator: Bool = false - @State private var accountSetupError: (Error?, Bool) = (nil, false) + @State private var accountError: (Error?, Bool) = (nil, false) var body: some View { NavigationView { @@ -32,7 +32,7 @@ struct ReaderAPIAddAccountView: View { if accountType != nil { AccountSectionHeader(accountType: accountType!) } - accountDetailsSection + accountDetails Section(footer: readerAccountExplainer) {} } .navigationTitle(Text(accountType?.localizedAccountName() ?? "")) @@ -43,20 +43,22 @@ struct ReaderAPIAddAccountView: View { .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button(action: { dismiss() }, label: { Text("CANCEL_BUTTON_TITLE", tableName: "Buttons") }) + .disabled(showProgressIndicator) } ToolbarItem(placement: .navigationBarTrailing) { if showProgressIndicator { ProgressView() } } } - .alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $accountSetupError.1) { + .alert(Text("ERROR_TITLE", tableName: "Errors"), isPresented: $accountError.1) { Button(role: .cancel) { // } label: { Text("DISMISS_BUTTON_TITLE", tableName: "Buttons") } } message: { - Text(accountSetupError.0?.localizedDescription ?? "") + Text(accountError.0?.localizedDescription ?? "") } + .interactiveDismissDisabled(showProgressIndicator) .dismissOnExternalContextLaunch() .dismissOnAccountAdd() } @@ -80,7 +82,7 @@ struct ReaderAPIAddAccountView: View { - var accountDetailsSection: some View { + var accountDetails: some View { Group { Section { TextField("Username", text: $accountUserName) @@ -93,32 +95,39 @@ struct ReaderAPIAddAccountView: View { .autocapitalization(.none) } } - - Section { - Button { - Task { - do { + } + } + + var accountButton: some View { + Section { + Button { + Task { + do { + if account == nil { + // Create a new account + try await executeAccountCredentials() + } else { + // Updating account credentials try await executeAccountCredentials() dismiss() - } catch { - accountSetupError = (error, true) } - } - } label: { - HStack { - Spacer() - if accountCredentials == nil { - Text("ADD_ACCOUNT_BUTTON_TITLE", tableName: "Buttons") - } else { - Text("UPDATE_CREDENTIALS_BUTTON_TITLE", tableName: "Buttons") - } - Spacer() + } catch { + accountError = (error, true) } } - .disabled(!validateCredentials()) + } label: { + HStack { + Spacer() + if accountCredentials == nil { + Text("ADD_ACCOUNT_BUTTON_TITLE", tableName: "Buttons") + } else { + Text("UPDATE_CREDENTIALS_BUTTON_TITLE", tableName: "Buttons") + } + Spacer() + } } + .disabled(!validateCredentials()) } - } // MARK: - API @@ -132,7 +141,7 @@ struct ReaderAPIAddAccountView: View { accountSecret = creds.secret } } catch { - accountSetupError = (error, true) + accountError = (error, true) } } } @@ -152,7 +161,6 @@ struct ReaderAPIAddAccountView: View { return true } - @MainActor private func executeAccountCredentials() async throws { let trimmedAccountUserName = accountUserName.trimmingWhitespace @@ -185,19 +193,23 @@ struct ReaderAPIAddAccountView: View { case .success: showProgressIndicator = false continuation.resume() + return case .failure(let error): showProgressIndicator = false continuation.resume(throwing: error) + return } }) } catch { showProgressIndicator = false - continuation.resume(throwing: error) + continuation.resume(throwing: LocalizedNetNewsWireError.keychainError) + return } } case .failure(let failure): showProgressIndicator = false continuation.resume(throwing: failure) + return } } } diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 1ec3d517b..f2142aed0 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -1169,15 +1169,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { func showAccountInspector(for account: Account) { let hosting = UIHostingController(rootView: InjectedNavigationView(injectedView: AccountInspectorView(account: account))) rootSplitViewController.present(hosting, animated: true, completion: nil) - -// let accountInspectorNavController = -// UIStoryboard.inspector.instantiateViewController(identifier: "AccountInspectorNavigationViewController") as! UINavigationController -// let accountInspectorController = accountInspectorNavController.topViewController as! AccountInspectorViewController -// accountInspectorNavController.modalPresentationStyle = .formSheet -// accountInspectorNavController.preferredContentSize = AccountInspectorViewController.preferredContentSizeForFormSheetDisplay -// accountInspectorController.isModal = true -// accountInspectorController.account = account -// rootSplitViewController.present(accountInspectorNavController, animated: true) } func showFeedInspector() { diff --git a/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift b/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift index 924dd3777..f7424af3f 100644 --- a/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift +++ b/iOS/Settings/Views/Account and Extensions/Accounts/AccountsManagementView.swift @@ -105,7 +105,9 @@ struct AccountsManagementView: View { .aspectRatio(contentMode: .fit) .frame(width: 25, height: 25) Text(account.nameForDisplay) - }.swipeActions(edge: .trailing, allowsFullSwipe: false) { + } + .transition(.move(edge: .top)) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { if account != AccountManager.shared.defaultAccount { Button(role: .destructive) { accountToRemove.wrappedValue = account @@ -118,17 +120,19 @@ struct AccountsManagementView: View { } }.tint(.red) } + Button { + withAnimation { + account.isActive.toggle() + } + } label: { + if account.isActive { + Image(systemName: "minus.circle") + } else { + Image(systemName: "togglepower") + } + }.tint(account.isActive ? .yellow : Color(uiColor: AppAssets.primaryAccentColor)) } } - - var inactiveFooterText: some View { - if AccountManager.shared.sortedAccounts.filter({ $0.isActive == false }).count == 0 { - return Text("NO_INACTIVE_ACCOUNT_FOOTER", tableName: "Settings") - } else { - return Text("") - } - } - } struct AddAccountView_Previews: PreviewProvider { diff --git a/iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift b/iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift index c049ab2d6..154ae86f0 100644 --- a/iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift +++ b/iOS/SwiftUI Extensions/View+DismissOnAccountAdd.swift @@ -22,6 +22,9 @@ struct DismissOnAccountAdd: ViewModifier { } extension View { + + /// Convenience modifier to dismiss a view when an account has been added. + /// - Returns: `View` func dismissOnAccountAdd() -> some View { modifier(DismissOnAccountAdd()) } diff --git a/iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift b/iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift index 67ec0bdf4..82bce90d1 100644 --- a/iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift +++ b/iOS/SwiftUI Extensions/View+DismissOnExternalContext.swift @@ -23,6 +23,10 @@ struct DismissOnExternalContext: ViewModifier { } extension View { + + /// This function dismisses a view when the user launches from + /// an external action, for example, opening the app from the widget. + /// - Returns: `View` func dismissOnExternalContextLaunch() -> some View { modifier(DismissOnExternalContext()) }