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

Support Encodable POST types #1151

Merged
merged 15 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57D94371296CCA2F0079EAB1 /* String+NonceValidation.swift */; };
800E78C429E0DD5300D1B0FC /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */; };
800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; };
804326BF2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */; };
80482F8029D39A1D007E5F50 /* BTThreeDSecureRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F7F29D39A1D007E5F50 /* BTThreeDSecureRequest.swift */; };
80482F8229D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8129D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift */; };
80482F8429D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80482F8329D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift */; };
Expand All @@ -85,6 +86,8 @@
8053F05929FB2F700076F988 /* URL+IsPayPal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8053F05829FB2F700076F988 /* URL+IsPayPal.swift */; };
80581A8C25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */; };
80581B1D2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */; };
8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; };
80BA3C292B23892700900BBB /* FakeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA3C282B23892700900BBB /* FakeRequest.swift */; };
80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; };
80BA64AC29D788E000E15264 /* BTLocalPaymentResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */; };
80BA64B229D7937E00E15264 /* BTLocalPaymentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80BA64B129D7937E00E15264 /* BTLocalPaymentRequest.swift */; };
Expand Down Expand Up @@ -691,6 +694,7 @@
800E23DC22206A8300C5D22E /* BTThreeDSecureAuthenticateJWT_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAuthenticateJWT_Tests.swift; sourceTree = "<group>"; };
800E78C329E0DD5300D1B0FC /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = "<group>"; };
800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayCardNonce_Tests.swift; sourceTree = "<group>"; };
804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePaymentTokensRequest.swift; sourceTree = "<group>"; };
80482F7F29D39A1D007E5F50 /* BTThreeDSecureRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequest.swift; sourceTree = "<group>"; };
80482F8129D39BF5007E5F50 /* BTThreeDSecureRequestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureRequestDelegate.swift; sourceTree = "<group>"; };
80482F8329D3A1D9007E5F50 /* BTThreeDSecureAccountType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAccountType.swift; sourceTree = "<group>"; };
Expand All @@ -703,9 +707,11 @@
80581A8B25531D0A00006F53 /* BTConfiguration+ThreeDSecure_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BTConfiguration+ThreeDSecure_Tests.swift"; sourceTree = "<group>"; };
80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTGraphQLHTTP_SSLPinning_IntegrationTests.swift; sourceTree = "<group>"; };
805FD35B2331780F0000B514 /* BTPostalAddress_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPostalAddress_Tests.swift; sourceTree = "<group>"; };
8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAPIRequest.swift; sourceTree = "<group>"; };
80A1EE3D2236AAC600F6218B /* BTThreeDSecureAdditionalInformation_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAdditionalInformation_Tests.swift; sourceTree = "<group>"; };
80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+URLOpener.swift"; sourceTree = "<group>"; };
80B6190C28C9535F00FB5022 /* PayPalCheckout.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PayPalCheckout.xcframework; path = Frameworks/XCFrameworks/PayPalCheckout.xcframework; sourceTree = "<group>"; };
80BA3C282B23892700900BBB /* FakeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeRequest.swift; sourceTree = "<group>"; };
80BA64AB29D788E000E15264 /* BTLocalPaymentResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTLocalPaymentResult.swift; sourceTree = "<group>"; };
80BA64B129D7937E00E15264 /* BTLocalPaymentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTLocalPaymentRequest.swift; sourceTree = "<group>"; };
80BA64B329D795D000E15264 /* BTLocalPaymentRequestDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTLocalPaymentRequestDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1169,6 +1175,7 @@
BE24C66D28E49A730067B11A /* BTAPIClientError.swift */,
BE24C67228E73E810067B11A /* BTAPIClientHTTPType.swift */,
BE9EC0972899CF040022EC63 /* BTAPIPinnedCertificates.swift */,
8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */,
579DAEC6286E064500FCE87F /* BTAppContextSwitchClient.swift */,
579DAEC4286E04A700FCE87F /* BTAppContextSwitcher.swift */,
BED00CAD28A5419900D74AEC /* BTBinData.swift */,
Expand Down Expand Up @@ -1457,6 +1464,7 @@
BE895C60299433FB008112AB /* BTApplePayCardNonce.swift */,
BE895C6429944BF5008112AB /* BTApplePayClient.swift */,
BE895C6229944BD3008112AB /* BTApplePayError.swift */,
804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */,
BE7A9643299FC5DE009AB920 /* BTConfiguration+ApplePay.swift */,
);
path = BraintreeApplePay;
Expand Down Expand Up @@ -1514,6 +1522,7 @@
isa = PBXGroup;
children = (
BE54C0342912B6BC009C6CEE /* BTHTTPTestProtocol.swift */,
80BA3C282B23892700900BBB /* FakeRequest.swift */,
428F976426727333001042E1 /* BTMockOpenURLContext.h */,
428F976526727333001042E1 /* BTMockOpenURLContext.m */,
BEBC6F3129380B82004E25A0 /* BTExceptionCatcher.m */,
Expand Down Expand Up @@ -2748,6 +2757,7 @@
8053F05929FB2F700076F988 /* URL+IsPayPal.swift in Sources */,
BEC3F11328A4401E0074DF0F /* BTHTTPError.swift in Sources */,
574891EB286F7E4F0020DA36 /* BTClientMetadataIntegration.swift in Sources */,
8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */,
BE698EA428AD2C10001D9B10 /* BTCoreConstants.swift in Sources */,
BE24C67528E7491E0067B11A /* BTAPIClientAuthorization.swift in Sources */,
BE32ACBC2907744400A61FED /* BTCardNetwork.swift in Sources */,
Expand Down Expand Up @@ -2856,6 +2866,7 @@
buildActionMask = 2147483647;
files = (
BEDB820229B109EE00075AF3 /* BTApplePayAnalytics.swift in Sources */,
804326BF2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift in Sources */,
BE895C6329944BD3008112AB /* BTApplePayError.swift in Sources */,
BE895C6529944BF5008112AB /* BTApplePayClient.swift in Sources */,
BE7A9644299FC5DE009AB920 /* BTConfiguration+ApplePay.swift in Sources */,
Expand Down Expand Up @@ -3037,6 +3048,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
80BA3C292B23892700900BBB /* FakeRequest.swift in Sources */,
A95229C524FD8F15006F7D25 /* BTVersion_Tests.swift in Sources */,
BE54C0332912B68E009C6CEE /* BTHTTP_Tests.swift in Sources */,
A908437124FD8BB0004134CA /* BTPostalAddress_Tests.swift in Sources */,
Expand Down
24 changes: 2 additions & 22 deletions Sources/BraintreeApplePay/BTApplePayClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,8 @@ import BraintreeCore
self.notifyFailure(with: BTApplePayError.unsupported, completion: completion)
return
}

let metaParameters: [String: String] = [
"source": self.apiClient.metadata.source.stringValue,
"integration": self.apiClient.metadata.integration.stringValue,
"sessionId": self.apiClient.metadata.sessionID
]

let parameters: [String: Any] = [
"applePaymentToken": self.parametersForPaymentToken(token: payment.token),
"_meta": metaParameters
]

let parameters = BTApplePaymentTokensRequest(token: payment.token)

self.apiClient.post("v1/payment_methods/apple_payment_tokens", parameters: parameters) { body, _, error in
if let error {
Expand Down Expand Up @@ -137,17 +128,6 @@ import BraintreeCore
}
}
}

