diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 613eab3472..9e53a88650 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -118,6 +118,10 @@ 80581B1D2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581B1C2553319C00006F53 /* BTGraphQLHTTP_SSLPinning_IntegrationTests.swift */; }; 806C85632B90EBED00A2754C /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 806C85622B90EBED00A2754C /* PrivacyInfo.xcprivacy */; }; 8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; }; + 807D22EE2C29A918009FFEA4 /* BTPayPalRecurringBillingDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D22ED2C29A918009FFEA4 /* BTPayPalRecurringBillingDetails.swift */; }; + 807D22F02C29A93A009FFEA4 /* BTPayPalBillingCycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D22EF2C29A93A009FFEA4 /* BTPayPalBillingCycle.swift */; }; + 807D22F22C29A972009FFEA4 /* BTPayPalBillingPricing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D22F12C29A972009FFEA4 /* BTPayPalBillingPricing.swift */; }; + 807D22F52C29ADE2009FFEA4 /* BTPayPalRecurringBillingPlanType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D22F42C29ADE2009FFEA4 /* BTPayPalRecurringBillingPlanType.swift */; }; 80842DA72B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */; }; 8087C10F2BFBACCA0020FC2E /* TokenizationKey_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8087C10E2BFBACCA0020FC2E /* TokenizationKey_Tests.swift */; }; 808E4A162C581CD40006A737 /* AnalyticsSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 808E4A152C581CD40006A737 /* AnalyticsSendable.swift */; }; @@ -858,6 +862,10 @@ 8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsRequest.swift; sourceTree = ""; }; 806C85622B90EBED00A2754C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAPIRequest.swift; sourceTree = ""; }; + 807D22ED2C29A918009FFEA4 /* BTPayPalRecurringBillingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalRecurringBillingDetails.swift; sourceTree = ""; }; + 807D22EF2C29A93A009FFEA4 /* BTPayPalBillingCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalBillingCycle.swift; sourceTree = ""; }; + 807D22F12C29A972009FFEA4 /* BTPayPalBillingPricing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalBillingPricing.swift; sourceTree = ""; }; + 807D22F42C29ADE2009FFEA4 /* BTPayPalRecurringBillingPlanType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalRecurringBillingPlanType.swift; sourceTree = ""; }; 80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 8087C10E2BFBACCA0020FC2E /* TokenizationKey_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenizationKey_Tests.swift; sourceTree = ""; }; 808E4A152C581CD40006A737 /* AnalyticsSendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSendable.swift; sourceTree = ""; }; @@ -1380,7 +1388,6 @@ 57544F572952298900DEB7B0 /* BTPayPalAccountNonce.swift */, 3B7A261029C0CAA40087059D /* BTPayPalAnalytics.swift */, 8014221B2BAE935B009F9999 /* BTPayPalApprovalURLParser.swift */, - BE6BC22D2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift */, BE8E5CEE294B6937001BF017 /* BTPayPalCheckoutRequest.swift */, 57544F5929524E4D00DEB7B0 /* BTPayPalClient.swift */, 5754481F294A2EBE00DEB7B0 /* BTPayPalCreditFinancing.swift */, @@ -1389,9 +1396,11 @@ BEF5D2E5294A18B300FFD56D /* BTPayPalLineItem.swift */, 57D9436D2968A8080079EAB1 /* BTPayPalLocaleCode.swift */, BE349112294B798300D2CF68 /* BTPayPalRequest.swift */, + BE6BC22D2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift */, BE6BC22B2BA9C67600C3E321 /* BTPayPalVaultBaseRequest.swift */, BE349110294B77E100D2CF68 /* BTPayPalVaultRequest.swift */, 62A659A32B98CB23008DFD67 /* PrivacyInfo.xcprivacy */, + 807D22F32C29ADA8009FFEA4 /* RecurringBillingMetadata */, ); path = BraintreePayPal; sourceTree = ""; @@ -1536,6 +1545,17 @@ path = Analytics; sourceTree = ""; }; + 807D22F32C29ADA8009FFEA4 /* RecurringBillingMetadata */ = { + isa = PBXGroup; + children = ( + 807D22ED2C29A918009FFEA4 /* BTPayPalRecurringBillingDetails.swift */, + 807D22F42C29ADE2009FFEA4 /* BTPayPalRecurringBillingPlanType.swift */, + 807D22EF2C29A93A009FFEA4 /* BTPayPalBillingCycle.swift */, + 807D22F12C29A972009FFEA4 /* BTPayPalBillingPricing.swift */, + ); + path = RecurringBillingMetadata; + sourceTree = ""; + }; 80824659252C19F5000AD8FF /* Sources */ = { isa = PBXGroup; children = ( @@ -3259,13 +3279,17 @@ BE549F122BF5449E00B6F441 /* BTPayPalVaultBaseRequest.swift in Sources */, 3B7A261129C0CAA40087059D /* BTPayPalAnalytics.swift in Sources */, BE8E5CEF294B6937001BF017 /* BTPayPalCheckoutRequest.swift in Sources */, + 807D22F02C29A93A009FFEA4 /* BTPayPalBillingCycle.swift in Sources */, 5754481E294A2A1D00DEB7B0 /* BTPayPalCreditFinancingAmount.swift in Sources */, 57D9436E2968A8080079EAB1 /* BTPayPalLocaleCode.swift in Sources */, 57544F582952298900DEB7B0 /* BTPayPalAccountNonce.swift in Sources */, 8014221C2BAE935B009F9999 /* BTPayPalApprovalURLParser.swift in Sources */, BE349111294B77E100D2CF68 /* BTPayPalVaultRequest.swift in Sources */, + 807D22F52C29ADE2009FFEA4 /* BTPayPalRecurringBillingPlanType.swift in Sources */, 57544820294A2EBE00DEB7B0 /* BTPayPalCreditFinancing.swift in Sources */, + 807D22EE2C29A918009FFEA4 /* BTPayPalRecurringBillingDetails.swift in Sources */, 57544F5A29524E4D00DEB7B0 /* BTPayPalClient.swift in Sources */, + 807D22F22C29A972009FFEA4 /* BTPayPalBillingPricing.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/CHANGELOG.md b/CHANGELOG.md index 196062d1ba..e194a1a51b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Braintree iOS SDK Release Notes +## unreleased +* BraintreePayPal + * Add `BTPayPalRecurringBillingDetails` and `BTPayPalRecurringBillingPlanType` opt-in request objects. Including these details will provide transparency to users on their billing schedule, dates, and amounts, as well as launch a modernized checkout UI. + ## 6.23.4 (2024-09-24) * BraintreePayPal * Send `isVaultRequest` for App Switch events to PayPal's analytics service (FPTI) diff --git a/Demo/Application/Base/ContainmentViewController.swift b/Demo/Application/Base/ContainmentViewController.swift index a5d61afdbe..65540e14e0 100644 --- a/Demo/Application/Base/ContainmentViewController.swift +++ b/Demo/Application/Base/ContainmentViewController.swift @@ -182,10 +182,21 @@ class ContainmentViewController: UIViewController { } } + // TODO: Remove this once ModXO goes GA case .newPayPalCheckoutTokenizationKey: - updateStatus("Fetching new checkout token...") - let newPayPalCheckoutTokenizationKey = "sandbox_rz48bqvw_jcyycfw6f9j4nj9c" - currentViewController = instantiateViewController(with: newPayPalCheckoutTokenizationKey) + updateStatus("Fetching modXO (origami) checkout token...") + + var tokenizationKey: String = "" + switch BraintreeDemoSettings.currentEnvironment { + case .sandbox: + tokenizationKey = "sandbox_rz48bqvw_jcyycfw6f9j4nj9c" + case .production: + tokenizationKey = "production_t2wns2y2_dfy45jdj3dxkmz5m" + default: + tokenizationKey = "development_testing_integration_merchant_id" + } + + currentViewController = instantiateViewController(with: tokenizationKey) case .mockedPayPalTokenizationKey: let tokenizationKey = "sandbox_q7v35n9n_555d2htrfsnnmfb3" diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift index 9a72de3a82..dd38980ada 100644 --- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift +++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift @@ -41,9 +41,18 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { }() let newPayPalCheckoutToggle = UISwitch() + + lazy var rbaDataToggleLabel: UILabel = { + let label = UILabel() + label.text = "Recurring Billing (RBA) Data" + label.font = .preferredFont(forTextStyle: .footnote) + return label + }() + + let rbaDataToggle = UISwitch() override func viewDidLoad() { - super.heightConstraint = 300 + super.heightConstraint = 350 super.viewDidLoad() } @@ -51,13 +60,17 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { let payPalCheckoutButton = createButton(title: "PayPal Checkout", action: #selector(tappedPayPalCheckout)) let payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(tappedPayPalVault)) let payPalAppSwitchButton = createButton(title: "PayPal App Switch", action: #selector(tappedPayPalAppSwitch)) + let oneTimeCheckoutStackView = buttonsStackView(label: "1-Time Checkout", views: [ UIStackView(arrangedSubviews: [payLaterToggleLabel, payLaterToggle]), UIStackView(arrangedSubviews: [newPayPalCheckoutToggleLabel, newPayPalCheckoutToggle]), payPalCheckoutButton ]) - let vaultStackView = buttonsStackView(label: "Vault", views: [payPalVaultButton, payPalAppSwitchButton]) - + let vaultStackView = buttonsStackView(label: "Vault", views: [ + UIStackView(arrangedSubviews: [rbaDataToggleLabel, rbaDataToggle]), + payPalVaultButton, + payPalAppSwitchButton + ]) let stackView = UIStackView(arrangedSubviews: [ UIStackView(arrangedSubviews: [emailLabel, emailTextField]), @@ -120,8 +133,41 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { sender.setTitle("Processing...", for: .disabled) sender.isEnabled = false - let request = BTPayPalVaultRequest() + var request = BTPayPalVaultRequest() request.userAuthenticationEmail = emailTextField.text + + if rbaDataToggle.isOn { + let billingPricing = BTPayPalBillingPricing( + pricingModel: .fixed, + amount: "9.99", + reloadThresholdAmount: "99.99" + ) + + let billingCycle = BTPayPalBillingCycle( + isTrial: true, + numberOfExecutions: 1, + interval: .month, + intervalCount: 1, + sequence: 1, + startDate: "2024-08-01", + pricing: billingPricing + ) + + let recurringBillingDetails = BTPayPalRecurringBillingDetails( + billingCycles: [billingCycle], + currencyISOCode: "USD", + totalAmount: "32.56", + productName: "Vogue Magazine Subscription", + productDescription: "Home delivery to Chicago, IL", + productQuantity: 1, + oneTimeFeeAmount: "9.99", + shippingAmount: "1.99", + productAmount: "19.99", + taxAmount: "0.59" + ) + + request = BTPayPalVaultRequest(recurringBillingDetails: recurringBillingDetails, recurringBillingPlanType: .subscription) + } payPalClient.tokenize(request) { nonce, error in sender.isEnabled = true diff --git a/Sources/BraintreePayPal/BTPayPalVaultRequest.swift b/Sources/BraintreePayPal/BTPayPalVaultRequest.swift index d565b659bc..96c7eb569a 100644 --- a/Sources/BraintreePayPal/BTPayPalVaultRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalVaultRequest.swift @@ -18,6 +18,12 @@ import BraintreeCore /// Defaults to `false`. /// - Warning: This property is currently in beta and may change or be removed in future releases. var enablePayPalAppSwitch: Bool = false + + /// Optional: Recurring billing plan type, or charge pattern. + var recurringBillingPlanType: BTPayPalRecurringBillingPlanType? + + /// Optional: Recurring billing product details. + var recurringBillingDetails: BTPayPalRecurringBillingDetails? // MARK: - Initializers @@ -40,8 +46,17 @@ import BraintreeCore /// Initializes a PayPal Vault request /// - Parameters: /// - offerCredit: Optional: Offers PayPal Credit if the customer qualifies. Defaults to `false`. + /// - recurringBillingDetails: Optional: Recurring billing product details. + /// - recurringBillingPlanType: Optional: Recurring billing plan type, or charge pattern. /// - userAuthenticationEmail: Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. - public init(offerCredit: Bool = false, userAuthenticationEmail: String? = nil) { + public init( + offerCredit: Bool = false, + recurringBillingDetails: BTPayPalRecurringBillingDetails? = nil, + recurringBillingPlanType: BTPayPalRecurringBillingPlanType? = nil, + userAuthenticationEmail: String? = nil + ) { + self.recurringBillingDetails = recurringBillingDetails + self.recurringBillingPlanType = recurringBillingPlanType self.userAuthenticationEmail = userAuthenticationEmail super.init(offerCredit: offerCredit) } @@ -67,6 +82,14 @@ import BraintreeCore return baseParameters.merging(appSwitchParameters) { $1 } } + + if let recurringBillingPlanType { + baseParameters["plan_type"] = recurringBillingPlanType.rawValue + } + + if let recurringBillingDetails { + baseParameters["plan_metadata"] = recurringBillingDetails.parameters() + } return baseParameters } diff --git a/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalBillingCycle.swift b/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalBillingCycle.swift new file mode 100644 index 0000000000..e67b45b6b9 --- /dev/null +++ b/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalBillingCycle.swift @@ -0,0 +1,85 @@ +import Foundation + +/// PayPal recurring billing cycle details. +public struct BTPayPalBillingCycle { + + // MARK: - Public Types + + /// The interval at which the payment is charged or billed. + public enum BillingInterval: String { + case day = "DAY" + case week = "WEEK" + case month = "MONTH" + case year = "YEAR" + } + + // MARK: - Private Properties + + private let isTrial: Bool + private let numberOfExecutions: Int + private let interval: BillingInterval? + private let intervalCount: Int? + private let sequence: Int? + private let startDate: String? + private let pricing: BTPayPalBillingPricing? + + // MARK: - Initializer + + /// Initialize a `BTPayPalBillingCycle` object. + /// - Parameters: + /// - isTrial: Required: The tenure type of the billing cycle. In case of a plan having trial cycle, only 2 trial cycles are allowed per plan. + /// - numberOfExecutions: Required: The number of times this billing cycle gets executed. Trial billing cycles can only be executed a finite number of times (value between 1 and 999). Regular billing cycles can be executed infinite times (value of 0) or a finite number of times (value between 1 and 999). + /// - interval: Optional: The number of intervals after which a subscriber is charged or billed. + /// - intervalCount: Optional: The number of times this billing cycle gets executed. For example, if the `intervalCount` is DAY with an `intervalCount` of 2, the subscription is billed once every two days. Maximum values {DAY -> 365}, {WEEK, 52}, {MONTH, 12}, {YEAR, 1}. + /// - sequence: Optional: The sequence of the billing cycle. Used to identify unique billing cycles. For example, sequence 1 could be a 3 month trial period, and sequence 2 could be a longer term full rater cycle. Max value 100. All billing cycles should have unique sequence values. + /// - startDate: Optional: The date and time when the billing cycle starts, in Internet date and time format `YYYY-MM-DD`. If not provided the billing cycle starts at the time of checkout. If provided and the merchant wants the billing cycle to start at the time of checkout, provide the current time. Otherwise the `startDate` can be in future. + /// - pricing: Optional: The active pricing scheme for this billing cycle. Required if `trial` is false. Optional if `trial` is true. + public init( + isTrial: Bool, + numberOfExecutions: Int, + interval: BillingInterval? = nil, + intervalCount: Int? = nil, + sequence: Int? = nil, + startDate: String? = nil, + pricing: BTPayPalBillingPricing? = nil + ) { + self.isTrial = isTrial + self.numberOfExecutions = numberOfExecutions + self.interval = interval + self.intervalCount = intervalCount + self.sequence = sequence + self.startDate = startDate + self.pricing = pricing + } + + // MARK: - Internal Methods + + func parameters() -> [String: Any] { + var parameters: [String: Any] = [ + "number_of_executions": numberOfExecutions, + "trial": isTrial + ] + + if let interval { + parameters["billing_frequency_unit"] = interval.rawValue + } + + if let intervalCount { + parameters["billing_frequency"] = intervalCount + } + + if let sequence { + parameters["sequence"] = sequence + } + + if let startDate { + parameters["start_date"] = startDate + } + + if let pricing { + parameters["pricing_scheme"] = pricing.parameters() + } + + return parameters + } +} diff --git a/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalBillingPricing.swift b/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalBillingPricing.swift new file mode 100644 index 0000000000..48c6876ff5 --- /dev/null +++ b/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalBillingPricing.swift @@ -0,0 +1,51 @@ +import Foundation + +/// PayPal Recurring Billing Agreement pricing details. +public struct BTPayPalBillingPricing { + + // MARK: - Public Types + + /// Recurring Billing Agreement pricing model types. + public enum PricingModel: String { + case fixed = "FIXED" + case variable = "VARIABLE" + case autoReload = "AUTO_RELOAD" + } + + // MARK: - Private Properties + + private let pricingModel: PricingModel + private let amount: String? + private let reloadThresholdAmount: String? + + // MARK: - Initializer + + /// Initialize a `BTPayPalBillingPricing` object. + /// - Parameters: + /// - pricingModel: Required: The pricing model associated with the billing agreement. + /// - amount: Optional: Price. The amount to charge for the subscription, recurring, UCOF or installments. + /// - reloadThresholdAmount: Optional: The reload trigger threshold condition amount when the customer is charged. + public init(pricingModel: PricingModel, amount: String? = nil, reloadThresholdAmount: String? = nil) { + self.pricingModel = pricingModel + self.amount = amount + self.reloadThresholdAmount = reloadThresholdAmount + } + + // MARK: - Internal Methods + + func parameters() -> [String: Any] { + var parameters: [String: Any] = [ + "pricing_model": pricingModel.rawValue + ] + + if let amount { + parameters["price"] = amount + } + + if let reloadThresholdAmount { + parameters["reload_threshold_amount"] = reloadThresholdAmount + } + + return parameters + } +} diff --git a/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalRecurringBillingDetails.swift b/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalRecurringBillingDetails.swift new file mode 100644 index 0000000000..312ca19636 --- /dev/null +++ b/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalRecurringBillingDetails.swift @@ -0,0 +1,96 @@ +import Foundation + +/// PayPal recurring billing product details. +public struct BTPayPalRecurringBillingDetails { + + // MARK: - Private Properties + + private let billingCycles: [BTPayPalBillingCycle] + private let currencyISOCode: String + private let totalAmount: String + private let productName: String? + private let productDescription: String? + private let productQuantity: Int? + private let oneTimeFeeAmount: String? + private let shippingAmount: String? + private let productAmount: String? + private let taxAmount: String? + + // MARK: - Initializer + + /// Initialize a `BTPayPalRecurringBillingDetails` object. + /// - Parameters: + /// - billingCycles: Required: An array of billing cycles for trial billing and regular billing. A plan can have at most two trial cycles and only one regular cycle. Exceeding 3 items in this array results in an error. + /// - currencyISOCode: Required: The three-character ISO-4217 currency code that identifies the currency. + /// - totalAmount: Required: The total amount associated with the billing cycle at the time of checkout. + /// - productName: Optional: The name of the plan to display at checkout. + /// - productDescription: Optional: Product description to display at the checkout. + /// - productQuantity: Optional: Quantity associated with the product. + /// - oneTimeFeeAmount: Optional: Price and currency for any one-time charges due at plan signup. + /// - shippingAmount: Optional: The shipping amount for the billing cycle at the time of checkout. + /// - productAmount: Optional: The item price for the product associated with the billing cycle at the time of checkout. + /// - taxAmount: Optional: The taxes for the billing cycle at the time of checkout. + public init( + billingCycles: [BTPayPalBillingCycle], + currencyISOCode: String, + totalAmount: String, + productName: String? = nil, + productDescription: String? = nil, + productQuantity: Int? = nil, + oneTimeFeeAmount: String? = nil, + shippingAmount: String? = nil, + productAmount: String? = nil, + taxAmount: String? = nil + ) { + self.billingCycles = billingCycles + self.currencyISOCode = currencyISOCode + self.totalAmount = totalAmount + self.productName = productName + self.productDescription = productDescription + self.productQuantity = productQuantity + self.oneTimeFeeAmount = oneTimeFeeAmount + self.shippingAmount = shippingAmount + self.productAmount = productAmount + self.taxAmount = taxAmount + } + + // MARK: - Internal Methods + + func parameters() -> [String: Any] { + var parameters: [String: Any] = [ + "total_amount": totalAmount, + "currency_iso_code": currencyISOCode, + "billing_cycles": billingCycles.map { $0.parameters() } + ] + + if let productName { + parameters["name"] = productName + } + + if let productDescription { + parameters["product_description"] = productDescription + } + + if let productQuantity { + parameters["product_quantity"] = productQuantity + } + + if let oneTimeFeeAmount { + parameters["one_time_fee_amount"] = oneTimeFeeAmount + } + + if let shippingAmount { + parameters["shipping_amount"] = shippingAmount + } + + if let productAmount { + parameters["product_price"] = productAmount + } + + if let taxAmount { + parameters["tax_amount"] = taxAmount + } + + return parameters + } +} diff --git a/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalRecurringBillingPlanType.swift b/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalRecurringBillingPlanType.swift new file mode 100644 index 0000000000..5b3326e3b8 --- /dev/null +++ b/Sources/BraintreePayPal/RecurringBillingMetadata/BTPayPalRecurringBillingPlanType.swift @@ -0,0 +1,15 @@ +/// PayPal recurring billing plan type, or charge pattern. +public enum BTPayPalRecurringBillingPlanType: String { + + /// Variable amount, fixed frequency, no defined duration. (E.g., utility bills, insurance). + case recurring = "RECURRING" + + /// Fixed amount, fixed frequency, defined duration. (E.g., pay for furniture using monthly payments). + case installment = "INSTALLMENT" + + /// Fixed or variable amount, variable freq, no defined duration. (E.g., Coffee shop card reload, prepaid road tolling). + case unscheduled = "UNSCHEDULED" + + /// Fixed amount, fixed frequency, no defined duration. (E.g., Streaming service). + case subscription = "SUBSCRIPTION" +} diff --git a/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift index dedae48fee..a7959bd4dd 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalVaultRequest_Tests.swift @@ -84,4 +84,64 @@ class BTPayPalVaultRequest_Tests: XCTestCase { XCTAssertTrue((parameters["os_type"] as! String).matches("iOS|iPadOS")) XCTAssertEqual(parameters["merchant_app_return_url"] as? String, "some-url") } + + func testParameters_withRecurringBillingDetails_returnsAllParams() { + let billingPricing = BTPayPalBillingPricing( + pricingModel: .autoReload, + amount: "test-price", + reloadThresholdAmount: "test-threshold" + ) + + let billingCycle = BTPayPalBillingCycle( + isTrial: false, + numberOfExecutions: 12, + interval: .month, + intervalCount: 13, + sequence: 9, + startDate: "test-date", + pricing: billingPricing + ) + + let recurringBillingDetails = BTPayPalRecurringBillingDetails( + billingCycles: [billingCycle], + currencyISOCode: "test-currency", + totalAmount: "test-total", + productName: "test-product-name", + productDescription: "test-product-description", + productQuantity: 1, + oneTimeFeeAmount: "test-fee", + shippingAmount: "test-shipping", + productAmount: "test-price", + taxAmount: "test-tax" + ) + + let request = BTPayPalVaultRequest(recurringBillingDetails: recurringBillingDetails, recurringBillingPlanType: .subscription) + + let parameters = request.parameters(with: configuration, universalLink: URL(string: "some-url")!) + XCTAssertEqual(parameters["plan_type"] as! String, "SUBSCRIPTION") + + guard let planMetadata = parameters["plan_metadata"] as? [String: Any] else { XCTFail(); return } + XCTAssertEqual(planMetadata["currency_iso_code"] as! String, "test-currency") + XCTAssertEqual(planMetadata["name"] as! String, "test-product-name") + XCTAssertEqual(planMetadata["product_description"] as! String, "test-product-description") + XCTAssertEqual(planMetadata["product_quantity"] as! Int, 1) + XCTAssertEqual(planMetadata["one_time_fee_amount"] as! String, "test-fee") + XCTAssertEqual(planMetadata["shipping_amount"] as! String, "test-shipping") + XCTAssertEqual(planMetadata["product_price"] as! String, "test-price") + XCTAssertEqual(planMetadata["tax_amount"] as! String, "test-tax") + XCTAssertEqual(planMetadata["total_amount"] as! String, "test-total") + + guard let billingCycles = planMetadata["billing_cycles"] as? [[String:Any]] else { XCTFail(); return } + XCTAssertEqual(billingCycles[0]["billing_frequency"] as! Int, 13) + XCTAssertEqual(billingCycles[0]["billing_frequency_unit"] as! String, "MONTH") + XCTAssertEqual(billingCycles[0]["number_of_executions"] as! Int, 12) + XCTAssertEqual(billingCycles[0]["sequence"] as! Int, 9) + XCTAssertEqual(billingCycles[0]["start_date"] as! String, "test-date") + XCTAssertFalse(billingCycles[0]["trial"] as! Bool) + + guard let pricingScheme = billingCycles[0]["pricing_scheme"] as? [String:String] else { XCTFail(); return } + XCTAssertEqual(pricingScheme["pricing_model"], "AUTO_RELOAD") + XCTAssertEqual(pricingScheme["price"], "test-price") + XCTAssertEqual(pricingScheme["reload_threshold_amount"], "test-threshold") + } }