Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store User information in CoreData #423

Merged
merged 4 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 95 additions & 22 deletions BeeKit/Managers/CurrentUserManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright (c) 2015 APB. All rights reserved.
//

import CoreData
import Foundation
import KeychainSwift
import SwiftyJSON
Expand Down Expand Up @@ -33,16 +34,19 @@ public class CurrentUserManager {

private let keychain = KeychainSwift(keyPrefix: CurrentUserManager.keychainPrefix)
private let requestManager: RequestManager

private let container: BeeminderPersistentContainer

fileprivate static var allKeys: [String] {
[accessTokenKey, usernameKey, deadbeatKey, defaultLeadtimeKey, defaultAlertstartKey, defaultDeadlineKey, beemTZKey]
}

let userDefaults = UserDefaults(suiteName: Constants.appGroupIdentifier)!

init(requestManager: RequestManager) {
init(requestManager: RequestManager, container: BeeminderPersistentContainer) {
self.requestManager = requestManager
migrateValues()
self.container = container
migrateValuesToGroupStore()
migrateValuesToCoreData()
}

/// Migrate settings values from the standard store to a group store
Expand All @@ -51,7 +55,7 @@ public class CurrentUserManager {
/// these values are not available within extensions. To address this now values are stored in a
/// group-scoped settings object. Values written by old versions of the app may be in the previous store
/// so we migrate any such values on initialization.
private func migrateValues() {
private func migrateValuesToGroupStore() {
for key in CurrentUserManager.allKeys {
let standardValue = UserDefaults.standard.object(forKey: key)
let groupValue = userDefaults.object(forKey: key)
Expand All @@ -74,7 +78,52 @@ public class CurrentUserManager {
userDefaults.removeObject(forKey: CurrentUserManager.accessTokenKey)
UserDefaults.standard.removeObject(forKey: CurrentUserManager.accessTokenKey)
}


// If there is an existing session based on UserDefaults, create a new User object
private func migrateValuesToCoreData() {
// If there is already a session do nothing
if user() != nil {
return
}

// If we are logged out, do nothing
if accessToken == nil || userDefaults.object(forKey: CurrentUserManager.usernameKey) == nil {
return
}

let context = container.newBackgroundContext()

// Create a new user
let _ = User(context: context,
username: userDefaults.object(forKey: CurrentUserManager.usernameKey) as! String,
deadbeat: userDefaults.object(forKey: CurrentUserManager.deadbeatKey) != nil,
timezone: userDefaults.object(forKey: CurrentUserManager.beemTZKey) as? String ?? "Unknown",
defaultAlertStart: (userDefaults.object(forKey: CurrentUserManager.defaultAlertstartKey) ?? 0) as! Int32,
defaultDeadline: (userDefaults.object(forKey: CurrentUserManager.defaultDeadlineKey) ?? 0) as! Int32,
defaultLeadTime: (userDefaults.object(forKey: CurrentUserManager.defaultLeadtimeKey) ?? 0) as! Int32
)
try! context.save()
}


private func user() -> User? {
// Fetch a user from the persistent store
let request = NSFetchRequest<User>(entityName: "User")
// TODO: Handle (or at least log) an error here
let users = try? container.viewContext.fetch(request)
return users?.first
}

private func deleteUser() throws {
let context = container.newBackgroundContext()

// Delete any existing users. We expect at most one, but delete all to be safe.
while let user = self.user() {
context.delete(user)
}
try context.save()
}

/// Write a value to the UserDefaults store
///
/// During migration to the appGroup shared store we still want to support users downgrading
Expand All @@ -95,31 +144,35 @@ public class CurrentUserManager {
}

public var username :String? {
return userDefaults.object(forKey: CurrentUserManager.usernameKey) as! String?
return user()?.username
}

public var signingUp : Bool = false

public func defaultLeadTime() -> NSNumber {
return (userDefaults.object(forKey: CurrentUserManager.defaultLeadtimeKey) ?? 0) as! NSNumber
return (user()?.defaultLeadTime ?? 0) as NSNumber
}

public func setDefaultLeadTime(_ leadtime : NSNumber) {
self.set(leadtime, forKey: CurrentUserManager.defaultLeadtimeKey) }

user()?.defaultDeadline = leadtime as! Int32
self.set(leadtime, forKey: CurrentUserManager.defaultLeadtimeKey)
}

public func defaultAlertstart() -> NSNumber {
return (userDefaults.object(forKey: CurrentUserManager.defaultAlertstartKey) ?? 0) as! NSNumber
return (user()?.defaultAlertStart ?? 0) as NSNumber
}

public func setDefaultAlertstart(_ alertstart : NSNumber) {
user()?.defaultAlertStart = alertstart as! Int32
self.set(alertstart, forKey: CurrentUserManager.defaultAlertstartKey)
}

public func defaultDeadline() -> NSNumber {
return (userDefaults.object(forKey: CurrentUserManager.defaultDeadlineKey) ?? 0) as! NSNumber
return (user()?.defaultDeadline ?? 0) as NSNumber
}

public func setDefaultDeadline(_ deadline : NSNumber) {
user()?.defaultDeadline = deadline as! Int32
self.set(deadline, forKey: CurrentUserManager.defaultDeadlineKey)
}

Expand All @@ -128,14 +181,15 @@ public class CurrentUserManager {
}

public func isDeadbeat() -> Bool {
return userDefaults.object(forKey: CurrentUserManager.deadbeatKey) != nil
return user()?.deadbeat ?? false
}

public func timezone() -> String {
return userDefaults.object(forKey: CurrentUserManager.beemTZKey) as? String ?? "Unknown"
return user()?.timezone ?? "Unknown"
}

public func setDeadbeat(_ deadbeat: Bool) {
user()?.deadbeat = deadbeat
if deadbeat {
self.set(true, forKey: CurrentUserManager.deadbeatKey)
} else {
Expand All @@ -149,14 +203,27 @@ public class CurrentUserManager {

public func signInWithEmail(_ email: String, password: String) async {
do {
let response = try await requestManager.post(url: "api/private/sign_in", parameters: ["user": ["login": email, "password": password], "beemios_secret": self.beemiosSecret] as Dictionary<String, Any>)
await self.handleSuccessfulSignin(JSON(response!))
let response = try await requestManager.post(url: "api/private/sign_in", parameters: ["user": ["login": email, "password": password], "beemios_secret": self.beemiosSecret] as Dictionary<String, Any>)
try! await self.handleSuccessfulSignin(JSON(response!))
} catch {
await self.handleFailedSignin(error, errorMessage: error.localizedDescription)
try! await self.handleFailedSignin(error, errorMessage: error.localizedDescription)
}
}

func handleSuccessfulSignin(_ responseJSON: JSON) async {
func handleSuccessfulSignin(_ responseJSON: JSON) async throws {
try deleteUser()

let context = container.newBackgroundContext()

let _ = User(context: context,
username: responseJSON[CurrentUserManager.usernameKey].string!,
deadbeat: responseJSON["deadbeat"].boolValue,
timezone: responseJSON[CurrentUserManager.beemTZKey].string!,
defaultAlertStart: responseJSON[CurrentUserManager.defaultAlertstartKey].int32!,
defaultDeadline: responseJSON[CurrentUserManager.defaultDeadlineKey].int32!,
defaultLeadTime: responseJSON[CurrentUserManager.defaultLeadtimeKey].int32!)
try context.save()

if responseJSON["deadbeat"].boolValue {
self.setDeadbeat(true)
}
Expand All @@ -174,25 +241,31 @@ public class CurrentUserManager {
public func syncNotificationDefaults() async throws {
let response = try await requestManager.get(url: "api/v1/users/\(username!).json", parameters: [:])
let responseJSON = JSON(response!)

let user = user()!
user.defaultAlertStart = responseJSON["default_alertstart"].int32!
user.defaultDeadline = responseJSON["default_deadline"].int32!
user.defaultLeadTime = responseJSON["default_leadtime"].int32!

self.set(responseJSON["default_alertstart"].number!, forKey: "default_alertstart")
self.set(responseJSON["default_deadline"].number!, forKey: "default_deadline")
self.set(responseJSON["default_leadtime"].number!, forKey: "default_leadtime")

}

func handleFailedSignin(_ responseError: Error, errorMessage : String?) async {
func handleFailedSignin(_ responseError: Error, errorMessage : String?) async throws {
await Task { @MainActor in
NotificationCenter.default.post(name: Notification.Name(rawValue: CurrentUserManager.failedSignInNotificationName), object: self, userInfo: ["error" : responseError])
}.value
await self.signOut()
try await self.signOut()
}

public func signOut() async {

public func signOut() async throws {
await Task { @MainActor in
NotificationCenter.default.post(name: Notification.Name(rawValue: CurrentUserManager.willSignOutNotificationName), object: self)
}.value

try deleteUser()

keychain.delete(CurrentUserManager.accessTokenKey)
self.removeObject(forKey: CurrentUserManager.deadbeatKey)
self.removeObject(forKey: CurrentUserManager.usernameKey)
Expand Down
2 changes: 1 addition & 1 deletion BeeKit/Managers/GoalManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public actor GoalManager {
/// Fetch and return the latest set of goals from the server
public func fetchGoals() async throws -> [Goal] {
guard let username = currentUserManager.username else {
await currentUserManager.signOut()
try await currentUserManager.signOut()
return []
}

Expand Down
2 changes: 1 addition & 1 deletion BeeKit/Managers/RequestManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class RequestManager {
if case .responseValidationFailed(let reason) = error {
if case .unacceptableStatusCode(let code) = reason {
if code == 401 {
await ServiceLocator.currentUserManager.signOut()
try? await ServiceLocator.currentUserManager.signOut()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22522" systemVersion="23C71" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="User" representedClassName=".User" syncable="YES">
<attribute name="deadbeat" attributeType="Boolean" usesScalarValueType="NO"/>
<attribute name="defaultAlertStart" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO"/>
<attribute name="defaultDeadline" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO"/>
<attribute name="defaultLeadTime" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO"/>
<attribute name="timezone" attributeType="String"/>
<attribute name="username" attributeType="String"/>
</entity>
</model>
11 changes: 11 additions & 0 deletions BeeKit/Model/BeeminderPersistantContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import UIKit
import CoreData

class BeeminderPersistentContainer: NSPersistentContainer {

override open class func defaultDirectoryURL() -> URL {
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.beeminder.beeminder")
return storeURL!
}

}
45 changes: 45 additions & 0 deletions BeeKit/Model/User.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation
import CoreData

public class User: NSManagedObject {
@NSManaged public var username: String
@NSManaged public var deadbeat: Bool
@NSManaged public var timezone: String
@NSManaged public var defaultAlertStart: Int32
@NSManaged public var defaultDeadline: Int32
@NSManaged public var defaultLeadTime: Int32

public init(context: NSManagedObjectContext,
username: String,
deadbeat: Bool,
timezone: String,
defaultAlertStart: Int32,
defaultDeadline: Int32,
defaultLeadTime: Int32
) {
let entity = NSEntityDescription.entity(forEntityName: "User", in: context)!
super.init(entity: entity, insertInto: context)
self.username = username
self.deadbeat = deadbeat
self.timezone = timezone
self.defaultAlertStart = defaultAlertStart
self.defaultDeadline = defaultDeadline
self.defaultLeadTime = defaultLeadTime
}

@available(*, unavailable)
public init() {
fatalError()
}

@available(*, unavailable)
public init(context: NSManagedObjectContext) {
fatalError()
}

public override init(entity: NSEntityDescription, insertInto: NSManagedObjectContext?) {
super.init(entity: entity, insertInto: insertInto)
}


}
12 changes: 11 additions & 1 deletion BeeKit/ServiceLocator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,19 @@
import Foundation

public class ServiceLocator {
static let persistentContainer: BeeminderPersistentContainer = {
let container = BeeminderPersistentContainer(name: "BeeminderModel")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
}
return container
}()

public static let requestManager = RequestManager()
public static let signedRequestManager = SignedRequestManager(requestManager: requestManager)
public static let currentUserManager = CurrentUserManager(requestManager: requestManager)
public static let currentUserManager = CurrentUserManager(requestManager: requestManager, container: persistentContainer)
public static let goalManager = GoalManager(requestManager: requestManager, currentUserManager: currentUserManager)
public static let healthStoreManager = HealthStoreManager()
public static let versionManager = VersionManager(requestManager: requestManager)
Expand Down
Loading
Loading