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

Created PostedTransactionCache #2911

Merged
merged 2 commits into from
Aug 7, 2023
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
8 changes: 8 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -267,6 +268,7 @@
4FBBC5682A61E42F0077281F /* NonEmptyStringDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */; };
4FC083292A4A35FB00A97089 /* Integer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC083282A4A35FB00A97089 /* Integer+Extensions.swift */; };
4FC0832B2A4A361700A97089 /* IntegerExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC0832A2A4A361700A97089 /* IntegerExtensionsTests.swift */; };
4FC6F8892A73E445002139B2 /* PostedTransactionCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC6F8882A73E445002139B2 /* PostedTransactionCacheTests.swift */; };
4FC972172A712DCC008593DE /* CachingProductsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FC972162A712DCC008593DE /* CachingProductsManagerTests.swift */; };
4FCBA84F2A15391B004134BD /* SnapshotTesting+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 576C8A9127D27DDD0058FA6E /* SnapshotTesting+Extensions.swift */; };
4FCBA8512A153940004134BD /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 4FCBA8502A153940004134BD /* SnapshotTesting */; };
Expand Down Expand Up @@ -955,6 +957,7 @@
4F54DF412A1D8D0700FD72BF /* MockTransactionPoster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTransactionPoster.swift; sourceTree = "<group>"; };
4F5C05BC2A43A21A00651C7D /* Locale+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Locale+Extensions.swift"; sourceTree = "<group>"; };
4F5C05BE2A43A2C500651C7D /* LocaleExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocaleExtensionsTests.swift; sourceTree = "<group>"; };
4F6423F52A72C20F0071BFD1 /* PostedTransactionCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostedTransactionCache.swift; sourceTree = "<group>"; };
4F69EB082A14406E00ED6D4B /* Matchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Matchers.swift; sourceTree = "<group>"; };
4F6BED582A26A14400CD9322 /* DebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugView.swift; sourceTree = "<group>"; };
4F6BEDD82A26B55C00CD9322 /* DebugViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewModel.swift; sourceTree = "<group>"; };
Expand All @@ -979,6 +982,7 @@
4FBBC5672A61E42F0077281F /* NonEmptyStringDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonEmptyStringDecodable.swift; sourceTree = "<group>"; };
4FC083282A4A35FB00A97089 /* Integer+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Integer+Extensions.swift"; sourceTree = "<group>"; };
4FC0832A2A4A361700A97089 /* IntegerExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerExtensionsTests.swift; sourceTree = "<group>"; };
4FC6F8882A73E445002139B2 /* PostedTransactionCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostedTransactionCacheTests.swift; sourceTree = "<group>"; };
4FC972162A712DCC008593DE /* CachingProductsManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachingProductsManagerTests.swift; sourceTree = "<group>"; };
4FCBA8522A1539D0004134BD /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = "<group>"; };
4FCEEA5D2A379B80002C2112 /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2143,6 +2147,7 @@
children = (
37E35D87B7E6F91E27E98F42 /* DeviceCacheTests.swift */,
37E35E3250FBBB03D92E06EC /* InMemoryCachedObjectTests.swift */,
4FC6F8882A73E445002139B2 /* PostedTransactionCacheTests.swift */,
);
path = Caching;
sourceTree = "<group>";
Expand Down Expand Up @@ -2601,6 +2606,7 @@
children = (
B3B5FBC0269E17CE00104A0C /* DeviceCache.swift */,
B3B5FBBE269E081E00104A0C /* InMemoryCachedObject.swift */,
4F6423F52A72C20F0071BFD1 /* PostedTransactionCache.swift */,
);
path = Caching;
sourceTree = "<group>";
Expand Down Expand Up @@ -3232,6 +3238,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 */,
Expand Down Expand Up @@ -3450,6 +3457,7 @@
5766AA5A283D4CAB00FA6091 /* IgnoreHashableTests.swift in Sources */,
B36824BF268FBC8700957E4C /* SubscriberAttributeTests.swift in Sources */,
351B51BC26D450E800BD2BD7 /* OfferingsTests.swift in Sources */,
4FC6F8892A73E445002139B2 /* PostedTransactionCacheTests.swift in Sources */,
5796A38827D6B85900653165 /* BackendPostReceiptDataTests.swift in Sources */,
5752E8482892DC500069281E /* ErrorUtilsTests.swift in Sources */,
57488BF229CB84D40000EE7E /* BackendOfflineEntitlementsTests.swift in Sources */,
Expand Down
66 changes: 46 additions & 20 deletions Sources/Caching/DeviceCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@
Logger.verbose(Strings.purchase.device_cache_deinit(self))
}

// MARK: - generic methods

func update<Key: DeviceCacheKeyType, Value: Codable>(
NachoSoto marked this conversation as resolved.
Show resolved Hide resolved
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(codable: value, forKey: key)
}
}

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

// MARK: - appUserID

