Skip to content

Commit

Permalink
Show mandate in flowcontroller edge case, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
yuki-stripe committed Sep 7, 2023
1 parent 6caf27a commit ce2aed4
Show file tree
Hide file tree
Showing 20 changed files with 493 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1140,6 +1140,13 @@ class PaymentSheetSnapshotTests: FBSnapshotTestCase {
},
fileMock: .saved_payment_methods_200
)
stubPaymentMethods(
stubRequestCallback: { urlRequest in
return urlRequest.url?.absoluteString.contains("/v1/payment_methods") ?? false
&& urlRequest.url?.absoluteString.contains("type=sepa_debit") ?? false
},
fileMock: .saved_payment_methods_200
)
stubCustomers()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,75 @@ class PaymentSheetStandardLPMUITests: PaymentSheetUITestCase {

XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 15.0))
}

func testSavedSEPADebitPaymentMethod_FlowController_ShowsMandate() {
var settings = PaymentSheetTestPlaygroundSettings.defaultValues()
settings.uiStyle = .flowController
settings.customerMode = .new
settings.applePayEnabled = .off // disable Apple Pay
settings.mode = .setup
settings.allowsDelayedPMs = .on
loadPlayground(app, settings)

let paymentMethodButton = app.buttons["Payment method"]
XCTAssertTrue(paymentMethodButton.waitForExistence(timeout: 60.0))
paymentMethodButton.tap()

// Save SEPA
app.buttons["+ Add"].waitForExistenceAndTap()
guard let sepa = scroll(collectionView: app.collectionViews.firstMatch, toFindCellWithId: "SEPA Debit") else { XCTFail("Couldn't find SEPA"); return; }
sepa.tap()

app.textFields["Full name"].tap()
app.typeText("John Doe" + XCUIKeyboardKey.return.rawValue)
app.typeText("[email protected]" + XCUIKeyboardKey.return.rawValue)
app.typeText("AT611904300234573201" + XCUIKeyboardKey.return.rawValue)
app.textFields["Address line 1"].tap()
app.typeText("510 Townsend St" + XCUIKeyboardKey.return.rawValue)
app.typeText("Floor 3" + XCUIKeyboardKey.return.rawValue)
app.typeText("San Francisco" + XCUIKeyboardKey.return.rawValue)
app.textFields["ZIP"].tap()
app.typeText("94102" + XCUIKeyboardKey.return.rawValue)
app.buttons["Continue"].tap()
app.buttons["Confirm"].tap()
XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0))

// Reload w/ same customer
reload(app, settings: settings)
// Unfortunately, the next time you check out, Link is still selected by default.
// Select the saved SEPA PM to make it the default and make sure we can still check out successfully.
paymentMethodButton.tap()
app.buttons["••••3201"].waitForExistenceAndTap()
XCTAssertTrue(app.otherElements.matching(identifier: "mandatetextview").element.exists)
app.buttons["Continue"].tap()
app.buttons["Confirm"].tap()
XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0))

// Reload w/ same customer
reload(app, settings: settings)
// This time, expect SEPA to be pre-selected as the default
XCTAssertEqual(paymentMethodButton.label, "••••3201")
// Tapping confirm without presenting flowcontroller should show the mandate
app.buttons["Confirm"].tap()
XCTAssertTrue(app.otherElements.matching(identifier: "mandatetextview").element.waitForExistence(timeout: 1))
// Tapping out should cancel the payment
app.tap()
XCTAssertTrue(app.staticTexts["Payment canceled."].waitForExistence(timeout: 10.0))
// Tapping confirm again and hitting continue should confirm the payment
app.buttons["Confirm"].tap()
app.buttons["Continue"].tap()
XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0))

// Reload w/ same customer
reload(app, settings: settings)
// If you present the flowcontroller and see the mandate...
app.buttons["••••3201"].waitForExistenceAndTap()
XCTAssertTrue(app.otherElements.matching(identifier: "mandatetextview").element.exists)
// ...you shouldn't see the mandate again when you confirm
app.buttons["Continue"].tap()
app.buttons["Confirm"].tap()
XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0))
}
}

class PaymentSheetDeferredUITests: PaymentSheetUITestCase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extension PaymentSheet {
}

