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 Checkout #1491

Merged
merged 24 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d53299c
Add file with paypal request properties
richherrera Dec 30, 2024
cf7ad1e
Add var to BTConfiguration extension
richherrera Dec 30, 2024
964d7e8
Add country iso code var
richherrera Dec 30, 2024
7460d9d
Add checkout properties to POST body model
richherrera Dec 30, 2024
0a3dbf4
Disable nesting lint
richherrera Jan 2, 2025
f5952e6
Address some lints
richherrera Jan 2, 2025
3168bf5
Add dot
richherrera Jan 2, 2025
623c681
Rename struct
richherrera Jan 2, 2025
09d964d
Add Encodable protocol to BTPayPalLineItem class
richherrera Jan 6, 2025
1553b22
Add PayPalRequest protocol, BTPayPalRequest conforms PayPalRequest
richherrera Jan 9, 2025
f9c90ec
BTPayPalCheckoutRequest conforms PayPalRequest
richherrera Jan 9, 2025
abe2c7e
Move lineItems parameters on init
richherrera Jan 9, 2025
b0cea93
BTPayPalVaultRequest conforms PayPalRequest
richherrera Jan 9, 2025
4a3b49c
Update BTPayPalRequest UTs
richherrera Jan 9, 2025
49886c7
Update BTPayPalClient to use an interface instead of concrete class
richherrera Jan 9, 2025
47727f4
Merge branch 'v7' into paypal-checkout-encodable
richherrera Jan 9, 2025
3c1701e
Disable cyclomatic_complexity lint
richherrera Jan 9, 2025
b6ce34b
Disable function_body_length lint
richherrera Jan 9, 2025
e446f03
Use if let syntax
richherrera Jan 10, 2025
ac6f13e
Address PR comments
richherrera Jan 10, 2025
e478437
Remove BTLineItems custom encode method
richherrera Jan 13, 2025
6dfb488
Revert unnecessary changes
richherrera Jan 13, 2025
f3ccfe4
Add quotation marks
richherrera Jan 21, 2025
876599d
Add docstrings and move the enums to their own file.
richherrera Jan 21, 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
12 changes: 12 additions & 0 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
458570782C34A699009CEF7A /* ConfigurationLoader_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 458570772C34A699009CEF7A /* ConfigurationLoader_Tests.swift */; };
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 */; };
45E8CE522D2C920000D7A2DC /* LocalPaymentAccountsPOSTBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E8CE512D2C91F000D7A2DC /* LocalPaymentAccountsPOSTBody.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 */; };
Expand Down Expand Up @@ -726,6 +727,7 @@
458570772C34A699009CEF7A /* ConfigurationLoader_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationLoader_Tests.swift; sourceTree = "<group>"; };
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>"; };
45E8CE512D2C91F000D7A2DC /* LocalPaymentAccountsPOSTBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPaymentAccountsPOSTBody.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>"; };
Expand Down Expand Up @@ -1300,6 +1302,7 @@
BE349112294B798300D2CF68 /* BTPayPalRequest.swift */,
BE6BC22D2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift */,
BE349110294B77E100D2CF68 /* BTPayPalVaultRequest.swift */,
45E8CE4A2D1F291400D7A2DC /* Models */,
62A659A32B98CB23008DFD67 /* PrivacyInfo.xcprivacy */,
807D22F32C29ADA8009FFEA4 /* RecurringBillingMetadata */,
62B811872CC002470024A688 /* BTPayPalPhoneNumber.swift */,
Expand All @@ -1316,6 +1319,14 @@
path = Models;
sourceTree = "<group>";
};
45E8CE4A2D1F291400D7A2DC /* Models */ = {
isa = PBXGroup;
children = (
45E8CE4B2D1F29BA00D7A2DC /* PayPalCheckoutPOSTBody.swift */,
);
path = Models;
sourceTree = "<group>";
};
570B93AE285397D20041BAFE /* BraintreeCore */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3112,6 +3123,7 @@
807D22F02C29A93A009FFEA4 /* BTPayPalBillingCycle.swift in Sources */,
5754481E294A2A1D00DEB7B0 /* BTPayPalCreditFinancingAmount.swift in Sources */,
57D9436E2968A8080079EAB1 /* BTPayPalLocaleCode.swift in Sources */,
45E8CE4C2D1F29BA00D7A2DC /* PayPalCheckoutPOSTBody.swift in Sources */,
57544F582952298900DEB7B0 /* BTPayPalAccountNonce.swift in Sources */,
8014221C2BAE935B009F9999 /* BTPayPalApprovalURLParser.swift in Sources */,
BE349111294B77E100D2CF68 /* BTPayPalVaultRequest.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,12 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
amount: "5.00",
intent: newPayPalCheckoutToggle.isOn ? .sale : .authorize,
offerPayLater: payLaterToggle.isOn,
lineItems: [lineItem],
userAuthenticationEmail: emailTextField.text,
userPhoneNumber: BTPayPalPhoneNumber(
countryCode: countryCodeTextField.text ?? "",
nationalNumber: nationalNumberTextField.text ?? ""
),
lineItems: [lineItem]
)
)

payPalClient.tokenize(request) { nonce, error in
Expand Down
10 changes: 10 additions & 0 deletions Sources/BraintreePayPal/BTConfiguration+PayPal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,14 @@ extension BTConfiguration {
var isBillingAgreementsEnabled: Bool {
json?["paypal"]["billingAgreementsEnabled"].isTrue ?? false
}

/// Retrieves the display name associated with the PayPal account.
var displayName: String? {
json?["paypal"]["displayName"].asString()
}

/// Retrieves the currencyIsoCode.
var currencyIsoCode: String? {
json?["paypal"]["currencyIsoCode"].asString()
}
}
115 changes: 100 additions & 15 deletions Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,32 @@ import BraintreeCore
}

/// Options for the PayPal Checkout flow.
@objcMembers open class BTPayPalCheckoutRequest: BTPayPalRequest {

@objcMembers public class BTPayPalCheckoutRequest: NSObject, PayPalRequest {
// MARK: - Internal Properties

let hermesPath: String
let paymentType: BTPayPalPaymentType
richherrera marked this conversation as resolved.
Show resolved Hide resolved

var amount: String
var intent: BTPayPalRequestIntent
var userAction: BTPayPalRequestUserAction
var offerPayLater: Bool
var billingAgreementDescription: String?
var currencyCode: String?
var displayName: String?
var isShippingAddressEditable: Bool = false
var isShippingAddressRequired: Bool = false
var landingPageType: BTPayPalRequestLandingPageType?
var lineItems: [BTPayPalLineItem]?
var localeCode: BTPayPalLocaleCode?
var merchantAccountID: String?
var requestBillingAgreement: Bool

var riskCorrelationID: String?
var shippingAddressOverride: BTPostalAddress?
var userAuthenticationEmail: String?
var userPhoneNumber: BTPayPalPhoneNumber?

// MARK: - Initializer

/// Initializes a PayPal Native Checkout request
Expand All @@ -75,48 +90,118 @@ import BraintreeCore
/// - intent: Optional: Payment intent. Defaults to `.authorize`. Only applies to PayPal Checkout.
/// - userAction: Optional: Changes the call-to-action in the PayPal Checkout flow. Defaults to `.none`.
/// - offerPayLater: Optional: Offers PayPal Pay Later if the customer qualifies. Defaults to `false`. Only available with PayPal Checkout.
/// - billingAgreementDescription: Optional: Display a custom description to the user for a billing agreement. For Checkout with Vault flows, you must also set
/// - currencyCode: Optional: A three-character ISO-4217 ISO currency code to use for the transaction. Defaults to merchant currency code if not set.
/// See https://developer.paypal.com/docs/api/reference/currency-codes/ for a list of supported currency codes.
/// - displayName: Optional: The merchant name displayed inside of the PayPal flow; defaults to the company name on your Braintree account
/// - isShippingAddressEditable: Defaults to false. Set to true to enable user editing of the shipping address.
/// - isShippingAddressRequired: Defaults to false. When set to true, the shipping address selector will be displayed.
/// - landingPageType: Optional: Landing page type. Defaults to `.none`.
/// - Note: Setting the BTPayPalRequest's landingPageType changes the PayPal page to display when a user lands on the PayPal site to complete the payment.
/// `.login` specifies a PayPal account login page is used.
/// `.billing` specifies a non-PayPal account landing page is used.
/// - lineItems: Optional: The line items for this transaction. It can include up to 249 line items.
/// - localeCode: Optional: A locale code to use for the transaction.
/// - merchantAccountID: Optional: A non-default merchant account to use for tokenization.
/// - requestBillingAgreement: Optional: If set to `true`, this enables the Checkout with Vault flow, where the customer will be prompted to consent to a billing agreement
/// during checkout. Defaults to `false`.
/// - riskCorrelationID: Optional: A risk correlation ID created with Set Transaction Context on your server.
/// - shippingAddressOverride: Optional: A valid shipping address to be displayed in the transaction flow. An error will occur if this address is not valid.
/// - userAuthenticationEmail: Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email.
/// - userPhoneNumber: Optional: A user's phone number to initiate a quicker authentication flow in the scenario where the user has a PayPal account
/// identified with the same phone number.
/// - lineItems: Optional: The line items for this transaction. It can include up to 249 line items.
public init(
amount: String,
intent: BTPayPalRequestIntent = .authorize,
userAction: BTPayPalRequestUserAction = .none,
offerPayLater: Bool = false,
billingAgreementDescription: String? = nil,
currencyCode: String? = nil,
displayName: String? = nil,
isShippingAddressEditable: Bool = false,
isShippingAddressRequired: Bool = false,
landingPageType: BTPayPalRequestLandingPageType = .none,
lineItems: [BTPayPalLineItem]? = nil,
localeCode: BTPayPalLocaleCode = .none,
merchantAccountID: String? = nil,
requestBillingAgreement: Bool = false,
riskCorrelationID: String? = nil,
shippingAddressOverride: BTPostalAddress? = nil,
userAuthenticationEmail: String? = nil,
userPhoneNumber: BTPayPalPhoneNumber? = nil,
lineItems: [BTPayPalLineItem]? = nil
userPhoneNumber: BTPayPalPhoneNumber? = nil
) {
self.hermesPath = "v1/paypal_hermes/create_payment_resource"
self.paymentType = .checkout
self.amount = amount
self.intent = intent
self.userAction = userAction
self.offerPayLater = offerPayLater
self.billingAgreementDescription = billingAgreementDescription
self.currencyCode = currencyCode
self.displayName = displayName
self.isShippingAddressEditable = isShippingAddressEditable
self.isShippingAddressRequired = isShippingAddressRequired
self.landingPageType = landingPageType
self.lineItems = lineItems
self.localeCode = localeCode
self.merchantAccountID = merchantAccountID
self.requestBillingAgreement = requestBillingAgreement
super.init(
hermesPath: "v1/paypal_hermes/create_payment_resource",
paymentType: .checkout,
lineItems: lineItems,
userAuthenticationEmail: userAuthenticationEmail,
userPhoneNumber: userPhoneNumber
)
self.riskCorrelationID = riskCorrelationID
self.shippingAddressOverride = shippingAddressOverride
self.userAuthenticationEmail = userAuthenticationEmail
self.userPhoneNumber = userPhoneNumber
}

// MARK: Internal Methods

override func parameters(
// swiftlint:disable cyclomatic_complexity
func parameters(
with configuration: BTConfiguration,
universalLink: URL? = nil,
isPayPalAppInstalled: Bool = false
) -> [String: Any] {
var baseParameters = super.parameters(with: configuration)
var experienceProfile: [String: Any] = [:]
richherrera marked this conversation as resolved.
Show resolved Hide resolved

experienceProfile["no_shipping"] = !isShippingAddressRequired
experienceProfile["brand_name"] = displayName != nil ? displayName : configuration.json?["paypal"]["displayName"].asString()

if landingPageType?.stringValue != nil {
richherrera marked this conversation as resolved.
Show resolved Hide resolved
experienceProfile["landing_page_type"] = landingPageType?.stringValue
}

if localeCode?.stringValue != nil {
experienceProfile["locale_code"] = localeCode?.stringValue
}

experienceProfile["address_override"] = shippingAddressOverride != nil ? !isShippingAddressEditable : false

var baseParameters: [String: Any] = [:]

if merchantAccountID != nil {
baseParameters["merchant_account_id"] = merchantAccountID
}

if riskCorrelationID != nil {
baseParameters["correlation_id"] = riskCorrelationID
}

if let lineItems, !lineItems.isEmpty {
let lineItemsArray = lineItems.compactMap { $0.requestParameters() }
baseParameters["line_items"] = lineItemsArray
}

if let userAuthenticationEmail, !userAuthenticationEmail.isEmpty {
baseParameters["payer_email"] = userAuthenticationEmail
}

if let userPhoneNumberDict = try? userPhoneNumber?.toDictionary() {
baseParameters["phone_number"] = userPhoneNumberDict
}

baseParameters["return_url"] = BTCoreConstants.callbackURLScheme + "://\(Self.callbackURLHostAndPath)success"
baseParameters["cancel_url"] = BTCoreConstants.callbackURLScheme + "://\(Self.callbackURLHostAndPath)cancel"
baseParameters["experience_profile"] = experienceProfile

var checkoutParameters: [String: Any] = [
"intent": intent.stringValue,
"amount": amount,
Expand Down
4 changes: 2 additions & 2 deletions Sources/BraintreePayPal/BTPayPalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import BraintreeDataCollector
var clientMetadataID: String?

/// Exposed for testing the intent associated with this request
var payPalRequest: BTPayPalRequest?
var payPalRequest: PayPalRequest?
richherrera marked this conversation as resolved.
Show resolved Hide resolved

/// Exposed for testing, the ASWebAuthenticationSession instance used for the PayPal flow
var webAuthenticationSession: BTWebAuthenticationSession
Expand Down Expand Up @@ -331,7 +331,7 @@ import BraintreeDataCollector
// MARK: - Private Methods

private func tokenize(
request: BTPayPalRequest,
request: PayPalRequest,
completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void
) {
linkType = (request as? BTPayPalVaultRequest)?.enablePayPalAppSwitch == true ? .universal : .deeplink
Expand Down
44 changes: 43 additions & 1 deletion Sources/BraintreePayPal/BTPayPalLineItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import Foundation

/// Credit
case credit

var stringValue: String {
switch self {
case .debit:
return "debit"
case .credit:
return "credit"
}
}
}

// swiftlint:disable identifier_name
Expand Down Expand Up @@ -61,7 +70,7 @@ import Foundation
}

/// A PayPal line item to be displayed in the PayPal checkout flow.
@objcMembers public class BTPayPalLineItem: NSObject {
@objcMembers public class BTPayPalLineItem: NSObject, Encodable {

// MARK: - Public Properties

Expand Down Expand Up @@ -114,6 +123,39 @@ import Foundation
self.kind = kind
}

enum CodingKeys: String, CodingKey {
case imageURL = "image_url"
case itemDescription = "description"
case kind
case name
case productCode = "product_code"
case quantity
case unitAmount = "unit_amount"
case unitTaxAmount = "unit_tax_amount"
case upcCode = "upc_code"
case upcType = "upc_type"
case url
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(quantity, forKey: .quantity)
try container.encode(unitAmount, forKey: .unitAmount)
try container.encode(name, forKey: .name)
try container.encode(kind.stringValue, forKey: .kind)

if let unitTaxAmount, !unitTaxAmount.isEmpty {
try container.encode(unitAmount, forKey: .unitTaxAmount)
}
richherrera marked this conversation as resolved.
Show resolved Hide resolved

try container.encodeIfPresent(itemDescription, forKey: .itemDescription)
try container.encodeIfPresent(productCode, forKey: .productCode)
try container.encodeIfPresent(url?.absoluteString, forKey: .url)
try container.encodeIfPresent(imageURL?.absoluteString, forKey: .imageURL)
try container.encodeIfPresent(upcCode, forKey: .upcCode)
try container.encodeIfPresent(upcType.stringValue, forKey: .upcType)
}

// MARK: - Internal Methods

/// Returns the line item in a dictionary.
Expand Down
44 changes: 37 additions & 7 deletions Sources/BraintreePayPal/BTPayPalRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,43 @@ import BraintreeCore
}
}

protocol PayPalRequest {
richherrera marked this conversation as resolved.
Show resolved Hide resolved
var hermesPath: String { get }
var paymentType: BTPayPalPaymentType { get }
var billingAgreementDescription: String? { get }
var displayName: String? { get }
var isShippingAddressEditable: Bool { get }
var isShippingAddressRequired: Bool { get }
var landingPageType: BTPayPalRequestLandingPageType? { get }
var lineItems: [BTPayPalLineItem]? { get }
var localeCode: BTPayPalLocaleCode? { get }
var merchantAccountID: String? { get }
var riskCorrelationID: String? { get }
var shippingAddressOverride: BTPostalAddress? { get }
var userAuthenticationEmail: String? { get }
var userPhoneNumber: BTPayPalPhoneNumber? { get }

// MARK: - Static Properties

static var callbackURLHostAndPath: String { get }

func parameters(
richherrera marked this conversation as resolved.
Show resolved Hide resolved
with configuration: BTConfiguration,
universalLink: URL?,
isPayPalAppInstalled: Bool
) -> [String: Any]
}

extension PayPalRequest {

static var callbackURLHostAndPath: String {
"onetouch/v1/"
}
}

/// Base options for PayPal Checkout and PayPal Vault flows.
/// - Note: Do not instantiate this class directly. Instead, use BTPayPalCheckoutRequest or BTPayPalVaultRequest.
@objcMembers open class BTPayPalRequest: NSObject {
@objcMembers open class BTPayPalRequest: NSObject, PayPalRequest {

// MARK: - Internal Properties

Expand All @@ -68,10 +102,6 @@ import BraintreeCore
var riskCorrelationID: String?
var userAuthenticationEmail: String?
var userPhoneNumber: BTPayPalPhoneNumber?

// MARK: - Static Properties

static let callbackURLHostAndPath: String = "onetouch/v1/"

// MARK: - Initializer

Expand All @@ -80,11 +110,11 @@ import BraintreeCore
/// - hermesPath: Required :nodoc: The hermes path or endpoint URI path. This property is not covered by semantic versioning.
/// - paymentType: Required :nodoc: The payment type, either checkout or vault. This property is not covered by semantic versioning.
/// - isShippingAddressRequired: Defaults to false. When set to true, the shipping address selector will be displayed.
/// - isShippingAddressEditable: Defaults to false. Set to true to enable user editing of the shipping address.
/// - isShippingAddressEditable: Defaults to false. Set to true to enable user editing of the shipping address.
/// - Note: Only applies when `shippingAddressOverride` is set.
/// - localeCode: Optional: A locale code to use for the transaction.
/// - shippingAddressOverride: Optional: A valid shipping address to be displayed in the transaction flow. An error will occur if this address is not valid.
/// - landingPageType: Optional: Landing page type. Defaults to `.none`.
/// - landingPageType: Optional: Landing page type. Defaults to `.none`.
/// - Note: Setting the BTPayPalRequest's landingPageType changes the PayPal page to display when a user lands on the PayPal site to complete the payment.
/// `.login` specifies a PayPal account login page is used.
/// `.billing` specifies a non-PayPal account landing page is used.
Expand Down
Loading