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

[DO NOT REVIEW] Merge RBA Metadata feature branch #1423

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
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
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.4 (2024-09-24)
* BraintreePayPal
* Send `isVaultRequest` for App Switch events to PayPal's analytics service (FPTI)
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 @@ -182,10 +182,21 @@ class ContainmentViewController: UIViewController {
}
}

// TODO: Remove this once ModXO goes GA
Copy link
Contributor

@scannillo scannillo Oct 1, 2024

Choose a reason for hiding this comment

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

We should be able to revert this. ModXO is live

Copy link
Contributor

Choose a reason for hiding this comment

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

Not a blocker for this merge though!

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
Loading