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

Fix Bug where fetchPaymentMethodNonces Return Incorrect Type #1100

Merged
merged 14 commits into from
Sep 29, 2023
Merged
8 changes: 2 additions & 6 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 53;
objectVersion = 54;
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -150,7 +150,6 @@
A95C411724FAEF5100045045 /* BTCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7A094F51B8276E500D732CC /* BTCardNonce_Tests.swift */; };
A95C411824FAEF5100045045 /* BTThreeDSecureInfo_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0301A42E22E2286C008A26BD /* BTThreeDSecureInfo_Tests.swift */; };
A95C411C24FAF21B00045045 /* BraintreeTestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A903E1A624F9D34000C314E1 /* BraintreeTestShared.framework */; };
A977A04124FEC36F006049AB /* BTPaymentMethodNonceParser_PayPal_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977A04024FEC36F006049AB /* BTPaymentMethodNonceParser_PayPal_Tests.swift */; platformFilter = ios; };
A977A06B24FECFC3006049AB /* NonceValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977A06A24FECFC3006049AB /* NonceValidator.swift */; };
A9A76CA724F9E92E0044EAEE /* TestClientTokenFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A76CA624F9E92E0044EAEE /* TestClientTokenFactory.swift */; };
A9E5C1A324FD5A7C00EE691F /* BraintreeDataCollector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A76D7C001BB1CAB00000FA6A /* BraintreeDataCollector.framework */; platformFilter = ios; };
Expand Down Expand Up @@ -778,7 +777,6 @@
A9589DD924FEA45C00AF4FF7 /* BTConfiguration+Venmo_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BTConfiguration+Venmo_Tests.swift"; sourceTree = "<group>"; };
A95C410824FAEF2100045045 /* BraintreeCardTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BraintreeCardTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
A95C410C24FAEF2100045045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A977A04024FEC36F006049AB /* BTPaymentMethodNonceParser_PayPal_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPaymentMethodNonceParser_PayPal_Tests.swift; sourceTree = "<group>"; };
A977A06924FECBAF006049AB /* BraintreeThreeDSecureTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BraintreeThreeDSecureTests-Bridging-Header.h"; sourceTree = "<group>"; };
A977A06A24FECFC3006049AB /* NonceValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceValidator.swift; sourceTree = "<group>"; };
A9A76CA624F9E92E0044EAEE /* TestClientTokenFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestClientTokenFactory.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1605,8 +1603,8 @@
428F48E32624C9B700EC8DB4 /* BTVenmoAppSwitchRedirectURL_Tests.swift */,
4245D668256C255F00F1A413 /* BTVenmoAppSwitchReturnURL_Tests.swift */,
A71F7DE61B6180A3005DA1B0 /* BTVenmoClient_Tests.swift */,
4228D8682624E5C3001D2564 /* BTVenmoRequest_Tests.swift */,
09357DCA2A2FBEC10096D449 /* BTVenmoLineItem_Tests.swift */,
4228D8682624E5C3001D2564 /* BTVenmoRequest_Tests.swift */,
A948D6F424FAC8F900F4F178 /* Info.plist */,
);
path = BraintreeVenmoTests;
Expand Down Expand Up @@ -1641,7 +1639,6 @@
children = (
A9E5C1F724FD672A00EE691F /* BraintreePayPalTests-Bridging-Header.h */,
A95229C624FD949D006F7D25 /* BTConfiguration+PayPal_Tests.swift */,
A977A04024FEC36F006049AB /* BTPaymentMethodNonceParser_PayPal_Tests.swift */,
3B7A261229C35B670087059D /* BTPayPalAnalytics_Tests.swift */,
42FC237025CE0E110047C49A /* BTPayPalCheckoutRequest_Tests.swift */,
427F32DF25D1D62D00435294 /* BTPayPalClient_Tests.swift */,
Expand Down Expand Up @@ -3014,7 +3011,6 @@
427F32E025D1D62D00435294 /* BTPayPalClient_Tests.swift in Sources */,
42FC218B25CDE0290047C49A /* BTPayPalRequest_Tests.swift in Sources */,
42FC237125CE0E110047C49A /* BTPayPalCheckoutRequest_Tests.swift in Sources */,
A977A04124FEC36F006049AB /* BTPaymentMethodNonceParser_PayPal_Tests.swift in Sources */,
427F329025D1A7B900435294 /* BTPayPalVaultRequest_Tests.swift in Sources */,
3B7A261429C35BD00087059D /* BTPayPalAnalytics_Tests.swift in Sources */,
A95229C724FD949D006F7D25 /* BTConfiguration+PayPal_Tests.swift in Sources */,
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Braintree iOS SDK Release Notes

## unreleased
* BraintreeCore
* Fix bug where `type` was always returned as `Unknown` in `fetchPaymentMethodNonces` (fixes #1099)

## 6.6.0 (2023-08-22)
* BraintreePayPalNativeCheckout
* Update PayPalCheckout from 1.0.0 to 1.1.0.
Expand Down
5 changes: 5 additions & 0 deletions Sources/BraintreeCore/BTAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,15 +203,20 @@ import Foundation
/// Fetches a customer's vaulted payment method nonces.
/// Must be using client token with a customer ID specified.
/// - Parameter completion: Callback that returns either an array of payment method nonces or an error
/// - Note: Only the top level `BTPaymentMethodNonce` type is returned, fetching any additional details will need to be done on the server
public func fetchPaymentMethodNonces(_ completion: @escaping ([BTPaymentMethodNonce]?, Error?) -> Void) {
fetchPaymentMethodNonces(false, completion: completion)
}

// NEXT_MAJOR_VERSION: this should move into the Drop-in for parity with Android
// This will also allow us to return the types directly which we were doing in the +load method
// previously in Obj-C - this is not available in Swift
Comment on lines +211 to +213
Copy link
Contributor

Choose a reason for hiding this comment

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

We've actually gotten multiple requests to move this back into core. We initially moved it to DropIn to get rid of circular dependencies, and we're planning to implement it the correct way in v5.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah interesting... we may have to rethink what we want to return to merchants if we move it back to core. Not sure if Android is capable of what we were doing in v5. We were using the load method to dynamically register types that don't exist in core (BTCardNonce/PayPalNonce/'VenmoNonce/ApplePayNonce`) at compile time. We don't have the ability to do that hacky workaround in Swift and importing those modules into Core would cause a circular dependency. How are y'all planning to correctly implement it in v5?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah it was a hack in Android v3 as well. We used compileOnly in our Gradle files to basically create weak references in the dependency tree. That allowed Gradle to build successfully, but it caused some issues in the long run.

By "correct" I'm thinking "in a non-hacky way." It's tough because there will probably be tradeoffs. An enum feels like the right way to me, but I'd be up to do some cross-platform pairing on this since we need a solution on Android too.

/// Fetches a customer's vaulted payment method nonces.
/// Must be using client token with a customer ID specified.
/// - Parameters:
/// - defaultFirst: Specifies whether to sort the fetched payment method nonces with the default payment method or the most recently used payment method first
/// - completion: Callback that returns either an array of payment method nonces or an error
/// - Note: Only the top level `BTPaymentMethodNonce` type is returned, fetching any additional details will need to be done on the server
public func fetchPaymentMethodNonces(_ defaultFirst: Bool, completion: @escaping ([BTPaymentMethodNonce]?, Error?) -> Void) {
if clientToken == nil {
completion(nil, BTAPIClientError.notAuthorized)
Expand Down
68 changes: 65 additions & 3 deletions Sources/BraintreeCore/BTPaymentMethodNonceParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,80 @@ import Foundation
return nil
}

if let completionHandler = completionHandler {
if let completionHandler {
return completionHandler(json)
}

if json?["nonce"].isString != false {
if json?["nonce"].isString == false {
return nil
}

let type = json?["type"].asString()

if type == "CreditCard", let cardType = json?["details"]["cardType"].asString() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The below logic mirrors the behavior of iOS v5 with regards to the type. Though this is with the constraints of not being able to cast to unknown nonce types in Swift. The load method used previously was a very hacky workaround and does not map to Swift - the method (load) in Obj-C is prohibited in Swift. Currently the JS SDK has the method fetchPaymentMethodNonces in Core while Android has this method in the Drop-in. We should consider where this method should live and align across all 3 platforms.

return BTPaymentMethodNonce(
nonce: json?["nonce"].asString() ?? "",
type: self.cardType(from: cardType),
isDefault: json?["default"].isTrue ?? false
)
} else if type == "ApplePayCard" {
return BTPaymentMethodNonce(
nonce: json?["nonce"].asString() ?? "",
type: json?["details"]["cardType"].asString() ?? "ApplePayCard",
isDefault: json?["default"].isTrue ?? false
)
} else if type == "PayPalAccount" {
return BTPaymentMethodNonce(
nonce: json?["nonce"].asString() ?? "",
type: "PayPal",
scannillo marked this conversation as resolved.
Show resolved Hide resolved
isDefault: json?["default"].isTrue ?? false
)
} else if type == "VenmoAccount" {
return BTPaymentMethodNonce(
nonce: json?["nonce"].asString() ?? "",
type: "Venmo",
scannillo marked this conversation as resolved.
Show resolved Hide resolved
isDefault: json?["default"].isTrue ?? false
)
} else {
return BTPaymentMethodNonce(
nonce: json?["nonce"].asString() ?? "",
type: "Unknown",
isDefault: json?["default"].isTrue ?? false
)
}
}

private func cardType(from cardType: String) -> String {
let cardType = cardType.lowercased()

if cardType == "american express" {
return "AMEX"
} else if cardType == "diners club" {
return "DinersClub"
} else if cardType == "unionpay" {
return "UnionPay"
} else if cardType == "discover" {
return "Discover"
} else if cardType == "mastercard" {
return "MasterCard"
} else if cardType == "jcb" {
return "JCB"
} else if cardType == "hiper" {
return "Hiper"
} else if cardType == "hipercard" {
return "Hipercard"
} else if cardType == "laser" {
return "Laser"
} else if cardType == "solo" {
return "Solo"
} else if cardType == "switch" {
return "Switch"
} else if cardType == "uk maestro" {
return "UKMaestro"
} else if cardType == "visa" {
return "Visa"
}

return nil
return "Unknown"
}
}
2 changes: 2 additions & 0 deletions UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,11 @@ class BTAPIClient_Tests: XCTestCase {

let firstNonce = paymentMethodNonces[0];
XCTAssertEqual(firstNonce.nonce, "fake-nonce1")
XCTAssertEqual(firstNonce.type, "CreditCard")

let secondNonce = paymentMethodNonces[1]
XCTAssertEqual(secondNonce.nonce, "fake-nonce2")
XCTAssertEqual(secondNonce.type, "PayPalAccount")

expectation.fulfill()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import XCTest
@testable import BraintreeCore

class BTPaymentMethodNonceParser_Tests: XCTestCase {
var parser : BTPaymentMethodNonceParser = BTPaymentMethodNonceParser()
var parser: BTPaymentMethodNonceParser = BTPaymentMethodNonceParser()

func testRegisterType_addsTypeToTypes() {
parser.registerType("MyType") { _ -> BTPaymentMethodNonce? in return nil}
Expand Down Expand Up @@ -67,4 +67,86 @@ class BTPaymentMethodNonceParser_Tests: XCTestCase {
XCTAssertEqual(unknownNonce.type, "Unknown")
XCTAssertTrue(unknownNonce.isDefault)
}

func testSharedParser_whenTypeIsApplePayCard_returnsApplePayType() {
let sharedParser = BTPaymentMethodNonceParser.shared
let applePayCard = BTJSON(value: [
"consumed": false,
"details": ["cardType": "American Express"],
"isLocked": false,
"nonce": "a-nonce",
"securityQuestions": [] as [Any],
"type": "ApplePayCard",
] as [String: Any])

let applePayCardNonce = sharedParser.parseJSON(applePayCard, withParsingBlockForType: "ApplePayCard")

XCTAssertEqual(applePayCardNonce?.nonce, "a-nonce")
XCTAssertEqual(applePayCardNonce?.type, "American Express")
}

func testSharedParser_whenTypeIsCreditCard_returnsCardType() {
let sharedParser = BTPaymentMethodNonceParser.shared

let creditCardJSON = BTJSON(value: [
"consumed": false,
"description": "ending in 31",
"details": [
"cardType": "American Express",
"lastTwo": "31",
],
"isLocked": false,
"nonce": "0099b1d0-7a1c-44c3-b1e4-297082290bb9",
"securityQuestions": ["cvv"],
"threeDSecureInfo": NSNull(),
"type": "CreditCard",
"default": true
] as [String: Any])

let cardNonce = sharedParser.parseJSON(creditCardJSON, withParsingBlockForType:"CreditCard")!

XCTAssertEqual(cardNonce.nonce, "0099b1d0-7a1c-44c3-b1e4-297082290bb9")
XCTAssertEqual(cardNonce.type, "AMEX")
XCTAssertTrue(cardNonce.isDefault)
}

func testSharedParser_whenTypeIsVenmo_returnsVenmoType() {
let sharedParser = BTPaymentMethodNonceParser.shared

let venmoAccountJSON = BTJSON(value: [
"consumed": false,
"description": "VenmoAccount",
"details": ["username": "[email protected]"],
"isLocked": false,
"nonce": "a-nonce",
"securityQuestions": [] as [Any],
"type": "VenmoAccount",
"default": true
] as [String: Any])

let venmoAccountNonce = sharedParser.parseJSON(venmoAccountJSON, withParsingBlockForType: "VenmoAccount")

XCTAssertEqual(venmoAccountNonce?.nonce, "a-nonce")
XCTAssertEqual(venmoAccountNonce?.type, "Venmo")
}

func testSharedParser_whenTypeIsPayPal_returnsPayPalType() {
let sharedParser = BTPaymentMethodNonceParser.shared

let payPalAccountJSON = BTJSON(value: [
"consumed": false,
"description": "[email protected]",
"details": ["email": "[email protected]"],
"isLocked": false,
"nonce": "a-nonce",
"securityQuestions": [] as [Any],
"type": "PayPalAccount",
"default": true
] as [String: Any])

let payPalAccountNonce = sharedParser.parseJSON(payPalAccountJSON, withParsingBlockForType: "PayPalAccount")

XCTAssertEqual(payPalAccountNonce?.nonce, "a-nonce")
XCTAssertEqual(payPalAccountNonce?.type, "PayPal")
}
}
Loading
Loading