From 50428f3179fbc7b2c36466613ec6b5af11181f18 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 28 Sep 2019 00:44:58 -0400 Subject: [PATCH] Allow adding Feed Wrangler accounts --- Frameworks/Account/Account.swift | 4 + .../Account/Account.xcodeproj/project.pbxproj | 20 ++ .../Account/Credentials/Credentials.swift | 2 + .../Credentials/URLRequest+RSWeb.swift | 13 ++ .../FeedWrangler/FeedWranglerAPICaller.swift | 59 ++++++ .../FeedWranglerAccountDelegate.swift | 156 +++++++++++++++ .../FeedWrangler/FeedWranglerConfig.swift | 17 ++ Mac/AppAssets.swift | 4 + .../Accounts/AccountsAddViewController.swift | 9 +- .../Accounts/AccountsFeedWrangler.xib | 185 ++++++++++++++++++ ...AccountsFeedWranglerWindowController.swift | 110 +++++++++++ .../Contents.json | 15 ++ .../accountFreshRSS.pdf | Bin 0 -> 4206 bytes NetNewsWire.xcodeproj/project.pbxproj | 18 +- 14 files changed, 606 insertions(+), 6 deletions(-) create mode 100644 Frameworks/Account/FeedWrangler/FeedWranglerAPICaller.swift create mode 100644 Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift create mode 100644 Frameworks/Account/FeedWrangler/FeedWranglerConfig.swift create mode 100644 Mac/Preferences/Accounts/AccountsFeedWrangler.xib create mode 100644 Mac/Preferences/Accounts/AccountsFeedWranglerWindowController.swift create mode 100644 Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/accountFreshRSS.pdf diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index d0f787da3..c2a3b76b9 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -226,6 +226,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport) case .feedly: self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport) + case .feedWrangler: + self.delegate = FeedWranglerAccountDelegate(dataFolder: dataFolder, transport: transport) default: return nil } @@ -302,6 +304,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) case .freshRSS: ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion) + case .feedWrangler: + FeedWranglerAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) default: break } diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index a27cac954..12b3b737f 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 3B29BC6E233EA83C002A346D /* FeedWranglerAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B29BC6C233EA83C002A346D /* FeedWranglerAPICaller.swift */; }; + 3B29BC6F233EA83C002A346D /* FeedWranglerAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B29BC6D233EA83C002A346D /* FeedWranglerAccountDelegate.swift */; }; + 3B90F51F233F0EF800D481CC /* FeedWranglerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B90F51E233F0EF800D481CC /* FeedWranglerConfig.swift */; }; 5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */; }; 5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; }; 5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; }; @@ -144,6 +147,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 3B29BC6C233EA83C002A346D /* FeedWranglerAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAPICaller.swift; sourceTree = ""; }; + 3B29BC6D233EA83C002A346D /* FeedWranglerAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAccountDelegate.swift; sourceTree = ""; }; + 3B90F51E233F0EF800D481CC /* FeedWranglerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedWranglerConfig.swift; sourceTree = ""; }; 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = ""; }; 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = ""; }; 5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = ""; }; @@ -268,6 +274,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3B29BC6B233EA83C002A346D /* FeedWrangler */ = { + isa = PBXGroup; + children = ( + 3B29BC6C233EA83C002A346D /* FeedWranglerAPICaller.swift */, + 3B29BC6D233EA83C002A346D /* FeedWranglerAccountDelegate.swift */, + 3B90F51E233F0EF800D481CC /* FeedWranglerConfig.swift */, + ); + path = FeedWrangler; + sourceTree = ""; + }; 515E4EB12324FF7D0057B0E7 /* Credentials */ = { isa = PBXGroup; children = ( @@ -405,6 +421,7 @@ 84245C7D1FDDD2580074AFBB /* Feedbin */, 552032EA229D5D5A009559E0 /* ReaderAPI */, 9EA31339231E368100268BA0 /* Feedly */, + 3B29BC6B233EA83C002A346D /* FeedWrangler */, 848935031F62484F00CEBD24 /* AccountTests */, 848934F71F62484F00CEBD24 /* Products */, 8469F80F1F6DC3C10084783E /* Frameworks */, @@ -665,14 +682,17 @@ 84C8B3F41F89DE430053CCA6 /* DataExtensions.swift in Sources */, 552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */, 84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */, + 3B29BC6F233EA83C002A346D /* FeedWranglerAccountDelegate.swift in Sources */, 9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */, 9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */, 8469F81C1F6DD15E0084783E /* Account.swift in Sources */, 9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */, 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */, 9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */, + 3B29BC6E233EA83C002A346D /* FeedWranglerAPICaller.swift in Sources */, 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */, 9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */, + 3B90F51F233F0EF800D481CC /* FeedWranglerConfig.swift in Sources */, 846E77451F6EF9B900A165E2 /* Container.swift in Sources */, 9E1D15532334304B00F4944C /* FeedlyGetCollectionStreamOperation.swift in Sources */, 9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */, diff --git a/Frameworks/Account/Credentials/Credentials.swift b/Frameworks/Account/Credentials/Credentials.swift index 22a99e8a3..3a6cf06bd 100644 --- a/Frameworks/Account/Credentials/Credentials.swift +++ b/Frameworks/Account/Credentials/Credentials.swift @@ -15,6 +15,8 @@ public enum CredentialsError: Error { public enum CredentialsType: String { case basic = "password" + case feedWranglerBasic = "feedWranglerBasic" + case feedWranglerToken = "feedWranglerToken" case readerBasic = "readerBasic" case readerAPIKey = "readerAPIKey" case oauthAccessToken = "oauthAccessToken" diff --git a/Frameworks/Account/Credentials/URLRequest+RSWeb.swift b/Frameworks/Account/Credentials/URLRequest+RSWeb.swift index 4374f285b..da7b04a49 100755 --- a/Frameworks/Account/Credentials/URLRequest+RSWeb.swift +++ b/Frameworks/Account/Credentials/URLRequest+RSWeb.swift @@ -25,6 +25,19 @@ public extension URLRequest { let base64 = data?.base64EncodedString() let auth = "Basic \(base64 ?? "")" setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization) + case .feedWranglerBasic: + + guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + return + } + components.queryItems = [ + URLQueryItem(name: "email", value: credentials.username), + URLQueryItem(name: "password", value: credentials.secret), + URLQueryItem(name: "client_key", value: FeedWranglerConfig.clientKey) + ] + self.url = components.url + case .feedWranglerToken: + fatalError() // TODO: implement case .readerBasic: setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") httpMethod = "POST" diff --git a/Frameworks/Account/FeedWrangler/FeedWranglerAPICaller.swift b/Frameworks/Account/FeedWrangler/FeedWranglerAPICaller.swift new file mode 100644 index 000000000..110ec5d8b --- /dev/null +++ b/Frameworks/Account/FeedWrangler/FeedWranglerAPICaller.swift @@ -0,0 +1,59 @@ +// +// FeedWranglerAPICaller.swift +// Account +// +// Created by Jonathan Bennett on 2019-08-29. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +import Foundation +import RSWeb + +final class FeedWranglerAPICaller: NSObject { + + private var transport: Transport! + + var credentials: Credentials? + weak var accountMetadata: AccountMetadata? + + init(transport: Transport) { + super.init() + self.transport = transport + } + + func validateCredentials(completion: @escaping (Result) -> Void) { + let callURL = FeedWranglerConfig.clientURL.appendingPathComponent("users/authorize") + let request = URLRequest(url: callURL, credentials: credentials) + let username = self.credentials?.username ?? "" + + transport.send(request: request) { result in + switch result { + case .success(let (_, data)): + guard let data = data else { + completion(.success(nil)) + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] { + if let accessToken = json["access_token"] as? String { + let authCredentials = Credentials(type: .feedWranglerToken, username: username, secret: accessToken) + completion(.success(authCredentials)) + return + } + } + + completion(.success(nil)) + } catch let error { + completion(.failure(error)) + } + case .failure(let error): + completion(.failure(error)) + } + + } + } + +} diff --git a/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift b/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift new file mode 100644 index 000000000..5829fcc3f --- /dev/null +++ b/Frameworks/Account/FeedWrangler/FeedWranglerAccountDelegate.swift @@ -0,0 +1,156 @@ +// +// FeedWranglerAccountDelegate.swift +// Account +// +// Created by Jonathan Bennett on 2019-08-29. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Articles +import RSCore +import RSParser +import RSWeb +import SyncDatabase +import os.log + +final class FeedWranglerAccountDelegate: AccountDelegate { + + var behaviors: AccountBehaviors = [] + + var isOPMLImportInProgress = false + var server: String? = FeedWranglerConfig.clientPath + var credentials: Credentials? + var accountMetadata: AccountMetadata? + var refreshProgress = DownloadProgress(numberOfTasks: 0) + + private let caller: FeedWranglerAPICaller + private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feed Wrangler") + private let database: SyncDatabase + + init(dataFolder: String, transport: Transport?) { + if let transport = transport { + caller = FeedWranglerAPICaller(transport: transport) + } else { + let sessionConfiguration = URLSessionConfiguration.default + sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData + sessionConfiguration.timeoutIntervalForRequest = 60.0 + sessionConfiguration.httpShouldSetCookies = false + sessionConfiguration.httpCookieAcceptPolicy = .never + sessionConfiguration.httpMaximumConnectionsPerHost = 1 + sessionConfiguration.httpCookieStorage = nil + sessionConfiguration.urlCache = nil + + if let userAgentHeaders = UserAgent.headers() { + sessionConfiguration.httpAdditionalHeaders = userAgentHeaders + } + + let session = URLSession(configuration: sessionConfiguration) + caller = FeedWranglerAPICaller(transport: session) + } + + database = SyncDatabase(databaseFilePath: dataFolder.appending("/Sync.sqlite")) + } + + func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { + refreshProgress.addToNumberOfTasksAndRemaining(6) + + self.sendArticleStatus(for: account) { + self.refreshArticleStatus(for: account) { + self.refreshArticles(for: account) { + self.refreshMissingArticles(for: account) { + self.refreshProgress.clear() + DispatchQueue.main.async { + completion(.success(())) + } + } + } + } + } + } + + func refreshArticles(for account: Account, completion: @escaping (() -> Void)) { + os_log(.debug, log: log, "Refreshing articles...") + } + + func refreshMissingArticles(for account: Account, completion: @escaping (() -> Void)) { + os_log(.debug, log: log, "Refreshing missing articles...") + completion() + } + + func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) { + os_log(.debug, log: log, "Sending article status...") + completion() + } + + func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) { + os_log(.debug, log: log, "Refreshing article status...") + completion() + } + + func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result) -> Void) { + fatalError() + } + + func addFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { + fatalError() + } + + func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { + fatalError() + } + + func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { + fatalError() + } + + func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { + fatalError() + } + + func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) { + fatalError() + } + + func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result) -> Void) { + fatalError() + } + + func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { + fatalError() + } + + func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { + fatalError() + } + + func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { + fatalError() + } + + func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { + fatalError() + } + + func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { + fatalError() + } + + func accountDidInitialize(_ account: Account) { + credentials = try? account.retrieveCredentials(type: .feedWranglerToken) + } + + static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result) -> Void) { + let caller = FeedWranglerAPICaller(transport: transport) + caller.credentials = credentials + caller.validateCredentials() { result in + DispatchQueue.main.async { + completion(result) + } + } + } +} + +// MARK: Private +private extension FeedWranglerAccountDelegate { + +} diff --git a/Frameworks/Account/FeedWrangler/FeedWranglerConfig.swift b/Frameworks/Account/FeedWrangler/FeedWranglerConfig.swift new file mode 100644 index 000000000..6e594b80f --- /dev/null +++ b/Frameworks/Account/FeedWrangler/FeedWranglerConfig.swift @@ -0,0 +1,17 @@ +// +// FeedWranglerConfig.swift +// NetNewsWire +// +// Created by Jonathan Bennett on 9/27/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +enum FeedWranglerConfig { + static let clientKey = "{FEEDWRANGLERKEY}" + static let clientPath = "https://feedwrangler.net/api/v2/" + static let clientURL = { + URL(string: FeedWranglerConfig.clientPath)! + }() +} diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift index b67f10559..3eec27951 100644 --- a/Mac/AppAssets.swift +++ b/Mac/AppAssets.swift @@ -38,6 +38,10 @@ struct AppAssets { return RSImage(named: "accountFeedly") }() + static var accountFeedWrangler: RSImage! = { + return RSImage(named: "accountFeedWrangler") + }() + static var accountFreshRSS: RSImage! = { return RSImage(named: "accountFreshRSS") }() diff --git a/Mac/Preferences/Accounts/AccountsAddViewController.swift b/Mac/Preferences/Accounts/AccountsAddViewController.swift index 02b87bd38..dababea22 100644 --- a/Mac/Preferences/Accounts/AccountsAddViewController.swift +++ b/Mac/Preferences/Accounts/AccountsAddViewController.swift @@ -15,7 +15,7 @@ class AccountsAddViewController: NSViewController { private var accountsAddWindowController: NSWindowController? - private let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .freshRSS] + private let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .feedWrangler, .freshRSS] init() { super.init(nibName: "AccountsAdd", bundle: nil) @@ -65,6 +65,9 @@ extension AccountsAddViewController: NSTableViewDelegate { case .feedbin: cell.accountNameLabel?.stringValue = NSLocalizedString("Feedbin", comment: "Feedbin") cell.accountImageView?.image = AppAssets.accountFeedbin + case .feedWrangler: + cell.accountNameLabel?.stringValue = NSLocalizedString("Feed Wrangler", comment: "Feed Wrangler") + cell.accountImageView?.image = AppAssets.accountFeedWrangler case .freshRSS: cell.accountNameLabel?.stringValue = NSLocalizedString("FreshRSS", comment: "FreshRSS") cell.accountImageView?.image = AppAssets.accountFreshRSS @@ -95,6 +98,10 @@ extension AccountsAddViewController: NSTableViewDelegate { let accountsFeedbinWindowController = AccountsFeedbinWindowController() accountsFeedbinWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsFeedbinWindowController + case .feedWrangler: + let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController() + accountsFeedWranglerWindowController.runSheetOnWindow(self.view.window!) + accountsAddWindowController = accountsFeedWranglerWindowController case .freshRSS: let accountsReaderAPIWindowController = AccountsReaderAPIWindowController() accountsReaderAPIWindowController.accountType = .freshRSS diff --git a/Mac/Preferences/Accounts/AccountsFeedWrangler.xib b/Mac/Preferences/Accounts/AccountsFeedWrangler.xib new file mode 100644 index 000000000..684cdb225 --- /dev/null +++ b/Mac/Preferences/Accounts/AccountsFeedWrangler.xib @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mac/Preferences/Accounts/AccountsFeedWranglerWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedWranglerWindowController.swift new file mode 100644 index 000000000..c4b4f6e1e --- /dev/null +++ b/Mac/Preferences/Accounts/AccountsFeedWranglerWindowController.swift @@ -0,0 +1,110 @@ +// +// AccountsFeedWranglerWindowController.swift +// NetNewsWire +// +// Created by Jonathan Bennett on 2019-08-29. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import AppKit +import Account +import RSWeb + +class AccountsFeedWranglerWindowController: NSWindowController { + @IBOutlet weak var progressIndicator: NSProgressIndicator! + @IBOutlet weak var usernameTextField: NSTextField! + @IBOutlet weak var passwordTextField: NSSecureTextField! + @IBOutlet weak var errorMessageLabel: NSTextField! + @IBOutlet weak var actionButton: NSButton! + + var account: Account? + + private weak var hostWindow: NSWindow? + + convenience init() { + self.init(windowNibName: NSNib.Name("AccountsFeedWrangler")) + } + + override func windowDidLoad() { + if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) { + usernameTextField.stringValue = credentials.username + actionButton.title = NSLocalizedString("Update", comment: "Update") + } else { + actionButton.title = NSLocalizedString("Create", comment: "Create") + } + } + + // MARK: API + + func runSheetOnWindow(_ hostWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) { + self.hostWindow = hostWindow + hostWindow.beginSheet(window!, completionHandler: handler) + } + + // MARK: Actions + + @IBAction func cancel(_ sender: Any) { + hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) + } + + @IBAction func action(_ sender: Any) { + self.errorMessageLabel.stringValue = "" + + guard !usernameTextField.stringValue.isEmpty && !passwordTextField.stringValue.isEmpty else { + self.errorMessageLabel.stringValue = NSLocalizedString("Username & password required.", comment: "Credentials Error") + return + } + + actionButton.isEnabled = false + progressIndicator.isHidden = false + progressIndicator.startAnimation(self) + + let credentials = Credentials(type: .feedWranglerBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue) + Account.validateCredentials(type: .feedWrangler, credentials: credentials) { [weak self] result in + + guard let self = self else { return } + + self.actionButton.isEnabled = true + self.progressIndicator.isHidden = true + self.progressIndicator.stopAnimation(self) + + switch result { + case .success(let validatedCredentials): + guard let validatedCredentials = validatedCredentials else { + self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") + return + } + var newAccount = false + if self.account == nil { + self.account = AccountManager.shared.createAccount(type: .feedWrangler) + newAccount = true + } + + do { + try self.account?.removeCredentials(type: .feedWranglerBasic) + try self.account?.removeCredentials(type: .feedWranglerToken) + try self.account?.storeCredentials(credentials) + try self.account?.storeCredentials(validatedCredentials) + if newAccount { + self.account?.refreshAll() { result in + switch result { + case .success: + break + case .failure(let error): + NSApplication.shared.presentError(error) + } + } + } + self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) + } catch { + self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") + } + + case .failure: + + self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error") + + } + } + } +} diff --git a/Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/Contents.json b/Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/Contents.json new file mode 100644 index 000000000..b2c62a136 --- /dev/null +++ b/Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "accountFreshRSS.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/accountFreshRSS.pdf b/Mac/Resources/Assets.xcassets/accountFeedWrangler.imageset/accountFreshRSS.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1ff98e115ed9096d447af7849c78bbd5906f8b00 GIT binary patch literal 4206 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