Merge pull request #173 from stripe-ios/davidme/handle-400-better
Retry `challenge_confirm` requests
This commit is contained in:
commit
2d51328b94
|
@ -1013,7 +1013,7 @@ extension STPAPIClient {
|
|||
// MARK: - ThreeDS2
|
||||
extension STPAPIClient {
|
||||
/// Kicks off 3DS2 authentication.
|
||||
func authenticate3DS2(
|
||||
@objc func authenticate3DS2(
|
||||
_ authRequestParams: STDSAuthenticationRequestParameters,
|
||||
sourceIdentifier sourceID: String,
|
||||
returnURL returnURLString: String?,
|
||||
|
@ -1049,7 +1049,7 @@ extension STPAPIClient {
|
|||
}
|
||||
|
||||
/// Endpoint to call to indicate that the challenge flow for a 3DS2 authentication has finished.
|
||||
func complete3DS2Authentication(
|
||||
@objc func complete3DS2Authentication(
|
||||
forSource sourceID: String, completion: @escaping STPBooleanSuccessBlock
|
||||
) {
|
||||
|
||||
|
|
|
@ -1393,8 +1393,9 @@ public class STPPaymentHandler: NSObject, SFSafariViewControllerDelegate, STPURL
|
|||
assert(false, "currentAction is an unknown type or nil intent.")
|
||||
}
|
||||
}
|
||||
|
||||
func _markChallengeCompleted(withCompletion completion: @escaping STPBooleanSuccessBlock) {
|
||||
|
||||
static let maxChallengeRetries = 3
|
||||
func _markChallengeCompleted(withCompletion completion: @escaping STPBooleanSuccessBlock, retryCount: Int = maxChallengeRetries) {
|
||||
guard let currentAction = currentAction,
|
||||
let threeDSSourceID = currentAction.nextAction()?.useStripeSDK?.threeDSSourceID
|
||||
else {
|
||||
|
@ -1431,10 +1432,23 @@ public class STPPaymentHandler: NSObject, SFSafariViewControllerDelegate, STPURL
|
|||
assert(false, "currentAction is an unknown type or nil intent.")
|
||||
}
|
||||
} else {
|
||||
completion(success, error)
|
||||
// This isn't guaranteed to succeed if the ACS isn't ready yet.
|
||||
// Try it a few more times if it fails with a 400. (RUN_MOBILESDK-126)
|
||||
if retryCount > 0 && (error as NSError?)?.code == STPErrorCode.invalidRequestError.rawValue {
|
||||
// Add some backoff time:
|
||||
let delayTime = TimeInterval(
|
||||
pow(Double(1 + Self.maxChallengeRetries - retryCount), Double(2))
|
||||
)
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delayTime) {
|
||||
self._markChallengeCompleted(withCompletion: completion,
|
||||
retryCount: retryCount - 1)
|
||||
}
|
||||
} else {
|
||||
completion(success, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
//
|
||||
// URLSession+Retry.swift
|
||||
// StripeiOS
|
||||
//
|
||||
// Created by David Estes on 3/26/21.
|
||||
// Copyright © 2021 Stripe, Inc. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
|
@ -7,7 +7,9 @@
|
|||
//
|
||||
|
||||
import XCTest
|
||||
import Foundation
|
||||
@testable import Stripe
|
||||
@testable import Stripe3DS2
|
||||
|
||||
class STPPaymentHandlerTests: XCTestCase {
|
||||
|
||||
|
@ -67,6 +69,60 @@ class STPPaymentHandlerTests: XCTestCase {
|
|||
// test in addition to fetching the payment intent
|
||||
wait(for: [paymentHandlerExpectation], timeout: 2*8)
|
||||
}
|
||||
|
||||
func testPaymentHandlerRetriesWithBackoff() {
|
||||
STPPaymentHandler.sharedHandler.apiClient = PaymentHandlerTestsMockAPIClient()
|
||||
let paymentHandlerExpectation = expectation(
|
||||
description: "paymentHandlerFinished")
|
||||
var inProgress = true
|
||||
|
||||
// Meaningless cert, generated for this test
|
||||
// Expires 3/2/2121: Apologies to future engineers!
|
||||
let cert = """
|
||||
MIIBijCB9AIBATANBgkqhkiG9w0BAQUFADANMQswCQYDVQQGEwJVUzAgFw0yMTAz
|
||||
MjYxODQyNDVaGA8yMTIxMDMwMjE4NDI0NVowDTELMAkGA1UEBhMCVVMwgZ8wDQYJ
|
||||
KoZIhvcNAQEBBQADgY0AMIGJAoGBAL6rIW6t+8eo1exqhvYt8H1vM+TyHNNychlD
|
||||
hILw745yXZQAy9ByRG3euYEydE3SFINgWBCUuwWmkNfsZUW7Uci1PBMglBFHJrE8
|
||||
8ZvtuJgnPkqmu97a9JkyROiaqAmqoMDP95HiZG5i3a1E/QPpPyYA3VJ/El17Qqkl
|
||||
aHN32qzjAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAUhxbGQ5sQMDUqFTvibU7RzqL
|
||||
dTaFhdjTDBu5YeIbXXUrJSG2AydXRq7OacRksnQhvNYXimfcgfse46XQG7rKUCfj
|
||||
kbazRiRxMZylTz8zbePAFcVq6zxJ+RBVrv51D+/JgbCcQ50nZiocllR0J9UL8CKZ
|
||||
obaUC2OjBbSuCZwF8Ig=
|
||||
"""
|
||||
let rootCA = """
|
||||
MIIBkDCB+gIJAJ3pmjFOkxTXMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNVBAYTAlVT
|
||||
MB4XDTIxMDMyNjE4NDEzMVoXDTIyMDMyNjE4NDEzMVowDTELMAkGA1UEBhMCVVMw
|
||||
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKmFDGPV77Fk/wgUMwbxjQk+bpUY
|
||||
cTjNBsjK3xMaUWeE17Sry6IguO1iWaXVey9YJ1Dm83PNO/5i9nHh3gmFhEJmc55T
|
||||
g+0tZQigjTcs5/BfmWtrfPYIWqKvIJqkkHrIEJnwavAS5OFGyDArHLwUtsgJbDmW
|
||||
tIeQg3EH/8BSWR0BAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEATY2aQvZZJLPgUr1/
|
||||
oDvRy6KZ6p7n3+jXF8DNvVOIaQRD4Ndk5NfStteIT5XvzfmD6QqpG3nlJ6Wy3oSP
|
||||
03KvO4GWIyP9cuP/QLaEmxJIYKwPrdxLkUHFfzyy8tN54xOWPxN4Up9gVN6pSdVk
|
||||
KWrsPfhPs3G57wir370Q69lV/8A=
|
||||
"""
|
||||
let iauss = STPIntentActionUseStripeSDK(encryptionInfo: ["certificate": cert, "directory_server_id": "0000000000", "root_certificate_authorities": [rootCA]], directoryServerName: "none", directoryServerKeyID: "none", serverTransactionID: "none", threeDSSourceID: "none", allResponseFields: [:])
|
||||
let action = STPIntentAction(type: .useStripeSDK, redirectToURL: nil, alipayHandleRedirect: nil, useStripeSDK: iauss, oxxoDisplayDetails: nil, allResponseFields: [:])
|
||||
let setupIntent = STPSetupIntent(stripeID: "test", clientSecret: "test", created: Date(), customerID: nil, stripeDescription: nil, livemode: false, nextAction: action, paymentMethodID: "test", paymentMethodTypes: [], status: .requiresAction, usage: .none, lastSetupError: nil, allResponseFields: [:])
|
||||
|
||||
// We expect this request to retry a few times with exponential backoff before calling the completion handler.
|
||||
STPPaymentHandler.sharedHandler._handleNextAction(for: setupIntent, with: self, returnURL: nil) { (status, si, error) in
|
||||
XCTAssertEqual(status, .failed)
|
||||
inProgress = false
|
||||
paymentHandlerExpectation.fulfill()
|
||||
}
|
||||
|
||||
let checkedStillInProgress = expectation(description: "Checked that we're still in progress after 2s")
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) {
|
||||
// Make sure we're still in progress after 2 seconds
|
||||
// This shows that we're retrying the 3DS2 request a few times
|
||||
// while applying an appropriate amount of backoff.
|
||||
XCTAssertEqual(inProgress, true)
|
||||
checkedStillInProgress.fulfill()
|
||||
}
|
||||
|
||||
wait(for: [paymentHandlerExpectation, checkedStillInProgress], timeout: 30)
|
||||
STPPaymentHandler.sharedHandler.apiClient = STPAPIClient.shared
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -77,3 +133,41 @@ extension STPPaymentHandlerTests: STPAuthenticationContext {
|
|||
|
||||
|
||||
}
|
||||
|
||||
class PaymentHandlerTestsMockAPIClient: STPAPIClient {
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
override func authenticate3DS2(_ authRequestParams: STDSAuthenticationRequestParameters, sourceIdentifier sourceID: String, returnURL returnURLString: String?, maxTimeout: Int, completion: @escaping STP3DS2AuthenticateCompletionBlock) {
|
||||
let jsonText = """
|
||||
{
|
||||
"state": "challenge_required",
|
||||
"livemode": "false",
|
||||
"ares" : {
|
||||
"dsTransID": "4e4750e7-6ab5-45a4-accf-9c668ed3b5a7",
|
||||
"acsTransID": "fa695a82-a48c-455d-9566-a652058dda27",
|
||||
"p_messageVersion": "1.0.5",
|
||||
"acsOperatorID": "acsOperatorUL",
|
||||
"sdkTransID": "D77EB83F-F317-4E29-9852-EBAAB55515B7",
|
||||
"eci": "00",
|
||||
"dsReferenceNumber": "3DS_LOA_DIS_PPFU_020100_00010",
|
||||
"acsReferenceNumber": "3DS_LOA_ACS_PPFU_020100_00009",
|
||||
"threeDSServerTransID": "fc7a39de-dc41-4b65-ba76-a322769b2efc",
|
||||
"messageVersion": "2.1.0",
|
||||
"authenticationValue": "AABBCCDDEEFFAABBCCDDEEFFAAA=",
|
||||
"messageType": "pArs",
|
||||
"transStatus": "C",
|
||||
"acsChallengeMandated": "NO"
|
||||
}
|
||||
}
|
||||
"""
|
||||
let json = try! JSONSerialization.jsonObject(with: jsonText.data(using: .utf8)!, options: []) as! [AnyHashable: Any]
|
||||
let response = STP3DS2AuthenticateResponse.decodedObject(fromAPIResponse: json)
|
||||
completion(response, nil)
|
||||
}
|
||||
|
||||
override func complete3DS2Authentication(forSource sourceID: String, completion: @escaping STPBooleanSuccessBlock) {
|
||||
return completion(false, NSError(domain: STPError.stripeDomain, code: STPErrorCode.invalidRequestError.rawValue, userInfo: [:]))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue