Skip to content

Commit

Permalink
Display a “default” badge for the default PM (#4333)
Browse files Browse the repository at this point in the history
## Summary
<!-- Simple summary of what was changed. -->
Display a "default" badge for the default PM in vertical and horizontal
mode across all UI shapes (payment sheet, flow controller, embedded)
## Motivation
<!-- Why are you making this change? If it's for fixing a bug, if
possible, please include a code snippet or example project that
demonstrates the issue. -->
[MOBILESDK-2800](https://jira.corp.stripe.com/browse/MOBILESDK-2800)
## Testing
<!-- How was the code tested? Be as specific as possible. -->


https://github.com/user-attachments/assets/e2dd8f48-e23c-4e4a-8732-bb55a98efa80


https://github.com/user-attachments/assets/8a0c25a5-7384-483d-9814-d8f8c70d898f


![test_all_saved_pms_and_apple_pay_and_link_default_badge@3x](https://github.com/user-attachments/assets/a32965f4-4b0f-4b07-a0e9-ce3d078f5e6b)


![test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDefaultBadge@3x](https://github.com/user-attachments/assets/01605dc3-b2a0-465f-a0a4-cfe641a1b860)

## Changelog
<!-- Is this a notable change that affects users? If so, add a line to
`CHANGELOG.md` and prefix the line with one of the following:
    - [Added] for new features.
    - [Changed] for changes in existing functionality.
    - [Deprecated] for soon-to-be removed features.
    - [Removed] for now removed features.
    - [Fixed] for any bug fixes.
    - [Security] in case of vulnerabilities.
-->
  • Loading branch information
joyceqin-stripe authored Dec 20, 2024
1 parent 2d98857 commit a052ae0
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -437,4 +437,11 @@ extension String.Localized {
"Promotional text for Affirm, displayed in a button that lets the customer pay with Affirm"
)
}

static var default_text: String {
STPLocalizedString(
"Default",
"Label for identifying the default payment method."
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,13 @@ struct ElementsCustomer: Equatable, Hashable {
return ElementsCustomer(paymentMethods: paymentMethods, defaultPaymentMethod: defaultPaymentMethod, customerSession: customerSession)
}

func getDefaultPaymentMethod() -> STPPaymentMethod? {
return paymentMethods.first { $0.stripeId == defaultPaymentMethod }
}

func getDefaultOrFirstPaymentMethod() -> STPPaymentMethod? {
// if customer has a default payment method from the elements session, return the default payment method
let defaultSavedPaymentMethod = paymentMethods.first { $0.stripeId == defaultPaymentMethod }
if let defaultSavedPaymentMethod = defaultSavedPaymentMethod {
return defaultSavedPaymentMethod
}
// otherwise, return the first payment method from the customer's list of saved payment methods
return paymentMethods.first
return getDefaultPaymentMethod() ?? paymentMethods.first
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ extension LinkPaymentMethodPicker {

private let defaultBadge = LinkBadgeView(
type: .neutral,
text: STPLocalizedString("Default", "Label for identifying the default payment method.")
text: String.Localized.default_text
)

private let alertIconView: UIImageView = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import UIKit
// MARK: - Constants
/// Entire cell size
private let cellSize: CGSize = CGSize(width: 106, height: 94)
private let cellSizeWithDefaultBadge: CGSize = CGSize(width: 106, height: 112)
/// Size of the rounded rectangle that contains the PM logo
let roundedRectangleSize = CGSize(width: 100, height: 64)
private let paymentMethodLogoSize: CGSize = CGSize(width: 54, height: 40)
Expand All @@ -24,12 +25,13 @@ private let paymentMethodLogoSize: CGSize = CGSize(width: 54, height: 40)
/// For internal SDK use only
@objc(STP_Internal_SavedPaymentMethodCollectionView)
class SavedPaymentMethodCollectionView: UICollectionView {
init(appearance: PaymentSheet.Appearance) {
init(appearance: PaymentSheet.Appearance, needsVerticalPaddingForBadge: Bool = false) {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.sectionInset = UIEdgeInsets(
top: -6, left: PaymentSheetUI.defaultPadding, bottom: 0,
right: PaymentSheetUI.defaultPadding)
self.needsVerticalPaddingForBadge = needsVerticalPaddingForBadge
layout.itemSize = cellSize
layout.minimumInteritemSpacing = 12
layout.minimumLineSpacing = 4
Expand All @@ -43,13 +45,23 @@ class SavedPaymentMethodCollectionView: UICollectionView {
}

var isRemovingPaymentMethods: Bool = false
let needsVerticalPaddingForBadge: Bool

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: 100)
return needsVerticalPaddingForBadge && isRemovingPaymentMethods ? CGSize(width: UIView.noIntrinsicMetric, height: 118) : CGSize(width: UIView.noIntrinsicMetric, height: 100)
}

func updateLayout() {
guard let layout = collectionViewLayout as? UICollectionViewFlowLayout else { return }
let newCellSize = needsVerticalPaddingForBadge && isRemovingPaymentMethods ? cellSizeWithDefaultBadge : cellSize
guard newCellSize != layout.itemSize else { return }
layout.itemSize = newCellSize
collectionViewLayout.invalidateLayout()
invalidateIntrinsicContentSize()
}
}

Expand Down Expand Up @@ -91,15 +103,38 @@ extension SavedPaymentMethodCollectionView {
button.accessibilityLabel = String.Localized.edit
return button
}()
lazy var defaultBadge: UILabel = {
let label = UILabel()
label.font = appearance.scaledFont(for: appearance.font.base.medium, style: .caption1, maximumPointSize: 20)
label.textColor = appearance.colors.textSecondary
label.adjustsFontForContentSizeCategory = true
label.text = String.Localized.default_text
label.isHidden = true
return label
}()

fileprivate var viewModel: SavedPaymentOptionsViewController.Selection?

var isRemovingPaymentMethods: Bool = false {
didSet {
updateVerticalConstraintsIfNeeded()
update()
}
}

func updateVerticalConstraintsIfNeeded() {
guard needsVerticalPaddingForBadge else {
return
}
if isRemovingPaymentMethods {
activateDefaultBadgeConstraints()
defaultBadge.setHiddenIfNecessary(!showDefaultPMBadge)
} else {
deactivateDefaultBadgeConstraints()
defaultBadge.setHiddenIfNecessary(true)
}
}

weak var delegate: PaymentOptionCellDelegate?
var appearance = PaymentSheet.Appearance.default {
didSet {
Expand All @@ -110,14 +145,17 @@ extension SavedPaymentMethodCollectionView {

var cbcEligible: Bool = false
var allowsPaymentMethodRemoval: Bool = true
var allowsSetAsDefaultPM: Bool = false
var needsVerticalPaddingForBadge: Bool = false
var showDefaultPMBadge: Bool = false

/// Indicates whether the cell for a saved payment method should display the edit icon.
/// True if payment methods can be removed or edited (will update this to include allowing set as default)
/// True if payment methods can be removed or edited
var showEditIcon: Bool {
guard UpdatePaymentMethodViewModel.supportedPaymentMethods.contains(where: { viewModel?.savedPaymentMethod?.type == $0 }) else {
fatalError("Payment method does not match supported saved payment methods.")
}
return allowsPaymentMethodRemoval || (viewModel?.savedPaymentMethod?.isCoBrandedCard ?? false && cbcEligible)
return allowsSetAsDefaultPM || allowsPaymentMethodRemoval || (viewModel?.savedPaymentMethod?.isCoBrandedCard ?? false && cbcEligible)
}

// MARK: - UICollectionViewCell
Expand All @@ -142,7 +180,7 @@ extension SavedPaymentMethodCollectionView {
paymentMethodLogo.contentMode = .scaleAspectFit
accessoryButton.addTarget(self, action: #selector(didSelectAccessory), for: .touchUpInside)
let views = [
label, shadowRoundedRectangle, paymentMethodLogo, plus, selectedIcon, accessoryButton,
label, shadowRoundedRectangle, paymentMethodLogo, plus, selectedIcon, accessoryButton, defaultBadge
]
views.forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
Expand All @@ -159,7 +197,7 @@ extension SavedPaymentMethodCollectionView {

label.topAnchor.constraint(
equalTo: shadowRoundedRectangle.bottomAnchor, constant: 4),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
labelBottomConstraint,
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 2),
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),

Expand Down Expand Up @@ -188,6 +226,7 @@ extension SavedPaymentMethodCollectionView {
equalTo: contentView.trailingAnchor, constant: 0),
accessoryButton.topAnchor.constraint(
equalTo: contentView.topAnchor, constant: 0),

])
}

Expand All @@ -208,15 +247,33 @@ extension SavedPaymentMethodCollectionView {
}
}

// MARK: - Internal Methods
private lazy var labelBottomConstraint: NSLayoutConstraint = {
return label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
}()
private lazy var labelHeightConstraint: NSLayoutConstraint = {
return label.heightAnchor.constraint(equalToConstant: 20)
}()
private lazy var defaultBadgeConstraints: [NSLayoutConstraint] = {
return [
defaultBadge.topAnchor.constraint(
equalTo: label.bottomAnchor, constant: 4),
defaultBadge.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
defaultBadge.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 2),
defaultBadge.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
]
}()

func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: Bool, allowsPaymentMethodRemoval: Bool) {
// MARK: - Internal Methods
func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: Bool, allowsPaymentMethodRemoval: Bool, allowsSetAsDefaultPM: Bool = false, needsVerticalPaddingForBadge: Bool = false, showDefaultPMBadge: Bool = false) {
paymentMethodLogo.isHidden = false
plus.isHidden = true
shadowRoundedRectangle.isHidden = false
self.viewModel = viewModel
self.cbcEligible = cbcEligible
self.allowsPaymentMethodRemoval = allowsPaymentMethodRemoval
self.allowsSetAsDefaultPM = allowsSetAsDefaultPM
self.needsVerticalPaddingForBadge = needsVerticalPaddingForBadge
self.showDefaultPMBadge = showDefaultPMBadge
update()
}

Expand Down Expand Up @@ -372,6 +429,17 @@ extension SavedPaymentMethodCollectionView {
}()
}
}

private func activateDefaultBadgeConstraints() {
NSLayoutConstraint.deactivate([labelBottomConstraint])
NSLayoutConstraint.activate([labelHeightConstraint] + defaultBadgeConstraints)
}

private func deactivateDefaultBadgeConstraints() {
NSLayoutConstraint.deactivate(defaultBadgeConstraints + [labelHeightConstraint])
NSLayoutConstraint.activate([labelBottomConstraint])
}

}

// A circle with an image in the middle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ class SavedPaymentOptionsViewController: UIViewController {
}
set {
collectionView.isRemovingPaymentMethods = newValue
collectionView.performBatchUpdates({
collectionView.reloadSections(IndexSet(integer: 0))
animateHeightChange{self.collectionView.updateLayout()}
})
UIView.transition(with: collectionView,
duration: 0.3,
options: .transitionCrossDissolve,
Expand All @@ -147,6 +151,11 @@ class SavedPaymentOptionsViewController: UIViewController {
}
}
}

var hasDefault: Bool {
return viewModels.contains(where: { isDefaultPaymentMethod(savedPaymentMethodId: $0.savedPaymentMethod?.stripeId) })
}

var bottomNoticeAttributedString: NSAttributedString? {
if case .saved(let paymentMethod, _) = selectedPaymentOption {
if paymentMethod.usBankAccount != nil {
Expand Down Expand Up @@ -267,7 +276,7 @@ class SavedPaymentOptionsViewController: UIViewController {

// MARK: - Views
private lazy var collectionView: SavedPaymentMethodCollectionView = {
let collectionView = SavedPaymentMethodCollectionView(appearance: appearance)
let collectionView = SavedPaymentMethodCollectionView(appearance: appearance, needsVerticalPaddingForBadge: hasDefault)
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
Expand Down Expand Up @@ -439,6 +448,11 @@ class SavedPaymentOptionsViewController: UIViewController {
collectionView.reloadItems(at: [selectedIndexPath])
}

private func isDefaultPaymentMethod(savedPaymentMethodId: String?) -> Bool {
guard configuration.allowsSetAsDefaultPM, let savedPaymentMethodId, let defaultPaymentMethod = elementsSession.customer?.getDefaultPaymentMethod() else { return false }
return savedPaymentMethodId == defaultPaymentMethod.stripeId
}

// MARK: - Helpers

/// Creates the list of viewmodels to display in the "saved payment methods" carousel e.g. `["+ Add", "Apple Pay", "Link", "Visa 4242"]`
Expand Down Expand Up @@ -508,7 +522,7 @@ extension SavedPaymentOptionsViewController: UICollectionViewDataSource, UIColle
stpAssertionFailure()
return UICollectionViewCell()
}
cell.setViewModel(viewModel, cbcEligible: cbcEligible, allowsPaymentMethodRemoval: self.configuration.allowsRemovalOfPaymentMethods)
cell.setViewModel(viewModel, cbcEligible: cbcEligible, allowsPaymentMethodRemoval: self.configuration.allowsRemovalOfPaymentMethods, allowsSetAsDefaultPM: configuration.allowsSetAsDefaultPM, needsVerticalPaddingForBadge: hasDefault, showDefaultPMBadge: isDefaultPaymentMethod(savedPaymentMethodId: viewModel.savedPaymentMethod?.stripeId))
cell.delegate = self
cell.isRemovingPaymentMethods = self.collectionView.isRemovingPaymentMethods
cell.appearance = appearance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ final class SavedPaymentMethodRowButton: UIView {
}
}

let showDefaultPMBadge: Bool

private var isEditing: Bool {
switch state {
case .selected, .unselected:
Expand Down Expand Up @@ -90,15 +92,21 @@ final class SavedPaymentMethodRowButton: UIView {
}()

private lazy var rowButton: RowButton = {
let button: RowButton = .makeForSavedPaymentMethod(paymentMethod: paymentMethod, appearance: appearance, rightAccessoryView: chevronButton, didTap: handleRowButtonTapped)
let button: RowButton = .makeForSavedPaymentMethod(paymentMethod: paymentMethod, appearance: appearance, badgeText: badgeText, rightAccessoryView: chevronButton, didTap: handleRowButtonTapped)

return button
}()

private lazy var badgeText: String? = {
return showDefaultPMBadge ? String.Localized.default_text : nil
}()

init(paymentMethod: STPPaymentMethod,
appearance: PaymentSheet.Appearance) {
appearance: PaymentSheet.Appearance,
showDefaultPMBadge: Bool = false) {
self.paymentMethod = paymentMethod
self.appearance = appearance
self.showDefaultPMBadge = showDefaultPMBadge
super.init(frame: .zero)

addAndPinSubview(rowButton)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,13 @@ class VerticalSavedPaymentMethodsViewController: UIViewController {
}

/// Indicates whether the chevron should be shown
/// True if any saved payment methods can be removed or edited (will update this to include allowing set as default)
/// True if any saved payment methods can be removed or edited
var canRemoveOrEdit: Bool {
let hasSupportedSavedPaymentMethods = paymentMethods.allSatisfy{ UpdatePaymentMethodViewModel.supportedPaymentMethods.contains($0.type) }
guard hasSupportedSavedPaymentMethods else {
fatalError("Saved payment methods contain unsupported payment methods.")
}
return canRemovePaymentMethods || canEditPaymentMethods
return configuration.allowsSetAsDefaultPM || canRemovePaymentMethods || canEditPaymentMethods
}

private var selectedPaymentMethod: STPPaymentMethod? {
Expand Down Expand Up @@ -174,10 +174,16 @@ class VerticalSavedPaymentMethodsViewController: UIViewController {
setInitialState(selectedPaymentMethod: selectedPaymentMethod)
}

private func isDefaultPaymentMethod(paymentMethodId: String) -> Bool {
guard configuration.allowsSetAsDefaultPM, let defaultPaymentMethod = elementsSession.customer?.getDefaultPaymentMethod() else { return false }
return configuration.allowsSetAsDefaultPM && paymentMethodId == defaultPaymentMethod.stripeId
}

private func buildPaymentMethodRows(paymentMethods: [STPPaymentMethod]) -> [SavedPaymentMethodRowButton] {
return paymentMethods.map { paymentMethod in
let button = SavedPaymentMethodRowButton(paymentMethod: paymentMethod,
appearance: configuration.appearance)
appearance: configuration.appearance,
showDefaultPMBadge: isDefaultPaymentMethod(paymentMethodId: paymentMethod.stripeId))
button.delegate = self
return button
}
Expand Down Expand Up @@ -363,7 +369,8 @@ extension VerticalSavedPaymentMethodsViewController: UpdatePaymentMethodViewCont
}

// Create the new button
let newButton = SavedPaymentMethodRowButton(paymentMethod: updatedPaymentMethod, appearance: configuration.appearance)
let newButton = SavedPaymentMethodRowButton(paymentMethod: updatedPaymentMethod, appearance: configuration.appearance, showDefaultPMBadge: isDefaultPaymentMethod(paymentMethodId: updatedPaymentMethod.stripeId))

newButton.delegate = self
newButton.previousSelectedState = oldButton.previousSelectedState
newButton.state = oldButton.state
Expand Down
Loading

0 comments on commit a052ae0

Please sign in to comment.