// MARK: - Internal Methods

func parametersForPaymentToken(token: PKPaymentToken) -> [String: Any?] {
[
"paymentData": token.paymentData.base64EncodedString(),
"transactionIdentifier": token.transactionIdentifier,
"paymentInstrumentName": token.paymentMethod.displayName,
"paymentNetwork": token.paymentMethod.network
]
}

// MARK: - Analytics Helper Methods

Expand Down
25 changes: 25 additions & 0 deletions Sources/BraintreeApplePay/BTApplePaymentTokensRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Foundation
import PassKit

/// The POST body for `v1/payment_methods/apple_payment_tokens`
struct BTApplePaymentTokensRequest: Encodable {

private let applePaymentToken: ApplePaymentToken

init(token: PKPaymentToken) {
self.applePaymentToken = ApplePaymentToken(
paymentData: token.paymentData.base64EncodedString(),
transactionIdentifier: token.transactionIdentifier,
paymentInstrumentName: token.paymentMethod.displayName,
paymentNetwork: token.paymentMethod.network?.rawValue
)
}

private struct ApplePaymentToken: Encodable {

let paymentData: String
let transactionIdentifier: String
let paymentInstrumentName: String?
let paymentNetwork: String?
}
}
36 changes: 36 additions & 0 deletions Sources/BraintreeCore/BTAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ import Foundation
}
}

