diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetSnapshotTests.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetSnapshotTests.swift index 69f8778fa92..e5f2db8f290 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetSnapshotTests.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetSnapshotTests.swift @@ -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() } diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift index 73acad368ea..f05bb2ebfa0 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift @@ -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("test@example.com" + 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 { diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift index 4e2cb23df10..4329af2102d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFlowController.swift @@ -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 { @@ -120,6 +120,7 @@ extension PaymentSheet { } private var isPresented = false + private(set) var didPresent: Bool = false // MARK: - Initializer (Internal) @@ -267,6 +268,7 @@ extension PaymentSheet { presentingViewController.presentAsBottomSheet(bottomSheetVC, appearance: self.configuration.appearance) self.isPresented = true + self.didPresent = true } showPaymentOptions() @@ -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) + } } } 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 002efb7acb1..e6375d19ad5 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift @@ -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 @@ -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 @@ -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) { @@ -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) @@ -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() } @@ -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 } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift index 314c7683df2..f70bbab1da4 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift @@ -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 @@ -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: [ @@ -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") } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/SepaMandateViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/SepaMandateViewController.swift new file mode 100644 index 00000000000..f810c90bd6a --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/SepaMandateViewController.swift @@ -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) + } +} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/SimpleMandateTextView.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/SimpleMandateTextView.swift index 3ddb21c3dcc..43c358baaa2 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/SimpleMandateTextView.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/SimpleMandateTextView.swift @@ -27,6 +27,7 @@ class SimpleMandateTextView: UIView { super.init(frame: .zero) label.text = mandateText installConstraints() + self.accessibilityIdentifier = "mandatetextview" } required init?(coder: NSCoder) { diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetFlowControllerViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetFlowControllerViewControllerSnapshotTests.swift new file mode 100644 index 00000000000..fa8e6841899 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetFlowControllerViewControllerSnapshotTests.swift @@ -0,0 +1,66 @@ +// +// PaymentSheetFlowControllerViewControllerSnapshotTests.swift +// StripePaymentSheetTests +// +// Created by Yuki Tokuhiro on 9/7/23. +// + +import iOSSnapshotTestCase +@_spi(STP) import StripeCore +import StripeCoreTestUtils +@_spi(STP) @testable import StripePaymentSheet + +import XCTest + +final class PaymentSheetFlowControllerViewControllerSnapshotTests: FBSnapshotTestCase { + + override func setUp() { + super.setUp() +// recordMode = true + } + + func testSavedScreen_card() { + let paymentMethods = [ + STPPaymentMethod._testCard(), + ] + let sut = PaymentSheetFlowControllerViewController( + intent: ._testValue(), + savedPaymentMethods: paymentMethods, + configuration: ._testValue_MostPermissive(), + isApplePayEnabled: false, + isLinkEnabled: false + ) + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } + + func testSavedScreen_us_bank_account() { + let paymentMethods = [ + STPPaymentMethod._testUSBankAccount(), + ] + let sut = PaymentSheetFlowControllerViewController( + intent: ._testValue(), + savedPaymentMethods: paymentMethods, + configuration: ._testValue_MostPermissive(), + isApplePayEnabled: false, + isLinkEnabled: false + ) + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } + + func testSavedScreen_SEPA_debit() { + let paymentMethods = [ + STPPaymentMethod._testSEPA(), + ] + let sut = PaymentSheetFlowControllerViewController( + intent: ._testValue(), + savedPaymentMethods: paymentMethods, + configuration: ._testValue_MostPermissive(), + isApplePayEnabled: false, + isLinkEnabled: false + ) + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } +} diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift index a7355e7d1de..402007a8972 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift @@ -22,13 +22,13 @@ import XCTest /// 👀 See `testIdealConfirmFlows` for an example with comments. @MainActor final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { - + enum MerchantCountry: String { case US = "us" case SG = "sg" case MY = "my" case BE = "be" - + var publishableKey: String { switch self { case .US: @@ -42,11 +42,11 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { } } } - + override func setUp() async throws { await PaymentSheetLoader.loadMiscellaneousSingletons() } - + /// 👋 👨‍🏫 Look at this test to understand how to write your own tests in this file func testiDEALConfirmFlows() async throws { try await _testConfirm(intentKinds: [.paymentIntent], currency: "EUR", paymentMethodType: .dynamic("ideal")) { form in @@ -60,7 +60,7 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { XCTAssertNil(form.getTextFieldElement("Email")) // Tip: To help you debug, print out `form.getAllUnwrappedSubElements()` } - + // If your payment method shows different fields depending on the kind of intent, you can call `_testConfirm` multiple times with different intents. // e.g. iDEAL should show an email field and mandate for PI+SFU and SIs, so we test those separately here: try await _testConfirm(intentKinds: [.paymentIntentWithSetupFutureUsage, .setupIntent], currency: "EUR", paymentMethodType: .dynamic("ideal")) { form in @@ -70,7 +70,7 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { XCTAssertNotNil(form.getMandateElement()) } } - + func testSEPADebitConfirmFlows() async throws { try await _testConfirm(intentKinds: [.paymentIntent, .paymentIntentWithSetupFutureUsage, .setupIntent], currency: "EUR", paymentMethodType: .dynamic("sepa_debit")) { form in form.getTextFieldElement("Full name")?.setText("Foo") @@ -82,21 +82,21 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { XCTAssertNotNil(form.getMandateElement()) } } - + func testBancontactConfirmFlows() async throws { try await _testConfirm(intentKinds: [.paymentIntent], currency: "EUR", paymentMethodType: .dynamic("bancontact")) { form in form.getTextFieldElement("Full name")?.setText("Foo") XCTAssertNil(form.getMandateElement()) XCTAssertNil(form.getTextFieldElement("Email")) } - + try await _testConfirm(intentKinds: [.paymentIntentWithSetupFutureUsage, .setupIntent], currency: "EUR", paymentMethodType: .dynamic("bancontact")) { form in form.getTextFieldElement("Full name")?.setText("Foo") form.getTextFieldElement("Email")?.setText("f@z.c") XCTAssertNotNil(form.getMandateElement()) } } - + func testSofortConfirmFlows() async throws { try await _testConfirm(intentKinds: [.paymentIntent], currency: "EUR", paymentMethodType: .dynamic("sofort")) { form in XCTAssertNotNil(form.getDropdownFieldElement("Country or region")) @@ -104,7 +104,7 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { XCTAssertNil(form.getTextFieldElement("Email")) XCTAssertNil(form.getMandateElement()) } - + try await _testConfirm(intentKinds: [.paymentIntentWithSetupFutureUsage, .setupIntent], currency: "EUR", paymentMethodType: .dynamic("sofort")) { form in XCTAssertNotNil(form.getDropdownFieldElement("Country or region")) form.getTextFieldElement("Full name")?.setText("Foo") @@ -112,7 +112,7 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { XCTAssertNotNil(form.getMandateElement()) } } - + func testGrabPayConfirmFlows() async throws { // GrabPay has no input fields try await _testConfirm(intentKinds: [.paymentIntent], @@ -121,7 +121,7 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { merchantCountry: .SG) { _ in } } - + func testFPXConfirmFlows() async throws { try await _testConfirm(intentKinds: [.paymentIntent], currency: "MYR", @@ -130,13 +130,13 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { XCTAssertNotNil(form.getDropdownFieldElement("FPX Bank")) } } - + func testBLIKConfirmFlows() async throws { try await _testConfirm(intentKinds: [.paymentIntent], currency: "PLN", paymentMethodType: .dynamic("blik"), merchantCountry: .BE) { form in form.getTextFieldElement("BLIK code")?.setText("123456") } } - + func testAmazonPayConfirmFlows() async throws { try await _testConfirm(intentKinds: [.paymentIntent], currency: "USD", @@ -146,14 +146,14 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { XCTAssertEqual(form.getAllSubElements().count, 1) } } - + func testSavedSEPA() async throws { let customer = "cus_OaMPphpKbeixCz" // A hardcoded customer on acct_1G6m1pFY0qyl6XeW let savedSepaPM = STPPaymentMethod.decodedObject(fromAPIResponse: [ "id": "pm_1NnBnhFY0qyl6XeW9ThDjAvw", // A hardcoded SEPA PM for the ^ customer - "type": "sepa_debit" + "type": "sepa_debit", ])! - + // Update the API client based on the merchant country let apiClient = STPAPIClient(publishableKey: MerchantCountry.US.publishableKey) let configuration: PaymentSheet.Configuration = { @@ -163,7 +163,7 @@ final class PaymentSheet_LPM_ConfirmFlowTests: XCTestCase { config.returnURL = "https://foo.com" return config }() - + // Confirm saved SEPA with every confirm variation for intentKind in IntentKind.allCases { for (description, intent) in try await makeTestIntents(intentKind: intentKind, currency: "eur", paymentMethod: .dynamic("sepa_debit"), merchantCountry: .US, customer: customer, apiClient: apiClient) { @@ -199,7 +199,7 @@ extension PaymentSheet_LPM_ConfirmFlowTests { case paymentIntentWithSetupFutureUsage case setupIntent } - + func _testConfirm(intentKinds: [IntentKind], currency: String, paymentMethodType: PaymentSheet.PaymentMethodType, merchantCountry: MerchantCountry = .US, formCompleter: (PaymentMethodElement) -> Void) async throws { for intentKind in intentKinds { try await _testConfirm(intentKind: intentKind, @@ -209,7 +209,7 @@ extension PaymentSheet_LPM_ConfirmFlowTests { formCompleter: formCompleter) } } - + /// A helper method that creates a form for the given `paymentMethodType` and tests three confirmation flows successfully complete: /// 1. normal" client-side confirmation /// 2. deferred client-side confirmation @@ -236,18 +236,18 @@ extension PaymentSheet_LPM_ConfirmFlowTests { return config }() let intents = try await makeTestIntents(intentKind: intentKind, currency: currency, paymentMethod: paymentMethodType, merchantCountry: merchantCountry, apiClient: apiClient) - + for (description, intent) in intents { // Make the form let formFactory = PaymentSheetFormFactory(intent: intent, configuration: .paymentSheet(configuration), paymentMethod: paymentMethodType) let paymentMethodForm = formFactory.make() let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 1000)) view.addAndPinSubview(paymentMethodForm.view) - + // Fill out the form sendEventToSubviews(.viewDidAppear, from: paymentMethodForm.view) // Simulate view appearance. This makes SimpleMandateElement mark its mandate as having been displayed. formCompleter(paymentMethodForm) - + // Generate params from the form guard let intentConfirmParams = paymentMethodForm.updateParams(params: IntentConfirmParams(type: paymentMethodType)) else { XCTFail("Form failed to create params. Validation state: \(paymentMethodForm.validationState)") @@ -262,7 +262,7 @@ extension PaymentSheet_LPM_ConfirmFlowTests { paymentHandler._handleWillForegroundNotification() redirectShimCalled = true } - + // Confirm the intent with the form details PaymentSheet.confirm( configuration: configuration, @@ -284,7 +284,7 @@ extension PaymentSheet_LPM_ConfirmFlowTests { await fulfillment(of: [e], timeout: 5) } } - + func makeTestIntents( intentKind: IntentKind, currency: String, @@ -308,7 +308,7 @@ extension PaymentSheet_LPM_ConfirmFlowTests { func makeDeferredIntent(_ intentConfig: PaymentSheet.IntentConfiguration) -> Intent { return .deferredIntent(elementsSession: ._testCardValue(), intentConfig: intentConfig) } - + var intents: [(String, Intent)] let paymentMethodTypes = [PaymentSheet.PaymentMethodType.string(from: paymentMethod)].compactMap { $0 } switch intentKind { @@ -322,7 +322,7 @@ extension PaymentSheet_LPM_ConfirmFlowTests { ) return try await apiClient.retrievePaymentIntent(clientSecret: clientSecret) }() - + let deferredCSC = PaymentSheet.IntentConfiguration(mode: .payment(amount: 1099, currency: currency)) { _, _ in return try await STPTestingAPIClient.shared.fetchPaymentIntent( types: paymentMethodTypes, @@ -331,12 +331,11 @@ extension PaymentSheet_LPM_ConfirmFlowTests { customerID: customer ) } - + intents = [ ("PaymentIntent", .paymentIntent(paymentIntent)), ("Deferred PaymentIntent - client side confirmation", makeDeferredIntent(deferredCSC)), ] - guard paymentMethod != .dynamic("blik") else { // Blik doesn't support server-side confirmation return intents @@ -351,11 +350,11 @@ extension PaymentSheet_LPM_ConfirmFlowTests { otherParams: paramsForServerSideConfirmation ) } - + intents += [ ("Deferred PaymentIntent - server side confirmation", makeDeferredIntent(deferredSSC)), ] - + return intents case .paymentIntentWithSetupFutureUsage: let paymentIntent: STPPaymentIntent = try await { diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetViewControllerSnapshotTests.swift new file mode 100644 index 00000000000..42e517cb316 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetViewControllerSnapshotTests.swift @@ -0,0 +1,83 @@ +// +// PaymentSheetViewControllerSnapshotTests.swift +// StripePaymentSheetTests +// +// Created by Yuki Tokuhiro on 9/7/23. +// + +import iOSSnapshotTestCase +@_spi(STP) import StripeCore +import StripeCoreTestUtils +@_spi(STP) @testable import StripePaymentSheet + +import XCTest + +final class PaymentSheetViewControllerSnapshotTests: FBSnapshotTestCase { + + override func setUp() { + super.setUp() +// recordMode = true + } + + func testSavedScreen_card() { + let paymentMethods = [ + STPPaymentMethod._testCard(), + ] + let sut = PaymentSheetViewController( + intent: ._testValue(), + savedPaymentMethods: paymentMethods, + configuration: ._testValue_MostPermissive(), + isApplePayEnabled: false, + isLinkEnabled: false, + delegate: self + ) + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } + + func testSavedScreen_us_bank_account() { + let paymentMethods = [ + STPPaymentMethod._testUSBankAccount(), + ] + let sut = PaymentSheetViewController( + intent: ._testValue(), + savedPaymentMethods: paymentMethods, + configuration: ._testValue_MostPermissive(), + isApplePayEnabled: false, + isLinkEnabled: false, + delegate: self + ) + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } + + func testSavedScreen_SEPA_debit() { + let paymentMethods = [ + STPPaymentMethod._testSEPA(), + ] + let sut = PaymentSheetViewController( + intent: ._testValue(), + savedPaymentMethods: paymentMethods, + configuration: ._testValue_MostPermissive(), + isApplePayEnabled: false, + isLinkEnabled: false, + delegate: self + ) + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } +} + +extension PaymentSheetViewControllerSnapshotTests: PaymentSheetViewControllerDelegate { + func paymentSheetViewControllerShouldConfirm(_ paymentSheetViewController: StripePaymentSheet.PaymentSheetViewController, with paymentOption: StripePaymentSheet.PaymentOption, completion: @escaping (StripePaymentSheet.PaymentSheetResult, StripeCore.STPAnalyticsClient.DeferredIntentConfirmationType?) -> Void) { + } + + func paymentSheetViewControllerDidFinish(_ paymentSheetViewController: StripePaymentSheet.PaymentSheetViewController, result: StripePaymentSheet.PaymentSheetResult) { + } + + func paymentSheetViewControllerDidCancel(_ paymentSheetViewController: StripePaymentSheet.PaymentSheetViewController) { + } + + func paymentSheetViewControllerDidSelectPayWithLink(_ paymentSheetViewController: StripePaymentSheet.PaymentSheetViewController) { + } +} diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift index 7f03a42d128..81fd50d536a 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift @@ -41,3 +41,53 @@ extension STPElementsSession { return elementsSession } } + +extension Intent { + static func _testValue() -> Intent { + return .paymentIntent(STPFixtures.paymentIntent()) + } +} + +extension STPPaymentMethod { + static func _testCard() -> STPPaymentMethod { + return STPPaymentMethod.decodedObject(fromAPIResponse: [ + "id": "pm_123card", + "type": "card", + "card": [ + "last4": "4242", + "brand": "visa", + ], + ])! + } + + static func _testUSBankAccount() -> STPPaymentMethod { + return STPPaymentMethod.decodedObject(fromAPIResponse: [ + "id": "pm_123", + "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], + ])! + } + + static func _testSEPA() -> STPPaymentMethod { + return STPPaymentMethod.decodedObject(fromAPIResponse: [ + "id": "pm_123", + "type": "sepa_debit", + "sepa_debit": [ + "last4": "1234", + ], + ])! + } +} diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SepaMandateViewControllerSnapshotTest.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SepaMandateViewControllerSnapshotTest.swift new file mode 100644 index 00000000000..5ec3daa7c86 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SepaMandateViewControllerSnapshotTest.swift @@ -0,0 +1,38 @@ +// +// SepaMandateViewControllerSnapshotTest.swift +// StripeiOSTests +// +// Created by Yuki Tokuhiro on 9/7/23. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils +@_spi(STP) @testable import StripePaymentSheet +import XCTest + +final class SepaMandateViewControllerSnapshotTest: FBSnapshotTestCase { + + override func setUp() { + super.setUp() +// recordMode = true + } + + func testView() { + let configuration = PaymentSheet.Configuration._testValue_MostPermissive() + let sut = SepaMandateViewController(configuration: configuration) { _ in + // no-op + } + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } + + func testViewWithAppearanceConfiguration() { + var configuration = PaymentSheet.Configuration._testValue_MostPermissive() + configuration.appearance = PaymentSheetTestUtils.snapshotTestTheme + let sut = SepaMandateViewController(configuration: configuration) { _ in + // no-op + } + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } +} diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_SEPA_debit@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_SEPA_debit@3x.png new file mode 100644 index 00000000000..806730c0a0d Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_SEPA_debit@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_card@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_card@3x.png new file mode 100644 index 00000000000..2d3995c597a Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_card@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_us_bank_account@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_us_bank_account@3x.png new file mode 100644 index 00000000000..3cd5bc8adec Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetFlowControllerViewControllerSnapshotTests/testSavedScreen_us_bank_account@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_SEPA_debit@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_SEPA_debit@3x.png new file mode 100644 index 00000000000..eb839f0faf9 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_SEPA_debit@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_card@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_card@3x.png new file mode 100644 index 00000000000..6cd9bcfd781 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_card@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_us_bank_account@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_us_bank_account@3x.png new file mode 100644 index 00000000000..55b2db6765e Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.PaymentSheetViewControllerSnapshotTests/testSavedScreen_us_bank_account@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.SepaMandateViewControllerSnapshotTest/testView@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.SepaMandateViewControllerSnapshotTest/testView@3x.png new file mode 100644 index 00000000000..c110680b680 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.SepaMandateViewControllerSnapshotTest/testView@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.SepaMandateViewControllerSnapshotTest/testViewWithAppearanceConfiguration@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.SepaMandateViewControllerSnapshotTest/testViewWithAppearanceConfiguration@3x.png new file mode 100644 index 00000000000..a613fee60ee Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.SepaMandateViewControllerSnapshotTest/testViewWithAppearanceConfiguration@3x.png differ