diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift b/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift index 1c235fd0a7b..2a26833dc1d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift @@ -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." + ) + } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/ElementsCustomer.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/ElementsCustomer.swift index 76ab7b66b6b..3419430cb97 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/ElementsCustomer.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/v1-elements-sessions/ElementsCustomer.swift @@ -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 } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Components/PaymentMethodPicker/LinkPaymentMethodPicker-Cell.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Components/PaymentMethodPicker/LinkPaymentMethodPicker-Cell.swift index b5efa868962..3f90eecd2af 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Components/PaymentMethodPicker/LinkPaymentMethodPicker-Cell.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Components/PaymentMethodPicker/LinkPaymentMethodPicker-Cell.swift @@ -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 = { diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift index f02ead84014..1b531c5d527 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift @@ -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) @@ -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 @@ -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() } } @@ -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 { @@ -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 @@ -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 @@ -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), @@ -188,6 +226,7 @@ extension SavedPaymentMethodCollectionView { equalTo: contentView.trailingAnchor, constant: 0), accessoryButton.topAnchor.constraint( equalTo: contentView.topAnchor, constant: 0), + ]) } @@ -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() } @@ -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 diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift index 07f58ae49c0..b9254d80d95 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift @@ -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, @@ -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 { @@ -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 @@ -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"]` @@ -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 diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/SavedPaymentMethodRowButton.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/SavedPaymentMethodRowButton.swift index c8dc6068aa8..695cbdc5728 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/SavedPaymentMethodRowButton.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/SavedPaymentMethodRowButton.swift @@ -47,6 +47,8 @@ final class SavedPaymentMethodRowButton: UIView { } } + let showDefaultPMBadge: Bool + private var isEditing: Bool { switch state { case .selected, .unselected: @@ -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) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift index 335a3dea8a8..14ba5ee3327 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift @@ -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? { @@ -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 } @@ -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 diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift index 0632ac92994..1ad36304c9c 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Vertical Main Screen/RowButton.swift @@ -32,6 +32,7 @@ class RowButton: UIView { let imageView: UIImageView let label: UILabel let sublabel: UILabel? + let defaultBadge: UILabel? let rightAccessoryView: UIView? let promoBadge: PromoBadgeView? private var promoBadgeConstraintToCheckmark: NSLayoutConstraint? @@ -47,6 +48,7 @@ class RowButton: UIView { radioButton?.isOn = isSelected checkmarkImageView?.isHidden = !isSelected updateAccessibilityTraits() + updateDefaultBadgeFont() if isFlatWithCheckmarkStyle { alignBadgeAndCheckmark() } @@ -63,12 +65,22 @@ class RowButton: UIView { } var heightConstraint: NSLayoutConstraint? + + private var selectedDefaultBadgeFont: UIFont { + return appearance.scaledFont(for: appearance.font.base.medium, style: .caption1, maximumPointSize: 20) + } + + private var defaultBadgeFont: UIFont { + return appearance.scaledFont(for: appearance.font.base.regular, style: .caption1, maximumPointSize: 20) + } + init( appearance: PaymentSheet.Appearance, originalCornerRadius: CGFloat? = nil, imageView: UIImageView, text: String, subtext: String? = nil, + badgeText: String? = nil, promoText: String? = nil, rightAccessoryView: UIView? = nil, shouldAnimateOnPress: Bool = false, @@ -95,6 +107,16 @@ class RowButton: UIView { } else { self.sublabel = nil } + if let badgeText { + let defaultBadge = UILabel() + defaultBadge.font = appearance.scaledFont(for: appearance.font.base.medium, style: .caption1, maximumPointSize: 20) + defaultBadge.textColor = appearance.colors.textSecondary + defaultBadge.adjustsFontForContentSizeCategory = true + defaultBadge.text = badgeText + self.defaultBadge = defaultBadge + } else { + self.defaultBadge = nil + } if let promoText { self.promoBadge = PromoBadgeView( appearance: appearance, @@ -175,7 +197,7 @@ class RowButton: UIView { } } - for view in [radioButton, imageView, labelsStackView].compactMap({ $0 }) { + for view in [radioButton, imageView, labelsStackView, defaultBadge].compactMap({ $0 }) { view.translatesAutoresizingMaskIntoConstraints = false view.isUserInteractionEnabled = false view.isAccessibilityElement = false @@ -231,6 +253,9 @@ class RowButton: UIView { labelsStackView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: insets), labelsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -insets), + defaultBadge?.leadingAnchor.constraint(equalTo: label.trailingAnchor, constant: 8), + defaultBadge?.centerYAnchor.constraint(equalTo: centerYAnchor), + imageViewBottomConstraint, imageViewTopConstraint, ].compactMap({ $0 })) @@ -271,6 +296,13 @@ class RowButton: UIView { promoBadgeConstraintToCheckmark?.isActive = isSelected } + private func updateDefaultBadgeFont() { + guard let defaultBadge else { + return + } + defaultBadge.font = isSelected ? selectedDefaultBadgeFont : defaultBadgeFont + } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -314,7 +346,7 @@ class RowButton: UIView { /// Sets icon, text, and sublabel alpha func setContentViewAlpha(_ alpha: CGFloat) { - [imageView, label, sublabel, promoBadge].compactMap { $0 }.forEach { + [imageView, label, sublabel, defaultBadge, promoBadge].compactMap { $0 }.forEach { $0.alpha = alpha } } @@ -435,10 +467,10 @@ extension RowButton { return button } - static func makeForSavedPaymentMethod(paymentMethod: STPPaymentMethod, appearance: PaymentSheet.Appearance, rightAccessoryView: UIView? = nil, isEmbedded: Bool = false, didTap: @escaping DidTapClosure) -> RowButton { + static func makeForSavedPaymentMethod(paymentMethod: STPPaymentMethod, appearance: PaymentSheet.Appearance, subtext: String? = nil, badgeText: String? = nil, rightAccessoryView: UIView? = nil, isEmbedded: Bool = false, didTap: @escaping DidTapClosure) -> RowButton { let imageView = UIImageView(image: paymentMethod.makeSavedPaymentMethodRowImage()) imageView.contentMode = .scaleAspectFit - let button = RowButton(appearance: appearance, imageView: imageView, text: paymentMethod.paymentSheetLabel, rightAccessoryView: rightAccessoryView, isEmbedded: isEmbedded, didTap: didTap) + let button = RowButton(appearance: appearance, imageView: imageView, text: paymentMethod.paymentSheetLabel, subtext: subtext, badgeText: badgeText, rightAccessoryView: rightAccessoryView, isEmbedded: isEmbedded, didTap: didTap) button.shadowRoundedRect.accessibilityLabel = paymentMethod.paymentSheetAccessibilityLabel return button } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift index 08ece3c4cf9..547f39f2627 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift @@ -26,20 +26,24 @@ final class SavedPaymentOptionsViewControllerSnapshotTests: STPSnapshotTestCase _test_all_saved_pms_and_apple_pay_and_link(darkMode: false, appearance: ._testMSPaintTheme) } - func _test_all_saved_pms_and_apple_pay_and_link(darkMode: Bool, appearance: PaymentSheet.Appearance = .default) { + func test_all_saved_pms_and_apple_pay_and_link_default_badge() { + _test_all_saved_pms_and_apple_pay_and_link(darkMode: false, showDefaultPMBadge: true) + } + + func _test_all_saved_pms_and_apple_pay_and_link(darkMode: Bool, appearance: PaymentSheet.Appearance = .default, showDefaultPMBadge: Bool = false) { let paymentMethods = [ STPPaymentMethod._testCard(), STPPaymentMethod._testUSBankAccount(), STPPaymentMethod._testSEPA(), ] - let config = SavedPaymentOptionsViewController.Configuration(customerID: "cus_123", showApplePay: true, showLink: true, removeSavedPaymentMethodMessage: nil, merchantDisplayName: "Test Merchant", isCVCRecollectionEnabled: false, isTestMode: false, allowsRemovalOfLastSavedPaymentMethod: false, allowsRemovalOfPaymentMethods: true, allowsSetAsDefaultPM: false) + let config = SavedPaymentOptionsViewController.Configuration(customerID: "cus_123", showApplePay: true, showLink: true, removeSavedPaymentMethodMessage: nil, merchantDisplayName: "Test Merchant", isCVCRecollectionEnabled: false, isTestMode: false, allowsRemovalOfLastSavedPaymentMethod: false, allowsRemovalOfPaymentMethods: true, allowsSetAsDefaultPM: showDefaultPMBadge) let intent = Intent.deferredIntent(intentConfig: .init(mode: .payment(amount: 0, currency: "USD", setupFutureUsage: nil, captureMethod: .automatic), confirmHandler: { _, _, _ in })) let sut = SavedPaymentOptionsViewController(savedPaymentMethods: paymentMethods, configuration: config, paymentSheetConfiguration: PaymentSheet.Configuration(), intent: intent, appearance: appearance, - elementsSession: .emptyElementsSession, + elementsSession: showDefaultPMBadge ? ._testDefaultCardValue(defaultPaymentMethod: paymentMethods.first?.stripeId ?? STPPaymentMethod._testCard().stripeId, paymentMethods: [testCardJSON, testUSBankAccountJSON, testSEPAJSON]) : .emptyElementsSession, analyticsHelper: ._testValue()) let testWindow = UIWindow() testWindow.isHidden = false @@ -50,10 +54,59 @@ final class SavedPaymentOptionsViewControllerSnapshotTests: STPSnapshotTestCase // Adding sut.view as the subview should be implied by the above line, but Autolayout can't lay out the view correctly on this pass of the runloop unless we explicitly addSubview. Maybe there are side effects that happen one turn of the runloop after setting the rootViewController. testWindow.addSubview(sut.view) sut.view.autosizeHeight(width: 1000) + if showDefaultPMBadge { + sut.isRemovingPaymentMethods = true + } NSLayoutConstraint.activate([ sut.view.topAnchor.constraint(equalTo: testWindow.topAnchor), sut.view.leftAnchor.constraint(equalTo: testWindow.leftAnchor), ]) STPSnapshotVerifyView(sut.view) } + + private let testCardJSON = [ + "id": "pm_123card", + "type": "card", + "card": [ + "last4": "4242", + "brand": "visa", + "fingerprint": "B8XXs2y2JsVBtB9f", + "networks": ["available": ["visa"]], + "exp_month": "01", + "exp_year": Calendar.current.component(.year, from: Date()) + 1 + ] + ] as [AnyHashable : Any] + private let testUSBankAccountJSON = [ + "id": "pm_123bank", + "type": "us_bank_account", + "us_bank_account": [ + "account_holder_type": "individual", + "account_type": "checking", + "bank_name": "STRIPE TEST BANK", + "fingerprint": "ickfX9sbxIyAlbuh", + "last4": "6789", + "networks": [ + "preferred": "ach", + "supported": [ + "ach", + ], + ] as [String: Any], + "routing_number": "110000000", + ] as [String: Any], + "billing_details": [ + "name": "Sam Stripe", + "email": "sam@stripe.com", + ] as [String: Any], + ] as [AnyHashable : Any] + private let testSEPAJSON = [ + "id": "pm_123sepa", + "type": "sepa_debit", + "sepa_debit": [ + "last4": "1234", + ], + "billing_details": [ + "name": "Sam Stripe", + "email": "sam@stripe.com", + ] as [String: Any], + ] as [AnyHashable : Any] } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift index 4e80480fdb0..5f5ac32cc9a 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift @@ -36,15 +36,24 @@ final class VerticalSavedPaymentMethodsViewControllerSnapshotTests: STPSnapshotT _test_VerticalSavedPaymentMethodsViewControllerSnapshotTests(darkMode: false, appearance: ._testMSPaintTheme, isEmbedded: true) } - func _test_VerticalSavedPaymentMethodsViewControllerSnapshotTests(darkMode: Bool, appearance: PaymentSheet.Appearance = .default, isEmbedded: Bool = false) { + func test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDefaultBadge() { + _test_VerticalSavedPaymentMethodsViewControllerSnapshotTests(darkMode: false, showDefaultPMBadge: true) + } + + func _test_VerticalSavedPaymentMethodsViewControllerSnapshotTests(darkMode: Bool, appearance: PaymentSheet.Appearance = .default, isEmbedded: Bool = false, showDefaultPMBadge: Bool = false) { var configuration = PaymentSheet.Configuration() configuration.appearance = appearance - let paymentMethods = generatePaymentMethods() + var paymentMethods = generatePaymentMethods() + if showDefaultPMBadge { + configuration.allowsSetAsDefaultPM = true + let card = STPPaymentMethod._testCard() + paymentMethods.insert(card, at: 0) + } let sut = VerticalSavedPaymentMethodsViewController(configuration: configuration, selectedPaymentMethod: paymentMethods.first, paymentMethods: paymentMethods, - elementsSession: ._testCardValue(), + elementsSession: showDefaultPMBadge ? ._testDefaultCardValue(defaultPaymentMethod: paymentMethods.first?.stripeId ?? STPPaymentMethod._testCard().stripeId, paymentMethods: [testCardJSON]) : ._testCardValue(), analyticsHelper: ._testValue() ) let bottomSheet: BottomSheetViewController @@ -73,12 +82,26 @@ final class VerticalSavedPaymentMethodsViewControllerSnapshotTests: STPSnapshotT } private func generatePaymentMethods() -> [STPPaymentMethod] { - return [STPFixtures.paymentMethod(), + return [ + STPFixtures.paymentMethod(), STPFixtures.usBankAccountPaymentMethod(), STPFixtures.usBankAccountPaymentMethod(bankName: "BANK OF AMERICA"), STPFixtures.usBankAccountPaymentMethod(bankName: "STRIPE"), STPFixtures.sepaDebitPaymentMethod(), ] } + + private let testCardJSON = [ + "id": "pm_123card", + "type": "card", + "card": [ + "last4": "4242", + "brand": "visa", + "fingerprint": "B8XXs2y2JsVBtB9f", + "networks": ["available": ["visa"]], + "exp_month": "01", + "exp_year": Calendar.current.component(.year, from: Date()) + 1 + ] + ] as [AnyHashable : Any] } final class StubBottomSheetContentViewController: UIViewController, BottomSheetContentViewController { diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.SavedPaymentOptionsViewControllerSnapshotTests/test_all_saved_pms_and_apple_pay_and_link_default_badge@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.SavedPaymentOptionsViewControllerSnapshotTests/test_all_saved_pms_and_apple_pay_and_link_default_badge@3x.png new file mode 100644 index 00000000000..544ef3c282d Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.SavedPaymentOptionsViewControllerSnapshotTests/test_all_saved_pms_and_apple_pay_and_link_default_badge@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDefaultBadge@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDefaultBadge@3x.png new file mode 100644 index 00000000000..acfe09785f7 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDefaultBadge@3x.png differ