// TODO: - Remove when all POST bodies use Codable, instead of BTJSON/raw dictionaries
/// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
///
/// Perfom an HTTP POST on a URL composed of the configured from environment and the given path.
Expand Down Expand Up @@ -304,6 +305,40 @@ import Foundation
http(for: httpType)?.post(path, parameters: postParameters, completion: completion)
}
}

/// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
///
/// Perfom an HTTP POST on a URL composed of the configured from environment and the given path.
/// - Parameters:
/// - path: The endpoint URI path.
/// - parameters: Optional set of query parameters to be encoded with the request.
/// - httpType: The underlying `BTAPIClientHTTPService` of the HTTP request. Defaults to `.gateway`.
/// - completion: A block object to be executed when the request finishes.
/// On success, `body` and `response` will contain the JSON body response and the
/// HTTP response and `error` will be `nil`; on failure, `body` and `response` will be
/// `nil` and `error` will contain the error that occurred.
@_documentation(visibility: private)
public func post(
_ path: String,
parameters: Encodable,
httpType: BTAPIClientHTTPService = .gateway,
completion: @escaping RequestCompletion
) {
fetchOrReturnRemoteConfiguration { [weak self] configuration, error in
guard let self else {
completion(nil, nil, BTAPIClientError.deallocated)
return
}

if let error {
completion(nil, nil, error)
return
}

let postParameters = BTAPIRequest(requestBody: parameters, metadata: metadata, httpType: httpType)
http(for: httpType)?.post(path, parameters: postParameters, completion: completion)
}
}

/// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
@_documentation(visibility: private)
Expand All @@ -318,6 +353,7 @@ import Foundation

// MARK: Analytics Internal Methods

