diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index c6d6aba14..61bf5f9b7 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. #### 4.x Releases + +## [4.3.7](https://github.com/checkout/frames-ios/releases/tag/4.3.7) + +Released on 2024-08-06 + +Updates: + +- Removing the need to await for Risk SDK completion + +## [4.3.6](https://github.com/checkout/frames-ios/releases/tag/4.3.6) + +Released on 2024-05-30 + +Updates: + +- Fixing a crash within the Risk SDK implementation. + ## [4.3.5](https://github.com/checkout/frames-ios/releases/tag/4.3.5) Released on 2024-05-01 diff --git a/Checkout.podspec b/Checkout.podspec index d656ce286..34f70515b 100644 --- a/Checkout.podspec +++ b/Checkout.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Checkout' - s.version = '4.3.6' + s.version = '4.3.7' s.summary = 'Checkout SDK for iOS' s.description = <<-DESC diff --git a/Checkout/Samples/CocoapodsSample/Podfile b/Checkout/Samples/CocoapodsSample/Podfile index 806658a66..1aab611b2 100644 --- a/Checkout/Samples/CocoapodsSample/Podfile +++ b/Checkout/Samples/CocoapodsSample/Podfile @@ -5,6 +5,5 @@ target 'CheckoutCocoapodsSample' do use_frameworks! # Pods for CheckoutSDKCocoapodsSample - pod 'Checkout', '4.3.6' - + pod 'Checkout', '4.3.7' end diff --git a/Checkout/Source/Logging/CheckoutLogEvent.swift b/Checkout/Source/Logging/CheckoutLogEvent.swift index c48d4162f..01dae8118 100644 --- a/Checkout/Source/Logging/CheckoutLogEvent.swift +++ b/Checkout/Source/Logging/CheckoutLogEvent.swift @@ -19,6 +19,7 @@ enum CheckoutLogEvent: Equatable { case cvvRequested(SecurityCodeTokenRequestData) case cvvResponse(SecurityCodeTokenRequestData, TokenResponseData) case riskSDKCompletion + case riskSDKTimeOut func event(date: Date) -> Event { Event( @@ -58,6 +59,8 @@ enum CheckoutLogEvent: Equatable { return "card_validator_cvv" case .riskSDKCompletion: return "risk_sdk_completion" + case .riskSDKTimeOut: + return "risk_sdk_time_out" } } @@ -70,7 +73,8 @@ enum CheckoutLogEvent: Equatable { .validateExpiryInteger, .validateCVV, .cvvRequested, - .riskSDKCompletion: + .riskSDKCompletion, + .riskSDKTimeOut: return .info case .tokenResponse(_, let tokenResponseData), .cvvResponse(_, let tokenResponseData): @@ -93,7 +97,8 @@ enum CheckoutLogEvent: Equatable { .validateExpiryString, .validateExpiryInteger, .validateCVV, - .riskSDKCompletion: + .riskSDKCompletion, + .riskSDKTimeOut: return [:] case let .tokenRequested(tokenRequestData): return [ diff --git a/Checkout/Source/Tokenisation/CheckoutAPIService.swift b/Checkout/Source/Tokenisation/CheckoutAPIService.swift index 6699e2ce4..0a86ddd99 100644 --- a/Checkout/Source/Tokenisation/CheckoutAPIService.swift +++ b/Checkout/Source/Tokenisation/CheckoutAPIService.swift @@ -145,6 +145,10 @@ final public class CheckoutAPIService: CheckoutAPIProtocol { } } + let timeoutInterval: TimeInterval = 5.0 + private let taskCompletionQueue = DispatchQueue(label: "taskCompletionQueue", qos: .userInitiated) + private var isTaskCompleted = false + private func createToken(requestParameters: NetworkManager.RequestParameters, paymentType: TokenRequest.TokenType, completion: @escaping (Result) -> Void) { @@ -164,19 +168,10 @@ final public class CheckoutAPIService: CheckoutAPIProtocol { return } - self.riskSDK.configure { configurationResult in - switch configurationResult { - case .failure: - completion(.success(tokenDetails)) - logManager.resetCorrelationID() - case .success(): - self.riskSDK.publishData(cardToken: tokenDetails.token) { _ in - logManager.queue(event: .riskSDKCompletion) - completion(.success(tokenDetails)) - logManager.resetCorrelationID() - } - } - } + self.callRiskSDK(tokenDetails: tokenDetails) { + completion(.success(tokenDetails)) + } + case .errorResponse(let errorResponse): completion(.failure(.serverError(errorResponse))) logManager.resetCorrelationID() @@ -187,6 +182,48 @@ final public class CheckoutAPIService: CheckoutAPIProtocol { } } + private func callRiskSDK(tokenDetails: TokenDetails, + completion: @escaping () -> Void) { + + /* Risk SDK calls can be finalised in 3 different ways + 1. When Risk SDK's configure(...) function completed successfully and publishData(...) completed successfully or not + 2. When Risk SDK's configure(...) function completed with failure + 3. When Risk SDK's configure(...) or publishData(...) functions hang and don't call their completion blocks. + In this case, we wait for `self.timeoutInterval` amount of time and call the completion block anyway. + + All these operations are done synchronously to avoid the completion closure getting called multiple times. + */ + + let finaliseRiskSDKCalls = { + self.taskCompletionQueue.sync { + if !self.isTaskCompleted { + self.isTaskCompleted = true + completion() + } + } + } + + DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + timeoutInterval) { + finaliseRiskSDKCalls() + self.logManager.queue(event: .riskSDKTimeOut) + } + + self.riskSDK.configure { [weak self] configurationResult in + guard let self else { return } + switch configurationResult { + case .failure: + finaliseRiskSDKCalls() + self.logManager.resetCorrelationID() + case .success(): + self.riskSDK.publishData(cardToken: tokenDetails.token) { _ in + self.logManager.queue(event: .riskSDKCompletion) + finaliseRiskSDKCalls() + self.logManager.resetCorrelationID() + } + } + } + } + private func logTokenResponse(tokenResponseResult: NetworkRequestResult, paymentType: TokenRequest.TokenType, httpURLResponse: HTTPURLResponse?) { diff --git a/Checkout/Source/Validation/Constants.swift b/Checkout/Source/Validation/Constants.swift index 2e078af46..6ea6dbf58 100644 --- a/Checkout/Source/Validation/Constants.swift +++ b/Checkout/Source/Validation/Constants.swift @@ -25,7 +25,7 @@ public enum Constants { } enum Product { - static let version = "4.3.6" + static let version = "4.3.7" static let name = "checkout-ios-sdk" static let userAgent = "checkout-sdk-ios/\(version)" } diff --git a/CheckoutTests/Stubs/StubRisk.swift b/CheckoutTests/Stubs/StubRisk.swift index b7985603c..d2103a696 100644 --- a/CheckoutTests/Stubs/StubRisk.swift +++ b/CheckoutTests/Stubs/StubRisk.swift @@ -14,15 +14,24 @@ class StubRisk: RiskProtocol { var configureCalledCount = 0 var publishDataCalledCount = 0 - + + // If set to false, Risk SDK will hang and not call the completion block for that specific function. + // 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. + var shouldConfigureFunctionCallCompletion: Bool = true + var shouldPublishFunctionCallCompletion: Bool = true + func configure(completion: @escaping (Result) -> Void) { configureCalledCount += 1 - completion(.success(())) + if shouldConfigureFunctionCallCompletion { + completion(.success(())) + } } func publishData (cardToken: String? = nil, completion: @escaping (Result) -> Void) { publishDataCalledCount += 1 - completion(.success(PublishRiskData(deviceSessionId: "dsid_testDeviceSessionId"))) + if shouldPublishFunctionCallCompletion { + completion(.success(PublishRiskData(deviceSessionId: "dsid_testDeviceSessionId"))) + } } } diff --git a/CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift b/CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift index 98780039e..a33ec53d8 100644 --- a/CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift +++ b/CheckoutTests/Tokenisation/CheckoutAPIServiceTests.swift @@ -354,3 +354,67 @@ extension CheckoutAPIServiceTests { } } } + +// Risk SDK Timeout Recovery Tests +extension CheckoutAPIServiceTests { + func testWhenRiskSDKCallsCompletionThenFramesReturnsSuccess() { + let card = StubProvider.createCard() + let tokenRequest = StubProvider.createTokenRequest() + let requestParameters = StubProvider.createRequestParameters() + let tokenResponse = StubProvider.createTokenResponse() + let tokenDetails = StubProvider.createTokenDetails() + + stubTokenRequestFactory.createToReturn = .success(tokenRequest) + stubRequestFactory.createToReturn = .success(requestParameters) + stubTokenDetailsFactory.createToReturn = tokenDetails + + var result: Result? + subject.createToken(.card(card)) { result = $0 } + stubRequestExecutor.executeCalledWithCompletion?(.response(tokenResponse), HTTPURLResponse()) + + XCTAssertEqual(stubRisk.configureCalledCount, 1) + XCTAssertEqual(stubRisk.publishDataCalledCount, 1) + XCTAssertEqual(result, .success(tokenDetails)) + } + + func testWhenRiskSDKConfigureHangsThenFramesSDKCancelsWaitingRiskSDKAndCallsCompletionBlockAnywayAfterTimeout() { + stubRisk.shouldConfigureFunctionCallCompletion = false // Configure function will hang forever before it calls its completion closure + verifyRiskSDKTimeoutRecovery(timeoutAddition: 1, expectedConfigureCallCount: 1, expectedPublishDataCallCount: 0) + } + + func testWhenRiskSDKPublishHangsThenFramesSDKCancelsWaitingRiskSDKAndCallsCompletionBlockAnywayAfterTimeout() { + stubRisk.shouldPublishFunctionCallCompletion = false // Publish data function will hang forever before it calls its completion closure + verifyRiskSDKTimeoutRecovery(timeoutAddition: 1, expectedConfigureCallCount: 1, expectedPublishDataCallCount: 1) + } + + func verifyRiskSDKTimeoutRecovery(timeoutAddition: Double, + expectedConfigureCallCount: Int, + expectedPublishDataCallCount: Int, + file: StaticString = #file, + line: UInt = #line) { + let card = StubProvider.createCard() + let tokenRequest = StubProvider.createTokenRequest() + let tokenResponse = StubProvider.createTokenResponse() + let requestParameters = StubProvider.createRequestParameters() + let tokenDetails = StubProvider.createTokenDetails() + + stubTokenRequestFactory.createToReturn = .success(tokenRequest) + stubRequestFactory.createToReturn = .success(requestParameters) + stubTokenDetailsFactory.createToReturn = tokenDetails + + let expectation = self.expectation(description: "Frames will time out awaiting Risk SDK result") + + var _: Result? + subject.createToken(.card(card)) { + + XCTAssertEqual(self.stubRisk.configureCalledCount, expectedConfigureCallCount, file: file, line: line) + XCTAssertEqual(self.stubRisk.publishDataCalledCount, expectedPublishDataCallCount, file: file, line: line) + XCTAssertEqual($0, .success(tokenDetails), file: file, line: line) + + expectation.fulfill() + } + stubRequestExecutor.executeCalledWithCompletion?(.response(tokenResponse), HTTPURLResponse()) + + waitForExpectations(timeout: subject.timeoutInterval + timeoutAddition) + } +} diff --git a/Frames.podspec b/Frames.podspec index e231e81bf..c50a2bc65 100644 --- a/Frames.podspec +++ b/Frames.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Frames" - s.version = "4.3.6" + s.version = "4.3.7" s.summary = "Checkout API Client, Payment Form UI and Utilities in Swift" s.description = <<-DESC Checkout API Client and Payment Form Utilities in Swift. @@ -21,6 +21,6 @@ Pod::Spec.new do |s| s.dependency 'PhoneNumberKit' s.dependency 'CheckoutEventLoggerKit', '~> 1.2.4' - s.dependency 'Checkout', '4.3.6' + s.dependency 'Checkout', '4.3.7' end diff --git a/Source/Core/Constants/Constants.swift b/Source/Core/Constants/Constants.swift index 316c89162..85e5a8f58 100644 --- a/Source/Core/Constants/Constants.swift +++ b/Source/Core/Constants/Constants.swift @@ -8,7 +8,7 @@ enum Constants { static let productName = "frames-ios-sdk" - static let version = "4.3.6" + static let version = "4.3.7" static let userAgent = "checkout-sdk-frames-ios/\(version)" enum Logging { diff --git a/iOS Example Frame SPM/iOS Example Frame SPM.xcodeproj/project.pbxproj b/iOS Example Frame SPM/iOS Example Frame SPM.xcodeproj/project.pbxproj index 665956249..93a60ebec 100644 --- a/iOS Example Frame SPM/iOS Example Frame SPM.xcodeproj/project.pbxproj +++ b/iOS Example Frame SPM/iOS Example Frame SPM.xcodeproj/project.pbxproj @@ -1239,7 +1239,7 @@ repositoryURL = "https://github.com/checkout/frames-ios"; requirement = { kind = exactVersion; - version = 4.3.6; + version = 4.3.7; }; }; 16C3F83E2A7927ED00690639 /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = { diff --git a/iOS Example Frame/Podfile b/iOS Example Frame/Podfile index aba082efa..7668c1d86 100644 --- a/iOS Example Frame/Podfile +++ b/iOS Example Frame/Podfile @@ -6,8 +6,7 @@ target 'iOS Example Frame' do use_frameworks! # Pods for iOS Example Custom - pod 'Frames', '4.3.6' - + pod 'Frames', '4.3.7' end post_install do |installer|