func cache(appUserID: String) {
Expand Down Expand Up @@ -143,11 +163,8 @@

func cache(offerings: Offerings, appUserID: String) {
self.cacheInMemory(offerings: offerings)

if let jsonData = try? JSONEncoder.default.encode(value: offerings.response, logErrors: true) {
self.userDefaults.write {
$0.set(jsonData, forKey: CacheKey.offerings(appUserID))
}
self.userDefaults.write {
$0.set(codable: offerings.response, forKey: CacheKey.offerings(appUserID))
}
}

Expand Down Expand Up @@ -323,7 +340,7 @@

// 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"
Expand All @@ -333,7 +350,7 @@

}

fileprivate enum CacheKey: CacheKeyType {
fileprivate enum CacheKey: DeviceCacheKeyType {

static let base = "com.revenuecat.userdefaults."
static let legacySubscriberAttributesBase = "\(Self.base)subscriberAttributes."
Expand Down Expand Up @@ -552,47 +569,56 @@
_ userDefaults: UserDefaults,
productEntitlementMapping mapping: ProductEntitlementMapping
) {
guard let data = try? JSONEncoder.default.encode(value: mapping, logErrors: true) else {
return
if userDefaults.set(codable: mapping,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any sort of error that we should be logging if this fails?

forKey: CacheKeys.productEntitlementMapping) {
userDefaults.set(Date(), forKey: CacheKeys.productEntitlementMappingLastUpdated)
}

userDefaults.set(data, forKey: CacheKeys.productEntitlementMapping)
userDefaults.set(Date(), forKey: CacheKeys.productEntitlementMappingLastUpdated)
}

}

fileprivate extension UserDefaults {

func value<T: Decodable>(forKey key: CacheKeyType) -> T? {
/// - 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joshdholtz this logErrors: true answers your question.

return false

Check warning on line 586 in Sources/Caching/DeviceCache.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Caching/DeviceCache.swift#L586

Added line #L586 was not covered by tests
}

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: 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)
}

Expand Down Expand Up @@ -642,7 +668,7 @@

}

private protocol CacheKeyType {
protocol DeviceCacheKeyType {

var rawValue: String { get }

Expand Down
56 changes: 56 additions & 0 deletions Sources/Caching/PostedTransactionCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// 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

/// A type that can keep track of which transactions have been posted to the backend.
protocol PostedTransactionCacheType: Sendable {

func savePostedTransaction(_ transaction: StoreTransactionType)
func hasPostedTransaction(_ transaction: StoreTransactionType) -> Bool

}

final class PostedTransactionCache: PostedTransactionCacheType {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses DeviceCache underneath (composition, not inheritance). It also doesn't make DeviceCache have any more domain-specific caching code.

This is how I want to refactor DeviceCache so it's split in smaller classes.


private typealias StoredTransactions = Set<String>

private let deviceCache: DeviceCache

init(deviceCache: DeviceCache) {
self.deviceCache = deviceCache
}

func savePostedTransaction(_ transaction: StoreTransactionType) {
self.deviceCache.update(key: CacheKey.transactions,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: do a quick test on device with N x 1000 transactions.

default: Set<String>()) { 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"

}

}
57 changes: 57 additions & 0 deletions Tests/UnitTests/Caching/PostedTransactionCacheTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// 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
//
// PostedTransactionCacheTests.swift
//
// Created by Nacho Soto on 7/28/23.

import Foundation
import Nimble
import XCTest

@testable import RevenueCat

class PostedTransactionCacheTests: TestCase {

private var userDefaults: UserDefaults!
private var deviceCache: DeviceCache!
private var cache: PostedTransactionCache!

override func setUpWithError() throws {
try super.setUpWithError()

self.userDefaults = try XCTUnwrap(.init(suiteName: UUID().uuidString))
self.deviceCache = .init(sandboxEnvironmentDetector: MockSandboxEnvironmentDetector(isSandbox: true),
userDefaults: self.userDefaults)
self.cache = .init(deviceCache: self.deviceCache)
Comment on lines +29 to +32
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to test with with real DeviceCache and UserDefaults which is a lot more accurate and less painful.

}

func testNoPostedTransactions() {
expect(self.cache.hasPostedTransaction(MockStoreTransaction())) == false
}

func testSavesFirstTransaction() {
let transaction = MockStoreTransaction()

self.cache.savePostedTransaction(transaction)
expect(self.cache.hasPostedTransaction(transaction)) == true
}

func testSaveMultipleTransactions() {
let transaction1 = MockStoreTransaction()
let transaction2 = MockStoreTransaction()

self.cache.savePostedTransaction(transaction1)
self.cache.savePostedTransaction(transaction2)

expect(self.cache.hasPostedTransaction(transaction1)) == true
expect(self.cache.hasPostedTransaction(transaction2)) == true
}

}
31 changes: 31 additions & 0 deletions Tests/UnitTests/Mocks/MockDeviceCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,37 @@ class MockDeviceCache: DeviceCache {
userDefaults: MockUserDefaults())
}

// MARK: - generic methods

var stubbedUpdateValues: [Any] = []
var invokedUpdateKey: Bool = false
var invokedUpdateKeyParameters: [(key: String, newValue: Any)] = []

override func update<Key: DeviceCacheKeyType, Value: Codable>(
key: Key,
default defaultValue: Value,
updater: @Sendable (inout Value) -> Void
) {
// swiftlint:disable:next force_cast
var value = (self.stubbedUpdateValues.popFirst() as! Value?) ?? defaultValue
updater(&value)

self.invokedUpdateKey = true
self.invokedUpdateKeyParameters.append((key: key.rawValue, newValue: value))
}

var stubbedValueForKey: [Any] = []
var invokedValueForKey: Bool = false
var invokedValueForKeyParameters: [String] = []

override func value<Key: DeviceCacheKeyType, Value: Codable>(for key: Key) -> Value? {
self.invokedValueForKey = true
self.invokedValueForKeyParameters.append(key.rawValue)

// swiftlint:disable:next force_cast
return self.stubbedValueForKey.popFirst() as! Value?
}

// MARK: appUserID

var stubbedAppUserID: String?
Expand Down