Skip to content

Commit

Permalink
Merge pull request #1423 from braintree/rba-metadata-feature
Browse files Browse the repository at this point in the history
[DO NOT REVIEW] Merge `RBA Metadata` feature branch
  • Loading branch information
richherrera authored Oct 15, 2024
2 parents da0fae1 + d9bd4d0 commit 5a42541
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 9 deletions.
26 changes: 25 additions & 1 deletion Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -858,6 +862,10 @@
8064F3962B1E63800059C4CB /* BTShopperInsightsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsRequest.swift; sourceTree = "<group>"; };
806C85622B90EBED00A2754C /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAPIRequest.swift; sourceTree = "<group>"; };
807D22ED2C29A918009FFEA4 /* BTPayPalRecurringBillingDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalRecurringBillingDetails.swift; sourceTree = "<group>"; };
807D22EF2C29A93A009FFEA4 /* BTPayPalBillingCycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalBillingCycle.swift; sourceTree = "<group>"; };
807D22F12C29A972009FFEA4 /* BTPayPalBillingPricing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalBillingPricing.swift; sourceTree = "<group>"; };
807D22F42C29ADE2009FFEA4 /* BTPayPalRecurringBillingPlanType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalRecurringBillingPlanType.swift; sourceTree = "<group>"; };
80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
8087C10E2BFBACCA0020FC2E /* TokenizationKey_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenizationKey_Tests.swift; sourceTree = "<group>"; };
808E4A152C581CD40006A737 /* AnalyticsSendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSendable.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 = "<group>";
Expand Down Expand Up @@ -1536,6 +1545,17 @@
path = Analytics;
sourceTree = "<group>";
};
807D22F32C29ADA8009FFEA4 /* RecurringBillingMetadata */ = {
isa = PBXGroup;
children = (
807D22ED2C29A918009FFEA4 /* BTPayPalRecurringBillingDetails.swift */,
807D22F42C29ADE2009FFEA4 /* BTPayPalRecurringBillingPlanType.swift */,
807D22EF2C29A93A009FFEA4 /* BTPayPalBillingCycle.swift */,
807D22F12C29A972009FFEA4 /* BTPayPalBillingPricing.swift */,
);
path = RecurringBillingMetadata;
sourceTree = "<group>";
};
80824659252C19F5000AD8FF /* Sources */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -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;
};
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
* 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.5 (2024-10-09)
* BraintreeCore
* Fix analytics bug where sessionID value in analytics payload was inaccurate; send separate FPTI POST requests per unique sessionID
Expand Down
17 changes: 14 additions & 3 deletions Demo/Application/Base/ContainmentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,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"
Expand Down
54 changes: 50 additions & 4 deletions Demo/Application/Features/PayPalWebCheckoutViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,36 @@ 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()
}

override func createPaymentButton() -> UIView {
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]),
Expand Down Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion Sources/BraintreePayPal/BTPayPalVaultRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading

0 comments on commit 5a42541

Please sign in to comment.