Skip to content

Commit 780c71c

Browse files
authored
Merge pull request #7012 from woocommerce/try/general-app-settings
Introduce direct access to settings
2 parents 39b9582 + bb6d2e0 commit 780c71c

13 files changed

+432
-84
lines changed

Storage/Storage.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@
218218
DEC51AE0275B41BE009F3DF4 /* WooCommerce.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DEC51AA4275B41BE009F3DF4 /* WooCommerce.xcdatamodeld */; };
219219
DEFD6D422641B70400E51E0D /* SitePlugin+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFD6D412641B70400E51E0D /* SitePlugin+CoreDataProperties.swift */; };
220220
DEFD6D432641B70400E51E0D /* SitePlugin+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFD6D402641B70400E51E0D /* SitePlugin+CoreDataClass.swift */; };
221+
E16D3741284F1CDA005676BC /* GeneralAppSettingsStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16D3740284F1CDA005676BC /* GeneralAppSettingsStorageTests.swift */; };
222+
E16D3743284F1E76005676BC /* MockInMemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16D3742284F1E76005676BC /* MockInMemoryStorage.swift */; };
223+
E1E632C02846245D00878082 /* GeneralAppSettingsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1E632BF2846245D00878082 /* GeneralAppSettingsStorage.swift */; };
221224
E1BCBE842844D35E00087C33 /* GeneralStoreSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BCBE832844D35E00087C33 /* GeneralStoreSettingsTests.swift */; };
222225
FEDD70AD26A5DBDD00194C3A /* EligibilityErrorInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEDD70AC26A5DBDD00194C3A /* EligibilityErrorInfo.swift */; };
223226
/* End PBXBuildFile section */
@@ -532,6 +535,9 @@
532535
DEFD6D402641B70400E51E0D /* SitePlugin+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SitePlugin+CoreDataClass.swift"; sourceTree = "<group>"; };
533536
DEFD6D412641B70400E51E0D /* SitePlugin+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SitePlugin+CoreDataProperties.swift"; sourceTree = "<group>"; };
534537
DF3D3B298350F68191CD1DAD /* Pods_Storage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Storage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
538+
E16D3740284F1CDA005676BC /* GeneralAppSettingsStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralAppSettingsStorageTests.swift; sourceTree = "<group>"; };
539+
E16D3742284F1E76005676BC /* MockInMemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockInMemoryStorage.swift; sourceTree = "<group>"; };
540+
E1E632BF2846245D00878082 /* GeneralAppSettingsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralAppSettingsStorage.swift; sourceTree = "<group>"; };
535541
E1BCBE832844D35E00087C33 /* GeneralStoreSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralStoreSettingsTests.swift; sourceTree = "<group>"; };
536542
F0439F2ADB3B211DF5C44D83 /* Pods-StorageTests.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-StorageTests.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-StorageTests/Pods-StorageTests.release-alpha.xcconfig"; sourceTree = "<group>"; };
537543
FEDD70AC26A5DBDD00194C3A /* EligibilityErrorInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityErrorInfo.swift; sourceTree = "<group>"; };
@@ -568,6 +574,7 @@
568574
574B774D24AA883D0042116F /* MockFileManager.swift */,
569575
572C099525475208005372E1 /* SpyFileManager.swift */,
570576
5772842625BF5BFB0092FB2C /* SpyPersistentStoreCoordinator.swift */,
577+
E16D3742284F1E76005676BC /* MockInMemoryStorage.swift */,
571578
);
572579
path = Mocks;
573580
sourceTree = "<group>";
@@ -683,6 +690,7 @@
683690
2619F72425B236E60006DAFF /* TypedPredicates.swift */,
684691
D87F61542265AA900031A13B /* PListFileStorage.swift */,
685692
027D3E6D23A0EEA4007D91B0 /* StorageType+Deletions.swift */,
693+
E1E632BF2846245D00878082 /* GeneralAppSettingsStorage.swift */,
686694
);
687695
path = Tools;
688696
sourceTree = "<group>";
@@ -803,6 +811,7 @@
803811
2619F71825AF95020006DAFF /* StorageTypeExtensionsTests.swift */,
804812
2685C11C263DEF0B00D9EE97 /* StorageTypeDeletionsTests.swift */,
805813
2619F78525B5D29B0006DAFF /* TypedPredicateTests.swift */,
814+
E16D3740284F1CDA005676BC /* GeneralAppSettingsStorageTests.swift */,
806815
);
807816
path = Tools;
808817
sourceTree = "<group>";
@@ -1223,6 +1232,7 @@
12231232
02C254EC2563B12E00A04423 /* ShippingLabelSettings+CoreDataProperties.swift in Sources */,
12241233
B5FD111E21D4046E00560344 /* OrderSearchResults+CoreDataProperties.swift in Sources */,
12251234
7471A515216CF0FE00219F7E /* SiteVisitStatsItem+CoreDataProperties.swift in Sources */,
1235+
E1E632C02846245D00878082 /* GeneralAppSettingsStorage.swift in Sources */,
12261236
031C1EA627AD3AFE00298699 /* WCPayCardPaymentDetails+CoreDataProperties.swift in Sources */,
12271237
B54CA5BD20A4BD3B00F38CD1 /* NSManagedObjectContext+Storage.swift in Sources */,
12281238
455C0C9125DD6D93007B6F38 /* AccountSettings+CoreDataProperties.swift in Sources */,
@@ -1362,6 +1372,7 @@
13621372
B59E11E020A9F5E6004121A4 /* Constants.swift in Sources */,
13631373
B54CA5C320A4BF6900F38CD1 /* NSManagedObjectContextStorageTests.swift in Sources */,
13641374
02A098272480D160002F8C7A /* MockCrashLogger.swift in Sources */,
1375+
E16D3743284F1E76005676BC /* MockInMemoryStorage.swift in Sources */,
13651376
B54CA5C720A4BFDC00F38CD1 /* DummyStack.swift in Sources */,
13661377
574B774E24AA883D0042116F /* MockFileManager.swift in Sources */,
13671378
B54CA5C220A4BF6900F38CD1 /* NSManagedObjectStorageTests.swift in Sources */,
@@ -1374,6 +1385,7 @@
13741385
57A29FB825226BDC004DEE01 /* MigrationTests.swift in Sources */,
13751386
2685C11D263DEF0C00D9EE97 /* StorageTypeDeletionsTests.swift in Sources */,
13761387
2619F78625B5D29B0006DAFF /* TypedPredicateTests.swift in Sources */,
1388+
E16D3741284F1CDA005676BC /* GeneralAppSettingsStorageTests.swift in Sources */,
13771389
5772842725BF5BFB0092FB2C /* SpyPersistentStoreCoordinator.swift in Sources */,
13781390
E1BCBE842844D35E00087C33 /* GeneralStoreSettingsTests.swift in Sources */,
13791391
5772842C25BF62A90092FB2C /* Assertions.swift in Sources */,

