Page MenuHomeMusing Studio

No OneTemporary

diff --git a/Shared/Account/AccountLogoutView.swift b/Shared/Account/AccountLogoutView.swift
index 16b9fd0..b86cf18 100644
--- a/Shared/Account/AccountLogoutView.swift
+++ b/Shared/Account/AccountLogoutView.swift
@@ -1,30 +1,30 @@
import SwiftUI
struct AccountLogoutView: View {
@EnvironmentObject var model: WriteFreelyModel
var body: some View {
VStack {
Spacer()
VStack {
- Text("Logged in as \(model.account.user?.username ?? "Anonymous")")
+ Text("Logged in as \(model.account.username ?? "Anonymous")")
Text("on \(model.account.server)")
}
Spacer()
Button(action: logoutHandler, label: {
Text("Logout")
})
}
}
func logoutHandler() {
model.logout()
}
}
struct AccountLogoutView_Previews: PreviewProvider {
static var previews: some View {
AccountLogoutView()
.environmentObject(WriteFreelyModel())
}
}
diff --git a/Shared/Account/AccountModel.swift b/Shared/Account/AccountModel.swift
index 9f20628..d8b2473 100644
--- a/Shared/Account/AccountModel.swift
+++ b/Shared/Account/AccountModel.swift
@@ -1,52 +1,71 @@
import Foundation
import WriteFreely
enum AccountError: Error {
case invalidPassword
case usernameNotFound
case serverNotFound
}
extension AccountError: LocalizedError {
public var errorDescription: String? {
switch self {
case .serverNotFound:
return NSLocalizedString(
"The server could not be found. Please check the information you've entered and try again.",
comment: ""
)
case .invalidPassword:
return NSLocalizedString(
"Invalid password. Please check that you've entered your password correctly and try logging in again.",
comment: ""
)
case .usernameNotFound:
return NSLocalizedString(
"Username not found. Did you use your email address by mistake?",
comment: ""
)
}
}
}
struct AccountModel {
+ private let defaults = UserDefaults.standard
+ let isLoggedInFlag = "isLoggedInFlag"
+ let usernameStringKey = "usernameStringKey"
+ let serverStringKey = "serverStringKey"
+
var server: String = ""
+ var username: String = ""
var hasError: Bool = false
var currentError: AccountError? {
didSet {
hasError = true
}
}
private(set) var user: WFUser?
private(set) var isLoggedIn: Bool = false
mutating func login(_ user: WFUser) {
self.user = user
+ self.username = user.username ?? ""
self.isLoggedIn = true
+ defaults.set(true, forKey: isLoggedInFlag)
+ defaults.set(user.username, forKey: usernameStringKey)
+ defaults.set(server, forKey: serverStringKey)
}
mutating func logout() {
self.user = nil
self.isLoggedIn = false
+ defaults.set(false, forKey: isLoggedInFlag)
+ defaults.removeObject(forKey: usernameStringKey)
+ defaults.removeObject(forKey: serverStringKey)
+ }
+
+ mutating func restoreState() {
+ isLoggedIn = defaults.bool(forKey: isLoggedInFlag)
+ server = defaults.string(forKey: serverStringKey) ?? ""
+ username = defaults.string(forKey: usernameStringKey) ?? ""
}
}
diff --git a/Shared/Models/WriteFreelyModel.swift b/Shared/Models/WriteFreelyModel.swift
index 99aa14b..b54ac8a 100644
--- a/Shared/Models/WriteFreelyModel.swift
+++ b/Shared/Models/WriteFreelyModel.swift
@@ -1,123 +1,210 @@
import Foundation
import WriteFreely
+import Security
// MARK: - WriteFreelyModel
class WriteFreelyModel: ObservableObject {
@Published var account = AccountModel()
@Published var preferences = PreferencesModel()
@Published var store = PostStore()
@Published var collections = CollectionListModel(with: [])
@Published var post: Post?
@Published var isLoggingIn: Bool = false
private var client: WFClient?
+ private let defaults = UserDefaults.standard
init() {
+ // Set the color scheme based on what's been saved in UserDefaults.
+ DispatchQueue.main.async {
+ self.preferences.appearance = self.defaults.integer(forKey: self.preferences.colorSchemeIntegerKey)
+ }
+
#if DEBUG
for post in testPostData { store.add(post) }
#endif
+
+ DispatchQueue.main.async {
+ self.account.restoreState()
+ }
}
}
// MARK: - WriteFreelyModel API
extension WriteFreelyModel {
- func login(
- to server: URL,
- as username: String,
- password: String
- ) {
+ func login(to server: URL, as username: String, password: String) {
isLoggingIn = true
account.server = server.absoluteString
client = WFClient(for: server)
client?.login(username: username, password: password, completion: loginHandler)
}
- func logout () {
- guard let loggedInClient = client else { return }
+ func logout() {
+ guard let loggedInClient = client else {
+ do {
+ try purgeTokenFromKeychain(username: account.username, server: account.server)
+ account.logout()
+ } catch {
+ fatalError("Failed to log out persisted state")
+ }
+ return
+ }
loggedInClient.logout(completion: logoutHandler)
}
func fetchUserCollections() {
guard let loggedInClient = client else { return }
loggedInClient.getUserCollections(completion: fetchUserCollectionsHandler)
}
}
private extension WriteFreelyModel {
func loginHandler(result: Result<WFUser, Error>) {
DispatchQueue.main.async {
self.isLoggingIn = false
}
do {
let user = try result.get()
fetchUserCollections()
+ saveTokenToKeychain(user.token, username: user.username, server: account.server)
DispatchQueue.main.async {
self.account.login(user)
}
} catch WFError.notFound {
DispatchQueue.main.async {
self.account.currentError = AccountError.usernameNotFound
}
} catch WFError.unauthorized {
DispatchQueue.main.async {
self.account.currentError = AccountError.invalidPassword
}
} catch {
if let error = error as? NSError, error.domain == NSURLErrorDomain, error.code == -1003 {
DispatchQueue.main.async {
self.account.currentError = AccountError.serverNotFound
}
}
}
}
func logoutHandler(result: Result<Bool, Error>) {
do {
_ = try result.get()
- client = nil
- DispatchQueue.main.async {
- self.account.logout()
- self.collections.clearUserCollection()
+ do {
+ try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
+ client = nil
+ DispatchQueue.main.async {
+ self.account.logout()
+ self.collections.clearUserCollection()
+ }
+ } catch {
+ print("Something went wrong purging the token from the Keychain.")
}
} catch WFError.notFound {
// The user token is invalid or doesn't exist, so it's been invalidated by the server. Proceed with
- // destroying the client object and setting the AccountModel to its logged-out state.
- client = nil
- DispatchQueue.main.async {
- self.account.logout()
- self.collections.clearUserCollection()
+ // purging the token from the Keychain, destroying the client object, and setting the AccountModel to its
+ // logged-out state.
+ do {
+ try purgeTokenFromKeychain(username: account.user?.username, server: account.server)
+ client = nil
+ DispatchQueue.main.async {
+ self.account.logout()
+ self.collections.clearUserCollection()
+ }
+ } catch {
+ print("Something went wrong purging the token from the Keychain.")
}
} catch {
// We get a 'cannot parse response' (similar to what we were seeing in the Swift package) NSURLError here,
// so we're using a hacky workaround — if we get the NSURLError, but the AccountModel still thinks we're
// logged in, try calling the logout function again and see what we get.
// Conditional cast from 'Error' to 'NSError' always succeeds but is the only way to check error properties.
if let error = error as? NSError,
error.domain == NSURLErrorDomain,
error.code == NSURLErrorCannotParseResponse {
if account.isLoggedIn {
self.logout()
}
}
}
}
func fetchUserCollectionsHandler(result: Result<[WFCollection], Error>) {
do {
let fetchedCollections = try result.get()
var fetchedCollectionsArray: [PostCollection] = []
for fetchedCollection in fetchedCollections {
var postCollection = PostCollection(title: fetchedCollection.title)
postCollection.wfCollection = fetchedCollection
fetchedCollectionsArray.append(postCollection)
}
DispatchQueue.main.async {
self.collections = CollectionListModel(with: fetchedCollectionsArray)
}
} catch {
print(error)
}
}
}
+
+private extension WriteFreelyModel {
+ // MARK: - Keychain Helpers
+ func saveTokenToKeychain(_ token: String, username: String?, server: String) {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecValueData as String: token.data(using: .utf8)!,
+ kSecAttrAccount as String: username ?? "anonymous",
+ kSecAttrService as String: server
+ ]
+
+ let status = SecItemAdd(query as CFDictionary, nil)
+ guard status == errSecDuplicateItem || status == errSecSuccess else {
+ fatalError("Error storing in Keychain with OSStatus: \(status)")
+ }
+ }
+
+ func purgeTokenFromKeychain(username: String?, server: String) throws {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrAccount as String: username ?? "anonymous",
+ kSecAttrService as String: server
+ ]
+
+ let status = SecItemDelete(query as CFDictionary)
+ guard status == errSecSuccess || status == errSecItemNotFound else {
+ fatalError("Error deleting from Keychain with OSStatus: \(status)")
+ }
+ }
+
+ func fetchTokenFromKeychain(username: String?, server: String) -> String? {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrAccount as String: username ?? "anonymous",
+ kSecAttrService as String: server,
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ kSecReturnAttributes as String: true,
+ kSecReturnData as String: true
+ ]
+
+ var secItem: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &secItem)
+
+ guard status != errSecItemNotFound else {
+ return nil
+ }
+
+ guard status == errSecSuccess else {
+ fatalError("Error fetching from Keychain with OSStatus: \(status)")
+ }
+
+ guard let existingSecItem = secItem as? [String: Any],
+ let tokenData = existingSecItem[kSecValueData as String] as? Data,
+ let token = String(data: tokenData, encoding: .utf8) else {
+ return nil
+ }
+
+ return token
+ }
+}
diff --git a/Shared/Preferences/PreferencesModel.swift b/Shared/Preferences/PreferencesModel.swift
index 39e9c24..c4ac721 100644
--- a/Shared/Preferences/PreferencesModel.swift
+++ b/Shared/Preferences/PreferencesModel.swift
@@ -1,54 +1,59 @@
import SwiftUI
class PreferencesModel: ObservableObject {
+ private let defaults = UserDefaults.standard
+ let colorSchemeIntegerKey = "colorSchemeIntegerKey"
+
/* We're stuck dropping into AppKit/UIKit to set light/dark schemes for now,
* because setting the .preferredColorScheme modifier on views in SwiftUI is
* currently unreliable.
*
* Feedback submitted to Apple:
*
* FB8382883: "On macOS 11β4, preferredColorScheme modifier does not respect .light ColorScheme"
* FB8383053: "On iOS 14β4/macOS 11β4, it is not possible to unset preferredColorScheme after setting
* it to either .light or .dark"
*/
#if os(iOS)
var window: UIWindow? {
guard let scene = UIApplication.shared.connectedScenes.first,
let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate,
let window = windowSceneDelegate.window else {
return nil
}
return window
}
#endif
@Published var selectedColorScheme: ColorScheme?
@Published var appearance: Int = 0 {
didSet {
switch appearance {
case 1:
// selectedColorScheme = .light
#if os(macOS)
NSApp.appearance = NSAppearance(named: .aqua)
#else
window?.overrideUserInterfaceStyle = .light
#endif
case 2:
// selectedColorScheme = .dark
#if os(macOS)
NSApp.appearance = NSAppearance(named: .darkAqua)
#else
window?.overrideUserInterfaceStyle = .dark
#endif
default:
// selectedColorScheme = .none
#if os(macOS)
NSApp.appearance = nil
#else
window?.overrideUserInterfaceStyle = .unspecified
#endif
}
+
+ defaults.set(appearance, forKey: colorSchemeIntegerKey)
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Jan 31, 10:28 AM (13 h, 40 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3611599

Event Timeline