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

refactor: store merchant profile data by client ID and merchant ID #19

Merged
merged 3 commits into from
Jan 29, 2024
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
77 changes: 77 additions & 0 deletions Demo/Demo.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C6E7C48D2A38AC56003C05E4"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C6E7C48D2A38AC56003C05E4"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C6E7C48D2A38AC56003C05E4"
BuildableName = "Demo.app"
BlueprintName = "Demo"
ReferencedContainer = "container:Demo.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
1 change: 0 additions & 1 deletion Demo/Demo/UIKitContentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,3 @@ extension UIKitContentViewController: PayPalMessageViewEventDelegate {
statusTextView.text = "Applied"
}
}
// swiftlint:disable:this file_length
13 changes: 6 additions & 7 deletions Sources/PayPalMessages/Extensions/UserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")")
}
}
29 changes: 18 additions & 11 deletions Sources/PayPalMessages/IO/MerchantProfileProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ protocol MerchantProfileHashGetable {
func getMerchantProfileHash(
environment: Environment,
clientID: String,
merchantID: String?,
onCompletion: @escaping (String?) -> Void
)
}
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -57,34 +59,39 @@ 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)
}
}
}

// 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)
}
}
6 changes: 4 additions & 2 deletions Sources/PayPalMessages/IO/MerchantProfileRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ protocol MerchantProfileRequestable {
func fetchMerchantProfile(
environment: Environment,
clientID: String,
merchantID: String?,
onCompletion: @escaping (Result<MerchantProfileData, Error>) -> Void
)
}
Expand All @@ -27,14 +28,15 @@ class MerchantProfileRequest: MerchantProfileRequestable {
func fetchMerchantProfile(
environment: Environment,
clientID: String,
merchantID: String?,
onCompletion: @escaping (Result<MerchantProfileData, Error>) -> 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 {
Expand Down
6 changes: 5 additions & 1 deletion Sources/PayPalMessages/PayPalMessageViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
67 changes: 53 additions & 14 deletions Tests/PayPalMessagesTests/MerchantProfileProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class MerchantProfileRequestMock: MerchantProfileRequestable {
func fetchMerchantProfile(
environment: Environment,
clientID: String,
merchantID: String?,
onCompletion: @escaping (Result<MerchantProfileData, Error>) -> Void
) {
requestsPerformed += 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class MerchantProfileProviderMock: MerchantProfileHashGetable {
func getMerchantProfileHash(
environment: Environment,
clientID: String,
merchantID: String?,
onCompletion: @escaping (String?) -> Void
) {
switch scenario {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down