Skip to content
Open
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
8 changes: 8 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
164681D12E6B577600854AA5 /* VideoPlayerViewUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164681CC2E6B577600854AA5 /* VideoPlayerViewUIView.swift */; };
164681D22E6B577600854AA5 /* VideoComponentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164681C92E6B577600854AA5 /* VideoComponentViewModel.swift */; };
164933242E5808BE00CE43A9 /* PaywallTransitionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 164933232E5808BE00CE43A9 /* PaywallTransitionTest.swift */; };
167D67092EA151C400B4503F /* SynchronizedLargeItemCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 167D67082EA151C400B4503F /* SynchronizedLargeItemCache.swift */; };
16A7F4C12E563B89001F9FC8 /* PaywallTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A7F4BF2E563B89001F9FC8 /* PaywallTransition.swift */; };
16A7F4C32E563B91001F9FC8 /* PaywallAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A7F4C22E563B91001F9FC8 /* PaywallAnimation.swift */; };
16A7F4C62E564B97001F9FC8 /* TransitionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16A7F4C52E564B97001F9FC8 /* TransitionModifier.swift */; };
Expand All @@ -87,6 +88,7 @@
16DA8EF42E4F7A2500283940 /* VideoComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16DA8EF32E4F7A2500283940 /* VideoComponentTests.swift */; };
16DA8F1E2E4FB6E200283940 /* ImageComponent.json in Resources */ = {isa = PBXBuildFile; fileRef = 16DA8F1B2E4FB6E200283940 /* ImageComponent.json */; };
16DA8F1F2E4FB6E200283940 /* VideoComponent.json in Resources */ = {isa = PBXBuildFile; fileRef = 16DA8F1C2E4FB6E200283940 /* VideoComponent.json */; };
16E2B7F12EA01DFA00F04A7A /* SynchronizedLargeItemCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16E2B7F02EA01DFA00F04A7A /* SynchronizedLargeItemCacheTests.swift */; };
16F376262E93F1E300ADF649 /* LargeItemCacheTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16F376252E93F1E300ADF649 /* LargeItemCacheTypeTests.swift */; };
1DB9B2A72E57373900252D58 /* OfferingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB9B2A62E57373600252D58 /* OfferingTests.swift */; };
1E2F911B2CC918ED00BDB016 /* ContactSupportUtilitiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2F911A2CC918ED00BDB016 /* ContactSupportUtilitiesTests.swift */; };
Expand Down Expand Up @@ -1506,6 +1508,7 @@
164681CB2E6B577600854AA5 /* VideoPlayerViewNSView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewNSView.swift; sourceTree = "<group>"; };
164681CC2E6B577600854AA5 /* VideoPlayerViewUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewUIView.swift; sourceTree = "<group>"; };
164933232E5808BE00CE43A9 /* PaywallTransitionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallTransitionTest.swift; sourceTree = "<group>"; };
167D67082EA151C400B4503F /* SynchronizedLargeItemCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedLargeItemCache.swift; sourceTree = "<group>"; };
16A7F4BF2E563B89001F9FC8 /* PaywallTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallTransition.swift; sourceTree = "<group>"; };
16A7F4C22E563B91001F9FC8 /* PaywallAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaywallAnimation.swift; sourceTree = "<group>"; };
16A7F4C52E564B97001F9FC8 /* TransitionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionModifier.swift; sourceTree = "<group>"; };
Expand All @@ -1527,6 +1530,7 @@
16DA8EF32E4F7A2500283940 /* VideoComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoComponentTests.swift; sourceTree = "<group>"; };
16DA8F1B2E4FB6E200283940 /* ImageComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ImageComponent.json; sourceTree = "<group>"; };
16DA8F1C2E4FB6E200283940 /* VideoComponent.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = VideoComponent.json; sourceTree = "<group>"; };
16E2B7F02EA01DFA00F04A7A /* SynchronizedLargeItemCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchronizedLargeItemCacheTests.swift; sourceTree = "<group>"; };
16F376252E93F1E300ADF649 /* LargeItemCacheTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeItemCacheTypeTests.swift; sourceTree = "<group>"; };
1DB9B2A62E57373600252D58 /* OfferingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingTests.swift; sourceTree = "<group>"; };
1E2F911A2CC918ED00BDB016 /* ContactSupportUtilitiesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactSupportUtilitiesTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4423,6 +4427,7 @@
37E35AE0CDC4C2AA8260FB58 /* Caching */ = {
isa = PBXGroup;
children = (
16E2B7F02EA01DFA00F04A7A /* SynchronizedLargeItemCacheTests.swift */,
16F376252E93F1E300ADF649 /* LargeItemCacheTypeTests.swift */,
16BCA36F2E4D9BA700B39E7F /* FileRepositoryTests.swift */,
16BCA36D2E4D9B9300B39E7F /* DeferredValueStoresTests.swift */,
Expand Down Expand Up @@ -5016,6 +5021,7 @@
57F3C0CB29B7A0B10004FD7E /* Concurrency */ = {
isa = PBXGroup;
children = (
167D67082EA151C400B4503F /* SynchronizedLargeItemCache.swift */,
57A0FBEF2749C0C2009E2FC3 /* Atomic.swift */,
57EAE526274324C60060EB74 /* Lock.swift */,
2DDA3E4624DB0B5400EDFE5B /* OperationDispatcher.swift */,
Expand Down Expand Up @@ -6645,6 +6651,7 @@
2D00A41D2767C08300FC3DD8 /* ManageSubscriptionsStrings.swift in Sources */,
2DD9F4BE274EADC20031AE2C /* Purchases+async.swift in Sources */,
03C730F32D35F14600297FEC /* PaywallV2CacheWarming.swift in Sources */,
167D67092EA151C400B4503F /* SynchronizedLargeItemCache.swift in Sources */,
B3C4AAD526B8911300E1B3C8 /* Backend.swift in Sources */,
35AAEB452BBB14D000A12548 /* DiagnosticsFileHandler.swift in Sources */,
537B4B302DA9742500CEFF4C /* HealthReportOperation.swift in Sources */,
Expand Down Expand Up @@ -7236,6 +7243,7 @@
B3F8418F26F3A93400E560FB /* ErrorCodeTests.swift in Sources */,
75D9DE082D79FC0E0068554F /* DiagnosticsEventEncodingTests.swift in Sources */,
5793397228E77A6E00C1232C /* MockPaymentQueue.swift in Sources */,
16E2B7F12EA01DFA00F04A7A /* SynchronizedLargeItemCacheTests.swift in Sources */,
4FFCED832AA941B200118EF4 /* PaywallEventsBackendTests.swift in Sources */,
5796A38A27D6B96300653165 /* BackendGetCustomerInfoTests.swift in Sources */,
);
Expand Down
92 changes: 42 additions & 50 deletions Sources/Caching/DeviceCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import Foundation

// swiftlint:disable file_length type_body_length
class DeviceCache {
private static let defaultBasePath = "RevenueCat"

var cachedAppUserID: String? { return self._cachedAppUserID.value }
var cachedLegacyAppUserID: String? { return self._cachedLegacyAppUserID.value }
var cachedOfferings: Offerings? { self.offeringsCachedObject.cachedInstance }

private let systemInfo: SystemInfo
private let userDefaults: SynchronizedUserDefaults
private let largeItemCache: SynchronizedLargeItemCache
private let offeringsCachedObject: InMemoryCachedObject<Offerings>

private let _cachedAppUserID: Atomic<String?>
Expand All @@ -31,15 +33,19 @@ class DeviceCache {
private var userDefaultsObserver: NSObjectProtocol?

private var offeringsCachePreferredLocales: [String] = []
private let cacheURL: URL?

init(systemInfo: SystemInfo,
userDefaults: UserDefaults,
fileManager: LargeItemCacheType = FileManager.default,
offeringsCachedObject: InMemoryCachedObject<Offerings> = .init()) {
self.offeringsCachedObject = offeringsCachedObject
self.systemInfo = systemInfo
self.userDefaults = .init(userDefaults: userDefaults)
self._cachedAppUserID = .init(userDefaults.string(forKey: CacheKeys.appUserDefaults))
self._cachedLegacyAppUserID = .init(userDefaults.string(forKey: CacheKeys.legacyGeneratedAppUserDefaults))
self.cacheURL = fileManager.createCacheDirectoryIfNeeded(basePath: Self.defaultBasePath)
self.largeItemCache = .init(cache: fileManager, basePath: "RevenueCat")

Logger.verbose(Strings.purchase.device_cache_init(self))
}
Expand All @@ -55,17 +61,26 @@ class DeviceCache {
default defaultValue: Value,
updater: @Sendable (inout Value) -> Void
) {
self.userDefaults.write {
var value: Value = $0.value(forKey: key) ?? defaultValue
self.largeItemCache.write { cache, cacheURL in
guard let cacheURL = cacheURL else { return }
let fileURL = cacheURL.appendingPathComponent(key.rawValue)

var value: Value = defaultValue
if let data = try? cache.loadFile(at: fileURL),
let decoded: Value = try? JSONDecoder.default.decode(jsonData: data, logErrors: true) {
value = decoded
}

updater(&value)
$0.set(codable: value, forKey: key)

if let data = try? JSONEncoder.default.encode(value: value, logErrors: true) {
try? cache.saveData(data, to: fileURL)
}
}
}

func value<Key: DeviceCacheKeyType, Value: Codable>(for key: Key) -> Value? {
self.userDefaults.read {
$0.value(forKey: key)
}
self.largeItemCache.value(forKey: key)
}

// MARK: - appUserID
Expand Down Expand Up @@ -106,6 +121,9 @@ class DeviceCache {
self._cachedAppUserID.value = newUserID
self._cachedLegacyAppUserID.value = nil
}

// Clear offerings cache from large item cache
self.largeItemCache.removeObject(forKey: CacheKey.offerings(oldAppUserID))
}

// MARK: - CustomerInfo
Expand Down Expand Up @@ -161,8 +179,14 @@ class DeviceCache {
// MARK: - Offerings

func cachedOfferingsResponseData(appUserID: String) -> Data? {
return self.userDefaults.read {
$0.data(forKey: CacheKey.offerings(appUserID))
guard let cacheURL = self.cacheURL else {
return nil
}

let fileURL = cacheURL.appendingPathComponent(CacheKey.offerings(appUserID).rawValue)

return self.largeItemCache.read { cache, _ in
try? cache.loadFile(at: fileURL)
}
}

Expand All @@ -172,9 +196,7 @@ class DeviceCache {
// For the cache we need the preferred locales that were used in the request.
self.cacheInMemory(offerings: offerings)
self.offeringsCachePreferredLocales = preferredLocales
self.userDefaults.write {
$0.set(codable: offerings.response, forKey: CacheKey.offerings(appUserID))
}
self.largeItemCache.set(codable: offerings.response, forKey: CacheKey.offerings(appUserID))
}

func cacheInMemory(offerings: Offerings) {
Expand All @@ -184,9 +206,7 @@ class DeviceCache {
func clearOfferingsCache(appUserID: String) {
self.offeringsCachedObject.clearCache()
self.offeringsCachePreferredLocales = []
self.userDefaults.write {
$0.removeObject(forKey: CacheKey.offerings(appUserID))
}
self.largeItemCache.removeObject(forKey: CacheKey.offerings(appUserID))
}

func isOfferingsCacheStale(isAppBackgrounded: Bool) -> Bool {
Expand Down Expand Up @@ -353,13 +373,18 @@ class DeviceCache {
}

func store(productEntitlementMapping: ProductEntitlementMapping) {
self.userDefaults.write {
Self.store($0, productEntitlementMapping: productEntitlementMapping)
if self.largeItemCache.set(
codable: productEntitlementMapping,
forKey: CacheKeys.productEntitlementMapping
) {
self.userDefaults.write {
$0.set(Date(), forKey: CacheKeys.productEntitlementMappingLastUpdated)
}
}
}

var cachedProductEntitlementMapping: ProductEntitlementMapping? {
return self.userDefaults.read(Self.productEntitlementMapping)
return self.largeItemCache.value(forKey: CacheKeys.productEntitlementMapping)
}

// MARK: - StoreKit 2
Expand Down Expand Up @@ -678,20 +703,6 @@ private extension DeviceCache {
return userDefaults.date(forKey: CacheKeys.productEntitlementMappingLastUpdated)
}

static func productEntitlementMapping(_ userDefaults: UserDefaults) -> ProductEntitlementMapping? {
return userDefaults.value(forKey: CacheKeys.productEntitlementMapping)
}

static func store(
_ userDefaults: UserDefaults,
productEntitlementMapping mapping: ProductEntitlementMapping
) {
if userDefaults.set(codable: mapping,
forKey: CacheKeys.productEntitlementMapping) {
userDefaults.set(Date(), forKey: CacheKeys.productEntitlementMappingLastUpdated)
}
}

static func virtualCurrenciesLastUpdated(
_ userDefaults: UserDefaults,
appUserID: String
Expand Down Expand Up @@ -724,25 +735,6 @@ private extension DeviceCache {

fileprivate extension UserDefaults {

/// - Returns: whether the value could be saved
@discardableResult
func set<T: Codable>(codable: T, forKey key: DeviceCacheKeyType) -> Bool {
guard let data = try? JSONEncoder.default.encode(value: codable, logErrors: true) else {
return false
}

self.set(data, forKey: key)
return true
}

func value<T: Decodable>(forKey key: DeviceCacheKeyType) -> T? {
guard let data = self.data(forKey: key) else {
return nil
}

return try? JSONDecoder.default.decode(jsonData: data, logErrors: true)
}

func set(_ value: Any?, forKey key: DeviceCacheKeyType) {
self.set(value, forKey: key.rawValue)
}
Expand Down
18 changes: 16 additions & 2 deletions Sources/Caching/LargeItemCacheType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
import Foundation

/// An inteface representing a simple cache
@available(iOS 15.0, macOS 12.0, tvOS 15.0, visionOS 1.0, watchOS 8.0, *)
protocol LargeItemCacheType {
/// Store data to a url
func saveData(_ data: Data, to url: URL) throws

/// Store data to a url
@available(iOS 15.0, macOS 12.0, tvOS 15.0, visionOS 1.0, watchOS 8.0, *)
func saveData(_ bytes: AsyncThrowingStream<UInt8, Error>, to url: URL, checksum: Checksum?) async throws

/// Check if there is content cached at the url
Expand All @@ -26,18 +28,26 @@ protocol LargeItemCacheType {
/// Load data from url
func loadFile(at url: URL) throws -> Data

/// delete data at url
func remove(_ url: URL) throws

/// Creates a directory in the cache from a base path
func createCacheDirectoryIfNeeded(basePath: String) -> URL?
}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, visionOS 1.0, watchOS 8.0, *)
extension FileManager: LargeItemCacheType {
/// A URL for a cache directory if one is present
private var cacheDirectory: URL? {
return urls(for: .cachesDirectory, in: .userDomainMask).first
}

/// Store data to a url
func saveData(_ data: Data, to url: URL) throws {
try data.write(to: url)
}

/// Store data to a url and validate that the file is correct before saving
@available(iOS 15.0, macOS 12.0, tvOS 15.0, visionOS 1.0, watchOS 8.0, *)
func saveData(
_ bytes: AsyncThrowingStream<UInt8, Error>,
to url: URL,
Expand Down Expand Up @@ -133,4 +143,8 @@ extension FileManager: LargeItemCacheType {
func loadFile(at url: URL) throws -> Data {
return try Data(contentsOf: url)
}

func remove(_ url: URL) throws {
try self.removeItem(at: url)
}
}
Loading