// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable import Foundation public extension UserDefaultsStorage { static var standard: UserDefaultsConfig = Dependencies.create(identifier: "standard") { _ in UserDefaults.standard } static var appGroup: UserDefaultsConfig = Dependencies.create(identifier: UserDefaults.applicationGroup) { _ in UserDefaults(suiteName: UserDefaults.applicationGroup)! } } // MARK: - UserDefaultsType public protocol UserDefaultsType: AnyObject { var allKeys: [String] { get } func object(forKey defaultName: String) -> Any? func string(forKey defaultName: String) -> String? func array(forKey defaultName: String) -> [Any]? func dictionary(forKey defaultName: String) -> [String : Any]? func data(forKey defaultName: String) -> Data? func stringArray(forKey defaultName: String) -> [String]? func integer(forKey defaultName: String) -> Int func float(forKey defaultName: String) -> Float func double(forKey defaultName: String) -> Double func bool(forKey defaultName: String) -> Bool func url(forKey defaultName: String) -> URL? func set(_ value: Any?, forKey defaultName: String) func set(_ value: Int, forKey defaultName: String) func set(_ value: Float, forKey defaultName: String) func set(_ value: Double, forKey defaultName: String) func set(_ value: Bool, forKey defaultName: String) func set(_ url: URL?, forKey defaultName: String) func removeAll() } extension UserDefaults: UserDefaultsType {} // MARK: - Convenience public extension UserDefaults { @ThreadSafeObject private static var cachedApplicationGroup: String = "" // stringlint:ignore_contents static let applicationGroup: String = { guard !cachedApplicationGroup.isEmpty else { let dynamicAppGroupsId: String = (Bundle.main.infoDictionary?["AppGroupsId"] as? String) .defaulting(to: "group.com.loki-project.loki-messenger") _cachedApplicationGroup.set(to: dynamicAppGroupsId) return dynamicAppGroupsId } return cachedApplicationGroup }() var allKeys: [String] { Array(self.dictionaryRepresentation().keys) } static func removeAll(using dependencies: Dependencies) { UserDefaultsStorage.standard.createInstance(dependencies).removeAll() UserDefaultsStorage.appGroup.createInstance(dependencies).removeAll() } func removeAll() { let data: [String: Any] = self.dictionaryRepresentation() data.forEach { key, _ in self.removeObject(forKey: key) } self.synchronize() // Shouldn't be needed but better safe than sorry } } // MARK: - UserDefault Values public extension UserDefaults.BoolKey { /// Indicates whether the user has synced an initial config message from this device static let hasSyncedInitialConfiguration: UserDefaults.BoolKey = "hasSyncedConfiguration" /// Indicates whether the user has seen the suggestion to enable link previews static let hasSeenLinkPreviewSuggestion: UserDefaults.BoolKey = "hasSeenLinkPreviewSuggestion" /// Indicates whether the user has seen the IP exposure warning when enabling calls /// /// **Note:** This is currently not in use (it was decided that it's better to warn the user every time they enable calls instead /// of just the first time) static let hasSeenCallIPExposureWarning: UserDefaults.BoolKey = "hasSeenCallIPExposureWarning" /// Indicates whether the user has seen the missed call tips modal static let hasSeenCallMissedTips: UserDefaults.BoolKey = "hasSeenCallMissedTips" /// Indicates whether the user is registered for APNS (ie. "Fast Mode" notifications) static let isUsingFullAPNs: UserDefaults.BoolKey = "isUsingFullAPNs" /// Indicates whether the device was unlinked from an account /// /// **Note:** This doesn't seem to be properly used (we basically just maintain the existing value) static let wasUnlinked: UserDefaults.BoolKey = "wasUnlinked" /// Indicates whether the main app is active, this is set to `true` while the app is in the foreground and `false` when /// the app is in the background static let isMainAppActive: UserDefaults.BoolKey = "isMainAppActive" /// Indicates whether there is an ongoing call static let isCallOngoing: UserDefaults.BoolKey = "isCallOngoing" /// Indicates whether we had the microphone permission the last time the app when to the background static let lastSeenHasMicrophonePermission: UserDefaults.BoolKey = "lastSeenHasMicrophonePermission" /// Indicates whether we had asked for the local network permission static let hasRequestedLocalNetworkPermission: UserDefaults.BoolKey = "hasRequestedLocalNetworkPermission" } public extension UserDefaults.DateKey { /// The date/time when the users profile picture was last uploaded to the server (used to rate-limit re-uploading) static let lastProfilePictureUpload: UserDefaults.DateKey = "lastProfilePictureUpload" /// The date/time when any open group last had a successful poll (used as a fallback date/time if the open group hasn't been polled /// this session) static let lastOpen: UserDefaults.DateKey = "lastOpen" /// The date/time when the last garbage collection was performed (used to rate-limit garbage collection) static let lastGarbageCollection: UserDefaults.DateKey = "lastGarbageCollection" /// The date/time when we received a call pre-offer (used to suppress call notifications which are too old) static let lastCallPreOffer: UserDefaults.DateKey = "lastCallPreOffer" } public extension UserDefaults.DoubleKey { /// The timestamp when we last successfully uploaded the users push token (used to rate-limit calling our subscription endpoint) static let lastDeviceTokenUpload: UserDefaults.DoubleKey = "lastDeviceTokenUploadTime" } public extension UserDefaults.IntKey { /// The latest hardfork value returned when interacting with a service node static let hardfork: UserDefaults.IntKey = "hardfork" /// The latest softfork value returned when interacting with a service node static let softfork: UserDefaults.IntKey = "softfork" } public extension UserDefaults.StringKey { /// The most recently subscribed APNS token static let deviceToken: UserDefaults.StringKey = "deviceToken" /// The warning to show at the top of the app static let topBannerWarningToShow: UserDefaults.StringKey = "topBannerWarningToShow" } // MARK: - Keys public extension UserDefaults { struct BoolKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { public let rawValue: String public init(_ rawValue: String) { self.rawValue = rawValue } public init?(rawValue: String) { self.rawValue = rawValue } public init(stringLiteral value: String) { self.init(value) } public init(unicodeScalarLiteral value: String) { self.init(value) } public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } struct DateKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { public let rawValue: String public init(_ rawValue: String) { self.rawValue = rawValue } public init?(rawValue: String) { self.rawValue = rawValue } public init(stringLiteral value: String) { self.init(value) } public init(unicodeScalarLiteral value: String) { self.init(value) } public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } struct DoubleKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { public let rawValue: String public init(_ rawValue: String) { self.rawValue = rawValue } public init?(rawValue: String) { self.rawValue = rawValue } public init(stringLiteral value: String) { self.init(value) } public init(unicodeScalarLiteral value: String) { self.init(value) } public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } struct IntKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { public let rawValue: String public init(_ rawValue: String) { self.rawValue = rawValue } public init?(rawValue: String) { self.rawValue = rawValue } public init(stringLiteral value: String) { self.init(value) } public init(unicodeScalarLiteral value: String) { self.init(value) } public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } struct StringKey: RawRepresentable, ExpressibleByStringLiteral, Hashable { public let rawValue: String public init(_ rawValue: String) { self.rawValue = rawValue } public init?(rawValue: String) { self.rawValue = rawValue } public init(stringLiteral value: String) { self.init(value) } public init(unicodeScalarLiteral value: String) { self.init(value) } public init(extendedGraphemeClusterLiteral value: String) { self.init(value) } } }