diff --git a/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme new file mode 100644 index 0000000..0ed6014 --- /dev/null +++ b/Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Demo/UIKitContentViewController.swift b/Demo/Demo/UIKitContentViewController.swift index 06eb910..1e370b8 100644 --- a/Demo/Demo/UIKitContentViewController.swift +++ b/Demo/Demo/UIKitContentViewController.swift @@ -362,4 +362,3 @@ extension UIKitContentViewController: PayPalMessageViewEventDelegate { statusTextView.text = "Applied" } } -// swiftlint:disable:this file_length diff --git a/Sources/PayPalMessages/Extensions/UserDefaults.swift b/Sources/PayPalMessages/Extensions/UserDefaults.swift index be66444..09cf687 100644 --- a/Sources/PayPalMessages/Extensions/UserDefaults.swift +++ b/Sources/PayPalMessages/Extensions/UserDefaults.swift @@ -7,12 +7,11 @@ extension UserDefaults { } // holds the data for a merchant profile, of type PayPalMessageMerchantData - static var merchantProfileData: Data? { - get { - standard.data(forKey: Key.merchantProfileData.rawValue) - } - set { - standard.set(newValue, forKey: Key.merchantProfileData.rawValue) - } + static func getMerchantProfileData(forClientID clientID: String, merchantID: String?) -> Data? { + return standard.data(forKey: "\(Key.merchantProfileData.rawValue).\(clientID).\(merchantID ?? "default")") + } + + static func setMerchantProfileData(_ value: Data?, forClientID clientID: String, merchantID: String?) { + standard.set(value, forKey: "\(Key.merchantProfileData.rawValue).\(clientID).\(merchantID ?? "default")") } } diff --git a/Sources/PayPalMessages/IO/MerchantProfileProvider.swift b/Sources/PayPalMessages/IO/MerchantProfileProvider.swift index 1c93a84..c969681 100644 --- a/Sources/PayPalMessages/IO/MerchantProfileProvider.swift +++ b/Sources/PayPalMessages/IO/MerchantProfileProvider.swift @@ -4,6 +4,7 @@ protocol MerchantProfileHashGetable { func getMerchantProfileHash( environment: Environment, clientID: String, + merchantID: String?, onCompletion: @escaping (String?) -> Void ) } @@ -25,14 +26,15 @@ class MerchantProfileProvider: MerchantProfileHashGetable { func getMerchantProfileHash( environment: Environment, clientID: String, + merchantID: String?, onCompletion: @escaping (String?) -> Void ) { let currentDate = Date() // hash must be inside ttl and non-null - guard let merchantProfileData = getCachedMerchantProfileData(), + guard let merchantProfileData = getCachedMerchantProfileData(clientID: clientID, merchantID: merchantID), currentDate < merchantProfileData.ttlHard else { - requestMerchantProfile(environment: environment, clientID: clientID) { merchantProfiledData in + requestMerchantProfile(environment: environment, clientID: clientID, merchantID: merchantID) { merchantProfiledData in guard let merchantProfiledData = merchantProfiledData else { onCompletion(nil) return @@ -46,7 +48,7 @@ class MerchantProfileProvider: MerchantProfileHashGetable { // if date is outside soft-ttl window, re-request data if currentDate > merchantProfileData.ttlSoft { // ignores the response as it will return hashed value - requestMerchantProfile(environment: environment, clientID: clientID) { _ in } + requestMerchantProfile(environment: environment, clientID: clientID, merchantID: merchantID) { _ in } } onCompletion(merchantProfileData.disabled ? nil : merchantProfileData.hash) @@ -57,17 +59,22 @@ class MerchantProfileProvider: MerchantProfileHashGetable { private func requestMerchantProfile( environment: Environment, clientID: String, + merchantID: String?, onCompletion: @escaping (MerchantProfileData?) -> Void ) { - merchantProfileRequest.fetchMerchantProfile(environment: environment, clientID: clientID) { [weak self] result in + merchantProfileRequest.fetchMerchantProfile( + environment: environment, + clientID: clientID, + merchantID: merchantID + ) { [weak self] result in switch result { case .success(let merchantProfileData): - log(.info, "Merchant Request Hash succeeded with \(merchantProfileData.hash)") - self?.setCachedMerchantProfileData(merchantProfileData) + log(.debug, "Merchant Request Hash succeeded with \(merchantProfileData.hash)") + self?.setCachedMerchantProfileData(merchantProfileData, clientID: clientID, merchantID: merchantID) onCompletion(merchantProfileData) case .failure(let error): - log(.info, "Merchant Request Hash failed with \(error.localizedDescription)") + log(.debug, "Merchant Request Hash failed with \(error.localizedDescription)") onCompletion(nil) } } @@ -75,16 +82,16 @@ class MerchantProfileProvider: MerchantProfileHashGetable { // MARK: - User Defaults Methods - private func getCachedMerchantProfileData() -> MerchantProfileData? { - guard let cachedData = UserDefaults.merchantProfileData else { + private func getCachedMerchantProfileData(clientID: String, merchantID: String?) -> MerchantProfileData? { + guard let cachedData = UserDefaults.getMerchantProfileData(forClientID: clientID, merchantID: merchantID) else { return nil } return try? JSONDecoder().decode(MerchantProfileData.self, from: cachedData) } - private func setCachedMerchantProfileData(_ data: MerchantProfileData) { + private func setCachedMerchantProfileData(_ data: MerchantProfileData, clientID: String, merchantID: String?) { let encodedData = try? JSONEncoder().encode(data) - UserDefaults.merchantProfileData = encodedData + UserDefaults.setMerchantProfileData(encodedData, forClientID: clientID, merchantID: merchantID) } } diff --git a/Sources/PayPalMessages/IO/MerchantProfileRequest.swift b/Sources/PayPalMessages/IO/MerchantProfileRequest.swift index 7a04476..b135c6f 100644 --- a/Sources/PayPalMessages/IO/MerchantProfileRequest.swift +++ b/Sources/PayPalMessages/IO/MerchantProfileRequest.swift @@ -4,6 +4,7 @@ protocol MerchantProfileRequestable { func fetchMerchantProfile( environment: Environment, clientID: String, + merchantID: String?, onCompletion: @escaping (Result) -> Void ) } @@ -27,14 +28,15 @@ class MerchantProfileRequest: MerchantProfileRequestable { func fetchMerchantProfile( environment: Environment, clientID: String, + merchantID: String?, onCompletion: @escaping (Result) -> Void ) { - guard let url = environment.url(.merchantProfile, ["client_id": clientID]) else { + guard let url = environment.url(.merchantProfile, ["client_id": clientID, "merchant_id": merchantID]) else { onCompletion(.failure(RequestError.invalidClientID)) return } - log(.info, "fetcheMerchantProfile URL is \(url)") + log(.debug, "fetcheMerchantProfile URL is \(url)") fetch(url, headers: headers, session: environment.urlSession) { data, _, error in guard let data = data, error == nil else { diff --git a/Sources/PayPalMessages/PayPalMessageViewModel.swift b/Sources/PayPalMessages/PayPalMessageViewModel.swift index fe2accf..a8b10dc 100644 --- a/Sources/PayPalMessages/PayPalMessageViewModel.swift +++ b/Sources/PayPalMessages/PayPalMessageViewModel.swift @@ -221,7 +221,11 @@ class PayPalMessageViewModel: PayPalMessageModalEventDelegate { stateDelegate.onLoading(messageView) } - merchantProfileProvider.getMerchantProfileHash(environment: environment, clientID: clientID) { [weak self] profileHash in + merchantProfileProvider.getMerchantProfileHash( + environment: environment, + clientID: clientID, + merchantID: merchantID + ) { [weak self] profileHash in guard let self else { return } let parameters = self.makeRequestParameters(merchantProfileHash: profileHash) diff --git a/Tests/PayPalMessagesTests/MerchantProfileProviderTests.swift b/Tests/PayPalMessagesTests/MerchantProfileProviderTests.swift index a48f002..e30a61a 100644 --- a/Tests/PayPalMessagesTests/MerchantProfileProviderTests.swift +++ b/Tests/PayPalMessagesTests/MerchantProfileProviderTests.swift @@ -4,12 +4,19 @@ import XCTest final class MerchantProfileProviderTests: XCTestCase { + let clientID = "testclientid" + let clientID2 = "testclientid2" + let merchantID = "testmerchantid" + let merchantID2 = "testmerchantid2" + override func setUp() { super.setUp() - UserDefaults.standard.removeObject( - forKey: UserDefaults.Key.merchantProfileData.rawValue - ) + // Clear out merchant profile cache + let dictionary = UserDefaults.standard.dictionaryRepresentation() + for key in dictionary.keys { + UserDefaults.standard.removeObject(forKey: key) + } } // MARK: - Tests @@ -23,7 +30,7 @@ final class MerchantProfileProviderTests: XCTestCase { XCTAssertEqual(requestMock.requestsPerformed, 0) // retrieve hash and check requests performed - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { hash in + provider.getMerchantProfileHash(environment: .live, clientID: clientID, merchantID: merchantID) { hash in XCTAssertNotNil(hash) XCTAssertEqual(requestMock.requestsPerformed, 1) } @@ -35,32 +42,64 @@ final class MerchantProfileProviderTests: XCTestCase { let provider = MerchantProfileProvider(merchantProfileRequest: requestMock) // retrieve hash and check requests performed - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { hash in + provider.getMerchantProfileHash(environment: .live, clientID: clientID, merchantID: merchantID) { hash in let firstHash = hash XCTAssertNotNil(hash) XCTAssertEqual(requestMock.requestsPerformed, 1) // after retrieving it again, the hash should not be empty -- and no new request performed - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { secondHash in + provider.getMerchantProfileHash(environment: .live, clientID: self.clientID, merchantID: self.merchantID) { secondHash in XCTAssertEqual(firstHash, secondHash) XCTAssertEqual(requestMock.requestsPerformed, 1) } } } + func testCacheMissWhenDifferentMerchant() { + let requestMock = MerchantProfileRequestMock(scenario: .success) + let provider = MerchantProfileProvider(merchantProfileRequest: requestMock) + + + provider.getMerchantProfileHash(environment: .live, clientID: clientID, merchantID: nil) { hash in + let firstHash = hash + XCTAssertNotNil(hash) + XCTAssertEqual(requestMock.requestsPerformed, 1) + + provider.getMerchantProfileHash(environment: .live, clientID: self.clientID2, merchantID: nil) { secondHash in + XCTAssertNotEqual(firstHash, secondHash) + XCTAssertEqual(requestMock.requestsPerformed, 2) + + provider.getMerchantProfileHash(environment: .live, clientID: self.clientID, merchantID: self.merchantID) { hash in + let firstHash = hash + XCTAssertNotNil(hash) + XCTAssertEqual(requestMock.requestsPerformed, 3) + + provider.getMerchantProfileHash( + environment: .live, + clientID: self.clientID, + merchantID: self.merchantID2 + ) { secondHash in + XCTAssertNotEqual(firstHash, secondHash) + XCTAssertEqual(requestMock.requestsPerformed, 4) + } + } + } + } + } + // simulates a request with soft ttl expired, returns value but performs additional request func testTtlSoftExpired() { let requestMock = MerchantProfileRequestMock(scenario: .ttlSoftExpired) let provider = MerchantProfileProvider(merchantProfileRequest: requestMock) // perform first request - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { hash in + provider.getMerchantProfileHash(environment: .live, clientID: clientID, merchantID: merchantID) { hash in let firstHash = hash XCTAssertNotNil(hash) XCTAssertEqual(requestMock.requestsPerformed, 1) // perform another request, cached value will be returned but another request perfoms on background - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { secondHash in + provider.getMerchantProfileHash(environment: .live, clientID: self.clientID, merchantID: self.merchantID) { secondHash in XCTAssertEqual(firstHash, secondHash) XCTAssertEqual(requestMock.requestsPerformed, 2) } @@ -73,13 +112,13 @@ final class MerchantProfileProviderTests: XCTestCase { let provider = MerchantProfileProvider(merchantProfileRequest: requestMock) // perform first request - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { hash in + provider.getMerchantProfileHash(environment: .live, clientID: clientID, merchantID: merchantID) { hash in let firstHash = hash XCTAssertNotNil(hash) XCTAssertEqual(requestMock.requestsPerformed, 1) // perform another request, new request performed - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { secondHash in + provider.getMerchantProfileHash(environment: .live, clientID: self.clientID, merchantID: self.merchantID) { secondHash in XCTAssertNotEqual(firstHash, secondHash) XCTAssertEqual(requestMock.requestsPerformed, 2) } @@ -93,11 +132,11 @@ final class MerchantProfileProviderTests: XCTestCase { let provider = MerchantProfileProvider(merchantProfileRequest: requestMock) // perform first request -- null expected - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { hash in + provider.getMerchantProfileHash(environment: .live, clientID: clientID, merchantID: merchantID) { hash in XCTAssertNil(hash) // perform another request, null expected and used cached result - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { hash in + provider.getMerchantProfileHash(environment: .live, clientID: self.clientID, merchantID: self.merchantID) { hash in XCTAssertNil(hash) XCTAssertEqual(requestMock.requestsPerformed, 1) } @@ -110,11 +149,11 @@ final class MerchantProfileProviderTests: XCTestCase { let provider = MerchantProfileProvider(merchantProfileRequest: requestMock) // perform first request -- null expected - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { hash in + provider.getMerchantProfileHash(environment: .live, clientID: clientID, merchantID: merchantID) { hash in XCTAssertNil(hash) // perform another request, null expected and new request performed - provider.getMerchantProfileHash(environment: .live, clientID: "testclientid") { hash in + provider.getMerchantProfileHash(environment: .live, clientID: self.clientID, merchantID: self.merchantID) { hash in XCTAssertNil(hash) XCTAssertEqual(requestMock.requestsPerformed, 2) } diff --git a/Tests/PayPalMessagesTests/Mocks/MerchantProfileRequestMock.swift b/Tests/PayPalMessagesTests/Mocks/MerchantProfileRequestMock.swift index f9eb736..66c38c2 100644 --- a/Tests/PayPalMessagesTests/Mocks/MerchantProfileRequestMock.swift +++ b/Tests/PayPalMessagesTests/Mocks/MerchantProfileRequestMock.swift @@ -34,6 +34,7 @@ class MerchantProfileRequestMock: MerchantProfileRequestable { func fetchMerchantProfile( environment: Environment, clientID: String, + merchantID: String?, onCompletion: @escaping (Result) -> Void ) { requestsPerformed += 1 diff --git a/Tests/PayPalMessagesTests/Mocks/PayPalMessageMerchantProviderMock.swift b/Tests/PayPalMessagesTests/Mocks/PayPalMessageMerchantProviderMock.swift index 96f077e..98e37b2 100644 --- a/Tests/PayPalMessagesTests/Mocks/PayPalMessageMerchantProviderMock.swift +++ b/Tests/PayPalMessagesTests/Mocks/PayPalMessageMerchantProviderMock.swift @@ -17,6 +17,7 @@ class MerchantProfileProviderMock: MerchantProfileHashGetable { func getMerchantProfileHash( environment: Environment, clientID: String, + merchantID: String?, onCompletion: @escaping (String?) -> Void ) { switch scenario { diff --git a/Tests/PayPalMessagesTests/PayPalMessageViewModelTests.swift b/Tests/PayPalMessagesTests/PayPalMessageViewModelTests.swift index 5cc5fba..66ff383 100644 --- a/Tests/PayPalMessagesTests/PayPalMessageViewModelTests.swift +++ b/Tests/PayPalMessagesTests/PayPalMessageViewModelTests.swift @@ -372,7 +372,6 @@ final class PayPalMessageViewModelTests: XCTestCase { private func assert(_ mockRequest: PayPalMessageRequestMock, calledTimes count: Int) { let predicate = NSPredicate { _, _ in - print(mockRequest.requestsPerformed, count) return mockRequest.requestsPerformed == count } let expectation = XCTNSPredicateExpectation(predicate: predicate, object: mockRequest)