diff --git a/RevenueCat.xcodeproj/project.pbxproj b/RevenueCat.xcodeproj/project.pbxproj index 937f778b3c..7b80b42c13 100644 --- a/RevenueCat.xcodeproj/project.pbxproj +++ b/RevenueCat.xcodeproj/project.pbxproj @@ -219,6 +219,7 @@ 4F5C05BF2A43A2C500651C7D /* LocaleExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5C05BE2A43A2C500651C7D /* LocaleExtensionsTests.swift */; }; 4F5D52D92A5713A800E1C758 /* DebugViewSwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCEEA602A379CF9002C2112 /* DebugViewSwiftUITests.swift */; }; 4F5D52EC2A57152B00E1C758 /* ImageSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FCEEA622A37A2E9002C2112 /* ImageSnapshot.swift */; }; + 4F6423F62A72C20F0071BFD1 /* PostedTransactionCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6423F52A72C20F0071BFD1 /* PostedTransactionCache.swift */; }; 4F69EB092A14406E00ED6D4B /* Matchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F69EB082A14406E00ED6D4B /* Matchers.swift */; }; 4F69EB0A2A14406E00ED6D4B /* Matchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F69EB082A14406E00ED6D4B /* Matchers.swift */; }; 4F6BED592A26A14400CD9322 /* DebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F6BED582A26A14400CD9322 /* DebugView.swift */; }; @@ -955,6 +956,7 @@ 4F54DF412A1D8D0700FD72BF /* MockTransactionPoster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTransactionPoster.swift; sourceTree = ""; }; 4F5C05BC2A43A21A00651C7D /* Locale+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = ""; }; 4F5C05BE2A43A2C500651C7D /* LocaleExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtensionsTests.swift; sourceTree = ""; }; + 4F6423F52A72C20F0071BFD1 /* PostedTransactionCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostedTransactionCache.swift; sourceTree = ""; }; 4F69EB082A14406E00ED6D4B /* Matchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matchers.swift; sourceTree = ""; }; 4F6BED582A26A14400CD9322 /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = ""; }; 4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewModel.swift; sourceTree = ""; }; @@ -2601,6 +2603,7 @@ children = ( B3B5FBC0269E17CE00104A0C /* DeviceCache.swift */, B3B5FBBE269E081E00104A0C /* InMemoryCachedObject.swift */, + 4F6423F52A72C20F0071BFD1 /* PostedTransactionCache.swift */, ); path = Caching; sourceTree = ""; @@ -3232,6 +3235,7 @@ 57E6C27C29723A94001AFE98 /* Signing.swift in Sources */, 57CB2AD429CCF21A00C91439 /* RedirectLoggerTaskDelegate.swift in Sources */, 57C381DC27961547009E3940 /* SK2StoreProductDiscount.swift in Sources */, + 4F6423F62A72C20F0071BFD1 /* PostedTransactionCache.swift in Sources */, B34605CA279A6E380031CA74 /* GetCustomerInfoOperation.swift in Sources */, 5751379527F4C4D80064AB2C /* Optional+Extensions.swift in Sources */, B3852FA026C1ED1F005384F8 /* IdentityManager.swift in Sources */, diff --git a/Sources/Caching/DeviceCache.swift b/Sources/Caching/DeviceCache.swift index 69aa67282b..5cb5d98bbf 100644 --- a/Sources/Caching/DeviceCache.swift +++ b/Sources/Caching/DeviceCache.swift @@ -46,6 +46,26 @@ class DeviceCache { Logger.verbose(Strings.purchase.device_cache_deinit(self)) } + // MARK: - generic methods + + func update( + key: Key, + default defaultValue: Value, + updater: @Sendable (inout Value) -> Void + ) { + self.userDefaults.write { + var value: Value = $0.value(forKey: key) ?? defaultValue + updater(&value) + $0.set(value, forKey: key) + } + } + + func value(for key: Key) -> Value? { + self.userDefaults.read { + $0.value(forKey: key) + } + } + // MARK: - appUserID func cache(appUserID: String) { @@ -323,7 +343,7 @@ class DeviceCache { // MARK: - Helper functions - internal enum CacheKeys: String, CacheKeyType { + internal enum CacheKeys: String, DeviceCacheKeyType { case legacyGeneratedAppUserDefaults = "com.revenuecat.userdefaults.appUserID" case appUserDefaults = "com.revenuecat.userdefaults.appUserID.new" @@ -333,7 +353,7 @@ class DeviceCache { } - fileprivate enum CacheKey: CacheKeyType { + fileprivate enum CacheKey: DeviceCacheKeyType { static let base = "com.revenuecat.userdefaults." static let legacySubscriberAttributesBase = "\(Self.base)subscriberAttributes." @@ -564,7 +584,7 @@ private extension DeviceCache { fileprivate extension UserDefaults { - func value(forKey key: CacheKeyType) -> T? { + func value(forKey key: DeviceCacheKeyType) -> T? { guard let data = self.data(forKey: key) else { return nil } @@ -572,27 +592,27 @@ fileprivate extension UserDefaults { return try? JSONDecoder.default.decode(jsonData: data, logErrors: true) } - func set(_ value: Any?, forKey key: CacheKeyType) { + func set(_ value: Any?, forKey key: DeviceCacheKeyType) { self.set(value, forKey: key.rawValue) } - func string(forKey defaultName: CacheKeyType) -> String? { + func string(forKey defaultName: DeviceCacheKeyType) -> String? { return self.string(forKey: defaultName.rawValue) } - func removeObject(forKey defaultName: CacheKeyType) { + func removeObject(forKey defaultName: DeviceCacheKeyType) { self.removeObject(forKey: defaultName.rawValue) } - func dictionary(forKey defaultName: CacheKeyType) -> [String: Any]? { + func dictionary(forKey defaultName: DeviceCacheKeyType) -> [String: Any]? { return self.dictionary(forKey: defaultName.rawValue) } - func date(forKey defaultName: CacheKeyType) -> Date? { + func date(forKey defaultName: DeviceCacheKeyType) -> Date? { return self.object(forKey: defaultName.rawValue) as? Date } - func data(forKey key: CacheKeyType) -> Data? { + func data(forKey key: DeviceCacheKeyType) -> Data? { return self.data(forKey: key.rawValue) } @@ -642,7 +662,7 @@ private extension DeviceCache { } -private protocol CacheKeyType { +protocol DeviceCacheKeyType { var rawValue: String { get } diff --git a/Sources/Caching/PostedTransactionCache.swift b/Sources/Caching/PostedTransactionCache.swift new file mode 100644 index 0000000000..195b3ab23c --- /dev/null +++ b/Sources/Caching/PostedTransactionCache.swift @@ -0,0 +1,58 @@ +// +// Copyright RevenueCat Inc. All Rights Reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// PostedTransactionCache.swift +// +// Created by Nacho Soto on 7/27/23. + +import Foundation + +protocol PostedTransactionCacheType { + + func savePostedTransaction(_ transaction: StoreTransactionType) + func hasPostedTransaction(_ transaction: StoreTransactionType) -> Bool + +} + +// TODO: tests +// TODO: integrate + +final class PostedTransactionCache: PostedTransactionCacheType { + + private typealias StoredTransactions = Set + + private let deviceCache: DeviceCache + + init(deviceCache: DeviceCache) { + self.deviceCache = deviceCache + } + + func savePostedTransaction(_ transaction: StoreTransactionType) { + self.deviceCache.update(key: CacheKey.transactions, + default: Set()) { transactions in + transactions.insert(transaction.transactionIdentifier) + } + } + + func hasPostedTransaction(_ transaction: StoreTransactionType) -> Bool { + let transactions: StoredTransactions = self.deviceCache.value(for: CacheKey.transactions) ?? [] + return transactions.contains(transaction.transactionIdentifier) + } + +} + +private extension PostedTransactionCache { + + enum CacheKey: String, DeviceCacheKeyType { + + case transactions = "com.revenuecat.cached_transaction_identifier" + + } + +}