Skip to content

Commit d262c9a

Browse files
Implement timeout for Risk SDK
1 parent 3d160ad commit d262c9a

File tree

7 files changed

+139
-22
lines changed

7 files changed

+139
-22
lines changed

Checkout/Samples/CocoapodsSample/Podfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ target 'CheckoutCocoapodsSample' do
55
use_frameworks!
66

77
# Pods for CheckoutSDKCocoapodsSample
8-
pod 'Checkout', '4.3.6'
8+
# pod 'Checkout', '4.3.7'
9+
pod 'Checkout', :git => 'https://github.com/checkout/frames-ios.git', :branch => 'feature/risk-sdk-timeout-recovery'
910

1011
end

Checkout/Source/Logging/CheckoutLogEvent.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ enum CheckoutLogEvent: Equatable {
1919
case cvvRequested(SecurityCodeTokenRequestData)
2020
case cvvResponse(SecurityCodeTokenRequestData, TokenResponseData)
2121
case riskSDKCompletion
22+
case riskSDKTimeOut
2223

2324
func event(date: Date) -> Event {
2425
Event(
@@ -58,6 +59,8 @@ enum CheckoutLogEvent: Equatable {
5859
return "card_validator_cvv"
5960
case .riskSDKCompletion:
6061
return "risk_sdk_completion"
62+
case .riskSDKTimeOut:
63+
return "risk_sdk_time_out"
6164
}
6265
}
6366

@@ -70,7 +73,8 @@ enum CheckoutLogEvent: Equatable {
7073
.validateExpiryInteger,
7174
.validateCVV,
7275
.cvvRequested,
73-
.riskSDKCompletion:
76+
.riskSDKCompletion,
77+
.riskSDKTimeOut:
7478
return .info
7579
case .tokenResponse(_, let tokenResponseData),
7680
.cvvResponse(_, let tokenResponseData):
@@ -93,7 +97,8 @@ enum CheckoutLogEvent: Equatable {
9397
.validateExpiryString,
9498
.validateExpiryInteger,
9599
.validateCVV,
96-
.riskSDKCompletion:
100+
.riskSDKCompletion,
101+
.riskSDKTimeOut:
97102
return [:]
98103
case let .tokenRequested(tokenRequestData):
99104
return [

Checkout/Source/Tokenisation/CheckoutAPIService.swift

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {
145145
}
146146
}
147147

148+
let timeoutInterval: TimeInterval = 5.0
149+
private let taskCompletionQueue = DispatchQueue(label: "taskCompletionQueue", qos: .userInitiated)
150+
private var isTaskCompleted = false
151+
148152
private func createToken(requestParameters: NetworkManager.RequestParameters,
149153
paymentType: TokenRequest.TokenType,
150154
completion: @escaping (Result<TokenDetails, TokenisationError.TokenRequest>) -> Void) {
@@ -164,19 +168,10 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {
164168
return
165169
}
166170

167-
self.riskSDK.configure { configurationResult in
168-
switch configurationResult {
169-
case .failure:
170-
completion(.success(tokenDetails))
171-
logManager.resetCorrelationID()
172-
case .success():
173-
self.riskSDK.publishData(cardToken: tokenDetails.token) { _ in
174-
logManager.queue(event: .riskSDKCompletion)
175-
completion(.success(tokenDetails))
176-
logManager.resetCorrelationID()
177-
}
178-
}
179-
}
171+
self.callRiskSDK(tokenDetails: tokenDetails) {
172+
completion(.success(tokenDetails))
173+
}
174+
180175
case .errorResponse(let errorResponse):
181176
completion(.failure(.serverError(errorResponse)))
182177
logManager.resetCorrelationID()
@@ -187,6 +182,48 @@ final public class CheckoutAPIService: CheckoutAPIProtocol {
187182
}
188183
}
189184

185+
private func callRiskSDK(tokenDetails: TokenDetails,
186+
completion: @escaping () -> Void) {
187+
188+
/* Risk SDK calls can be finalised in 3 different ways
189+
1. When Risk SDK's configure(...) function completed successfully and publishData(...) completed successfully or not
190+
2. When Risk SDK's configure(...) function completed with failure
191+
3. When Risk SDK's configure(...) or publishData(...) functions hang and don't call their completion blocks.
192+
In this case, we wait for `self.timeoutInterval` amount of time and call the completion block anyway.
193+
194+
All these operations are done synchronously to avoid the completion closure getting called multiple times.
195+
*/
196+
197+
let finaliseRiskSDKCalls = {
198+
self.taskCompletionQueue.sync {
199+
if !self.isTaskCompleted {
200+
self.isTaskCompleted = true
201+
completion()
202+
}
203+
}
204+
}
205+
206+
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + timeoutInterval) {
207+
finaliseRiskSDKCalls()
208+
self.logManager.queue(event: .riskSDKTimeOut)
209+
}
210+
211+
self.riskSDK.configure { [weak self] configurationResult in
212+
guard let self else { return }
213+
switch configurationResult {
214+
case .failure:
215+
finaliseRiskSDKCalls()
216+
logManager.resetCorrelationID()
217+
case .success():
218+
self.riskSDK.publishData(cardToken: tokenDetails.token) { _ in
219+
self.logManager.queue(event: .riskSDKCompletion)
220+
finaliseRiskSDKCalls()
221+
self.logManager.resetCorrelationID()
222+
}
223+
}
224+
}
225+
}
226+
190227
private func logTokenResponse(tokenResponseResult: NetworkRequestResult<TokenResponse, TokenisationError.ServerError>,
191228
paymentType: TokenRequest.TokenType,
192229
httpURLResponse: HTTPURLResponse?) {

CheckoutTests/Stubs/StubRisk.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,24 @@ class StubRisk: RiskProtocol {
1414

1515
var configureCalledCount = 0
1616
var publishDataCalledCount = 0
17-
17+
18+
// If set to false, Risk SDK will hang and not call the completion block for that specific function.
19+
// It will mimic the behaviour of a bug we have. We need to call Frames's completion block after the defined timeout period in that case.
20+
var shouldConfigureFunctionCallCompletion: Bool = true
21+
var shouldPublishFunctionCallCompletion: Bool = true
22+
1823
func configure(completion: @escaping (Result<Void, RiskError.Configuration>) -> Void) {
1924
configureCalledCount += 1
20-
completion(.success(()))
25+
if shouldConfigureFunctionCallCompletion {
26+
completion(.success(()))
27+
}
2128
}
2229

2330
func publishData (cardToken: String? = nil, completion: @escaping (Result<PublishRiskData, RiskError.Publish>) -> Void) {
2431
publishDataCalledCount += 1
25-
completion(.success(PublishRiskData(deviceSessionId: "dsid_testDeviceSessionId")))
32+
if shouldPublishFunctionCallCompletion {
33+
completion(.success(PublishRiskData(deviceSessionId: "dsid_testDeviceSessionId")))
34+
}
2635
}
2736
}
2837

CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,67 @@ extension CheckoutAPIServiceTests {
354354
}
355355
}
356356
}
357+
358+
// Risk SDK Timeout Recovery Tests
359+
extension CheckoutAPIServiceTests {
360+
func testWhenRiskSDKCallsCompletionThenFramesReturnsSuccess() {
361+
let card = StubProvider.createCard()
362+
let tokenRequest = StubProvider.createTokenRequest()
363+
let requestParameters = StubProvider.createRequestParameters()
364+
let tokenResponse = StubProvider.createTokenResponse()
365+
let tokenDetails = StubProvider.createTokenDetails()
366+
367+
stubTokenRequestFactory.createToReturn = .success(tokenRequest)
368+
stubRequestFactory.createToReturn = .success(requestParameters)
369+
stubTokenDetailsFactory.createToReturn = tokenDetails
370+
371+
var result: Result<TokenDetails, TokenisationError.TokenRequest>?
372+
subject.createToken(.card(card)) { result = $0 }
373+
stubRequestExecutor.executeCalledWithCompletion?(.response(tokenResponse), HTTPURLResponse())
374+
375+
XCTAssertEqual(stubRisk.configureCalledCount, 1)
376+
XCTAssertEqual(stubRisk.publishDataCalledCount, 1)
377+
XCTAssertEqual(result, .success(tokenDetails))
378+
}
379+
380+
func testWhenRiskSDKConfigureHangsThenFramesSDKCancelsWaitingRiskSDKAndCallsCompletionBlockAnywayAfterTimeout() {
381+
stubRisk.shouldConfigureFunctionCallCompletion = false // Configure function will hang forever before it calls its completion closure
382+
verifyRiskSDKTimeoutRecovery(timeoutAddition: 1, expectedConfigureCallCount: 1, expectedPublishDataCallCount: 0)
383+
}
384+
385+
func testWhenRiskSDKPublishHangsThenFramesSDKCancelsWaitingRiskSDKAndCallsCompletionBlockAnywayAfterTimeout() {
386+
stubRisk.shouldPublishFunctionCallCompletion = false // Publish data function will hang forever before it calls its completion closure
387+
verifyRiskSDKTimeoutRecovery(timeoutAddition: 1, expectedConfigureCallCount: 1, expectedPublishDataCallCount: 1)
388+
}
389+
390+
func verifyRiskSDKTimeoutRecovery(timeoutAddition: Double,
391+
expectedConfigureCallCount: Int,
392+
expectedPublishDataCallCount: Int,
393+
file: StaticString = #file,
394+
line: UInt = #line) {
395+
let card = StubProvider.createCard()
396+
let tokenRequest = StubProvider.createTokenRequest()
397+
let tokenResponse = StubProvider.createTokenResponse()
398+
let requestParameters = StubProvider.createRequestParameters()
399+
let tokenDetails = StubProvider.createTokenDetails()
400+
401+
stubTokenRequestFactory.createToReturn = .success(tokenRequest)
402+
stubRequestFactory.createToReturn = .success(requestParameters)
403+
stubTokenDetailsFactory.createToReturn = tokenDetails
404+
405+
let expectation = self.expectation(description: "Frames will time out awaiting Risk SDK result")
406+
407+
var _: Result<TokenDetails, TokenisationError.TokenRequest>?
408+
subject.createToken(.card(card)) {
409+
410+
XCTAssertEqual(self.stubRisk.configureCalledCount, expectedConfigureCallCount, file: file, line: line)
411+
XCTAssertEqual(self.stubRisk.publishDataCalledCount, expectedPublishDataCallCount, file: file, line: line)
412+
XCTAssertEqual($0, .success(tokenDetails), file: file, line: line)
413+
414+
expectation.fulfill()
415+
}
416+
stubRequestExecutor.executeCalledWithCompletion?(.response(tokenResponse), HTTPURLResponse())
417+
418+
waitForExpectations(timeout: subject.timeoutInterval + timeoutAddition)
419+
}
420+
}

iOS Example Frame SPM/iOS Example Frame SPM.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1238,8 +1238,8 @@
12381238
isa = XCRemoteSwiftPackageReference;
12391239
repositoryURL = "https://github.com/checkout/frames-ios";
12401240
requirement = {
1241-
kind = exactVersion;
1242-
version = 4.3.6;
1241+
branch = "feature/risk-sdk-timeout-recovery";
1242+
kind = branch;
12431243
};
12441244
};
12451245
16C3F83E2A7927ED00690639 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {

iOS Example Frame/Podfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ target 'iOS Example Frame' do
66
use_frameworks!
77

88
# Pods for iOS Example Custom
9-
pod 'Frames', '4.3.6'
9+
# pod 'Frames', '4.3.6'
10+
pod 'Frames', :git => 'https://github.com/checkout/frames-ios.git', :branch => 'feature/risk-sdk-timeout-recovery'
1011

1112
end
1213

0 commit comments

Comments
 (0)