Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update promo in MPE after bank flow #4370

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@
CB225E962CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB225E952CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift */; };
CB46EF492CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB46EF482CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift */; };
CB46EF4B2CED1BDA00E9A7F2 /* PromoBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB46EF4A2CED1BDA00E9A7F2 /* PromoBadgeView.swift */; };
CB7EB8D02D36F2B6009E2EC3 /* IncentiveOwner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB7EB8CF2D36F2B6009E2EC3 /* IncentiveOwner.swift */; };
CBF7BE542D11BF5300A4C172 /* BankAccountInfoViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF7BE532D11BF5300A4C172 /* BankAccountInfoViewSnapshotTests.swift */; };
CD19725E26DBDB9960D828CB /* BottomSheetPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8F09CF961C943E36D76860F /* BottomSheetPresentationAnimator.swift */; };
CF2AD2C7F761C46AE559E563 /* SavedPaymentOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ECDF6CF9AABD573F86CA2 /* SavedPaymentOptionsViewController.swift */; };
Expand Down Expand Up @@ -770,6 +771,7 @@
CB225E952CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodTypeCollectionViewCellSnapshotTests.swift; sourceTree = "<group>"; };
CB46EF482CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodIncentive.swift; sourceTree = "<group>"; };
CB46EF4A2CED1BDA00E9A7F2 /* PromoBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoBadgeView.swift; sourceTree = "<group>"; };
CB7EB8CF2D36F2B6009E2EC3 /* IncentiveOwner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncentiveOwner.swift; sourceTree = "<group>"; };
CBCFE3D39D670C3C77C59722 /* cs-CZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "cs-CZ"; path = "cs-CZ.lproj/Localizable.strings"; sourceTree = "<group>"; };
CBF7BE532D11BF5300A4C172 /* BankAccountInfoViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BankAccountInfoViewSnapshotTests.swift; sourceTree = "<group>"; };
CC3498CF4AEAA8F169616CDF /* STPCardBrandChoice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardBrandChoice.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1628,6 +1630,7 @@
3595F1786387A6B562FA472F /* BankAccountInfoView.swift */,
D3F8B6F8B253A009E6216478 /* USBankAccountPaymentMethodElement.swift */,
6A5997182BC88E28002A44CB /* InstantDebitsPaymentMethodElement.swift */,
CB7EB8CF2D36F2B6009E2EC3 /* IncentiveOwner.swift */,
);
path = USBankAccount;
sourceTree = "<group>";
Expand Down Expand Up @@ -2283,6 +2286,7 @@
BBA94A936D05C7DA2721F557 /* BacsDDMandateView.swift in Sources */,
B8A217F26AAEC592B9B0D2E1 /* CardScanButton.swift in Sources */,
436A212E364FD78C3745DDA3 /* CardScanningView.swift in Sources */,
CB7EB8D02D36F2B6009E2EC3 /* IncentiveOwner.swift in Sources */,
19A6D9D9951E13377F305263 /* CircularButton.swift in Sources */,
3147CEBB2CC07E960067B5E4 /* LinkUtils.swift in Sources */,
6BA8D33A2B0C1FBF008C51FF /* CVCPaymentMethodInformationView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ class EmbeddedFormViewController: UIViewController {
paymentMethodType: paymentMethodType,
// Special case: use "New Card" instead of "Card" if the displayed saved PM is a card
shouldUseNewCardHeader: shouldUseNewCardNewCardHeader,
appearance: configuration.appearance
appearance: configuration.appearance,
incentive: elementsSession.incentive?.takeIfAppliesTo(paymentMethodType)
)

return PaymentMethodFormViewController(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@ extension AddPaymentMethodViewController: PaymentMethodTypeCollectionViewDelegat
extension AddPaymentMethodViewController: PaymentMethodFormViewControllerDelegate {
func didUpdate(_ viewController: PaymentMethodFormViewController) {
delegate?.didUpdate(self)

if let instantDebitsFormElement = viewController.form as? InstantDebitsPaymentMethodElement {
let incentive = instantDebitsFormElement.displayableIncentive
paymentMethodTypesView.setIncentive(incentive)
}
}

func updateErrorLabel(for error: Swift.Error?) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ class PaymentMethodTypeCollectionView: UICollectionView {
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: PaymentMethodTypeCollectionView.cellHeight)
}

func setIncentive(_ incentive: PaymentMethodIncentive?) {
guard self.incentive != incentive, let index = self.indexPathsForSelectedItems?.first else {
return
}

self.incentive = incentive

// Prevent the selected cell from being unselected following the reload
reloadItems(at: [index])
selectItem(at: index, animated: false, scrollPosition: [])
}
}

// MARK: - UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation
@_spi(STP) import StripeCore
@_spi(STP) import StripePayments

struct PaymentMethodIncentive {
struct PaymentMethodIncentive: Equatable {

let identifier: String
let displayText: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// IncentiveOwner.swift
// StripePaymentSheet
//
// Created by Till Hellmund on 1/14/25.
//

import Foundation

protocol IncentiveOwner {
var showIncentiveInHeader: Bool { get }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,21 @@ final class InstantDebitsPaymentMethodElement: ContainerElement {
let emailElement: TextFieldElement?
let phoneElement: PhoneNumberElement?
let addressElement: AddressSectionElement?
private let promoDisclaimerElement: StaticElement?

private var linkedBankElements: [Element] {
return [linkedBankInfoSectionElement]
}
private let linkedBankInfoSectionElement: SectionElement
private let linkedBankInfoView: BankAccountInfoView
private var linkedBank: InstantDebitsLinkedBank?
private var linkedBank: InstantDebitsLinkedBank? {
didSet {
renderLinkedBank(linkedBank)
}
}
private let theme: ElementsAppearance
var presentingViewControllerDelegate: PresentingViewControllerDelegate?
var incentive: PaymentMethodIncentive?
private let incentive: PaymentMethodIncentive?

var delegate: ElementDelegate?
var view: UIView {
Expand Down Expand Up @@ -164,6 +169,13 @@ final class InstantDebitsPaymentMethodElement: ContainerElement {

return nameValid && emailValid && phoneValid && addressValid
}

var displayableIncentive: PaymentMethodIncentive? {
// We can show the incentive if we haven't linked a bank yet, meaning
// that we have no indication that the session is ineligible.
let canShowIncentive = linkedBank?.incentiveEligible ?? true
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason we default to true here? I'd expect the opposite

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If we don’t have a linked bank yet (and therefore no info if the session is eligible), we assume that the link_consumer_incentive as coming from the backend is valid. Otherwise, we would never show the incentive before the user completes the flow 🙈

return canShowIncentive ? incentive : nil
}

init(
configuration: PaymentSheetFormFactoryConfig,
Expand Down Expand Up @@ -195,8 +207,7 @@ final class InstantDebitsPaymentMethodElement: ContainerElement {
self.linkedBankInfoSectionElement.view.isHidden = true
self.incentive = incentive
self.theme = theme

let promoDisclaimerElement = incentive.flatMap {
self.promoDisclaimerElement = incentive.flatMap {
let label = ElementsUI.makeNoticeTextField(theme: theme)
label.attributedText = $0.promoDisclaimerText(with: theme, isPaymentIntent: isPaymentIntent)
label.textContainerInset = .zero
Expand Down Expand Up @@ -224,18 +235,30 @@ final class InstantDebitsPaymentMethodElement: ContainerElement {

func setLinkedBank(_ linkedBank: InstantDebitsLinkedBank) {
self.linkedBank = linkedBank
if let last4ofBankAccount = linkedBank.last4, let bankName = linkedBank.bankName {
self.delegate?.didUpdate(element: self)
}

fileprivate func renderLinkedBank(_ linkedBank: InstantDebitsLinkedBank?) {
if let linkedBank, let last4ofBankAccount = linkedBank.last4, let bankName = linkedBank.bankName {
linkedBankInfoView.setBankName(text: bankName)
linkedBankInfoView.setLastFourOfBank(text: "••••\(last4ofBankAccount)")
// TODO: Take the eligibility from the linked bank
linkedBankInfoView.setIncentiveEligible(false)
linkedBankInfoView.setIncentiveEligible(linkedBank.incentiveEligible)
}

formElement.toggleElements(
linkedBankElements,
hidden: linkedBank == nil,
animated: true
)

if let promoDisclaimerElement {
let hidePromoBadge = incentive == nil || linkedBank?.incentiveEligible == false
formElement.toggleElements(
linkedBankElements,
hidden: false,
[promoDisclaimerElement],
hidden: hidePromoBadge,
animated: true
)
}
self.delegate?.didUpdate(element: self)
}

func getLinkedBank() -> InstantDebitsLinkedBank? {
Expand All @@ -249,11 +272,6 @@ extension InstantDebitsPaymentMethodElement: BankAccountInfoViewDelegate {

func didTapXIcon() {
let hideLinkedBankElement = {
self.formElement.toggleElements(
self.linkedBankElements,
hidden: true,
animated: true
)
self.linkedBank = nil
self.delegate?.didUpdate(element: self)
}
Expand Down Expand Up @@ -323,6 +341,15 @@ extension InstantDebitsPaymentMethodElement: ElementDelegate {
}
}

// MARK: - IncentiveOwner

extension InstantDebitsPaymentMethodElement: IncentiveOwner {

var showIncentiveInHeader: Bool {
linkedBank == nil
}
}

private extension PaymentSheet.Address {
/// An address is valid if all fields except `line2` are not empty.
var isValid: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,22 @@ final class FormHeaderView: UIView {
return PaymentMethodTypeImageView(paymentMethodType: paymentMethodType, backgroundColor: appearance.colors.background)
}
}()

private var promoBadgeView: PromoBadgeView?

private lazy var spacerView: UIView = {
// This spacer makes sure that the promo badge is aligned correctly
let spacerView = UIView()
spacerView.setContentHuggingPriority(.defaultLow, for: .horizontal)
return spacerView
}()

private lazy var stackView: UIStackView = {
let views = [imageView, label].compactMap { $0 }
var views = [imageView, label].compactMap { $0 }
if let promoBadgeView {
views.append(contentsOf: [promoBadgeView, spacerView])
}

let stackView = UIStackView(arrangedSubviews: views)
stackView.spacing = 12
if imageView == nil {
Expand All @@ -51,13 +64,22 @@ final class FormHeaderView: UIView {
private let paymentMethodType: PaymentSheet.PaymentMethodType
private let shouldUseNewCardHeader: Bool // true if the customer has a saved payment method that is type card
private let appearance: PaymentSheet.Appearance
private var incentive: PaymentMethodIncentive?

init(paymentMethodType: PaymentSheet.PaymentMethodType, shouldUseNewCardHeader: Bool, appearance: PaymentSheet.Appearance) {
init(
paymentMethodType: PaymentSheet.PaymentMethodType,
shouldUseNewCardHeader: Bool,
appearance: PaymentSheet.Appearance,
incentive: PaymentMethodIncentive?
) {
self.paymentMethodType = paymentMethodType
self.shouldUseNewCardHeader = shouldUseNewCardHeader
self.appearance = appearance
self.incentive = incentive
self.promoBadgeView = Self.makePromoBadge(for: incentive, with: appearance)
super.init(frame: .zero)
addAndPinSubview(stackView)

if let imageView {
NSLayoutConstraint.activate([
imageView.widthAnchor.constraint(equalToConstant: 20),
Expand All @@ -69,4 +91,43 @@ final class FormHeaderView: UIView {
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

func setIncentive(_ incentive: PaymentMethodIncentive?) {
guard incentive != self.incentive else {
return
}

if let promoBadgeView {
stackView.removeArrangedSubview(promoBadgeView)
promoBadgeView.removeFromSuperview()

stackView.removeArrangedSubview(spacerView)
spacerView.removeFromSuperview()
}

self.incentive = incentive

if let incentive {
promoBadgeView = Self.makePromoBadge(for: incentive, with: appearance)
if let promoBadgeView {
stackView.addArrangedSubview(promoBadgeView)
stackView.addArrangedSubview(spacerView)
}
}
}

private static func makePromoBadge(
for incentive: PaymentMethodIncentive?,
with appearance: PaymentSheet.Appearance
) -> PromoBadgeView? {
guard let incentive else {
return nil
}

return PromoBadgeView(
appearance: appearance,
tinyMode: false,
text: incentive.displayText
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class RowButton: UIView {
}
}()
promoBadge.translatesAutoresizingMaskIntoConstraints = false
promoBadge.isUserInteractionEnabled = false
promoBadge.isAccessibilityElement = false
addSubview(promoBadge)
NSLayoutConstraint.activate([
promoBadge.centerYAnchor.constraint(equalTo: centerYAnchor),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ class VerticalPaymentMethodListViewController: UIViewController {
private(set) var currentSelection: VerticalPaymentMethodListSelection?
let stackView = UIStackView()
let appearance: PaymentSheet.Appearance
private(set) var incentive: PaymentMethodIncentive?
weak var delegate: VerticalPaymentMethodListViewControllerDelegate?

// Properties moved from initializer captures
private var overrideHeaderView: UIView?
private var savedPaymentMethod: STPPaymentMethod?
private var initialSelection: VerticalPaymentMethodListSelection?
private var savedPaymentMethodAccessoryType: RowButton.RightAccessoryButton.AccessoryType?
private var shouldShowApplePay: Bool
private var shouldShowLink: Bool
private var paymentMethodTypes: [PaymentSheet.PaymentMethodType]

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
Expand All @@ -43,11 +53,30 @@ class VerticalPaymentMethodListViewController: UIViewController {
incentive: PaymentMethodIncentive?,
delegate: VerticalPaymentMethodListViewControllerDelegate
) {
self.delegate = delegate
self.appearance = appearance
self.incentive = incentive
self.delegate = delegate
self.overrideHeaderView = overrideHeaderView
self.savedPaymentMethod = savedPaymentMethod
self.initialSelection = initialSelection
self.savedPaymentMethodAccessoryType = savedPaymentMethodAccessoryType
self.shouldShowApplePay = shouldShowApplePay
self.shouldShowLink = shouldShowLink
self.paymentMethodTypes = paymentMethodTypes

super.init(nibName: nil, bundle: nil)

self.renderContent()
}

private func refreshContent() {
stackView.arrangedSubviews.forEach { subview in
subview.removeFromSuperview()
}

renderContent()
}

private func renderContent() {
// Add the header - either the passed in `header` or "Select payment method"
let header = overrideHeaderView ?? PaymentSheetUI.makeHeaderLabel(title: .Localized.select_payment_method, appearance: appearance)
stackView.addArrangedSubview(header)
Expand Down Expand Up @@ -119,7 +148,7 @@ class VerticalPaymentMethodListViewController: UIViewController {
promoText: incentive?.takeIfAppliesTo(paymentMethodType)?.displayText,
appearance: appearance,
// Enable press animation if tapping this transitions the screen to a form instead of becoming selected
shouldAnimateOnPress: !delegate.shouldSelectPaymentMethod(selection)
shouldAnimateOnPress: delegate?.shouldSelectPaymentMethod(selection) == true
) { [weak self] in
self?.didTap(rowButton: $0, selection: selection)
}
Expand Down Expand Up @@ -170,6 +199,15 @@ class VerticalPaymentMethodListViewController: UIViewController {
@objc func didTapAccessoryButton() {
delegate?.didTapSavedPaymentMethodAccessoryButton()
}

func setIncentive(_ incentive: PaymentMethodIncentive?) {
guard self.incentive != incentive else {
return
}

self.incentive = incentive
self.refreshContent()
}

static func makeSectionLabel(text: String, appearance: PaymentSheet.Appearance) -> UILabel {
let label = UILabel()
Expand Down
Loading
Loading