/// A class that presents the individual steps of a payment flow
public class FlowController {
public class FlowController {
// MARK: - Public properties
/// Contains details about a payment method that can be displayed to the customer
public struct PaymentOptionDisplayData {
Expand Down Expand Up @@ -120,6 +120,7 @@ extension PaymentSheet {
}

private var isPresented = false
private(set) var didPresent: Bool = false

// MARK: - Initializer (Internal)

Expand Down Expand Up @@ -267,6 +268,7 @@ extension PaymentSheet {

presentingViewController.presentAsBottomSheet(bottomSheetVC, appearance: self.configuration.appearance)
self.isPresented = true
self.didPresent = true
}

showPaymentOptions()
Expand Down Expand Up @@ -305,34 +307,54 @@ extension PaymentSheet {

let authenticationContext = AuthenticationContext(presentingViewController: presentingViewController, appearance: configuration.appearance)

PaymentSheet.confirm(
configuration: configuration,
authenticationContext: authenticationContext,
intent: intent,
paymentOption: paymentOption,
paymentHandler: paymentHandler,
isFlowController: true
) { [intent, configuration] result, deferredIntentConfirmationType in
STPAnalyticsClient.sharedClient.logPaymentSheetPayment(
isCustom: true,
paymentMethod: paymentOption.analyticsValue,
result: result,
linkEnabled: intent.supportsLink,
activeLinkSession: LinkAccountContext.shared.account?.sessionState == .verified,
linkSessionType: intent.linkPopupWebviewOption,
currency: intent.currency,
intentConfig: intent.intentConfig,
deferredIntentConfirmationType: deferredIntentConfirmationType,
paymentMethodTypeAnalyticsValue: paymentOption.paymentMethodTypeAnalyticsValue,
error: result.error
)

if case .completed = result, case .link = paymentOption {
// Remember Link as default payment method for users who just created an account.
CustomerPaymentOption.setDefaultPaymentMethod(.link, forCustomer: configuration.customer?.id)
if viewController.selectedPaymentMethodType == .dynamic("sepa_debit"), !didPresent {
// We're legally required to show the customer the SEPA mandate before every payment/setup
// In the edge case where the customer never opened the sheet, and thus never saw the mandate, we present the mandate directly
let sepaMandateVC = SepaMandateViewController(configuration: configuration) { didAcceptMandate in
presentingViewController.dismiss(animated: true) {
if didAcceptMandate {
confirm()
} else {
completion(.canceled)
}
}
}
let bottomSheet = Self.makeBottomSheetViewController(sepaMandateVC, configuration: configuration)
presentingViewController.presentAsBottomSheet(bottomSheet, appearance: configuration.appearance)
} else {
confirm()
}

completion(result)
func confirm() {
PaymentSheet.confirm(
configuration: configuration,
authenticationContext: authenticationContext,
intent: intent,
paymentOption: paymentOption,
paymentHandler: paymentHandler,
isFlowController: true
) { [intent, configuration] result, deferredIntentConfirmationType in
STPAnalyticsClient.sharedClient.logPaymentSheetPayment(
isCustom: true,
paymentMethod: paymentOption.analyticsValue,
result: result,
linkEnabled: intent.supportsLink,
activeLinkSession: LinkAccountContext.shared.account?.sessionState == .verified,
linkSessionType: intent.linkPopupWebviewOption,
currency: intent.currency,
intentConfig: intent.intentConfig,
deferredIntentConfirmationType: deferredIntentConfirmationType,
paymentMethodTypeAnalyticsValue: paymentOption.paymentMethodTypeAnalyticsValue,
error: result.error
)

if case .completed = result, case .link = paymentOption {
// Remember Link as default payment method for users who just created an account.
CustomerPaymentOption.setDefaultPaymentMethod(.link, forCustomer: configuration.customer?.id)
}

completion(result)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ class SavedPaymentOptionsViewController: UIViewController {
if case .saved(let paymentMethod) = selectedPaymentOption {
if paymentMethod.usBankAccount != nil {
return USBankAccountPaymentMethodElement.attributedMandateTextSavedPaymentMethod(theme: appearance.asElementsTheme)
// } else if paymentMethod.type == .SEPADebit {
// let string = NSMutableAttributedString(string: String(format: String.Localized.sepa_mandate_text, configuration.merchantDisplayName))
// let style = NSMutableParagraphStyle()
// style.alignment = .left
// string.addAttributes([.paragraphStyle: style,
// .font: UIFont.preferredFont(forTextStyle: .footnote),
// .foregroundColor: appearance.asElementsTheme.colors.secondaryText,
// ],
// range: NSRange(location: 0, length: string.length))
// return string
}
}
return nil
Expand Down Expand Up @@ -156,14 +166,14 @@ class SavedPaymentOptionsViewController: UIViewController {
collectionView.dataSource = self
return collectionView
}()

/// This contains views to display below the saved PM collectionView
private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [sepaMandateView])
stackView.axis = .vertical
return stackView
}()

private lazy var sepaMandateView: UIView = {
let view = UIView()
let mandateView = sepaMandateElement.view
Expand Down Expand Up @@ -193,6 +203,7 @@ class SavedPaymentOptionsViewController: UIViewController {
self.appearance = appearance
self.delegate = delegate
super.init(nibName: nil, bundle: nil)
updateUI() // Unfortunately this call is needed
}

required init?(coder: NSCoder) {
Expand All @@ -202,7 +213,7 @@ class SavedPaymentOptionsViewController: UIViewController {
// MARK: - UIViewController
override func viewDidLoad() {
super.viewDidLoad()

for subview in [collectionView, stackView] {
subview.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(subview)
Expand All @@ -214,7 +225,7 @@ class SavedPaymentOptionsViewController: UIViewController {
collectionView.bottomAnchor.constraint(equalTo: stackView.topAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
updateUI()
}
Expand Down Expand Up @@ -252,15 +263,15 @@ class SavedPaymentOptionsViewController: UIViewController {
collectionView.scrollToItem(at: IndexPath(item: 0, section: 0), at: .left, animated: false)
updateMandateView()
}

private func updateMandateView() {
guard let selectedViewModelIndex else {
guard let selectedViewModelIndex, let viewModel = viewModels.stp_boundSafeObject(at: selectedViewModelIndex) else {
return
}
let viewModel = viewModels[selectedViewModelIndex]
let shouldHideSEPA: Bool
if case .saved(paymentMethod: let paymentMethod) = viewModel, paymentMethod.type == .SEPADebit {
shouldHideSEPA = false
// shouldHideSEPA = true // TODO Is this better?
} else {
shouldHideSEPA = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ class PaymentSheetFlowControllerViewController: UIViewController {
private var isSavingInProgress: Bool = false
private var isVerificationInProgress: Bool = false
private let isApplePayEnabled: Bool

private let isLinkEnabled: Bool

// MARK: - Views
Expand Down Expand Up @@ -180,6 +179,7 @@ class PaymentSheetFlowControllerViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = configuration.appearance.colors.background

// One stack view contains all our subviews
let stackView = UIStackView(arrangedSubviews: [
Expand Down Expand Up @@ -559,6 +559,6 @@ extension PaymentSheetFlowControllerViewController: SheetNavigationBarDelegate {
// MARK: - PaymentSheetPaymentMethodType Helpers
extension PaymentSheet.PaymentMethodType {
var requiresMandateDisplayForSavedSelection: Bool {
return self == .USBankAccount
return self == .USBankAccount || self == .dynamic("sepa_debit")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// SepaMandateViewController.swift
// StripePaymentSheet
//
// Created by Yuki Tokuhiro on 9/6/23.
//

@_spi(STP) import StripeCore
import UIKit
// @_spi(STP) import StripePayments
@_spi(STP) import StripeUICore

class SepaMandateViewController: UIViewController, BottomSheetContentViewController {
let requiresFullScreen: Bool = false

lazy var navigationBar: SheetNavigationBar = {
let navBar = SheetNavigationBar(isTestMode: false, appearance: configuration.appearance)
navBar.setStyle(.none)
navBar.delegate = self
return navBar
}()
private lazy var sepaMandateElement: SimpleMandateElement = {
let mandateText = String(format: String.Localized.sepa_mandate_text, configuration.merchantDisplayName)
return SimpleMandateElement(mandateText: mandateText, theme: configuration.appearance.asElementsTheme)
}()

private lazy var confirmButton: ConfirmButton = {
let button = ConfirmButton(
callToAction: .customWithLock(title: String.Localized.continue),
appearance: configuration.appearance,
didTap: { [weak self] in
self?.completion(true)
}
)
return button
}()

let configuration: PaymentSheet.Configuration
let completion: (Bool) -> Void

/// - Parameter completion: Called with `true` after the customer accepts the mandate by tapping the "continue" button, or called with `false` after the customer dismisses the view (either by tapping out or swiping down). Does not dismiss the view controller.
required init(configuration: PaymentSheet.Configuration, completion: @escaping (Bool) -> Void) {
self.configuration = configuration
self.completion = completion
super.init(nibName: nil, bundle: nil)
}

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

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = configuration.appearance.colors.background
let stackView = UIStackView(arrangedSubviews: [sepaMandateElement.view, confirmButton])
stackView.axis = .vertical
stackView.spacing = PaymentSheetUI.defaultPadding

view.addAndPinSubviewToSafeArea(stackView, insets: PaymentSheetUI.defaultSheetMargins/*.insets(
top: PaymentSheetUI.defaultSheetMargins.top,
leading: PaymentSheetUI.defaultSheetMargins.leading,
bottom: PaymentSheetUI.defaultSheetMargins.bottom,
trailing: PaymentSheetUI.defaultSheetMargins.trailing
)*/)
}

func didTapOrSwipeToDismiss() {
self.completion(false)
}
}

extension SepaMandateViewController: SheetNavigationBarDelegate {
func sheetNavigationBarDidClose(_ sheetNavigationBar: SheetNavigationBar) {
self.completion(false)
}

func sheetNavigationBarDidBack(_ sheetNavigationBar: SheetNavigationBar) {
self.completion(false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class SimpleMandateTextView: UIView {
super.init(frame: .zero)
label.text = mandateText
installConstraints()
self.accessibilityIdentifier = "mandatetextview"
}

required init?(coder: NSCoder) {
Expand Down
Loading

0 comments on commit ce2aed4

Please sign in to comment.