Skip to content

Add the saved payment method row components 3/6 - #1887

Merged
jaxdesmarais merged 4 commits into
braintree:view-edit-fi-beta-featurefrom
saurabh-P9525:feature/saved-pm-3-components
Oct 19, 2026
Merged

jaxdesmarais merged 4 commits into
braintree:view-edit-fi-beta-featurefrom
saurabh-P9525:feature/saved-pm-3-components

Conversation

@saurabh-P9525

@saurabh-P9525 saurabh-P9525 commented Sep 30, 2026 •

Copy link
Copy Markdown

Part 3 of the View/Edit FI saved payment method component. Adds the SwiftUI rows the component is built from.

Part 3 of a 6-part split, rebased on view-edit-fi-beta-feature now that #1884 (1/6) and #1886 (2/6) have merged. It builds on its own and all unit tests pass.

Summary of changes

  • EditFIRow renders the brand mark (PayPal badge + label) and the funding-instrument pill: card art (or a card/bank fallback glyph) + masked last digits + edit pencil. Only regular cards and banks show ••last4; PayPal products (PayPal Credit, PayPal Debit, PayPal balance, Pay in 4, ...) show their label. It also covers the email-only and brand-only fallbacks. At large accessibility text sizes it stacks instead of truncating the last digits.
  • CreditMessagingRow renders the Pay Later message with an inline "Learn more" link. It takes the message, link text and URL as plain values and hands the tap back through onLearnMore, so the view model decides how the lander opens.
  • Card, bank and edit-pencil assets.
  • All sizes and spacing go through EditFIStyleGuard.fontSize / dimension, and fonts follow the view's Dynamic Type size.

Screenshots

The rows aren't on a screen until part 4, so these are rendered from the real EditFIRow and CreditMessagingRow views in a test.

Row states and merchant styling

Card + Pay Later message

Bank

Card with no art (fallback glyph)

PayPal product (label instead of digits)

Pay in 4 (no art)

Email only, editable

Email only, not editable

Brand only (no FI or email)

Merchant link color (no underline)

Large accessibility text (stacked)

Dark merchant theme

showPayPalLogo: false

showPayPalLabel: false

Both off

Notes for reviewers

AI Usage

Which AI Agent Was Used?

  • Copilot
  • Claude
  • Other (Type Name Here)

How was AI used?
Used Claude to write the row components and their render tests, and to split this part out.

Estimated AI Code Contribution

  • less than 30%
  • 30 - 60%
  • 60 - 100%

Checklist

  • Added a changelog entry
  • Tested and confirmed payment flows affected by this change are functioning as expected

Authors

List GitHub usernames for everyone who contributed to this pull request.


Inner Source Process

Internal to PayPal contributors should fill out this section. All others can delete.

PR should follow these steps before codeowners review will begin:

  1. Comment /inner source on this PR — this will automatically add the inner source and tech lead review required labels. Open the PR in a draft state.
  2. PR should be reviewed by and approved by your team's technical lead, we do not allow LGTM reviews, there should be comments and feedback provided on all PR reviews
  3. Once the above steps are completed, comment /ready on this PR — this will automatically remove the tech lead review required label. Move the PR to ready to review.
  4. PR comments must be addressed within 24 hours, if you are unable to address within this timeframe, move the PR back to a draft state so our team knows not to review

Inner Source Checklist

  • Added all labels to the PR
  • Provide steps to test the flows changed, if applicable in the summary
  • Demo video of the functionality, if applicable
  • All upstream dependencies are merged in and this PR can be released at any time; PRs should not be opened until this is true
  • Unit tests and builds have been run locally and pass/compile as expected

