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

[v7] Update BTThreeDSecureRequest Parameters #1452

Open
wants to merge 11 commits into
base: v7
Choose a base branch
from
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* Update `BTSEPADirectDebitRequest` to make all properties accessible on the initializer only vs via the dot syntax.
* BraintreeLocalPayment
* Update `BTLocalPaymentRequest` to make all properties accessible on the initializer only vs via the dot syntax.
* BraintreeThreeDSecure
* Update `BraintreeThreeDSecure` to make all properties accessible on the initializer only vs via the dot syntax.

## unreleased
* BraintreePayPal
Expand Down
31 changes: 16 additions & 15 deletions Demo/Application/Features/ThreeDSecureViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,7 @@ class ThreeDSecureViewController: PaymentButtonBaseViewController {
}

private func createThreeDSecureRequest(with nonce: String) -> BTThreeDSecureRequest {
let request = BTThreeDSecureRequest()

request.threeDSecureRequestDelegate = self
request.amount = 10.32
request.nonce = nonce
request.accountType = .credit
request.requestedExemptionType = .lowValue
request.email = "[email protected]"
request.shippingMethod = .sameDay
request.uiType = .both
request.renderTypes = [.otp, .singleSelect, .multiSelect, .oob, .html]

let billingAddress = BTThreeDSecurePostalAddress()
billingAddress.givenName = "Jill"
billingAddress.surname = "Doe"
Expand All @@ -112,10 +101,22 @@ class ThreeDSecureViewController: PaymentButtonBaseViewController {
billingAddress.countryCodeAlpha2 = "US"
billingAddress.postalCode = "12345"
billingAddress.phoneNumber = "8101234567"

request.billingAddress = billingAddress
request.v2UICustomization = createUICustomization()


let request = BTThreeDSecureRequest(
accountType: .credit,
amount: 10.32,
billingAddress: billingAddress,
email: "[email protected]",
nonce: nonce,
renderTypes: [.otp, .singleSelect, .multiSelect, .oob, .html],
requestedExemptionType: .lowValue,
shippingMethod: .sameDay,
uiType: .both,
v2UICustomization: createUICustomization()
)

request.threeDSecureRequestDelegate = self

return request
}

Expand Down
152 changes: 82 additions & 70 deletions Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,51 +7,27 @@ import BraintreeCore
/// Used to initialize a 3D Secure payment flow
@objcMembers public class BTThreeDSecureRequest: NSObject {

// MARK: - Public Properties

/// A nonce to be verified by ThreeDSecure
public var nonce: String?
// MARK: - Internal Properties

var accountType: BTThreeDSecureAccountType
var additionalInformation: BTThreeDSecureAdditionalInformation?
var amount: NSDecimalNumber?
var billingAddress: BTThreeDSecurePostalAddress?
var cardAddChallengeRequested: Bool
var challengeRequested: Bool
var customFields: [String: String]?
var dataOnlyRequested: Bool
var dfReferenceID: String?
var email: String?
var exemptionRequested: Bool
var mobilePhoneNumber: String?
var nonce: String?
var renderTypes: [BTThreeDSecureRenderType]?
var requestedExemptionType: BTThreeDSecureRequestedExemptionType
var shippingMethod: BTThreeDSecureShippingMethod
var uiType: BTThreeDSecureUIType
var v2UICustomization: BTThreeDSecureV2UICustomization?

/// Object where each key is the name of a custom field which has been configured in the Control Panel. In the Control Panel you can configure 3D Secure Rules which trigger on certain values.
public var customFields: [String: String]?

/// The amount for the transaction
public var amount: NSDecimalNumber? = 0

/// Optional. The account type selected by the cardholder
/// - Note: Some cards can be processed using either a credit or debit account and cardholders have the option to choose which account to use.
public var accountType: BTThreeDSecureAccountType = .unspecified

/// Optional. The billing address used for verification
public var billingAddress: BTThreeDSecurePostalAddress?

/// Optional. The mobile phone number used for verification
/// - Note: Only numbers. Remove dashes, parentheses and other characters
public var mobilePhoneNumber: String?

/// Optional. The email used for verification
public var email: String?

/// Optional. The shipping method chosen for the transaction
public var shippingMethod: BTThreeDSecureShippingMethod = .unspecified

/// Optional. The additional information used for verification
public var additionalInformation: BTThreeDSecureAdditionalInformation?

/// Optional. If set to true, an authentication challenge will be forced if possible.
public var challengeRequested: Bool = false

/// Optional. If set to true, an exemption to the authentication challenge will be requested.
public var exemptionRequested: Bool = false

/// Optional. The exemption type to be requested. If an exemption is requested and the exemption's conditions are satisfied, then it will be applied.
public var requestedExemptionType: BTThreeDSecureRequestedExemptionType = .unspecified

/// Optional. Indicates whether to use the data only flow. In this flow, frictionless 3DS is ensured for Mastercard cardholders as the card scheme provides a risk score
/// for the issuer to determine whether to approve. If data only is not supported by the processor, a validation error will be raised.
/// Non-Mastercard cardholders will fallback to a normal 3DS flow.
public var dataOnlyRequested: Bool = false

// NEXT_MAJOR_VERSION remove cardAddChallenge in favor of cardAddChallengeRequested
/// Optional. An authentication created using this property should only be used for adding a payment method to the merchant's vault and not for creating transactions.
///
Expand All @@ -66,38 +42,74 @@ import BraintreeCore
get { _cardAddChallenge }
set { _cardAddChallenge = newValue }
}

// swiftlint:disable identifier_name
/// Internal property for `cardAddChallenge`. Created to avoid deprecation warnings upon accessing
/// `cardAddChallenge` directly within our SDK. Use this value internally instead.
var _cardAddChallenge: BTThreeDSecureCardAddChallenge = .unspecified
// swiftlint:enable identifier_name

/// Optional. An authentication created using this flag should only be used for vaulting operations (creation of customers' credit cards or payment methods) and not for creating transactions.
/// If set to `true`, a card-add challenge will be requested from the issuer.
/// If set to `false`, a card-add challenge will not be requested.
/// If the parameter is missing, a card-add challenge will only be requested for $0 amount.
public var cardAddChallengeRequested: Bool = false

/// Optional. UI Customization for 3DS2 challenge views.
public var v2UICustomization: BTThreeDSecureV2UICustomization?

/// Optional. Sets all UI types that the device supports for displaying specific challenge user interfaces in the 3D Secure challenge.
///
/// Defaults to `.both`
public var uiType: BTThreeDSecureUIType = .both

/// Optional. List of all the render types that the device supports for displaying specific challenge user interfaces within the 3D Secure challenge.
///
/// - Note: When using `BTThreeDSecureUIType.both` or `BTThreeDSecureUIType.html`, all `BTThreeDSecureRenderType` options must be set.
/// When using `BTThreeDSecureUIType.native`, all `BTThreeDSecureRenderType` options except `.html` must be set.
public var renderTypes: [BTThreeDSecureRenderType]?


/// A delegate for receiving information about the ThreeDSecure payment flow.
public weak var threeDSecureRequestDelegate: BTThreeDSecureRequestDelegate?

// MARK: - Internal Properties
// MARK: - Initializer

/// The dfReferenceID for the session. Exposed for testing.
var dfReferenceID: String?
/// Creates a `BTThreeDSecureRequest`
/// - Parameters:
/// - accountType: Optional. The account type selected by the cardholder. Some cards can be processed using either a credit or debit account and cardholders have the option to choose which account to use.
/// - additionalInformation: Optional. The additional information used for verification.
/// - amount: The amount for the transaction.
agedd marked this conversation as resolved.
Show resolved Hide resolved
/// - billingAddress: Optional. The billing address used for verification
/// - cardAddChallengeRequested: Optional. An authentication created using this flag should only be used for vaulting operations (creation of customers' credit cards or payment methods) and not for creating transactions. If set to `true`, a card-add challenge will be requested from the issuer. If set to `false`, a card-add challenge will not be requested. If the parameter is missing, a card-add challenge will only be requested for $0 amount.
/// - challengeRequested: Optional. If set to true, an authentication challenge will be forced if possible.
/// - customFields: Object where each key is the name of a custom field which has been configured in the Control Panel. In the Control Panel you can configure 3D Secure Rules which trigger on certain values.
agedd marked this conversation as resolved.
Show resolved Hide resolved
/// - dataOnlyRequested: Optional. Indicates whether to use the data only flow. In this flow, frictionless 3DS is ensured for Mastercard cardholders as the card scheme provides a risk score for the issuer to determine whether to approve. If data only is not supported by the processor, a validation error will be raised. Non-Mastercard cardholders will fallback to a normal 3DS flow.
/// - dfReferenceID: The dfReferenceID for the session. Exposed for testing.
agedd marked this conversation as resolved.
Show resolved Hide resolved
/// - email: Optional. The email used for verification.
/// - exemptionRequested: Optional. If set to true, an exemption to the authentication challenge will be requested.
/// - mobilePhoneNumber: Optional. The mobile phone number used for verification. Only numbers. Remove dashes, parentheses and other characters.
/// - nonce: A nonce to be verified by ThreeDSecure
agedd marked this conversation as resolved.
Show resolved Hide resolved
/// - renderTypes: Optional: List of all the render types that the device supports for displaying specific challenge user interfaces within the 3D Secure challenge. When using `BTThreeDSecureUIType.both` or `BTThreeDSecureUIType.html`, all `BTThreeDSecureRenderType` options must be set. When using `BTThreeDSecureUIType.native`, all `BTThreeDSecureRenderType` options except `.html` must be set.
/// - requestedExemptionType: Optional. The exemption type to be requested. If an exemption is requested and the exemption's conditions are satisfied, then it will be applied.
/// - shippingMethod: Optional. The shipping method chosen for the transaction
/// - uiType: Optional: Sets all UI types that the device supports for displaying specific challenge user interfaces in the 3D Secure challenge. Defaults to `.both`
/// - v2UICustomization: Optional. UI Customization for 3DS2 challenge views.
public init(
accountType: BTThreeDSecureAccountType = .unspecified,
additionalInformation: BTThreeDSecureAdditionalInformation? = nil,
amount: NSDecimalNumber? = 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on this logic it looks like both amount and the delegate may be required:

if request.amount?.decimalValue.isNaN == true || request.amount == nil {
NSLog("%@ BTThreeDSecureRequest amount can not be nil or NaN.", BTLogLevelDescription.string(for: .critical))
let error = BTThreeDSecureError.configuration("BTThreeDSecureRequest amount can not be nil or NaN.")
notifyFailure(with: error, completion: completion)
return
}
if request.threeDSecureRequestDelegate == nil {
let message = "Configuration Error: threeDSecureRequestDelegate can not be nil when versionRequested is 2."
let error = BTThreeDSecureError.configuration(message)
notifyFailure(with: error, completion: completion)
return
}

Copy link
Contributor Author

@agedd agedd Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh yep, good catch! the amount does seem to be required (i've also confirmed this by raising a BT 3D Secure General Inquiry workflow request) - as for the delegate, it seems that it was also previously optional so i wonder if it's okay to leave that as is 👀

billingAddress: BTThreeDSecurePostalAddress? = nil,
cardAddChallengeRequested: Bool = false,
challengeRequested: Bool = false,
customFields: [String : String]? = nil,
dataOnlyRequested: Bool = false,
dfReferenceID: String? = nil,
email: String? = nil,
exemptionRequested: Bool = false,
mobilePhoneNumber: String? = nil,
nonce: String? = nil,
renderTypes: [BTThreeDSecureRenderType]? = nil,
requestedExemptionType: BTThreeDSecureRequestedExemptionType = .unspecified,
shippingMethod: BTThreeDSecureShippingMethod = .unspecified,
uiType: BTThreeDSecureUIType = .both,
v2UICustomization: BTThreeDSecureV2UICustomization? = nil
) {
agedd marked this conversation as resolved.
Show resolved Hide resolved
self.accountType = accountType
self.additionalInformation = additionalInformation
self.amount = amount
self.billingAddress = billingAddress
self.cardAddChallengeRequested = cardAddChallengeRequested
self.challengeRequested = challengeRequested
self.customFields = customFields
self.dataOnlyRequested = dataOnlyRequested
self.dfReferenceID = dfReferenceID
self.email = email
self.exemptionRequested = exemptionRequested
self.mobilePhoneNumber = mobilePhoneNumber
self.nonce = nonce
self.renderTypes = renderTypes
self.requestedExemptionType = requestedExemptionType
self.shippingMethod = shippingMethod
self.uiType = uiType
self.v2UICustomization = v2UICustomization
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import XCTest
class BTThreeDSecureClient_Tests: XCTestCase {

var mockAPIClient = MockAPIClient(authorization: TestClientTokenFactory.token(withVersion: 3))!
var threeDSecureRequest = BTThreeDSecureRequest()
var threeDSecureRequest: BTThreeDSecureRequest!
var client: BTThreeDSecureClient!
var mockThreeDSecureRequestDelegate : MockThreeDSecureRequestDelegate!

Expand All @@ -18,8 +18,7 @@ class BTThreeDSecureClient_Tests: XCTestCase {

override func setUp() {
super.setUp()
threeDSecureRequest.amount = 10.0
threeDSecureRequest.nonce = "fake-card-nonce"
threeDSecureRequest = BTThreeDSecureRequest(amount: 10.0, nonce: "fake-card-nonce")
client = BTThreeDSecureClient(apiClient: mockAPIClient)
client.cardinalSession = MockCardinalSession()
mockThreeDSecureRequestDelegate = MockThreeDSecureRequestDelegate()
Expand All @@ -29,20 +28,7 @@ class BTThreeDSecureClient_Tests: XCTestCase {

func testPerformThreeDSecureLookup_sendsAllParameters() {
let expectation = self.expectation(description: "willCallCompletion")

threeDSecureRequest.nonce = "fake-card-nonce"
threeDSecureRequest.amount = 9.97
threeDSecureRequest.dfReferenceID = "df-reference-id"
threeDSecureRequest.accountType = .credit
threeDSecureRequest.challengeRequested = true
threeDSecureRequest.exemptionRequested = true
threeDSecureRequest.dataOnlyRequested = true
threeDSecureRequest.cardAddChallenge = .requested

threeDSecureRequest.mobilePhoneNumber = "5151234321"
threeDSecureRequest.email = "[email protected]"
threeDSecureRequest.shippingMethod = .priority


let billingAddress = BTThreeDSecurePostalAddress()
billingAddress.givenName = "Joe"
billingAddress.surname = "Guy"
Expand All @@ -54,7 +40,21 @@ class BTThreeDSecureClient_Tests: XCTestCase {
billingAddress.region = "CA"
billingAddress.countryCodeAlpha2 = "US"
billingAddress.postalCode = "54321"
threeDSecureRequest.billingAddress = billingAddress

threeDSecureRequest = BTThreeDSecureRequest(
accountType: .credit,
amount: 9.97,
billingAddress: billingAddress,
cardAddChallengeRequested: true,
challengeRequested: true,
dataOnlyRequested: true,
dfReferenceID: "df-reference-id",
email: "[email protected]",
exemptionRequested: true,
mobilePhoneNumber: "5151234321",
nonce: "fake-card-nonce",
shippingMethod: .priority
)

client.performThreeDSecureLookup(threeDSecureRequest) { (lookup, error) in
XCTAssertEqual(self.mockAPIClient.lastPOSTParameters!["amount"] as! NSDecimalNumber, 9.97)
Expand Down Expand Up @@ -109,12 +109,13 @@ class BTThreeDSecureClient_Tests: XCTestCase {

func testPerformThreeDSecureLookup_whenCardAddChallengeNotRequested_sendsCardAddFalse() {
let expectation = self.expectation(description: "willCallCompletion")

threeDSecureRequest.nonce = "fake-card-nonce"
threeDSecureRequest.amount = 9.97
threeDSecureRequest.dfReferenceID = "df-reference-id"

threeDSecureRequest.cardAddChallenge = .notRequested

threeDSecureRequest = BTThreeDSecureRequest(
amount: 9.97,
cardAddChallengeRequested: false,
dfReferenceID: "df-reference-id",
nonce: "fake-card-nonce"
)

client.performThreeDSecureLookup(threeDSecureRequest) { (lookup, error) in
XCTAssertFalse(self.mockAPIClient.lastPOSTParameters!["cardAdd"] as! Bool)
Expand All @@ -127,13 +128,15 @@ class BTThreeDSecureClient_Tests: XCTestCase {

func testPerformThreeDSecureLookup_whenCardAddChallengeRequestedNotSet_doesNotSendCardAddParameter() {
let expectation = self.expectation(description: "willCallCompletion")

threeDSecureRequest.nonce = "fake-card-nonce"
threeDSecureRequest.amount = 9.97
threeDSecureRequest.dfReferenceID = "df-reference-id"

threeDSecureRequest = BTThreeDSecureRequest(
amount: 9.97,
dfReferenceID: "df-reference-id",
nonce: "fake-card-nonce"
)

client.performThreeDSecureLookup(threeDSecureRequest) { (lookup, error) in
XCTAssertNil(self.mockAPIClient.lastPOSTParameters!["cardAdd"] as? Bool)
XCTAssertFalse(self.mockAPIClient.lastPOSTParameters!["cardAdd"] as! Bool)

expectation.fulfill()
}
Expand All @@ -142,10 +145,13 @@ class BTThreeDSecureClient_Tests: XCTestCase {
}

func testPerformThreeDSecureLookup_whenCardAddChallengeRequested_sendsCardAddTrue() {
threeDSecureRequest.nonce = "fake-card-nonce"
threeDSecureRequest.amount = 9.97
threeDSecureRequest.dfReferenceID = "df-reference-id"
threeDSecureRequest.cardAddChallengeRequested = true

threeDSecureRequest = BTThreeDSecureRequest(
amount: 9.97,
cardAddChallengeRequested: true,
dfReferenceID: "df-reference-id",
nonce: "fake-card-nonce"
)

let expectation = expectation(description: "willCallCompletion")

Expand Down
6 changes: 5 additions & 1 deletion V7_MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ _Documentation for v7 will be published to https://developer.paypal.com/braintre
1. [Venmo](#venmo)
1. [SEPA Direct Debit](#sepa-direct-debit)
1. [Local Payments](#local-payments)
1. [3D Secure](#3d-secure)]
1. [PayPal Native Checkout](#paypal-native-checkout)


## Supported Versions

v7 bumps to a minimum deployment target of iOS 16+.
Expand All @@ -29,7 +31,9 @@ All properties within `BTSEPADirectDebitRequest` can only be accessed on the ini
## Local Payments
v7 updates `BTLocalPaymentRequest` to require setting all properties through the initializer, removing support for dot syntax. To construct a `BTLocalPaymentRequest`, pass the properties directly in the initializer.

## 3D Secure
All properties within `BTThreeDSecureRequest` can only be accessed on the initializer vs via the dot syntax.

## PayPal Native Checkout
The PayPal Native Checkout integration is no longer supported. Please remove it from your app and
use the [PayPal (web)](https://developer.paypal.com/braintree/docs/guides/paypal/overview/ios/v6) integration.

Loading