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

[QL] Stricter Logic For BA Token for App Switch #1274

Merged
merged 10 commits into from
Apr 24, 2024
2 changes: 1 addition & 1 deletion Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1713,11 +1713,11 @@
A95229C624FD949D006F7D25 /* BTConfiguration+PayPal_Tests.swift */,
BEDEAF102AC1D049004EA970 /* BTPayPalAccountNonce_Tests.swift */,
3B7A261229C35B670087059D /* BTPayPalAnalytics_Tests.swift */,
BEBA590E2BB1B5B9005FA8A2 /* BTPayPalReturnURL_Tests.swift */,
42FC237025CE0E110047C49A /* BTPayPalCheckoutRequest_Tests.swift */,
427F32DF25D1D62D00435294 /* BTPayPalClient_Tests.swift */,
BECB10C52B5999EE008D398E /* BTPayPalLineItem_Tests.swift */,
42FC218A25CDE0290047C49A /* BTPayPalRequest_Tests.swift */,
BEBA590E2BB1B5B9005FA8A2 /* BTPayPalReturnURL_Tests.swift */,
427F328F25D1A7B900435294 /* BTPayPalVaultRequest_Tests.swift */,
A9E5C1E424FD665D00EE691F /* Info.plist */,
);
Expand Down
45 changes: 28 additions & 17 deletions Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import BraintreeCore
#endif

