From 23bcce5f929c0de7b9c679e43c28c75f2892cd6b Mon Sep 17 00:00:00 2001 From: John Woo Date: Thu, 2 May 2024 15:16:46 -0700 Subject: [PATCH 1/6] Add prototype for enable/disable removal of spms --- .../CustomerSheetTestPlayground.swift | 1 + ...ustomerSheetTestPlaygroundController.swift | 1 + .../CustomerSheetTestPlaygroundSettings.swift | 10 +++- .../PaymentSheetTestPlayground.swift | 1 + .../PaymentSheetTestPlaygroundSettings.swift | 8 +++ .../PlaygroundController.swift | 2 + .../CustomerSheetUITest.swift | 42 +++++++++++++++ .../PaymentSheetUITest.swift | 51 +++++++++++++++++++ ...ymentMethodsCollectionViewController.swift | 11 ++-- .../CustomerSheetConfiguration.swift | 2 + .../PaymentSheetConfiguration.swift | 3 ++ .../SavedPaymentMethodCollectionView.swift | 16 +++--- .../SavedPaymentOptionsViewController.swift | 10 ++-- 13 files changed, 141 insertions(+), 17 deletions(-) diff --git a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlayground.swift b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlayground.swift index 2929a5b9167..70fdc4773bf 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlayground.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlayground.swift @@ -59,6 +59,7 @@ struct CustomerSheetTestPlayground: View { SettingView(setting: $playgroundController.settings.autoreload) TextField("headerTextForSelectionScreen", text: headerTextForSelectionScreenBinding) SettingView(setting: $playgroundController.settings.allowsRemovalOfLastSavedPaymentMethod) + SettingView(setting: $playgroundController.settings.paymentMethodRemove) HStack { Text("Macros").font(.headline) Spacer() diff --git a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundController.swift b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundController.swift index bcaf6c7ec8e..bfc32935ab2 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundController.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundController.swift @@ -118,6 +118,7 @@ class CustomerSheetTestPlaygroundController: ObservableObject { configuration.returnURL = "payments-example://stripe-redirect" configuration.headerTextForSelectionScreen = settings.headerTextForSelectionScreen configuration.allowsRemovalOfLastSavedPaymentMethod = settings.allowsRemovalOfLastSavedPaymentMethod == .on + configuration.paymentMethodRemove = settings.paymentMethodRemove == .enabled if settings.defaultBillingAddress == .on { configuration.defaultBillingDetails.name = "Jane Doe" diff --git a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundSettings.swift b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundSettings.swift index 01f021a9f15..a402d2cd6e4 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundSettings.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundSettings.swift @@ -104,6 +104,12 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable { case on case off } + enum PaymentMethodRemove: String, PickerEnum { + static let enumName: String = "PaymentMethodRemove" + + case enabled + case disabled + } var customerMode: CustomerMode var customerId: String? @@ -122,6 +128,7 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable { var merchantCountryCode: MerchantCountry var preferredNetworksEnabled: PreferredNetworksEnabled var allowsRemovalOfLastSavedPaymentMethod: AllowsRemovalOfLastSavedPaymentMethod + var paymentMethodRemove: PaymentMethodRemove static func defaultValues() -> CustomerSheetTestPlaygroundSettings { return CustomerSheetTestPlaygroundSettings(customerMode: .new, @@ -139,7 +146,8 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable { collectAddress: .automatic, merchantCountryCode: .US, preferredNetworksEnabled: .off, - allowsRemovalOfLastSavedPaymentMethod: .on) + allowsRemovalOfLastSavedPaymentMethod: .on, + paymentMethodRemove: .enabled) } var base64Data: String { diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift index 6fb08800a5e..b36c7b70457 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift @@ -29,6 +29,7 @@ struct PaymentSheetTestPlayground: View { SettingView(setting: $playgroundController.settings.applePayEnabled) SettingView(setting: $playgroundController.settings.applePayButtonType) SettingView(setting: $playgroundController.settings.allowsDelayedPMs) + SettingView(setting: $playgroundController.settings.paymentMethodRemove) } Group { SettingPickerView(setting: $playgroundController.settings.defaultBillingAddress) diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift index f72ecec904d..191cd3b8369 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift @@ -157,6 +157,12 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable { case on case off } + enum PaymentMethodRemove: String, PickerEnum { + static var enumName: String { "PaymentMethodRemove" } + + case enabled + case disabled + } enum DefaultBillingAddress: String, PickerEnum { static var enumName: String { "Default billing address" } @@ -348,6 +354,7 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable { var applePayEnabled: ApplePayEnabled var applePayButtonType: ApplePayButtonType var allowsDelayedPMs: AllowsDelayedPMs + var paymentMethodRemove: PaymentMethodRemove var defaultBillingAddress: DefaultBillingAddress var customEmail: String? var linkEnabled: LinkEnabled @@ -382,6 +389,7 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable { applePayEnabled: .on, applePayButtonType: .buy, allowsDelayedPMs: .off, + paymentMethodRemove: .enabled, defaultBillingAddress: .off, customEmail: nil, linkEnabled: .off, diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift index 2e9d5d1eedf..b033e112968 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift @@ -150,6 +150,8 @@ class PlaygroundController: ObservableObject { if settings.allowsDelayedPMs == .on { configuration.allowsDelayedPaymentMethods = true } + configuration.paymentMethodRemove = settings.paymentMethodRemove == .enabled + if settings.shippingInfo != .off { configuration.allowsPaymentMethodsRequiringShippingAddress = true configuration.shippingDetails = { [weak self] in diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift index fac26689f44..198366ad6a7 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift @@ -1011,6 +1011,48 @@ class CustomerSheetUITest: XCTestCase { // ...but should not be able to remove it. XCTAssertFalse(app.buttons["Remove card"].exists) } + // MARK: - PaymentMethodRemove w/ CBC + func testPaymentMethodRemove() throws { + var settings = CustomerSheetTestPlaygroundSettings.defaultValues() + settings.merchantCountryCode = .FR + settings.customerMode = .new + settings.applePay = .on + settings.paymentMethodRemove = .disabled + loadPlayground( + app, + settings + ) + + // Save a card + app.staticTexts["None"].waitForExistenceAndTap() + app.buttons["+ Add"].waitForExistenceAndTap() + try! fillCardData(app, postalEnabled: true) + app.buttons["Save"].tap() + XCTAssertTrue(app.buttons["Confirm"].waitForExistence(timeout: timeout)) + + // Shouldn't be able to edit, only one saved PM when paymentMethodRemove = .disabled + XCTAssertFalse(app.staticTexts["Edit"].waitForExistence(timeout: 1)) + + // Add a CBC enabled PM + app.buttons["+ Add"].waitForExistenceAndTap() + try! fillCardData(app, cardNumber: "4000002500001001", postalEnabled: true) + app.buttons["Save"].tap() + XCTAssertTrue(app.buttons["Confirm"].waitForExistence(timeout: timeout)) + + // Should be able to edit because of CBC saved PMs + XCTAssertTrue(app.staticTexts["Edit"].waitForExistenceAndTap()) + XCTAssertTrue(app.staticTexts["Done"].waitForExistence(timeout: 1)) // Sanity check "Done" button is there + + // Assert there are no remove buttons on each tile and the update screen + XCTAssertNil(scroll(collectionView: app.collectionViews.firstMatch, toFindButtonWithId: "CircularButton.Remove")) + XCTAssertTrue(app.buttons["CircularButton.Edit"].waitForExistenceAndTap(timeout: timeout)) + XCTAssertFalse(app.buttons["Remove card"].exists) + + //Dismiss Sheet. + app.buttons["Back"].waitForExistenceAndTap(timeout: timeout) + app.buttons["Done"].waitForExistenceAndTap(timeout: timeout) + app.buttons["Close"].waitForExistenceAndTap(timeout: timeout) + } // MARK: - Helpers diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift index 1c9a969da1b..ec0287196ae 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift @@ -2460,6 +2460,57 @@ class PaymentSheetDeferredServerSideUITests: PaymentSheetUITestCase { XCTAssertTrue(app.buttons["Close"].waitForExistence(timeout: 1)) } + // MARK: - PaymentMethodRemoval w/ CBC + func testPaymentMethodRemove() { + + var settings = PaymentSheetTestPlaygroundSettings.defaultValues() + settings.mode = .paymentWithSetup + settings.uiStyle = .paymentSheet + settings.customerKeyType = .customerSession + settings.customerMode = .new + settings.merchantCountryCode = .FR + settings.currency = .eur + settings.applePayEnabled = .on + settings.apmsEnabled = .off + settings.paymentMethodRemove = .disabled + + loadPlayground( + app, + settings + ) + + app.buttons["Present PaymentSheet"].waitForExistenceAndTap() + + try! fillCardData(app, cardNumber: "4000002500001001", postalEnabled: true) + + // Complete payment + app.buttons["Pay €50.99"].tap() + XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0)) + + // Reload w/ same customer + reload(app, settings: settings) + app.buttons["Present PaymentSheet"].waitForExistenceAndTap() + app.buttons["+ Add"].waitForExistenceAndTap() + try! fillCardData(app) + app.buttons["Pay €50.99"].tap() + XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0)) + + // Reload w/ same customer + reload(app, settings: settings) + app.buttons["Present PaymentSheet"].waitForExistenceAndTap() + XCTAssertTrue(app.staticTexts["Edit"].waitForExistenceAndTap()) + XCTAssertTrue(app.staticTexts["Done"].waitForExistence(timeout: 1)) // Sanity check "Done" button is there + + // Detect there are no remove buttons on each tile and the update screen + XCTAssertNil(scroll(collectionView: app.collectionViews.firstMatch, toFindButtonWithId: "CircularButton.Remove")?.tap()) + XCTAssertTrue(app.buttons["CircularButton.Edit"].waitForExistenceAndTap(timeout: 5)) + XCTAssertFalse(app.buttons["Remove card"].exists) + + app.buttons["Back"].waitForExistenceAndTap(timeout: 5) + app.buttons["Done"].waitForExistenceAndTap(timeout: 5) + app.buttons["Close"].waitForExistenceAndTap(timeout: 5) + } + func testPreservesSelectionAfterDismissPaymentSheetFlowController() throws { var settings = PaymentSheetTestPlaygroundSettings.defaultValues() settings.uiStyle = .flowController diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift index 89a63812fc7..99065709625 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift @@ -91,11 +91,13 @@ class CustomerSavedPaymentMethodsCollectionViewController: UIViewController { return false case 1: // If there's exactly one PM, customer can only edit if configuration allows removal or if that single PM is editable - return configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { + return savedPaymentMethodsConfiguration.paymentMethodRemove && configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible }) default: - return true + return savedPaymentMethodsConfiguration.paymentMethodRemove || viewModels.contains(where: { + $0.isCoBrandedCard && cbcEligible + }) } } @@ -385,7 +387,8 @@ extension CustomerSavedPaymentMethodsCollectionViewController: UICollectionViewD } cell.setViewModel(viewModel.toSavedPaymentOptionsViewControllerSelection(), - cbcEligible: cbcEligible) + cbcEligible: cbcEligible, + paymentMethodRemove: savedPaymentMethodsConfiguration.paymentMethodRemove) cell.delegate = self cell.isRemovingPaymentMethods = self.collectionView.isRemovingPaymentMethods cell.appearance = appearance @@ -435,7 +438,7 @@ extension CustomerSavedPaymentMethodsCollectionViewController: PaymentOptionCell removeSavedPaymentMethodMessage: savedPaymentMethodsConfiguration.removeSavedPaymentMethodMessage, appearance: appearance, hostedSurface: .customerSheet, - canRemoveCard: savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod, + canRemoveCard: savedPaymentMethodsConfiguration.paymentMethodRemove && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod), isTestMode: configuration.isTestMode) editVc.delegate = self self.bottomSheetController?.pushContentViewController(editVc) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift index 7505946386f..d8630941d9d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift @@ -74,6 +74,8 @@ extension CustomerSheet { /// If false, the customer can't delete if they only have one saved payment method remaining. @_spi(STP) public var allowsRemovalOfLastSavedPaymentMethod = true + /// Prototype: Remove when added to customer session configuration + @_spi(STP) public var paymentMethodRemove = true public init () { } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift index 60626bdf0dc..dd85c31d52a 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift @@ -182,6 +182,9 @@ extension PaymentSheet { /// Optional configuration to display a custom message when a saved payment method is removed. public var removeSavedPaymentMethodMessage: String? + /// Prototype: To be added to customer session configuration + @_spi(STP) public var paymentMethodRemove = true + /// Configuration for external payment methods. public var externalPaymentMethodConfiguration: ExternalPaymentMethodConfiguration? 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 9bc3e8d7bad..63e15fd0897 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift @@ -110,6 +110,7 @@ extension SavedPaymentMethodCollectionView { } var cbcEligible: Bool = false + var paymentMethodRemove: Bool = true /// Indicates whether the cell should be editable or just removable. /// If the card is a co-branded card and the merchant is eligible for card brand choice, then @@ -189,10 +190,6 @@ extension SavedPaymentMethodCollectionView { ]) } - override func layoutSubviews() { - super.layoutSubviews() - } - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -223,12 +220,13 @@ extension SavedPaymentMethodCollectionView { // MARK: - Internal Methods - func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: Bool) { + func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: Bool, paymentMethodRemove: Bool) { paymentMethodLogo.isHidden = false plus.isHidden = true shadowRoundedRectangle.isHidden = false self.viewModel = viewModel self.cbcEligible = cbcEligible + self.paymentMethodRemove = paymentMethodRemove update() } @@ -250,7 +248,7 @@ extension SavedPaymentMethodCollectionView { private func didSelectAccessory() { if shouldAllowEditing { delegate?.paymentOptionCellDidSelectEdit(self) - } else { + } else if paymentMethodRemove { delegate?.paymentOptionCellDidSelectRemove(self) } } @@ -336,19 +334,21 @@ extension SavedPaymentMethodCollectionView { if isRemovingPaymentMethods { if case .saved = viewModel { - accessoryButton.isHidden = false if shouldAllowEditing { + accessoryButton.isHidden = false accessoryButton.set(style: .edit, with: appearance.colors.danger) accessoryButton.backgroundColor = UIColor.dynamic( light: .systemGray5, dark: appearance.colors.componentBackground.lighten(by: 0.075)) accessoryButton.iconColor = appearance.colors.icon - } else { + } else if paymentMethodRemove { + accessoryButton.isHidden = false accessoryButton.set(style: .remove, with: appearance.colors.danger) accessoryButton.backgroundColor = appearance.colors.danger accessoryButton.iconColor = appearance.colors.danger.contrastingColor } contentView.bringSubviewToFront(accessoryButton) applyDefaultStyle() + } else { accessoryButton.isHidden = true 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 02ba863276b..2400ae7065d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift @@ -92,11 +92,13 @@ class SavedPaymentOptionsViewController: UIViewController { return false case 1: // If there's exactly one PM, customer can only edit if configuration allows removal or if that single PM allows for the card brand choice to be updated. - return configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { + return paymentSheetConfiguration.paymentMethodRemove && configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible }) default: - return true + return paymentSheetConfiguration.paymentMethodRemove || viewModels.contains(where: { + $0.isCoBrandedCard && cbcEligible + }) } } @@ -481,7 +483,7 @@ extension SavedPaymentOptionsViewController: UICollectionViewDataSource, UIColle stpAssertionFailure() return UICollectionViewCell() } - cell.setViewModel(viewModel, cbcEligible: cbcEligible) + cell.setViewModel(viewModel, cbcEligible: cbcEligible, paymentMethodRemove: self.paymentSheetConfiguration.paymentMethodRemove) cell.delegate = self cell.isRemovingPaymentMethods = self.collectionView.isRemovingPaymentMethods cell.appearance = appearance @@ -549,7 +551,7 @@ extension SavedPaymentOptionsViewController: PaymentOptionCellDelegate { removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, appearance: appearance, hostedSurface: .paymentSheet, - canRemoveCard: savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod, + canRemoveCard: paymentSheetConfiguration.paymentMethodRemove && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod), isTestMode: configuration.isTestMode) editVc.delegate = self self.bottomSheetController?.pushContentViewController(editVc) From 940d481deb15089b34b8c2d7e974c472249bb3b6 Mon Sep 17 00:00:00 2001 From: John Woo Date: Fri, 10 May 2024 17:40:03 -0700 Subject: [PATCH 2/6] lint --- .../PaymentSheetUITest/CustomerSheetUITest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift index 198366ad6a7..2746cd01612 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift @@ -1048,7 +1048,7 @@ class CustomerSheetUITest: XCTestCase { XCTAssertTrue(app.buttons["CircularButton.Edit"].waitForExistenceAndTap(timeout: timeout)) XCTAssertFalse(app.buttons["Remove card"].exists) - //Dismiss Sheet. + // Dismiss Sheet app.buttons["Back"].waitForExistenceAndTap(timeout: timeout) app.buttons["Done"].waitForExistenceAndTap(timeout: timeout) app.buttons["Close"].waitForExistenceAndTap(timeout: timeout) From f31020b68d149167b74118c4e25459a1d543ebd8 Mon Sep 17 00:00:00 2001 From: John Woo <99628984+wooj-stripe@users.noreply.github.com> Date: Tue, 14 May 2024 15:34:55 -0700 Subject: [PATCH 3/6] Update Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift Co-authored-by: Yuki --- .../PaymentSheetUITest/CustomerSheetUITest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift index 2746cd01612..915ab488976 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift @@ -1030,7 +1030,7 @@ class CustomerSheetUITest: XCTestCase { app.buttons["Save"].tap() XCTAssertTrue(app.buttons["Confirm"].waitForExistence(timeout: timeout)) - // Shouldn't be able to edit, only one saved PM when paymentMethodRemove = .disabled + // Shouldn't be able to edit non-CBC eligible card when paymentMethodRemove = .disabled XCTAssertFalse(app.staticTexts["Edit"].waitForExistence(timeout: 1)) // Add a CBC enabled PM From 6691bf5816cf9acf107ed5ec07ce5d8ce72a1d06 Mon Sep 17 00:00:00 2001 From: John Woo Date: Tue, 14 May 2024 15:43:07 -0700 Subject: [PATCH 4/6] updating per review --- ...erSavedPaymentMethodsCollectionViewController.swift | 4 ++-- .../SavedPaymentMethodCollectionView.swift | 10 +++++----- .../SavedPaymentOptionsViewController.swift | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift index 99065709625..45aa7e643bb 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift @@ -91,9 +91,9 @@ class CustomerSavedPaymentMethodsCollectionViewController: UIViewController { return false case 1: // If there's exactly one PM, customer can only edit if configuration allows removal or if that single PM is editable - return savedPaymentMethodsConfiguration.paymentMethodRemove && configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { + return savedPaymentMethodsConfiguration.paymentMethodRemove && (configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible - }) + })) default: return savedPaymentMethodsConfiguration.paymentMethodRemove || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible 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 63e15fd0897..9fe68a0f015 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift @@ -110,7 +110,7 @@ extension SavedPaymentMethodCollectionView { } var cbcEligible: Bool = false - var paymentMethodRemove: Bool = true + var allowsPaymentMethodRemoval: Bool = true /// Indicates whether the cell should be editable or just removable. /// If the card is a co-branded card and the merchant is eligible for card brand choice, then @@ -220,13 +220,13 @@ extension SavedPaymentMethodCollectionView { // MARK: - Internal Methods - func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: Bool, paymentMethodRemove: Bool) { + func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: Bool, allowsPaymentMethodRemoval: Bool) { paymentMethodLogo.isHidden = false plus.isHidden = true shadowRoundedRectangle.isHidden = false self.viewModel = viewModel self.cbcEligible = cbcEligible - self.paymentMethodRemove = paymentMethodRemove + self.allowsPaymentMethodRemoval = allowsPaymentMethodRemoval update() } @@ -248,7 +248,7 @@ extension SavedPaymentMethodCollectionView { private func didSelectAccessory() { if shouldAllowEditing { delegate?.paymentOptionCellDidSelectEdit(self) - } else if paymentMethodRemove { + } else if allowsPaymentMethodRemoval { delegate?.paymentOptionCellDidSelectRemove(self) } } @@ -340,7 +340,7 @@ extension SavedPaymentMethodCollectionView { accessoryButton.backgroundColor = UIColor.dynamic( light: .systemGray5, dark: appearance.colors.componentBackground.lighten(by: 0.075)) accessoryButton.iconColor = appearance.colors.icon - } else if paymentMethodRemove { + } else if allowsPaymentMethodRemoval { accessoryButton.isHidden = false accessoryButton.set(style: .remove, with: appearance.colors.danger) accessoryButton.backgroundColor = appearance.colors.danger 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 2400ae7065d..5211f626af7 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift @@ -92,9 +92,9 @@ class SavedPaymentOptionsViewController: UIViewController { return false case 1: // If there's exactly one PM, customer can only edit if configuration allows removal or if that single PM allows for the card brand choice to be updated. - return paymentSheetConfiguration.paymentMethodRemove && configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { + return paymentSheetConfiguration.paymentMethodRemove && (configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible - }) + })) default: return paymentSheetConfiguration.paymentMethodRemove || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible From 4db7f29204cdb20dc931e494c2f65d8497b0bd83 Mon Sep 17 00:00:00 2001 From: John Woo Date: Tue, 14 May 2024 16:36:12 -0700 Subject: [PATCH 5/6] update --- .../CustomerSavedPaymentMethodsCollectionViewController.swift | 2 +- .../SavedPaymentOptionsViewController.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift index 45aa7e643bb..dea510cf19c 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift @@ -388,7 +388,7 @@ extension CustomerSavedPaymentMethodsCollectionViewController: UICollectionViewD cell.setViewModel(viewModel.toSavedPaymentOptionsViewControllerSelection(), cbcEligible: cbcEligible, - paymentMethodRemove: savedPaymentMethodsConfiguration.paymentMethodRemove) + allowsPaymentMethodRemoval: savedPaymentMethodsConfiguration.paymentMethodRemove) cell.delegate = self cell.isRemovingPaymentMethods = self.collectionView.isRemovingPaymentMethods cell.appearance = appearance 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 5211f626af7..902099db8a1 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift @@ -483,7 +483,7 @@ extension SavedPaymentOptionsViewController: UICollectionViewDataSource, UIColle stpAssertionFailure() return UICollectionViewCell() } - cell.setViewModel(viewModel, cbcEligible: cbcEligible, paymentMethodRemove: self.paymentSheetConfiguration.paymentMethodRemove) + cell.setViewModel(viewModel, cbcEligible: cbcEligible, allowsPaymentMethodRemoval: self.paymentSheetConfiguration.paymentMethodRemove) cell.delegate = self cell.isRemovingPaymentMethods = self.collectionView.isRemovingPaymentMethods cell.appearance = appearance From c168cbae5c13c39752352f12fbb64ae70a06c49f Mon Sep 17 00:00:00 2001 From: John Woo Date: Wed, 15 May 2024 13:41:19 -0700 Subject: [PATCH 6/6] Adding tests to catch case where there is 1 card with cbc --- .../CustomerSheetUITest.swift | 37 ++++++++++++++- .../PaymentSheetUITest.swift | 45 ++++++++++++++++++- ...ymentMethodsCollectionViewController.swift | 4 +- .../SavedPaymentOptionsViewController.swift | 4 +- 4 files changed, 84 insertions(+), 6 deletions(-) diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift index 915ab488976..5667a2170e4 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift @@ -1012,12 +1012,13 @@ class CustomerSheetUITest: XCTestCase { XCTAssertFalse(app.buttons["Remove card"].exists) } // MARK: - PaymentMethodRemove w/ CBC - func testPaymentMethodRemove() throws { + func testCSPaymentMethodRemoveTwoCards() throws { var settings = CustomerSheetTestPlaygroundSettings.defaultValues() settings.merchantCountryCode = .FR settings.customerMode = .new settings.applePay = .on settings.paymentMethodRemove = .disabled + settings.allowsRemovalOfLastSavedPaymentMethod = .on loadPlayground( app, settings @@ -1054,6 +1055,40 @@ class CustomerSheetUITest: XCTestCase { app.buttons["Close"].waitForExistenceAndTap(timeout: timeout) } + func testCSPaymentMethodRemoveTwoCards_keeplastSavedPaymentMethod_CBC() throws { + var settings = CustomerSheetTestPlaygroundSettings.defaultValues() + settings.merchantCountryCode = .FR + settings.customerMode = .new + settings.applePay = .on + settings.paymentMethodRemove = .disabled + settings.allowsRemovalOfLastSavedPaymentMethod = .off + loadPlayground( + app, + settings + ) + + // Save a card + app.staticTexts["None"].waitForExistenceAndTap() + app.buttons["+ Add"].waitForExistenceAndTap() + try! fillCardData(app, cardNumber: "4000002500001001", postalEnabled: true) + app.buttons["Save"].tap() + XCTAssertTrue(app.buttons["Confirm"].waitForExistence(timeout: timeout)) + + // Should be able to edit because of CBC saved PMs + XCTAssertTrue(app.staticTexts["Edit"].waitForExistenceAndTap()) + XCTAssertTrue(app.staticTexts["Done"].waitForExistence(timeout: 1)) // Sanity check "Done" button is there + + // Assert there are no remove buttons on each tile and the update screen + XCTAssertNil(scroll(collectionView: app.collectionViews.firstMatch, toFindButtonWithId: "CircularButton.Remove")) + XCTAssertTrue(app.buttons["CircularButton.Edit"].waitForExistenceAndTap(timeout: timeout)) + XCTAssertFalse(app.buttons["Remove card"].exists) + + // Dismiss Sheet + app.buttons["Back"].waitForExistenceAndTap(timeout: timeout) + app.buttons["Done"].waitForExistenceAndTap(timeout: timeout) + app.buttons["Close"].waitForExistenceAndTap(timeout: timeout) + } + // MARK: - Helpers func presentCSAndAddCardFrom(buttonLabel: String, cardNumber: String? = nil, tapAdd: Bool = true) { diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift index ec0287196ae..439f9ffd01c 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift @@ -2461,7 +2461,7 @@ class PaymentSheetDeferredServerSideUITests: PaymentSheetUITestCase { } // MARK: - PaymentMethodRemoval w/ CBC - func testPaymentMethodRemove() { + func testPSPaymentMethodRemoveTwoCards() { var settings = PaymentSheetTestPlaygroundSettings.defaultValues() settings.mode = .paymentWithSetup @@ -2473,6 +2473,7 @@ class PaymentSheetDeferredServerSideUITests: PaymentSheetUITestCase { settings.applePayEnabled = .on settings.apmsEnabled = .off settings.paymentMethodRemove = .disabled + settings.allowsRemovalOfLastSavedPaymentMethod = .on loadPlayground( app, @@ -2510,7 +2511,49 @@ class PaymentSheetDeferredServerSideUITests: PaymentSheetUITestCase { app.buttons["Done"].waitForExistenceAndTap(timeout: 5) app.buttons["Close"].waitForExistenceAndTap(timeout: 5) } + func testPSPaymentMethodRemoveDisabled_keeplastSavedPaymentMethod_CBC() { + var settings = PaymentSheetTestPlaygroundSettings.defaultValues() + settings.mode = .paymentWithSetup + settings.uiStyle = .paymentSheet + settings.customerKeyType = .customerSession + settings.customerMode = .new + settings.merchantCountryCode = .FR + settings.currency = .eur + settings.applePayEnabled = .on + settings.apmsEnabled = .off + settings.paymentMethodRemove = .disabled + settings.allowsRemovalOfLastSavedPaymentMethod = .off + + loadPlayground( + app, + settings + ) + + app.buttons["Present PaymentSheet"].waitForExistenceAndTap() + + try! fillCardData(app, cardNumber: "4000002500001001", postalEnabled: true) + + // Complete payment + app.buttons["Pay €50.99"].tap() + XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0)) + + // Reload w/ same customer + reload(app, settings: settings) + + app.buttons["Present PaymentSheet"].waitForExistenceAndTap() + XCTAssertTrue(app.staticTexts["Edit"].waitForExistenceAndTap()) + XCTAssertTrue(app.staticTexts["Done"].waitForExistence(timeout: 1)) // Sanity check "Done" button is there + + // Detect there are no remove buttons on each tile and the update screen + XCTAssertNil(scroll(collectionView: app.collectionViews.firstMatch, toFindButtonWithId: "CircularButton.Remove")?.tap()) + XCTAssertTrue(app.buttons["CircularButton.Edit"].waitForExistenceAndTap(timeout: 5)) + XCTAssertFalse(app.buttons["Remove card"].exists) + + app.buttons["Back"].waitForExistenceAndTap(timeout: 5) + app.buttons["Done"].waitForExistenceAndTap(timeout: 5) + app.buttons["Close"].waitForExistenceAndTap(timeout: 5) + } func testPreservesSelectionAfterDismissPaymentSheetFlowController() throws { var settings = PaymentSheetTestPlaygroundSettings.defaultValues() settings.uiStyle = .flowController diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift index dea510cf19c..3ba1d829e19 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift @@ -91,9 +91,9 @@ class CustomerSavedPaymentMethodsCollectionViewController: UIViewController { return false case 1: // If there's exactly one PM, customer can only edit if configuration allows removal or if that single PM is editable - return savedPaymentMethodsConfiguration.paymentMethodRemove && (configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { + return (savedPaymentMethodsConfiguration.paymentMethodRemove && configuration.allowsRemovalOfLastSavedPaymentMethod) || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible - })) + }) default: return savedPaymentMethodsConfiguration.paymentMethodRemove || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible 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 902099db8a1..07b8ac10586 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift @@ -92,9 +92,9 @@ class SavedPaymentOptionsViewController: UIViewController { return false case 1: // If there's exactly one PM, customer can only edit if configuration allows removal or if that single PM allows for the card brand choice to be updated. - return paymentSheetConfiguration.paymentMethodRemove && (configuration.allowsRemovalOfLastSavedPaymentMethod || viewModels.contains(where: { + return (paymentSheetConfiguration.paymentMethodRemove && configuration.allowsRemovalOfLastSavedPaymentMethod) || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible - })) + }) default: return paymentSheetConfiguration.paymentMethodRemove || viewModels.contains(where: { $0.isCoBrandedCard && cbcEligible