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] Add Encodable protocol for PayPal Vault #1492

Draft
wants to merge 21 commits into
base: paypal-checkout-encodable
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8006e18
Add Encodable protocol to BTPostalAddress class
richherrera Jan 3, 2025
d1c1cd1
Add Encodable protocol to BTPayPalBillingCycle struct
richherrera Jan 3, 2025
14390fd
Add Encodable protocol to BTPayPalBillingPricing struct
richherrera Jan 3, 2025
06fd294
Add Encodable protocol to BTPayPalRecurringBillingDetails struct
richherrera Jan 3, 2025
6f0daad
Add Encodable protocol to BTPayPalRecurringBillingPlanType enum
richherrera Jan 3, 2025
428b0a4
Add PayPalVaultPOSTBody file
richherrera Jan 3, 2025
2b072da
Add encodable properties
richherrera Jan 3, 2025
c7e226e
Merge branch 'paypal-checkout-encodable' into paypal-vault-encodable
richherrera Jan 6, 2025
9543469
Use encodable for BTLineItems
richherrera Jan 6, 2025
27c0a44
Add error to catch encodable exception
richherrera Jan 6, 2025
4a6e7d8
Use encoded objects
richherrera Jan 6, 2025
42af3f7
Use Configuration.isPayPalEnabled instead of json format
richherrera Jan 6, 2025
2200e5c
Add PayPalExperienceProfile as a shared object to be use for Checkout…
richherrera Jan 6, 2025
50c23ac
Merge branch 'paypal-checkout-encodable' into paypal-vault-encodable
richherrera Jan 9, 2025
8e12bf8
Address PR comments
richherrera Jan 9, 2025
8b18aeb
Remove parameters method
richherrera Jan 9, 2025
b903ba2
Remove BTPayPalRequest class
richherrera Jan 9, 2025
44d61f4
Add PayPalConstanst enum
richherrera Jan 9, 2025
7f0a207
Remove unnecessary error
richherrera Jan 9, 2025
39ca19f
Move encoded method
richherrera Jan 9, 2025
169ff3a
Update UTs
richherrera Jan 9, 2025
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
8 changes: 8 additions & 0 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
4585707A2C34B1E1009CEF7A /* MockClientAuthorization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 458570792C34B1E1009CEF7A /* MockClientAuthorization.swift */; };
4585707C2C34B7B5009CEF7A /* MockConfigurationLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4585707B2C34B7B5009CEF7A /* MockConfigurationLoader.swift */; };
45E8CE4C2D1F29BA00D7A2DC /* PayPalCheckoutPOSTBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E8CE4B2D1F29BA00D7A2DC /* PayPalCheckoutPOSTBody.swift */; };
45E8CE4E2D28611700D7A2DC /* PayPalVaultPOSTBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E8CE4D2D28611700D7A2DC /* PayPalVaultPOSTBody.swift */; };
45E8CE502D2C773600D7A2DC /* PayPalExperienceProfilePOSTBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E8CE4F2D2C772000D7A2DC /* PayPalExperienceProfilePOSTBody.swift */; };
45EFC3972C2DBF32005E7F5B /* ConfigurationLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45EFC3962C2DBF32005E7F5B /* ConfigurationLoader.swift */; };
460C0C220F594AE8EE205E57 /* Pods_Tests_BraintreeCoreTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9239C9FE850C3587DE61A3A2 /* Pods_Tests_BraintreeCoreTests.framework */; };
5708E0A628809AD9007946B9 /* BTJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5708E0A528809AD9007946B9 /* BTJSON.swift */; };
Expand Down Expand Up @@ -727,6 +729,8 @@
458570792C34B1E1009CEF7A /* MockClientAuthorization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockClientAuthorization.swift; sourceTree = "<group>"; };
4585707B2C34B7B5009CEF7A /* MockConfigurationLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockConfigurationLoader.swift; sourceTree = "<group>"; };
45E8CE4B2D1F29BA00D7A2DC /* PayPalCheckoutPOSTBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalCheckoutPOSTBody.swift; sourceTree = "<group>"; };
45E8CE4D2D28611700D7A2DC /* PayPalVaultPOSTBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalVaultPOSTBody.swift; sourceTree = "<group>"; };
45E8CE4F2D2C772000D7A2DC /* PayPalExperienceProfilePOSTBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalExperienceProfilePOSTBody.swift; sourceTree = "<group>"; };
45EFC3962C2DBF32005E7F5B /* ConfigurationLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationLoader.swift; sourceTree = "<group>"; };
463DED22C0F426A474E6D7E2 /* Pods-Tests-BraintreeCoreTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-BraintreeCoreTests.release.xcconfig"; path = "Target Support Files/Pods-Tests-BraintreeCoreTests/Pods-Tests-BraintreeCoreTests.release.xcconfig"; sourceTree = "<group>"; };
541AEE40A1F01913E0638CC9 /* Pods-Tests-BraintreeCoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests-BraintreeCoreTests.debug.xcconfig"; path = "Target Support Files/Pods-Tests-BraintreeCoreTests/Pods-Tests-BraintreeCoreTests.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1320,6 +1324,8 @@
isa = PBXGroup;
children = (
45E8CE4B2D1F29BA00D7A2DC /* PayPalCheckoutPOSTBody.swift */,
45E8CE4D2D28611700D7A2DC /* PayPalVaultPOSTBody.swift */,
45E8CE4F2D2C772000D7A2DC /* PayPalExperienceProfilePOSTBody.swift */,
);
path = Models;
sourceTree = "<group>";
Expand Down Expand Up @@ -3113,10 +3119,12 @@
BEF5D2E6294A18B300FFD56D /* BTPayPalLineItem.swift in Sources */,
BE349113294B798300D2CF68 /* BTPayPalRequest.swift in Sources */,
BE549F112BF5445F00B6F441 /* BTPayPalReturnURL.swift in Sources */,
45E8CE502D2C773600D7A2DC /* PayPalExperienceProfilePOSTBody.swift in Sources */,
57544F5C295254A500DEB7B0 /* BTJSON+PayPal.swift in Sources */,
3B7A261129C0CAA40087059D /* BTPayPalAnalytics.swift in Sources */,
BE8E5CEF294B6937001BF017 /* BTPayPalCheckoutRequest.swift in Sources */,
807D22F02C29A93A009FFEA4 /* BTPayPalBillingCycle.swift in Sources */,
45E8CE4E2D28611700D7A2DC /* PayPalVaultPOSTBody.swift in Sources */,
5754481E294A2A1D00DEB7B0 /* BTPayPalCreditFinancingAmount.swift in Sources */,
57D9436E2968A8080079EAB1 /* BTPayPalLocaleCode.swift in Sources */,
45E8CE4C2D1F29BA00D7A2DC /* PayPalCheckoutPOSTBody.swift in Sources */,
Expand Down
12 changes: 11 additions & 1 deletion Sources/BraintreeCore/BTPostalAddress.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// Generic postal address
@objcMembers public class BTPostalAddress: NSObject {
@objcMembers public class BTPostalAddress: NSObject, Encodable {

// Property names follow the `Braintree_Address` convention as documented at:
// https://developer.paypal.com/braintree/docs/reference/request/address/create
Expand All @@ -27,4 +27,14 @@ import Foundation

/// Either a two-letter state code (for the US), or an ISO-3166-2 country subdivision code of up to three letters.
public var region: String?

enum CodingKeys: String, CodingKey {
case countryCodeAlpha2 = "country_code"
case extendedAddress = "line2"
case locality = "city"
case postalCode = "postal_code"
case region = "state"
case recipientName = "recipient_name"
case streetAddress = "line1"
}
}
29 changes: 23 additions & 6 deletions Sources/BraintreePayPal/BTPayPalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -350,19 +350,21 @@ import BraintreeDataCollector

self.isConfigFromCache = configuration.isFromCache

guard json["paypalEnabled"].isTrue else {
guard configuration.isPayPalEnabled else {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can also remove let json = configuration.json above on line 346

self.notifyFailure(with: BTPayPalError.disabled, completion: completion)
return
}

self.payPalRequest = request

guard let parameters = self.encodedPostBody(fromRequest: request, configuration: configuration) else {
self.notifyFailure(with: BTPayPalError.failedToCreateEncodable, completion: completion)
return
}

self.apiClient.post(
request.hermesPath,
parameters: request.parameters(
with: configuration,
universalLink: self.universalLink,
isPayPalAppInstalled: self.application.isPayPalAppInstalled()
)
parameters: parameters
) { body, _, error in
if let error = error as? NSError {
guard let jsonResponseBody = error.userInfo[BTCoreConstants.jsonResponseBodyKey] as? BTJSON else {
Expand Down Expand Up @@ -401,6 +403,21 @@ import BraintreeDataCollector
}
}
}

private func encodedPostBody(fromRequest payPalRequest: BTPayPalRequest, configuration: BTConfiguration) -> Encodable? {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private func encodedPostBody(fromRequest payPalRequest: BTPayPalRequest, configuration: BTConfiguration) -> Encodable? {
private func encodedPostBody(from payPalRequest: BTPayPalRequest, configuration: BTConfiguration) -> Encodable? {

if let checkoutRequest = payPalRequest as? BTPayPalCheckoutRequest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we instead do a switch here on payPalRequest.paymentType?

return PayPalCheckoutPOSTBody(payPalRequest: checkoutRequest, configuration: configuration)
} else if let vaultRequest = payPalRequest as? BTPayPalVaultRequest {
return PayPalVaultPOSTBody(
payPalRequest: vaultRequest,
configuration: configuration,
isPayPalAppInstalled: application.isPayPalAppInstalled(),
universalLink: universalLink
)
} else {
return nil
}
}

private func launchPayPalApp(
with payPalAppRedirectURL: URL,
Expand Down
7 changes: 7 additions & 0 deletions Sources/BraintreePayPal/BTPayPalError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable {

/// 13. Missing PayPal Request
case missingPayPalRequest

/// 14. Unable to create Encodable
case failedToCreateEncodable

public static var errorDomain: String {
"com.braintreepayments.BTPayPalErrorDomain"
Expand Down Expand Up @@ -79,6 +82,8 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable {
return 12
case .missingPayPalRequest:
return 13
case .failedToCreateEncodable:
return 14
}
}

Expand Down Expand Up @@ -114,6 +119,8 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable {
return "Missing BA Token for PayPal App Switch."
case .missingPayPalRequest:
return "The PayPal Request was missing or invalid."
case .failedToCreateEncodable:
return "Unable to create Encodable object"
Copy link
Contributor

Choose a reason for hiding this comment

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

This may make it a bit more clear, but take it or leave it:

Suggested change
return "Unable to create Encodable object"
return "The request was not a Checkout or Vault request."

}
}

Expand Down
48 changes: 2 additions & 46 deletions Sources/BraintreePayPal/Models/PayPalCheckoutPOSTBody.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ struct PayPalCheckoutPOSTBody: Encodable {
private let userPhoneNumber: BTPayPalPhoneNumber?
private let returnURL: String
private let cancelURL: String
private let experienceProfile: ExperienceProfile
private let experienceProfile: PayPalExperienceProfile

private var billingAgreementDescription: BillingAgreemeentDescription?
private var currencyCode: String?
Expand Down Expand Up @@ -84,7 +84,7 @@ struct PayPalCheckoutPOSTBody: Encodable {
self.userPhoneNumber = payPalRequest.userPhoneNumber
self.returnURL = BTCoreConstants.callbackURLScheme + "://\(BTPayPalRequest.callbackURLHostAndPath)success"
self.cancelURL = BTCoreConstants.callbackURLScheme + "://\(BTPayPalRequest.callbackURLHostAndPath)cancel"
self.experienceProfile = ExperienceProfile(payPalRequest: payPalRequest, configuration: configuration)
self.experienceProfile = PayPalExperienceProfile(payPalRequest: payPalRequest, configuration: configuration)
}

enum CodingKeys: String, CodingKey {
Expand Down Expand Up @@ -128,48 +128,4 @@ extension PayPalCheckoutPOSTBody {
self.description = description
}
}

struct ExperienceProfile: Encodable {

// MARK: - Private Properties

private let displayName: String?
private let isShippingAddressRequired: Bool
private let shippingAddressOverride: Bool

private var landingPageType: String?
private var localeCode: String?
private var userAction: String?

// MARK: - Initializer

init(payPalRequest: BTPayPalCheckoutRequest, configuration: BTConfiguration) {
self.displayName = payPalRequest.displayName != nil ? payPalRequest.displayName : configuration.displayName
self.isShippingAddressRequired = !payPalRequest.isShippingAddressRequired

if let landingPageType = payPalRequest.landingPageType?.stringValue {
self.landingPageType = landingPageType
}

if let localeCode = payPalRequest.localeCode?.stringValue {
self.localeCode = localeCode
}

self.shippingAddressOverride = payPalRequest.shippingAddressOverride != nil ? !payPalRequest.isShippingAddressEditable : false

if payPalRequest.userAction != .none {
self.userAction = payPalRequest.userAction.stringValue
}
}

// swiftlint:disable nesting
enum CodingKeys: String, CodingKey {
case isShippingAddressRequired = "no_shipping"
case displayName = "brand_name"
case landingPageType = "landing_page_type"
case localeCode = "locale_code"
case shippingAddressOverride = "address_override"
case userAction = "user_action"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Foundation

#if canImport(BraintreeCore)
import BraintreeCore
#endif

struct PayPalExperienceProfile: Encodable {

// MARK: - Private Properties

private let displayName: String?
private let isShippingAddressRequired: Bool
private let shippingAddressOverride: Bool

private var landingPageType: String?
private var localeCode: String?
private var userAction: String?

// MARK: - Initializer

init(payPalRequest: BTPayPalCheckoutRequest, configuration: BTConfiguration) {
self.displayName = payPalRequest.displayName != nil ? payPalRequest.displayName : configuration.displayName
self.isShippingAddressRequired = !payPalRequest.isShippingAddressRequired

if let landingPageType = payPalRequest.landingPageType?.stringValue {
self.landingPageType = landingPageType
}

if let localeCode = payPalRequest.localeCode?.stringValue {
self.localeCode = localeCode
}

self.shippingAddressOverride = payPalRequest.shippingAddressOverride != nil ? !payPalRequest.isShippingAddressEditable : false

if payPalRequest.userAction != .none {
self.userAction = payPalRequest.userAction.stringValue
}
}

init(payPalRequest: BTPayPalVaultRequest, configuration: BTConfiguration) {
self.displayName = payPalRequest.displayName != nil ? payPalRequest.displayName : configuration.displayName
self.isShippingAddressRequired = !payPalRequest.isShippingAddressRequired

if let landingPageType = payPalRequest.landingPageType?.stringValue {
self.landingPageType = landingPageType
}

if let localeCode = payPalRequest.localeCode?.stringValue {
self.localeCode = localeCode
}

self.shippingAddressOverride = payPalRequest.shippingAddressOverride != nil ? !payPalRequest.isShippingAddressEditable : false
}

enum CodingKeys: String, CodingKey {
case isShippingAddressRequired = "no_shipping"
case displayName = "brand_name"
case landingPageType = "landing_page_type"
case localeCode = "locale_code"
case shippingAddressOverride = "address_override"
case userAction = "user_action"
}
}
107 changes: 107 additions & 0 deletions Sources/BraintreePayPal/Models/PayPalVaultPOSTBody.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Foundation
import UIKit

#if canImport(BraintreeCore)
import BraintreeCore
#endif

/// The POST body for v1/paypal_hermes/setup_billing_agreement
struct PayPalVaultPOSTBody: Encodable {

// MARK: - Private Properties

private let userPhoneNumber: BTPayPalPhoneNumber?
private let returnURL: String
private let cancelURL: String
private let experienceProfile: PayPalExperienceProfile

private var billingAgreementDescription: String?
private var enablePayPalAppSwitch: Bool?
private var lineItems: [BTPayPalLineItem]?
private var merchantAccountID: String?
private var offerCredit = false
private var osType: String?
private var osVersion: String?
private var recurringBillingPlanType: BTPayPalRecurringBillingPlanType?
private var recurringBillingDetails: BTPayPalRecurringBillingDetails?
private var riskCorrelationID: String?
private var shippingAddressOverride: BTPostalAddress?
private var universalLink: String?
private var userAuthenticationEmail: String?

// MARK: - Initializer

init(
payPalRequest: BTPayPalVaultRequest,
configuration: BTConfiguration,
isPayPalAppInstalled: Bool,
universalLink: URL?
) {
if let merchantAccountID = payPalRequest.merchantAccountID {
self.merchantAccountID = merchantAccountID
}

if let riskCorrelationID = payPalRequest.riskCorrelationID {
self.riskCorrelationID = riskCorrelationID
}

if let lineItems = payPalRequest.lineItems, !lineItems.isEmpty {
self.lineItems = lineItems
}

if let userAuthenticationEmail = payPalRequest.userAuthenticationEmail, !userAuthenticationEmail.isEmpty {
self.userAuthenticationEmail = userAuthenticationEmail
}

self.userPhoneNumber = payPalRequest.userPhoneNumber
self.returnURL = BTCoreConstants.callbackURLScheme + "://\(BTPayPalRequest.callbackURLHostAndPath)success"
self.cancelURL = BTCoreConstants.callbackURLScheme + "://\(BTPayPalRequest.callbackURLHostAndPath)cancel"
self.experienceProfile = PayPalExperienceProfile(payPalRequest: payPalRequest, configuration: configuration)

if let universalLink, payPalRequest.enablePayPalAppSwitch, isPayPalAppInstalled {
self.enablePayPalAppSwitch = payPalRequest.enablePayPalAppSwitch
self.osType = UIDevice.current.systemName
self.osVersion = UIDevice.current.systemVersion
self.universalLink = universalLink.absoluteString
return
}

if let recurringBillingPlanType = payPalRequest.recurringBillingPlanType {
self.recurringBillingPlanType = recurringBillingPlanType
}

if let recurringBillingDetails = payPalRequest.recurringBillingDetails {
self.recurringBillingDetails = recurringBillingDetails
}

self.offerCredit = payPalRequest.offerCredit

if let billingAgreementDescription = payPalRequest.billingAgreementDescription {
self.billingAgreementDescription = billingAgreementDescription
}

if let shippingAddressOverride = payPalRequest.shippingAddressOverride {
self.shippingAddressOverride = shippingAddressOverride
}
}

enum CodingKeys: String, CodingKey {
case billingAgreementDescription = "description"
case cancelURL = "cancel_url"
case enablePayPalAppSwitch = "launch_paypal_app"
case experienceProfile = "experience_profile"
case lineItems = "line_items"
case merchantAccountID = "merchant_account_id"
case offerCredit = "offer_paypal_credit"
case osType = "os_type"
case osVersion = "os_version"
case recurringBillingDetails = "plan_metadata"
case recurringBillingPlanType = "plan_type"
case returnURL = "return_url"
case riskCorrelationID = "correlation_id"
case shippingAddressOverride = "shipping_address"
case universalLink = "merchant_app_return_url"
case userAuthenticationEmail = "payer_email"
case userPhoneNumber = "phone_number"
}
}
Loading
Loading