/// The type of PayPal authentication flow to occur
enum PayPalRedirectType {
enum PayPalRedirectType: Equatable {

/// The in-app browser (ASWebAuthenticationSession) web checkout flow
case webBrowser(url: URL)
Expand All @@ -17,31 +17,42 @@ enum PayPalRedirectType {
/// Parses response body from `/v1/paypal_hermes/*` POST requests to determine the `PayPalRedirectType`
struct BTPayPalApprovalURLParser {

var redirectType: PayPalRedirectType

var pairingID: String? {
switch redirectType {
case .webBrowser(let url), .payPalApp(let url):
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?
.queryItems?
.compactMap { $0 }

if let baToken = queryItems?.filter({ $0.name == "ba_token" }).first?.value, !baToken.isEmpty {
return baToken
} else if let ecToken = queryItems?.filter({ $0.name == "token" }).first?.value, !ecToken.isEmpty {
return ecToken
}
let redirectType: PayPalRedirectType

return nil
private let url: URL

var ecToken: String? {
tdchow marked this conversation as resolved.
Show resolved Hide resolved
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?
.queryItems?
.compactMap { $0 }

if let ecToken = queryItems?.filter({ $0.name == "token" }).first?.value, !ecToken.isEmpty {
return ecToken
}

return nil
}


var baToken: String? {
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?
.queryItems?
.compactMap { $0 }

if let baToken = queryItems?.filter({ $0.name == "ba_token" }).first?.value, !baToken.isEmpty {
return baToken
}

return nil
}

init?(body: BTJSON, linkType: String?) {
if linkType == "universal", let payPalAppRedirectURL = body["agreementSetup"]["paypalAppApprovalUrl"].asURL() {
redirectType = .payPalApp(url: payPalAppRedirectURL)
url = payPalAppRedirectURL
} else if let approvalURL = body["paymentResource"]["redirectUrl"].asURL() ??
body["agreementSetup"]["approvalUrl"].asURL() {
redirectType = .webBrowser(url: approvalURL)
url = approvalURL
} else {
return nil
}
Expand Down
17 changes: 11 additions & 6 deletions Sources/BraintreePayPal/BTPayPalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -332,17 +332,22 @@ import BraintreeDataCollector
return
}

self.payPalContextID = approvalURL.pairingID
self.payPalContextID = approvalURL.baToken ?? approvalURL.ecToken
tdchow marked this conversation as resolved.
Show resolved Hide resolved

// TODO: remove NotificationCenter before merging into main DTBTSDK-3766
NotificationCenter.default.post(name: Notification.Name("BAToken"), object: self.payPalContextID)

let dataCollector = BTDataCollector(apiClient: self.apiClient)
self.clientMetadataID = self.payPalRequest?.riskCorrelationID ?? dataCollector.clientMetadataID(approvalURL.pairingID)
self.clientMetadataID = self.payPalRequest?.riskCorrelationID ?? dataCollector.clientMetadataID(self.payPalContextID)

switch approvalURL.redirectType {
case .payPalApp(let url):
self.launchPayPalApp(with: url, completion: completion)
guard let baToken = approvalURL.baToken else {
self.notifyFailure(with: BTPayPalError.missingBAToken, completion: completion)
return
}

self.launchPayPalApp(with: url, baToken: baToken, completion: completion)
case .webBrowser(let url):
self.handlePayPalRequest(with: url, paymentType: request.paymentType, completion: completion)
}
Expand All @@ -357,7 +362,7 @@ import BraintreeDataCollector
return application.canOpenURL(paypalURL)
}

private func launchPayPalApp(with payPalAppRedirectURL: URL, completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void) {
private func launchPayPalApp(with payPalAppRedirectURL: URL, baToken: String, completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void) {
apiClient.sendAnalyticsEvent(
BTPayPalAnalytics.appSwitchStarted,
linkType: linkType,
Expand All @@ -367,7 +372,7 @@ import BraintreeDataCollector

var urlComponents = URLComponents(url: payPalAppRedirectURL, resolvingAgainstBaseURL: true)
urlComponents?.queryItems = [
URLQueryItem(name: "ba_token", value: payPalContextID),
URLQueryItem(name: "ba_token", value: baToken),
URLQueryItem(name: "source", value: "braintree_sdk"),
URLQueryItem(name: "switch_initiated_time", value: String(Int(round(Date().timeIntervalSince1970 * 1000))))
]
Expand Down
7 changes: 7 additions & 0 deletions Sources/BraintreePayPal/BTPayPalError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable {
/// 11. App Switch could not complete
case appSwitchFailed

/// 12. Missing BA Token for App Switch
case missingBAToken

public static var errorDomain: String {
"com.braintreepayments.BTPayPalErrorDomain"
}
Expand Down Expand Up @@ -69,6 +72,8 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable {
return 10
case .appSwitchFailed:
return 11
case .missingBAToken:
return 12
}
}

Expand Down Expand Up @@ -98,6 +103,8 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable {
return "The App Switch return URL did not contain the cancel or success path."
case .appSwitchFailed:
return "UIApplication failed to perform app switch to PayPal."
case .missingBAToken:
return "Missing BA Token for PayPal App Switch."
}
}

Expand Down
52 changes: 51 additions & 1 deletion UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,26 @@ class BTPayPalClient_Tests: XCTestCase {
XCTAssertTrue(mockAPIClient.postedAnalyticsEvents.contains("paypal:tokenize:handle-return:started"))
}

func testTokenize_whenApprovalUrlContainsBAToken_sendsBATokenAsPayPalContextIDInAnalytics() {
mockAPIClient.cannedResponseBody = BTJSON(value: [
"agreementSetup": [
"approvalUrl": "https://www.paypal.com/agreements/approve?ba_token=A_FAKE_BA_TOKEN"
]
])

let mockWebAuthenticationSession = MockWebAuthenticationSession()
mockWebAuthenticationSession.cannedResponseURL = URL(string: "sdk.ios.braintree://onetouch/v1/success")
payPalClient.webAuthenticationSession = mockWebAuthenticationSession

let request = BTPayPalVaultRequest()
payPalClient.tokenize(request) { _, _ in }

XCTAssertEqual(mockAPIClient.postedPayPalContextID, "A_FAKE_BA_TOKEN")
XCTAssertEqual(mockAPIClient.postedLinkType, "deeplink")
XCTAssertEqual(mockAPIClient.postedPayPalAppInstalled, "false")
XCTAssertTrue(mockAPIClient.postedAnalyticsEvents.contains("paypal:tokenize:handle-return:started"))
}

// MARK: - Browser switch

func testTokenizePayPalAccount_whenPayPalPayLaterOffered_performsSwitchCorrectly() {
Expand Down Expand Up @@ -761,7 +781,37 @@ class BTPayPalClient_Tests: XCTestCase {
XCTFail("Expected integer value for query param `switch_initiated_time`")
}
}


func testTokenizeVaultAccount_whenPayPalAppApprovalURLMissingBAToken_returnsError() {
let fakeApplication = FakeApplication()
payPalClient.application = fakeApplication

mockAPIClient.cannedResponseBody = BTJSON(value: [
"agreementSetup": [
"paypalAppApprovalUrl": "https://www.some-url.com/some-path?token=value1"
]
])

let vaultRequest = BTPayPalVaultRequest(
userAuthenticationEmail: "fake@gmail.com",
enablePayPalAppSwitch: true,
universalLink: URL(string: "https://paypal.com")!
)

let expectation = expectation(description: "completion block called")
payPalClient.tokenize(vaultRequest) { nonce, error in
XCTAssertNil(nonce)

guard let error = error as NSError? else { XCTFail(); return }
XCTAssertEqual(error.code, 12)
XCTAssertEqual(error.localizedDescription, "Missing BA Token for PayPal App Switch.")
XCTAssertEqual(error.domain, "com.braintreepayments.BTPayPalErrorDomain")
expectation.fulfill()
}

waitForExpectations(timeout: 1)
}

func testTokenizeVaultAccount_whenOpenURLReturnsFalse_returnsError() {
let fakeApplication = FakeApplication()
fakeApplication.cannedOpenURLSuccess = false
Expand Down
Loading