// TODO: - Remove once all POSTs moved to Encodable
func metadataParametersWith(_ parameters: [String: Any]? = [:], for httpType: BTAPIClientHTTPService) -> [String: Any]? {
switch httpType {
case .gateway:
Expand Down
39 changes: 39 additions & 0 deletions Sources/BraintreeCore/BTAPIRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

/// An `Encodable` type containing POST body details & metadata params formatted for the BT Gateway & BT GraphQL API
struct BTAPIRequest: Encodable {

private let requestBody: Encodable
private let metadata: BTClientMetadata
private let httpType: BTAPIClientHTTPService

private enum MetadataKeys: String, CodingKey {
case gatewayMetadataKey = "_meta"
case graphQLMetadataKey = "clientSdkMetadata"
}

/// Initialize a `BTAPIRequest` to format a POST body with metadata params for BT APIs.
/// - Parameters:
/// - requestBody: The actual POST body details.
/// - metadata: The metadata details to append into the POST body.
/// - httpType: The Braintree API type for this request.
init(requestBody: Encodable, metadata: BTClientMetadata, httpType: BTAPIClientHTTPService) {
self.requestBody = requestBody
self.metadata = metadata
self.httpType = httpType
}

func encode(to encoder: Encoder) throws {
try requestBody.encode(to: encoder)

var metadataContainer = encoder.container(keyedBy: MetadataKeys.self)
switch httpType {
case .gateway:
let metadataEncoder = metadataContainer.superEncoder(forKey: .gatewayMetadataKey)
Copy link
Contributor

Choose a reason for hiding this comment

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

TIL about superEncoder - super cool!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeh, it was a little tricky to figure out how to do this, but this stack overflow post is what helped me - https://stackoverflow.com/questions/50461744/swift-codable-how-to-encode-top-level-data-into-nested-container

try self.metadata.encode(to: metadataEncoder)
case .graphQLAPI:
let metadataEncoder = metadataContainer.superEncoder(forKey: .graphQLMetadataKey)
try self.metadata.encode(to: metadataEncoder)
}
}
}
34 changes: 27 additions & 7 deletions Sources/BraintreeCore/BTClientMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
/// used. In this case, the source and integration may change over time, while
/// the sessionID should remain constant.
@_documentation(visibility: private)
@objcMembers public class BTClientMetadata: NSObject {
public class BTClientMetadata: Encodable {

/// :nodoc: This property is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
/// Integration type
Expand All @@ -24,8 +24,13 @@ import Foundation

/// :nodoc: This property is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
/// Auto-generated UUID
public var sessionID: String
public var sessionID = UUID().uuidString.replacingOccurrences(of: "-", with: "")

public var platform = "iOS"

public var version = BTCoreConstants.braintreeSDKVersion

// TODO: - Remove once all POSTs moved to Codable
/// Additional metadata parameters
var parameters: [String: Any] {
[
Expand All @@ -37,10 +42,25 @@ import Foundation
]
}

override init() {
self.integration = .custom
self.source = .unknown
self.sessionID = UUID().uuidString.replacingOccurrences(of: "-", with: "")
super.init()
private enum CodingKeys: String, CodingKey {
case integration = "integration"
case sessionID = "sessionId"
case source = "source"
case platform = "platform"
case version = "version"
}

init(integration: BTClientMetadataIntegration = .custom, source: BTClientMetadataSource = .unknown) {
self.integration = integration
self.source = source
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(integration.stringValue, forKey: .integration)
try container.encode(sessionID, forKey: .sessionID)
try container.encode(source.stringValue, forKey: .source)
try container.encode(platform, forKey: .platform)
try container.encode(version, forKey: .version)
}
}
23 changes: 8 additions & 15 deletions UnitTests/BraintreeApplePayTests/BTApplePay_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,13 @@ class BTApplePay_Tests: XCTestCase {
XCTFail()
return
}
let metaParameters = lastPostParameters["_meta"] as! NSDictionary
XCTAssertEqual(metaParameters["source"] as? String, "unknown")
XCTAssertEqual(metaParameters["integration"] as? String, "custom")
XCTAssertEqual(metaParameters["sessionId"] as? String, mockAPIClient.metadata.sessionID)

let applePayTokenParams = lastPostParameters["applePaymentToken"] as! NSDictionary
XCTAssertEqual(applePayTokenParams["paymentData"] as? String, Data().base64EncodedString())
XCTAssertEqual(applePayTokenParams["transactionIdentifier"] as? String, "fake-transaction-id")
// The following are expected nil in tests since we cannot mock `PKPaymentToken.paymentMethod`
XCTAssertNil(applePayTokenParams["paymentInstrumentName"])
XCTAssertNil(applePayTokenParams["paymentNetwork"])
}

class MockPKPaymentToken : PKPaymentToken {
Expand All @@ -335,17 +338,7 @@ class BTApplePay_Tests: XCTestCase {
}
override var transactionIdentifier : String {
get {
return "transaction-id"
}
}
override var paymentInstrumentName : String {
get {
return "payment-instrument-name"
}
}
override var paymentNetwork : String {
get {
return "payment-network"
return "fake-transaction-id"
}
}
}
Expand Down
Loading
Loading