Storage/Storage/Model/GeneralAppSettings.swift

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,36 @@ public struct GeneralAppSettings: Codable, Equatable, GeneratedCopiable {
1313
/// Note that this is not accurate because this property/setting was created when we have
1414
/// thousands of users already.
1515
///
16-
public let installationDate: Date?
16+
public var installationDate: Date?
1717

1818
/// Key/Value type to store feedback settings
1919
/// Key: A `FeedbackType` to identify the feedback
2020
/// Value: A `FeedbackSetting` to store the feedback state
21-
public let feedbacks: [FeedbackType: FeedbackSettings]
21+
public var feedbacks: [FeedbackType: FeedbackSettings]
2222

2323
/// The state(`true` or `false`) for the view add-on beta feature switch.
2424
///
25-
public let isViewAddOnsSwitchEnabled: Bool
25+
public var isViewAddOnsSwitchEnabled: Bool
2626

2727
/// The state(`true` or `false`) for the Product SKU Input Scanner feature switch.
2828
///
29-
public let isProductSKUInputScannerSwitchEnabled: Bool
29+
public var isProductSKUInputScannerSwitchEnabled: Bool
3030

3131
/// The state for the Coupon Management feature switch.
3232
///
33-
public let isCouponManagementSwitchEnabled: Bool
33+
public var isCouponManagementSwitchEnabled: Bool
3434

3535
/// A list (possibly empty) of known card reader IDs - i.e. IDs of card readers that should be reconnected to automatically
3636
/// e.g. ["CHB204909005931"]
3737
///
38-
public let knownCardReaders: [String]
38+
public var knownCardReaders: [String]
3939

4040
/// The last known eligibility error information persisted locally.
4141
///
42-
public let lastEligibilityErrorInfo: EligibilityErrorInfo?
42+
public var lastEligibilityErrorInfo: EligibilityErrorInfo?
4343

4444
/// The last time the Jetpack benefits banner is dismissed.
45-
public let lastJetpackBenefitsBannerDismissedTime: Date?
45+
public var lastJetpackBenefitsBannerDismissedTime: Date?
4646

4747
public init(installationDate: Date?,
4848
feedbacks: [FeedbackType: FeedbackSettings],
@@ -62,6 +62,16 @@ public struct GeneralAppSettings: Codable, Equatable, GeneratedCopiable {
6262
self.lastJetpackBenefitsBannerDismissedTime = lastJetpackBenefitsBannerDismissedTime
6363
}
6464

65+
public static var `default`: Self {
66+
.init(installationDate: nil,
67+
feedbacks: [:],
68+
isViewAddOnsSwitchEnabled: false,
69+
isProductSKUInputScannerSwitchEnabled: false,
70+
isCouponManagementSwitchEnabled: false,
71+
knownCardReaders: [],
72+
lastEligibilityErrorInfo: nil)
73+
}
74+
6575
/// Returns the status of a given feedback type. If the feedback is not stored in the feedback array. it is assumed that it has a pending status.
6676
///
6777
public func feedbackStatus(of type: FeedbackType) -> FeedbackSettings.Status {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import Foundation
2+
import Combine
3+
4+
// MARK: - Public API
5+
6+
/// Provides access to the stored GeneralAppSettings
7+
///
8+
public struct GeneralAppSettingsStorage {
9+
private let fileStorage: FileStorage
10+
11+
/// This subject is used internally to force a refresh of any settings publisher.
12+
/// Every time the underlying settings change, we should emit a value here.
13+
///
14+
/// Since there is no guarantee that there will be a single instance of GeneralAppSettingsStorage,
15+
/// we use a shared static property so that any instance that writes changes to settings emits a
16+
/// value that would refresh the data on any other instance.
17+
///
18+
private static let refreshSubject = CurrentValueSubject<Void, Never>(())
19+
20+
public init(fileStorage: FileStorage = PListFileStorage()) {
21+
self.fileStorage = fileStorage
22+
}
23+
24+
/// Reads the value of the stored setting for the given key path
25+
///
26+
public func value<T>(for setting: KeyPath<GeneralAppSettings, T>) -> T {
27+
return settings[keyPath: setting]
28+
}
29+
30+
/// Returns a publisher that emits updates every time the value at the given key path changes.
31+
///
32+
public func publisher<T>(for setting: KeyPath<GeneralAppSettings, T>) -> AnyPublisher<T, Never> where T: Equatable {
33+
settingsPublisher
34+
.map(setting)
35+
.removeDuplicates()
36+
.eraseToAnyPublisher()
37+
}
38+
39+
/// Writes the value to the stored setting for the given key path
40+
///
41+
public func setValue<T>(_ value: T, for setting: WritableKeyPath<GeneralAppSettings, T>) throws {
42+
var settings = settings
43+
settings[keyPath: setting] = value
44+
try saveSettings(settings)
45+
}
46+
47+
/// Returns the GeneralAppSettings object
48+
///
49+
public var settings: GeneralAppSettings {
50+
loadOrCreateGeneralAppSettings()
51+
}
52+
53+
/// Returns a publisher that emits updates every time the settings change..
54+
///
55+
public var settingsPublisher: AnyPublisher<GeneralAppSettings, Never> {
56+
Self.refreshSubject
57+
.map { settings }
58+
.removeDuplicates()
59+
.eraseToAnyPublisher()
60+
}
61+
62+
/// Writes a new GeneralAppSettings object to storage
63+
///
64+
public func saveSettings(_ settings: GeneralAppSettings) throws {
65+
try saveGeneralAppSettings(settings)
66+
Self.refreshSubject.send(())
67+
}
68+
}
69+
70+
// MARK: - Storage
71+
private extension GeneralAppSettingsStorage {
72+
73+
/// Load the `GeneralAppSettings` from file or create an empty one if it doesn't exist.
74+
func loadOrCreateGeneralAppSettings() -> GeneralAppSettings {
75+
guard let settings: GeneralAppSettings = try? fileStorage.data(for: Constants.generalAppSettingsFileURL) else {
76+
return GeneralAppSettings.default
77+
}
78+
79+
return settings
80+
}
81+
82+
/// Save the `GeneralAppSettings` to the appropriate file.
83+
func saveGeneralAppSettings(_ settings: GeneralAppSettings) throws {
84+
try fileStorage.write(settings, to: Constants.generalAppSettingsFileURL)
85+
}
86+
}
87+
88+
// MARK: - Constants
89+
90+
/// Constants
91+
///
92+
private enum Constants {
93+
94+
// MARK: File Names
95+
static let generalAppSettingsFileName = "general-app-settings.plist"
96+
static let generalAppSettingsFileURL: URL! = {
97+
let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
98+
return documents!.appendingPathComponent(generalAppSettingsFileName)
99+
}()
100+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Foundation
2+
import Storage
3+
4+
/// Mock implementation of the FileStorage protocol.
5+
/// It reads and writes the data from and to an object in memory.
6+
///
7+
final class MockInMemoryStorage: FileStorage {
8+
/// A boolean value to test if a write to disk is requested
9+
///
10+
var dataWriteIsHit: Bool = false
11+
12+
/// A boolean value to test if a file deletion is requested
13+
///
14+
var deleteIsHit: Bool = false
15+
16+
private(set) var data: [URL: Codable] = [:]
17+
18+
func data<T>(for fileURL: URL) throws -> T where T: Decodable {
19+
guard let data = data[fileURL] as? T else {
20+
throw Error.readFailed
21+
}
22+
return data
23+
}
24+
25+
func write<T>(_ data: T, to fileURL: URL) throws where T: Encodable {
26+
self.data[fileURL] = data as? Codable
27+
dataWriteIsHit = true
28+
}
29+
30+
func deleteFile(at fileURL: URL) throws {
31+
data.removeValue(forKey: fileURL)
32+
deleteIsHit = true
33+
}
34+
}
35+
36+
extension MockInMemoryStorage {
37+
enum Error: Swift.Error {
38+
case readFailed
39+
}
40+
}

0 commit comments

Comments
 (0)