@saurabh-P9525 saurabh-P9525 changed the title 3/6 Add the saved payment method row components Add the saved payment method row components 3/6 Sep 30, 2026
@saurabh-P9525
saurabh-P9525 force-pushed the feature/saved-pm-3-components branch from 6ee40a9 to 7deb3d1 Compare October 5, 2026 20:32
Comment thread Sources/BraintreePayPalSavedPaymentMethod/EditFIRow.swift Outdated
Comment thread Sources/BraintreePayPalSavedPaymentMethod/EditFIRow.swift
Comment thread Sources/BraintreePayPalSavedPaymentMethod/EditFIRow.swift
Comment thread Sources/BraintreePayPalSavedPaymentMethod/EditFIRow.swift Outdated
Comment thread Sources/BraintreePayPalSavedPaymentMethod/EditFIRow.swift Outdated
Comment thread Sources/BraintreePayPalSavedPaymentMethod/EditFIRow.swift Outdated
Comment thread Sources/BraintreePayPalSavedPaymentMethod/CreditMessagingRow.swift Outdated
Comment thread Sources/BraintreePayPalSavedPaymentMethod/CreditMessagingRow.swift Outdated
Comment thread Sources/BraintreePayPalSavedPaymentMethod/CreditMessagingRow.swift Outdated

@rainaarya rainaarya left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Please update the PR description as per the new changes, if required.

