Skip to content
Merged
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
16 changes: 16 additions & 0 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@
BE2F98D028A2BCCD008EF189 /* BTConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE2F98CF28A2BCCD008EF189 /* BTConfiguration.swift */; };
BE32ACBC2907744400A61FED /* BTCardNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE32ACBB2907744400A61FED /* BTCardNetwork.swift */; };
BE349111294B77E100D2CF68 /* BTPayPalVaultRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE349110294B77E100D2CF68 /* BTPayPalVaultRequest.swift */; };
F8512A85BB80A0C30548EBA7 /* PayPalSavedPaymentMethodView_RenderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B86B9D7895CB2D004DD4D45 /* PayPalSavedPaymentMethodView_RenderTests.swift */; };
84D815FE3026E88FC3037895 /* PayPalSavedPaymentMethodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B52AE84A91F5609E2B73E0 /* PayPalSavedPaymentMethodView.swift */; };
D04BC3F076EB65C0195500A8 /* PayPalCreditMessagingLanderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EBAC2950CE2C6AED55617D1 /* PayPalCreditMessagingLanderView.swift */; };
4F73E0FC437630FD8018D5E5 /* PayPalSavedPaymentMethodSkeletonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0379D3235C0E03E37B53A3F /* PayPalSavedPaymentMethodSkeletonRow.swift */; };
BE349113294B798300D2CF68 /* BTPayPalRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE349112294B798300D2CF68 /* BTPayPalRequest.swift */; };
BE48CE4829D5DDA600F0825C /* BTThreeDSecureV2TextBoxCustomization.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE82E74029C4A1330059FE97 /* BTThreeDSecureV2TextBoxCustomization.swift */; };
BE549F112BF5445F00B6F441 /* BTPayPalReturnURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6BC22D2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift */; };
Expand Down Expand Up @@ -1242,6 +1246,10 @@
BE2F98CF28A2BCCD008EF189 /* BTConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTConfiguration.swift; sourceTree = "<group>"; };
BE32ACBB2907744400A61FED /* BTCardNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCardNetwork.swift; sourceTree = "<group>"; };
BE349110294B77E100D2CF68 /* BTPayPalVaultRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalVaultRequest.swift; sourceTree = "<group>"; };
2B86B9D7895CB2D004DD4D45 /* PayPalSavedPaymentMethodView_RenderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalSavedPaymentMethodView_RenderTests.swift; sourceTree = "<group>"; };
80B52AE84A91F5609E2B73E0 /* PayPalSavedPaymentMethodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalSavedPaymentMethodView.swift; sourceTree = "<group>"; };
7EBAC2950CE2C6AED55617D1 /* PayPalCreditMessagingLanderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalCreditMessagingLanderView.swift; sourceTree = "<group>"; };
D0379D3235C0E03E37B53A3F /* PayPalSavedPaymentMethodSkeletonRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalSavedPaymentMethodSkeletonRow.swift; sourceTree = "<group>"; };
BE349112294B798300D2CF68 /* BTPayPalRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalRequest.swift; sourceTree = "<group>"; };
BE549F132BF6576300B6F441 /* BTAnalyticsEventsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAnalyticsEventsStorage.swift; sourceTree = "<group>"; };
BE54C0322912B68E009C6CEE /* BTHTTP_Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BTHTTP_Tests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2152,6 +2160,7 @@
FE0100000000000000000011 /* PayPalSavedPaymentMethodSummary_Tests.swift */,
FE0100000000000000000092 /* EditFIStyleGuard_Tests.swift */,
FE0100000000000000000094 /* PayPalSavedPaymentMethodFont_Tests.swift */,
2B86B9D7895CB2D004DD4D45 /* PayPalSavedPaymentMethodView_RenderTests.swift */,
FE01000000000000000000A0 /* PayPalSavedPaymentMethodViewStyle_Tests.swift */,
FE01000000000000000000A1 /* MockPayPalClient.swift */,
FE0100000000000000000017 /* PayPalCreditMessaging_Tests.swift */,
Expand Down Expand Up @@ -2649,6 +2658,9 @@
E4EAA7FE2AB18C9D955E8A23 /* BraintreePayPalSavedPaymentMethod */ = {
isa = PBXGroup;
children = (
D0379D3235C0E03E37B53A3F /* PayPalSavedPaymentMethodSkeletonRow.swift */,
7EBAC2950CE2C6AED55617D1 /* PayPalCreditMessagingLanderView.swift */,
80B52AE84A91F5609E2B73E0 /* PayPalSavedPaymentMethodView.swift */,
FE0300000000000000000001 /* Models */,
FE0100000000000000000009 /* PayPalSavedPaymentMethodClient.swift */,
FE0100000000000000000007 /* PayPalSavedPaymentMethodError.swift */,
Expand Down Expand Up @@ -3978,6 +3990,9 @@
FE0200000000000000000005 /* PayPalSavedPaymentMethodSummary.swift in Sources */,
FE0200000000000000000002 /* PayPalSavedPaymentMethodType.swift in Sources */,
FE0200000000000000000003 /* PayPalPayer.swift in Sources */,
4F73E0FC437630FD8018D5E5 /* PayPalSavedPaymentMethodSkeletonRow.swift in Sources */,
D04BC3F076EB65C0195500A8 /* PayPalCreditMessagingLanderView.swift in Sources */,
84D815FE3026E88FC3037895 /* PayPalSavedPaymentMethodView.swift in Sources */,
FE0200000000000000000016 /* PayPalCreditMessagingPOSTBody.swift in Sources */,
C29B5D0B52FEB8B1D3C12C52 /* CreditMessageContent.swift in Sources */,
917D66AD9EE2BFBF9A34237A /* PayPalSavedPaymentMethodViewModel.swift in Sources */,
Expand All @@ -4003,6 +4018,7 @@
FE0200000000000000000011 /* PayPalSavedPaymentMethodSummary_Tests.swift in Sources */,
FE0200000000000000000092 /* EditFIStyleGuard_Tests.swift in Sources */,
FE0200000000000000000094 /* PayPalSavedPaymentMethodFont_Tests.swift in Sources */,
F8512A85BB80A0C30548EBA7 /* PayPalSavedPaymentMethodView_RenderTests.swift in Sources */,
FE02000000000000000000A0 /* PayPalSavedPaymentMethodViewStyle_Tests.swift in Sources */,
FE02000000000000000000A1 /* MockPayPalClient.swift in Sources */,
FE0200000000000000000017 /* PayPalCreditMessaging_Tests.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## unreleased
* BraintreePayPal
* Send the `edit_billing_agreement_jwt` sourced from the client token for the PayPal Edit FI beta flow
* BraintreePayPalSavedPaymentMethod
* Add `PayPalSavedPaymentMethodView` (beta) to show a returning buyer's saved PayPal funding instrument at checkout and let them change it

## 7.9.0 (2026-07-21)
* BraintreeUIComponents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ enum EditFIStyleDefaultConstants {
static let stackedLayoutSpacing: CGFloat = 6

static let creditMessageFontSize: CGFloat = 16

/// Gap between the FI row and the credit-messaging line; tighter while the skeleton shows.
static let rowSpacing: CGFloat = 6
static let loadingRowSpacing: CGFloat = 4

static let shimmerBarHeight: CGFloat = 16
static let shimmerBarCornerRadius: CGFloat = 4
/// Space left after the shimmer bar so it stops short of the trailing edge.
static let shimmerBarTrailingGap: CGFloat = 40
static let shimmerBarColor = UIColor.systemGray5
static let shimmerHighlightOpacity: Double = 0.65
/// The sweeping highlight's width, as a fraction of the bar's width.
static let shimmerHighlightWidthRatio: CGFloat = 0.6
static let shimmerDuration: Double = 1.2

static let editLoaderOverlayOpacity: Double = 0.9
static let editLoaderSpinnerSide: CGFloat = 48
static let editLoaderSpinnerColor = UIColor.black
static let editLoaderSpinnerRotationDuration: Double = 1
}

/// Resolves `PayPalSavedPaymentMethodViewStyle` values for rendering.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 PayPalCreditMessagingLanderView: UIViewControllerRepresentable {

let url: URL

/// Called when the buyer taps Safari's "Done" button, so the presenter can clear its state.
let onFinish: () -> Void

func makeUIViewController(context: Context) -> SFSafariViewController {
let controller = SFSafariViewController(url: url)
controller.delegate = context.coordinator
return controller
}

func updateUIViewController(_ controller: SFSafariViewController, context: Context) {
context.coordinator.onFinish = onFinish
}

func makeCoordinator() -> Coordinator {
Coordinator(onFinish: onFinish)
}
Comment thread
saurabh-P9525 marked this conversation as resolved.

// MARK: - Coordinator

final class Coordinator: NSObject, SFSafariViewControllerDelegate {

var onFinish: () -> Void

init(onFinish: @escaping () -> Void) {
self.onFinish = onFinish
}

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
onFinish()
}
}
}
Comment on lines +7 to +42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

it seems like there's no SFSafariViewControllerDelegate here, so tapping safari's built-in "Done" button never fires safariViewControllerDidFinish(_:) — and without that callback dismissing it, nothing flips viewModel.isLanderPresented back to false. this could potentially result in a case where buyer who taps "Done" could be stuck looking at a lander with no way back short of swiping the sheet down manually.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I added an SFSafariViewControllerDelegate coordinator, so tapping "Done" now calls back and clears the presented lander in the view model.

Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import SwiftUI

/// A left-to-right shimmer sweep, masked to the content's silhouette. Used for the skeleton loading state.
struct ShimmerModifier: ViewModifier {

@State private var animating = false

func body(content: Content) -> some View {
content
.overlay(
GeometryReader { geo in
let highlightWidth = geo.size.width * EditFIStyleDefaultConstants.shimmerHighlightWidthRatio
LinearGradient(
gradient: Gradient(
colors: [.clear, Color.white.opacity(EditFIStyleDefaultConstants.shimmerHighlightOpacity), .clear]
),
startPoint: .leading,
endPoint: .trailing
)
.frame(width: highlightWidth)
.offset(x: animating ? geo.size.width : -highlightWidth)
}
)
.mask(content)
.onAppear {
withAnimation(.linear(duration: EditFIStyleDefaultConstants.shimmerDuration).repeatForever(autoreverses: false)) {
animating = true
}
}
}
}

extension View {

/// Applies an animated shimmer sweep, masked to this view. Use on placeholder shapes.
func shimmering() -> some View {
modifier(ShimmerModifier())
}
}

/// A rounded shimmer placeholder bar that fills the available width and stops short of the trailing edge.
struct ShimmerBar: View {

var body: some View {
RoundedRectangle(cornerRadius: EditFIStyleDefaultConstants.shimmerBarCornerRadius)
.fill(Color(uiColor: EditFIStyleDefaultConstants.shimmerBarColor))
.frame(height: EditFIStyleDefaultConstants.shimmerBarHeight)
.frame(maxWidth: .infinity)
.padding(.trailing, EditFIStyleDefaultConstants.shimmerBarTrailingGap)
.shimmering()
}
}

/// Loading placeholder for the FI row: the real PayPal brand mark stays visible while a
/// shimmer bar fills the space where the FI pill will appear.
struct PayPalSavedPaymentMethodSkeletonRow: View {

let style: PayPalSavedPaymentMethodViewStyle

private var fundingInstrumentGap: CGFloat {
EditFIStyleGuard.dimension(
style.container?.fundingInstrument?.leadingGap,
default: EditFIStyleDefaultConstants.fundingInstrumentLeadingGap
)
}

var body: some View {
HStack(spacing: 0) {
// A hidden brand mark would still take the gap, so it is left out entirely.
if style.showPayPalLogo || style.showPayPalLabel {
PayPalBrandCluster(style: style)
.padding(.trailing, fundingInstrumentGap)
}
ShimmerBar()
}
.accessibilityElement()
.accessibilityLabel("Loading saved payment method")
}
}

/// Loading placeholder for the credit-messaging line: a shimmer bar that stops short of the edge.
struct CreditMessageSkeleton: View {

var body: some View {
ShimmerBar()
.accessibilityHidden(true)
}
}
Loading
Loading