Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
afba72d
feat(paypal-saved-payment-method): add View/Edit FI fetch data layer
Aug 9, 2026
971159e
pr-feedback(paypal-saved-payment-method): drop malformed funding inst…
Aug 11, 2026
c0172cd
feat(paypal-saved-payment-method): add credit messaging fetch data layer
Aug 10, 2026
43c5b99
pr-feedback(paypal-saved-payment-method): treat a message with no tex…
Aug 14, 2026
2309de3
pr-feedback(paypal-saved-payment-method): trim comments that restate …
Aug 14, 2026
7b57dd9
pr-feedback(paypal-saved-payment-method): require real copy before re…
Aug 24, 2026
813b467
pr-feedback(paypal-saved-payment-method): document the enum cases and…
Aug 26, 2026
b9d3838
pr-feedback(paypal-saved-payment-method): drop the TEXT_VARIABLE mess…
Aug 26, 2026
cbf53c3
pr-feedback(paypal-saved-payment-method): expose the error type and r…
Aug 31, 2026
d5b6223
pr-feedback(paypal-saved-payment-method): fold the repeated guards in…
Sep 2, 2026
da7d82e
pr-feedback(paypal-saved-payment-method): break up the message placem…
Sep 2, 2026
5a72479
Assemble UI layer on top of data-layer base
Aug 14, 2026
46de896
Wire display path: sticky-FI fetch + summary mapping + credit messaging
Aug 14, 2026
5941eeb
Adopt PayPal-button API + wire edit tokenize; compose credit message
Aug 14, 2026
6befbec
Review fixes: client-token JWT key, SFSafari lander, credit-fail anal…
Aug 14, 2026
50f1ac6
Take amount as a View init param; default currency to USD
Aug 14, 2026
4739940
Map empty FI summary to .hidden (no FI + no email), per FIState contract
Aug 14, 2026
c0ac37b
Add full-screen edit loader; delegate edit + FI refresh to data layer
Aug 17, 2026
bf537d7
Scope editBillingAgreement to the module via SPI; wire the real edit …
Aug 18, 2026
8cc3199
Add a Demo screen for the saved payment method component
Aug 18, 2026
dfe19c9
Align BTPayPalSavedPaymentMethodViewStyle with the approved styling c…
Aug 18, 2026
09fee85
Reuse one BTPayPalClient per component instead of one per edit
Aug 19, 2026
eaaf801
Honour the credit message's embeddable flag for the Learn more lander
Aug 19, 2026
974ed7b
Refresh the demo sandbox client token
Aug 19, 2026
1469d34
Hide credit messaging once an edit completes
Aug 20, 2026
ecd68f7
Pass universalLink in saved payment method client tests
Aug 24, 2026
20449c7
Introduce BTPayPalSavedPaymentMethodRequest for component inputs
Aug 24, 2026
34bee32
Address internal review feedback on the saved payment method component
Aug 25, 2026
a440354
Add view model and style guard unit tests
Aug 25, 2026
d99ef06
Drop receiverInfo from BTPayPalSavedPaymentMethodRequest
Aug 25, 2026
e46707f
Fix SwiftLint strict violations
Aug 25, 2026
95aef09
Remove the Demo screen from this PR
Aug 25, 2026
17ee15d
Fix CocoaPods build and raise unit test coverage above threshold
Aug 25, 2026
792d6f1
Address review feedback and render PayPal Credit instruments
Sep 3, 2026
d2c6612
Hide the Pay Later row when the funding instrument fetch fails
Sep 3, 2026
3779094
Gate the Pay Later row on whether the funding instrument is actionable
Sep 3, 2026
30f5679
Correct the @nonobjc doc comment on withEditBillingAgreement
Sep 3, 2026
552319d
Narrow the edit-billing-agreement SPI to a concrete return type
Sep 3, 2026
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
128 changes: 124 additions & 4 deletions Braintree.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

18 changes: 16 additions & 2 deletions Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,25 @@ import BraintreeCore

extension BTPayPalCheckoutRequest {

/// :nodoc: Not part of the public API. `@nonobjc` keeps this out of the generated Objective-C header.
/// :nodoc: Not part of the public API. `public` is required by `@_spi` because the symbol
/// crosses a module boundary; `@_spi` is what restricts visibility, and the method does not
/// exist under a plain `import BraintreePayPal`.
///
/// `@_spi` has no bearing on Objective-C header generation, and this type is `@objcMembers`,
/// so `@nonobjc` guards that side independently. It is inert while this signature stays
/// `async rethrows`, and load-bearing if it is ever simplified.
///
/// Scoped rather than a plain setter so the merchant's request is not left in edit mode after
/// the call — they may reuse the same instance for an ordinary checkout.
@_documentation(visibility: private)
@_spi(BraintreePayPalSavedPaymentMethod)
@nonobjc
public func enableEditBillingAgreement() {
public func withEditBillingAgreement(
_ body: () async throws -> BTPayPalAccountNonce
) async rethrows -> BTPayPalAccountNonce {
let previous = editBillingAgreement
editBillingAgreement = true
defer { editBillingAgreement = previous }
return try await body()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Foundation

#if canImport(BraintreeCore)
import BraintreeCore
#endif

/// A single block of a presentment message. Blocks are returned in the order they must be displayed.
struct BTPayPalCreditMessageItem: Equatable {

// MARK: - Internal Properties

/// The kind of block, or `nil` when PayPal returns a type this SDK version does not recognize.
let type: BTPayPalCreditMessageItemType?

/// The text to display, for example `"4 interest-free payments of $13.75 with "`.
let text: String?

/// The screen reader text for blocks whose `text` relies on symbols or abbreviations.
let alternativeText: String?

/// The URL to open when a `link` or `image` block is tapped.
let clickURL: URL?

/// The image to render for an `image` block.
let sourceURL: URL?

/// A non-unique identifier for the block, for example `"periodic_payment_count"` or `"paypal_logo"`.
let name: String?

/// Whether `clickURL` may be loaded in an embedded web view rather than an external browser.
let isEmbeddable: Bool

// MARK: - Initializer

/// Parses a single content block of a presentment message.
/// - Returns: `nil` when the block is not an object.
init?(json: BTJSON) {
guard json.isObject else {
return nil
}

self.type = json["type"].asString().flatMap(BTPayPalCreditMessageItemType.init(rawValue:))
self.text = json["text"].asString()
self.alternativeText = json["alternative_text"].asString()
self.clickURL = json["click_url"].asURL()
self.sourceURL = json["source_url"].asURL()
self.name = json["name"].asString()
self.isEmbeddable = json["embeddable"].asBool() ?? false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

/// The kind of content block making up a presentment message.
enum BTPayPalCreditMessageItemType: String {

/// A logo image, with `alternativeText` as its alt text.
case image = "IMAGE"

/// Tappable copy that opens `clickURL`, such as "Learn more".
case link = "LINK"

/// Plain copy.
case text = "TEXT"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import SwiftUI
import SafariServices

/// Presents the credit-messaging "Learn more" lander (`click_url`) in an
/// `SFSafariViewController`, giving buyers Safari's built-in security and reader chrome
/// without pulling in a new dependency or triggering an app-switch consent alert.
struct BTPayPalCreditMessagingLanderView: UIViewControllerRepresentable {

let url: URL

func makeUIViewController(context: Context) -> SFSafariViewController {
SFSafariViewController(url: url)
}

func updateUIViewController(_ controller: SFSafariViewController, context: Context) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Foundation

#if canImport(BraintreeCore)
import BraintreeCore
#endif

/// The Pay Later message to display alongside a vaulted PayPal payment method.
struct BTPayPalCreditMessagingResult: Equatable {

// MARK: - Internal Properties

/// The identifier of the message that was selected.
let messageID: String?

/// The template the message was built from, for example `"PLST_SQ"`.
let messageType: String?

/// The main text and logo blocks of the message.
let mainItems: [BTPayPalCreditMessageItem]

/// Legal disclaimers that PayPal requires to be displayed with `mainItems`.
let disclaimerItems: [BTPayPalCreditMessageItem]

/// The interactive blocks of the message, such as the "Learn more" link.
let actionItems: [BTPayPalCreditMessageItem]

/// The tracking beacon to fire once the message is on screen.
let impressionURL: URL?

// MARK: - Initializer

/// Parses the preferred message out of a `v2/credit/fetch-presentment-messages` response.
/// - Returns: `nil` when PayPal has no message to show for this buyer.
init?(json: BTJSON) {
let preferredMessage = json["messages"][0]["preferred_message"]
let content = preferredMessage["content"]
let mainItems = content["main_items"].asArray()?.compactMap(BTPayPalCreditMessageItem.init) ?? []

// Reporting success with no copy would fire the impression beacon for a message the buyer never saw.
// Image blocks carry their copy in `alternativeText` rather than `text`.
guard mainItems.contains(where: { $0.text?.isEmpty == false || $0.alternativeText?.isEmpty == false }) else {
return nil
}

self.messageID = preferredMessage["id"].asString()
self.messageType = preferredMessage["type"].asString()
self.mainItems = mainItems
self.disclaimerItems = content["disclaimer_items"].asArray()?.compactMap(BTPayPalCreditMessageItem.init) ?? []
self.actionItems = content["action_items"].asArray()?.compactMap(BTPayPalCreditMessageItem.init) ?? []
self.impressionURL = preferredMessage["analytics"]["impression_url"].asURL()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

/// Determines which funding instrument the Braintree GraphQL API resolves, and therefore which identity field is required.
enum BTPayPalFundingInstrumentFetchType: String {

/// The default funding instrument vaulted on the buyer's billing agreement. Resolved from the client token's payment method ID JWT.
case stickyFI = "STICKY_FI"

/// The funding instrument the buyer selected while approving a checkout. Resolved from that order's ID.
case fiFromApprovedCheckout = "FI_FROM_APPROVED_CHECKOUT"
}
30 changes: 30 additions & 0 deletions Sources/BraintreePayPalSavedPaymentMethod/BTPayPalPayer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

#if canImport(BraintreeCore)
import BraintreeCore
#endif

/// The buyer's PayPal account.
struct BTPayPalPayer: Equatable {

// MARK: - Internal Properties

/// The email address associated with the buyer's PayPal account.
let email: String?

/// Whether the buyer is allowed to change the funding instrument PayPal will charge.
let isEditable: Bool

// MARK: - Initializer

/// Parses the `payer` field of a funding instrument details response.
/// - Returns: `nil` when PayPal returns no payer.
init?(json: BTJSON) {
guard json.isObject else {
return nil
}

self.email = json["email"].asString()
self.isEditable = json["editable"].asBool() ?? false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation

#if canImport(BraintreeCore)
import BraintreeCore
#endif

/// A funding instrument PayPal can charge on the buyer's behalf.
struct BTPayPalSavedPaymentMethod: Equatable {

// MARK: - Internal Properties

/// The kind of funding instrument, or `nil` when PayPal returns a type this SDK version does not recognize.
let type: BTPayPalSavedPaymentMethodType?

/// The display name of the funding instrument, for example `"Visa"` or `"CREDIT UNION 1"`.
let label: String?

/// The card art or bank glyph to render alongside the label.
let imageURL: URL?

/// The last digits of the funding instrument's account number.
let lastDigits: String?

/// A further qualifier on `type`, for example the card's product name.
let subtype: String?

// MARK: - Initializer

/// Parses a single entry of the `paymentMethods` array.
/// - Returns: `nil` when the entry is not an object.
init?(json: BTJSON) {
guard json.isObject else {
return nil
}

self.type = json["type"].asString().flatMap(BTPayPalSavedPaymentMethodType.init(rawValue:))
self.label = json["label"].asString()
self.imageURL = json["imageUrl"].asURL()
self.lastDigits = json["lastDigits"].asString()
self.subtype = json["subtype"].asString()
}
}
Loading
Loading