Once the reviews are done or PR-2 (#1886) gets merged, please rebase this PR on top of the previous PR.

@saurabh-P9525

Copy link
Copy Markdown
Author

/inner source

@github-actions github-actions Bot added inner source This PR is internal to PP but external to the mobile SDK team tech lead review required labels Oct 6, 2026
@rvmondeti-svg

Copy link
Copy Markdown

Approving. Earlier feedback is addressed and the rows look good.

Please check the pill’s tap height against the 44pt guideline, since the whole pill is the edit button. Also add the changelog entry, then /ready.

Comment on lines +154 to +161
private func fiAccessibilityLabel(for summary: PayPalSavedPaymentMethod) -> String {
guard let lastDigits = lastDigits(for: summary) else {
return summary.label ?? ""
}
// Spaced so VoiceOver reads each digit instead of one number.
let spokenDigits = lastDigits.map(String.init).joined(separator: " ")
return [summary.label, "ending in \(spokenDigits)"].compactMap { $0 }.joined(separator: ", ")
}

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.

Non-blocking: I don’t see anything wrong with this implementation—I like that it spaces the last
digits so VoiceOver reads them individually. Could we add coverage for the expected accessibility-
label output to prevent regressions?

For example:

// CARD, label: "Visa", lastDigits: "1234"
"Visa, ending in 1 2 3 4"

// PAYPAL_CREDIT, label: "PayPal Credit", lastDigits: "1234"
"PayPal Credit"

This could be covered by making this helper testable within the module, or by asserting the button’s
accessibility label through an accessibility/UI test.

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 made fiAccessibilityLabel(for:) internal for testing and added a table-driven test covering card, bank, PayPal Credit, missing digits and a missing label.

@Juan219 Juan219 left a comment

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.

Added a Non-blocking comment, please check before requesting review from BT team.

@saurabh-P9525

Copy link
Copy Markdown
Author

/ready

@saurabh-P9525
saurabh-P9525 marked this pull request as ready for review October 14, 2026 15:42
@saurabh-P9525
saurabh-P9525 requested a review from a team as a code owner October 14, 2026 15:42

@buzzamus buzzamus left a comment

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.

I am not sure if this is hooked up and able to yet, but is there any way to show some screenshots of what these new views look like?

/// Internal for testing.
func fiAccessibilityLabel(for summary: PayPalSavedPaymentMethod) -> String {
guard let lastDigits = lastDigits(for: summary) else {
return summary.label ?? ""

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.

Will this empty text cause issues for accessibility features if the label is actually empty?

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.

An empty label is now treated as missing, and if there's no label and no digits, VoiceOver reads "Saved payment method" instead of an empty string. Added test cases for both.

Spacer(minLength: 0)
}
HStack(spacing: 0) {
fiCluster

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.

stackedLayoutSpacing is 6, so if fiCluster renders an empty view I think this will add an extra gap spacing here won't it?

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.

Yes, it did. The stacked layout now leaves out the FI row for brand-only (and the brand row when it's hidden), so no spacing is added

var text = AttributedString(message)
text.foregroundColor = style.resolvedTextColor

guard let learnMoreText, let url = learnMoreURL else {

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 looks like the guard only checks learnMoreText != nil, not non-empty. can we add something like this:

Suggested change
guard let learnMoreText, let url = learnMoreURL else {
guard let learnMoreText, !learnMoreText.isEmpty, let url = learnMoreURL else {

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.

Thanks, applied

Comment on lines +38 to +45
)) {
if style.showPayPalLogo {
Image("PayPalBadge", bundle: .payPalSavedPaymentMethod)
.resizable()
.scaledToFit()
.frame(width: logoSide, height: logoSide)
.accessibilityLabel("PayPal")
// The label already reads "PayPal", so the logo only speaks when the label is hidden.

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.

if a merchant sets both showPayPalLogo and showPayPalLabel to false, the brand cluster renders nothing — visually or for accessibility — leaving the pill looking orphaned.

should PayPalSavedPaymentMethodViewStyle guard against/disallow this combination

@saurabh-P9525 saurabh-P9525 Oct 15, 2026 •

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’ve kept hiding both as a supported merchant option, consistent with the JS alpha—for example, when merchants use their own PayPal header. I also fixed the orphaned pill spacing and VoiceOver label. In Part 4, brand-only with both flags off now hides the component entirely.

EditFIRow, CreditMessagingRow and PayPalBrandCluster, with the card, bank and pencil assets and their render tests. Rebased onto view-edit-fi-beta-feature after braintree#1886 merged.
@saurabh-P9525
saurabh-P9525 force-pushed the feature/saved-pm-3-components branch from 2d2c91c to 054af26 Compare October 15, 2026 07:14
…en the brand mark is hidden, and ignore empty Learn more text
@saurabh-P9525

Copy link
Copy Markdown
Author

I am not sure if this is hooked up and able to yet, but is there any way to show some screenshots of what these new views look like?

These rows aren't wired into a screen until PR4, so I rendered the real views in a test and added a Screenshots section to the description covering each row state, a dark theme, large accessibility text and the logo/label settings.

@jaxdesmarais

Copy link
Copy Markdown
Contributor

@saurabh-P9525 this PR is missing a XO Tech Lead review and is being moved back to draft

@jaxdesmarais
jaxdesmarais marked this pull request as draft October 15, 2026 13:40
@saurabh-P9525

Copy link
Copy Markdown
Author

@saurabh-P9525 this PR is missing a XO Tech Lead review and is being moved back to draft

XO tech lead - @Juan219 has reviewed , and has approved the PR. @jaxdesmarais

@vkspune

vkspune commented Oct 15, 2026

Copy link
Copy Markdown

Approved

@saurabh-P9525
saurabh-P9525 marked this pull request as ready for review October 15, 2026 16:06
.accessibilityElement(children: .ignore)
.accessibilityLabel(label)
.accessibilityAddTraits(.isButton)
.accessibilityHint("Change payment method")

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 looks like this accessibility hint also applies to "PayPal" / "Saved payment method" / "ending in" nearby so should these VoiceOver strings be localized, or is that planned in a later PR in the stack?

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.

The SDK doesn't localize strings today, and PayPalButton and the card fields hard-code their accessibility text in English too, so I've kept these consistent with them


/// The whole pill is the tap target, not just the pencil: a 20pt glyph is well under the 44pt
/// minimum and is the only affordance the buyer has to change what will be charged.
private func editable(label: String, @ViewBuilder _ content: () -> some View) -> some View {

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.

minor: could we give this trailing closure a parameter label (e.g. content:) instead of _? slightly more readable at call sites and matches the labeled style used elsewhere in this file

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.

Done, editable now takes content:, and I made the same change to fiPill so both helpers match.

import UIKit

/// The inline credit (Pay Later) messaging line rendered below the FI row.
///

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.

nit: can remove the empty line

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.

Done, thanks.

@saurabh-P9525
saurabh-P9525 requested a review from agedd October 19, 2026 15:31

@agedd agedd left a comment

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.

thanks for making these changes, looks good!

@buzzamus buzzamus left a comment

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.

Approved and re-ran the failing integration test CI that appears to be due to some backend flakiness

@jaxdesmarais
jaxdesmarais merged commit 4364b8c into braintree:view-edit-fi-beta-feature Oct 19, 2026
22 of 24 checks passed
saurabh-P9525 pushed a commit to saurabh-P9525/braintree_ios that referenced this pull request Oct 19, 2026
The public view, skeleton row and shimmer, edit loader, Learn more lander and LoadingSpinner asset, with render tests. Rebased onto view-edit-fi-beta-feature after braintree#1887 merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inner source This PR is internal to PP but external to the mobile SDK team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants