Skip to content

Commit

Permalink
Add prototype for enable/disable removal of spms (#3577)
Browse files Browse the repository at this point in the history
## Summary
Prototyping code under config flag to enable/disable payment method
removal

## Motivation
Implemented as a client side flag, which will eventually be on
CustomerSession

## Testing
Added ui tests

---------

Co-authored-by: Yuki <yuki@stripe.com>
  • Loading branch information
wooj-stripe and yuki-stripe committed May 15, 2024
1 parent 6f1c87b commit 1a9ad18
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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,
Expand All @@ -139,7 +146,8 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable {
collectAddress: .automatic,
merchantCountryCode: .US,
preferredNetworksEnabled: .off,
allowsRemovalOfLastSavedPaymentMethod: .on)
allowsRemovalOfLastSavedPaymentMethod: .on,
paymentMethodRemove: .enabled)
}

var base64Data: String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -382,6 +389,7 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {
applePayEnabled: .on,
applePayButtonType: .buy,
allowsDelayedPMs: .off,
paymentMethodRemove: .enabled,
defaultBillingAddress: .off,
customEmail: nil,
linkEnabled: .off,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,83 @@ class CustomerSheetUITest: XCTestCase {
// ...but should not be able to remove it.
XCTAssertFalse(app.buttons["Remove card"].exists)
}
// MARK: - PaymentMethodRemove w/ CBC
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
)

// 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 non-CBC eligible card 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)
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2460,6 +2460,100 @@ class PaymentSheetDeferredServerSideUITests: PaymentSheetUITestCase {
XCTAssertTrue(app.buttons["Close"].waitForExistence(timeout: 1))
}

// MARK: - PaymentMethodRemoval w/ CBC
func testPSPaymentMethodRemoveTwoCards() {

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 = .on

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 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}
}

Expand Down Expand Up @@ -385,7 +387,8 @@ extension CustomerSavedPaymentMethodsCollectionViewController: UICollectionViewD
}

cell.setViewModel(viewModel.toSavedPaymentOptionsViewControllerSelection(),
cbcEligible: cbcEligible)
cbcEligible: cbcEligible,
allowsPaymentMethodRemoval: savedPaymentMethodsConfiguration.paymentMethodRemove)
cell.delegate = self
cell.isRemovingPaymentMethods = self.collectionView.isRemovingPaymentMethods
cell.appearance = appearance
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ extension SavedPaymentMethodCollectionView {
}

var cbcEligible: Bool = false
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
Expand Down Expand Up @@ -189,10 +190,6 @@ extension SavedPaymentMethodCollectionView {
])
}

override func layoutSubviews() {
super.layoutSubviews()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
Expand Down Expand Up @@ -223,12 +220,13 @@ extension SavedPaymentMethodCollectionView {

// MARK: - Internal Methods

func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: 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.allowsPaymentMethodRemoval = allowsPaymentMethodRemoval
update()
}

Expand All @@ -250,7 +248,7 @@ extension SavedPaymentMethodCollectionView {
private func didSelectAccessory() {
if shouldAllowEditing {
delegate?.paymentOptionCellDidSelectEdit(self)
} else {
} else if allowsPaymentMethodRemoval {
delegate?.paymentOptionCellDidSelectRemove(self)
}
}
Expand Down Expand Up @@ -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 allowsPaymentMethodRemoval {
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

Expand Down

0 comments on commit 1a9ad18

Please sign in to comment.