diff --git a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlayground.swift b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlayground.swift index 124e7af0ab6..87ce5dc2fe9 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlayground.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlayground.swift @@ -74,6 +74,7 @@ struct CustomerSheetTestPlayground: View { SettingView(setting: $playgroundController.settings.autoreload) TextField("headerTextForSelectionScreen", text: headerTextForSelectionScreenBinding) SettingView(setting: $playgroundController.settings.allowsRemovalOfLastSavedPaymentMethod) + SettingView(setting: $playgroundController.settings.defaultSPMNavigation) 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 3e1ce36af06..0c5e0273ba4 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundController.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundController.swift @@ -4,7 +4,7 @@ // import Combine -@_spi(STP) @_spi(CustomerSessionBetaAccess) @_spi(CardBrandFilteringBeta) import StripePaymentSheet +@_spi(STP) @_spi(CustomerSessionBetaAccess) @_spi(CardBrandFilteringBeta) @_spi(DefaultSPMNavigation) import StripePaymentSheet import SwiftUI class CustomerSheetTestPlaygroundController: ObservableObject { @@ -147,6 +147,7 @@ class CustomerSheetTestPlaygroundController: ObservableObject { case .allowVisa: configuration.cardBrandAcceptance = .allowed(brands: [.visa]) } + configuration.defaultSPMNavigation = settings.defaultSPMNavigation == .on return configuration } diff --git a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundSettings.swift b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundSettings.swift index d543b56ea9e..422194b3b97 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundSettings.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/CustomerSheetTestPlaygroundSettings.swift @@ -142,6 +142,12 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable { case allowVisa } + enum DefaultSPMNavigationEnabled: String, PickerEnum { + static let enumName: String = "defaultSPMNavigation" + case on + case off + } + var customerMode: CustomerMode var customerId: String? var customerKeyType: CustomerKeyType @@ -162,6 +168,7 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable { var paymentMethodRemove: PaymentMethodRemove var paymentMethodAllowRedisplayFilters: PaymentMethodAllowRedisplayFilters var cardBrandAcceptance: CardBrandAcceptance + var defaultSPMNavigation: DefaultSPMNavigationEnabled static func defaultValues() -> CustomerSheetTestPlaygroundSettings { return CustomerSheetTestPlaygroundSettings(customerMode: .new, @@ -182,7 +189,8 @@ public struct CustomerSheetTestPlaygroundSettings: Codable, Equatable { allowsRemovalOfLastSavedPaymentMethod: .on, paymentMethodRemove: .enabled, paymentMethodAllowRedisplayFilters: .always, - cardBrandAcceptance: .all) + cardBrandAcceptance: .all, + defaultSPMNavigation: .off) } var base64Data: String { diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift index 55726377701..2aa55a19cd3 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift @@ -46,6 +46,7 @@ struct PaymentSheetTestPlayground: View { SettingView(setting: $playgroundController.settings.autoreload) SettingView(setting: $playgroundController.settings.shakeAmbiguousViews) SettingView(setting: $playgroundController.settings.instantDebitsInDeferredIntents) + SettingView(setting: $playgroundController.settings.defaultSPM) } var body: some View { diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift index 93039706b1f..c1227534c37 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift @@ -438,6 +438,13 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable { case allowVisa } + enum DefaultSPM: String, PickerEnum { + static let enumName: String = "defaultSPM" + case allowDefaultSPM + case navigationOnly + case off + } + var uiStyle: UIStyle var layout: Layout var mode: Mode @@ -483,6 +490,7 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable { var formSheetAction: FormSheetAction var embeddedViewDisplaysMandateText: DisplaysMandateTextEnabled var cardBrandAcceptance: CardBrandAcceptance + var defaultSPM: DefaultSPM static func defaultValues() -> PaymentSheetTestPlaygroundSettings { return PaymentSheetTestPlaygroundSettings( @@ -527,7 +535,9 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable { collectAddress: .automatic, formSheetAction: .confirm, embeddedViewDisplaysMandateText: .on, - cardBrandAcceptance: .all) + cardBrandAcceptance: .all, + defaultSPM: .off + ) } static let nsUserDefaultsKey = "PaymentSheetTestPlaygroundSettings" diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift index 183f8545825..1a8e29eb290 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift @@ -14,7 +14,7 @@ import Contacts import PassKit @_spi(STP) import StripeCore @_spi(STP) import StripePayments -@_spi(CustomerSessionBetaAccess) @_spi(STP) @_spi(PaymentSheetSkipConfirmation) @_spi(ExperimentalAllowsRemovalOfLastSavedPaymentMethodAPI) @_spi(EmbeddedPaymentElementPrivateBeta) @_spi(CardBrandFilteringBeta) import StripePaymentSheet +@_spi(CustomerSessionBetaAccess) @_spi(STP) @_spi(PaymentSheetSkipConfirmation) @_spi(ExperimentalAllowsRemovalOfLastSavedPaymentMethodAPI) @_spi(EmbeddedPaymentElementPrivateBeta) @_spi(CardBrandFilteringBeta) @_spi(DefaultSPM) import StripePaymentSheet import SwiftUI import UIKit @@ -184,6 +184,16 @@ class PlaygroundController: ObservableObject { case .allowVisa: configuration.cardBrandAcceptance = .allowed(brands: [.visa]) } + + switch settings.defaultSPM { + case .allowDefaultSPM: + configuration.defaultSPM = .allowsDefaultSPM + case .navigationOnly: + configuration.defaultSPM = .navigationOnly + case .off: + configuration.defaultSPM = .off + } + return configuration } @@ -272,6 +282,14 @@ class PlaygroundController: ObservableObject { configuration.cardBrandAcceptance = .allowed(brands: [.visa]) } + switch settings.defaultSPM { + case .allowDefaultSPM: + configuration.defaultSPM = .allowsDefaultSPM + case .navigationOnly: + configuration.defaultSPM = .navigationOnly + case .off: + configuration.defaultSPM = .off + } return configuration } diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift index 16b7aca9c1a..3399d88993b 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift @@ -396,14 +396,14 @@ class CustomerSheetUITest: XCTestCase { XCTAssertTrue(app.pickerWheels.firstMatch.waitForExistence(timeout: timeout)) app.pickerWheels.firstMatch.swipeUp() app.toolbars.buttons["Done"].tap() - app.buttons["Update"].waitForExistenceAndTap(timeout: timeout) + app.buttons["Save"].waitForExistenceAndTap(timeout: timeout) // We should have updated to Visa XCTAssertTrue(app.images["carousel_card_visa"].waitForExistence(timeout: timeout)) // Remove this card XCTAssertTrue(app.buttons["CircularButton.Edit"].waitForExistenceAndTap(timeout: timeout)) - XCTAssertTrue(app.buttons["Remove card"].waitForExistenceAndTap(timeout: timeout)) + XCTAssertTrue(app.buttons["Remove"].waitForExistenceAndTap(timeout: timeout)) let confirmRemoval = app.alerts.buttons["Remove"] XCTAssertTrue(confirmRemoval.waitForExistence(timeout: timeout)) confirmRemoval.tap() @@ -536,10 +536,10 @@ class CustomerSheetUITest: XCTestCase { // Should be able to edit CBC enabled PM even though it's the only one XCTAssertTrue(app.buttons["CircularButton.Edit"].waitForExistenceAndTap(timeout: timeout)) - XCTAssertTrue(app.buttons["Update"].waitForExistence(timeout: timeout)) + XCTAssertTrue(app.buttons["Save"].waitForExistence(timeout: timeout)) // ...but should not be able to remove it. - XCTAssertFalse(app.buttons["Remove card"].exists) + XCTAssertFalse(app.buttons["Remove"].exists) } // MARK: - PaymentMethodRemove w/ CBC func testCSPaymentMethodRemoveTwoCards() throws { @@ -578,7 +578,7 @@ class CustomerSheetUITest: XCTestCase { // 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) + XCTAssertFalse(app.buttons["Remove"].exists) // Dismiss Sheet app.buttons["Back"].waitForExistenceAndTap(timeout: timeout) @@ -613,7 +613,7 @@ class CustomerSheetUITest: XCTestCase { // 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) + XCTAssertFalse(app.buttons["Remove"].exists) // Dismiss Sheet app.buttons["Back"].waitForExistenceAndTap(timeout: timeout) diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/EmbeddedUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/EmbeddedUITest.swift index d31a80878f3..be14cd69373 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/EmbeddedUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/EmbeddedUITest.swift @@ -148,8 +148,8 @@ class EmbeddedUITests: PaymentSheetUITestCase { app.otherElements["Card Brand Dropdown"].waitForExistenceAndTap() app.pickerWheels.firstMatch.swipeUp() app.buttons["Done"].waitForExistenceAndTap() - app.buttons["Update"].waitForExistenceAndTap() - XCTAssertFalse(app.staticTexts["Update card brand"].waitForExistence(timeout: 3)) + app.buttons["Save"].waitForExistenceAndTap() + XCTAssertFalse(app.staticTexts["Manage card"].waitForExistence(timeout: 3)) // Ensure card preference is switched to visa XCTAssertTrue(card1001Button.waitForExistence(timeout: 3)) @@ -178,12 +178,12 @@ class EmbeddedUITests: PaymentSheetUITestCase { // Remove last card while selected state is NOT on the card app.buttons["Edit"].waitForExistenceAndTap() - XCTAssertTrue(app.staticTexts["Update card brand"].waitForExistence(timeout: 3.0)) - app.buttons["Remove card"].waitForExistenceAndTap() + XCTAssertTrue(app.staticTexts["Manage card"].waitForExistence(timeout: 3.0)) + app.buttons["Remove"].waitForExistenceAndTap() dismissAlertView(alertBody: "Visa •••• 1001", alertTitle: "Remove card?", buttonToTap: "Remove") // Apple pay should be continued to be selected - XCTAssertFalse(app.staticTexts["Update card brand"].waitForExistence(timeout: 3.0)) + XCTAssertFalse(app.staticTexts["Manage card"].waitForExistence(timeout: 3.0)) XCTAssertFalse(app.images["stp_card_visa"].waitForExistence(timeout: 3)) XCTAssertFalse(app.images["stp_card_cartes_bancaires"].waitForExistence(timeout: 3)) XCTAssertTrue(applePayButton.isSelected) @@ -228,8 +228,8 @@ class EmbeddedUITests: PaymentSheetUITestCase { // Remove last card while selected state is on the card app.buttons["Edit"].waitForExistenceAndTap() - XCTAssertTrue(app.staticTexts["Update card brand"].waitForExistence(timeout: 3.0)) - app.buttons["Remove card"].waitForExistenceAndTap() + XCTAssertTrue(app.staticTexts["Manage card"].waitForExistence(timeout: 3.0)) + app.buttons["Remove"].waitForExistenceAndTap() dismissAlertView(alertBody: "Cartes Bancaires •••• 1001", alertTitle: "Remove card?", buttonToTap: "Remove") // Nothing should be selected @@ -264,7 +264,7 @@ class EmbeddedUITests: PaymentSheetUITestCase { app.otherElements["Card Brand Dropdown"].waitForExistenceAndTap() app.pickerWheels.firstMatch.swipeUp() app.buttons["Done"].waitForExistenceAndTap() - app.buttons["Update"].waitForExistenceAndTap() + app.buttons["Save"].waitForExistenceAndTap() // Tap done on manage payment methods screen, then select 4242 card app.buttons["Done"].waitForExistenceAndTap() diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetLPMUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetLPMUITest.swift index 58ee3af0df5..db8dfbe98d9 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetLPMUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetLPMUITest.swift @@ -985,7 +985,7 @@ class PaymentSheetStandardLPMUICBCTests: PaymentSheetStandardLPMUICase { XCTAssertTrue(app.pickerWheels.firstMatch.waitForExistence(timeout: 5)) app.pickerWheels.firstMatch.swipeUp() app.toolbars.buttons["Done"].tap() - app.buttons["Update"].waitForExistenceAndTap(timeout: 5) + app.buttons["Save"].waitForExistenceAndTap(timeout: 5) // We should have updated to Visa XCTAssertTrue(app.images["carousel_card_visa"].waitForExistence(timeout: 5)) @@ -996,7 +996,7 @@ class PaymentSheetStandardLPMUICBCTests: PaymentSheetStandardLPMUICase { XCTAssertTrue(app.pickerWheels.firstMatch.waitForExistence(timeout: 5)) app.pickerWheels.firstMatch.swipeDown() app.toolbars.buttons["Done"].tap() - app.buttons["Update"].waitForExistenceAndTap(timeout: 5) + app.buttons["Save"].waitForExistenceAndTap(timeout: 5) // We should have updated to Cartes Bancaires XCTAssertTrue(app.images["carousel_card_cartes_bancaires"].waitForExistence(timeout: 5)) @@ -1016,7 +1016,7 @@ class PaymentSheetStandardLPMUICBCTests: PaymentSheetStandardLPMUICase { // Remove this card XCTAssertTrue(app.staticTexts["Edit"].waitForExistenceAndTap(timeout: 60.0)) XCTAssertTrue(app.buttons["CircularButton.Edit"].waitForExistenceAndTap(timeout: 5)) - XCTAssertTrue(app.buttons["Remove card"].waitForExistenceAndTap(timeout: 5)) + XCTAssertTrue(app.buttons["Remove"].waitForExistenceAndTap(timeout: 5)) let confirmRemoval = app.alerts.buttons["Remove"] XCTAssertTrue(confirmRemoval.waitForExistence(timeout: 5)) confirmRemoval.tap() diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift index 11e0feee934..94e5a1a4cd8 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift @@ -1486,7 +1486,7 @@ class PaymentSheetCustomerSessionCBCUITests: PaymentSheetUITestCase { // 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) + XCTAssertFalse(app.buttons["Remove"].exists) app.buttons["Back"].waitForExistenceAndTap(timeout: 5) app.buttons["Done"].waitForExistenceAndTap(timeout: 5) @@ -1528,7 +1528,7 @@ class PaymentSheetCustomerSessionCBCUITests: PaymentSheetUITestCase { // 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) + XCTAssertFalse(app.buttons["Remove"].exists) app.buttons["Back"].waitForExistenceAndTap(timeout: 5) app.buttons["Done"].waitForExistenceAndTap(timeout: 5) diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetVerticalUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetVerticalUITest.swift index c16012b8253..adf0f9d20d2 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetVerticalUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetVerticalUITest.swift @@ -256,7 +256,7 @@ class PaymentSheetVerticalUITests: PaymentSheetUITestCase { app.buttons["CircularButton.Edit"].firstMatch.waitForExistenceAndTap() // Should present the update card view controller - XCTAssertTrue(app.staticTexts["Update card brand"].waitForExistence(timeout: 2.0)) + XCTAssertTrue(app.staticTexts["Manage card"].waitForExistence(timeout: 2.0)) // Update card brand to Visa XCTAssertTrue(app.textFields["Cartes Bancaires"].waitForExistenceAndTap(timeout: 5)) @@ -269,14 +269,14 @@ class PaymentSheetVerticalUITests: PaymentSheetUITestCase { XCTAssertTrue(app.textFields["Visa"].waitForExistence(timeout: 5)) // Update the card - app.buttons["Update"].waitForExistenceAndTap(timeout: 5) + app.buttons["Save"].waitForExistenceAndTap(timeout: 5) // We should have updated to Visa XCTAssertTrue(app.buttons["Visa ending in 1 0 0 1"].waitForExistence(timeout: 1.0)) // Reselect edit icon and delete the card from the update view controller app.buttons["Edit"].firstMatch.waitForExistenceAndTap() - app.buttons["Remove card"].waitForExistenceAndTap() + app.buttons["Remove"].waitForExistenceAndTap() XCTAssertTrue(app.alerts.buttons["Remove"].waitForExistenceAndTap()) // Verify we are kicked out to the main screen after removing all saved payment methods diff --git a/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings b/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings index 2d8c8a41f28..e2abd013f1d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings +++ b/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings @@ -106,6 +106,9 @@ /* Button text on a screen asking the user to approve a payment */ "Cancel and pay another way" = "Cancel and pay another way"; +/* Text on a screen that indicates card details cannot be changed. */ +"Card details cannot be changed." = "Card details cannot be changed."; + /* Title for a button that allows the user to use a different email in the signup flow. */ "Change email" = "Change email"; @@ -173,6 +176,9 @@ re-entering the security code (CVV/CVC). */ /* Title of the logout action. */ "Log out of Link" = "Log out of Link"; +/* Title shown above a view containing the customer's card that they can delete or update */ +"Manage card" = "Manage card"; + /* Title shown above a view containing the customer's payment method that they can delete or update */ "Manage payment method" = "Manage payment method"; @@ -268,7 +274,8 @@ e.g, 'Pay faster at Example, Inc. and thousands of businesses.' */ /* Label for a button that re-sends the a login code when tapped */ "Resend code" = "Resend code"; -/* A button used for saving a new payment method */ +/* A button used for saving a new payment method + Label on a button that when tapped, updates a card brand. */ "Save" = "Save"; /* Title shown above a form where the customer can enter payment information like credit card details, email, billing address, etc. */ diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift b/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift index fdf4a8c043a..d6bf4221dab 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift @@ -77,6 +77,27 @@ extension String.Localized { STPLocalizedString("Back", "Text for back button") } + static var card_details_cannot_be_changed: String { + STPLocalizedString( + "Card details cannot be changed.", + "Text on a screen that indicates card details cannot be changed." + ) + } + + static var manage_card: String { + STPLocalizedString( + "Manage card", + "Title shown above a view containing the customer's card that they can delete or update" + ) + } + + static var save: String { + STPLocalizedString( + "Save", + "Label on a button that when tapped, updates a card brand." + ) + } + static var update_card: String { STPLocalizedString( "Update card", diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift index dc070da6f5f..4b300dd482e 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift @@ -82,6 +82,7 @@ class CustomerSavedPaymentMethodsCollectionViewController: UIViewController { let allowsRemovalOfLastSavedPaymentMethod: Bool let paymentMethodRemove: Bool let isTestMode: Bool + let defaultSPMNavigation: Bool } /// Whether or not you can edit save payment methods by removing or updating them. @@ -386,7 +387,9 @@ extension CustomerSavedPaymentMethodsCollectionViewController: UICollectionViewD cell.setViewModel(viewModel.toSavedPaymentOptionsViewControllerSelection(), cbcEligible: cbcEligible, - allowsPaymentMethodRemoval: configuration.paymentMethodRemove) + allowsPaymentMethodRemoval: configuration.paymentMethodRemove, + defaultSPM: .off + ) cell.delegate = self cell.isRemovingPaymentMethods = self.collectionView.isRemovingPaymentMethods cell.appearance = appearance @@ -435,9 +438,12 @@ extension CustomerSavedPaymentMethodsCollectionViewController: PaymentOptionCell removeSavedPaymentMethodMessage: savedPaymentMethodsConfiguration.removeSavedPaymentMethodMessage, appearance: appearance, hostedSurface: .customerSheet, + canEditCard: paymentMethod.isCoBrandedCard && cbcEligible, canRemoveCard: configuration.paymentMethodRemove && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod), isTestMode: configuration.isTestMode, - cardBrandFilter: savedPaymentMethodsConfiguration.cardBrandFilter) + cardBrandFilter: savedPaymentMethodsConfiguration.cardBrandFilter, + defaultSPM: .off + ) editVc.delegate = self self.bottomSheetController?.pushContentViewController(editVc) } @@ -537,6 +543,10 @@ extension CustomerSavedPaymentMethodsCollectionViewController: UpdateCardViewCon let updatedViewModel: Selection = .saved(paymentMethod: updatedPaymentMethod) viewModels[row] = updatedViewModel + // Update savedPaymentMethods + if let row = self.savedPaymentMethods.firstIndex(where: { $0.stripeId == updatedPaymentMethod.stripeId }) { + self.savedPaymentMethods[row] = updatedPaymentMethod + } collectionView.reloadData() _ = viewController.bottomSheetController?.popContentViewController() } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsViewController.swift index 1f1ed4c1349..b6f45654a2d 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsViewController.swift @@ -103,7 +103,8 @@ class CustomerSavedPaymentMethodsViewController: UIViewController { showApplePay: showApplePay, allowsRemovalOfLastSavedPaymentMethod: configuration.allowsRemovalOfLastSavedPaymentMethod, paymentMethodRemove: paymentMethodRemove, - isTestMode: configuration.apiClient.isTestmode + isTestMode: configuration.apiClient.isTestmode, + defaultSPMNavigation: configuration.defaultSPMNavigation ), appearance: configuration.appearance, cbcEligible: cbcEligible, @@ -654,7 +655,8 @@ class CustomerSavedPaymentMethodsViewController: UIViewController { showApplePay: isApplePayEnabled, allowsRemovalOfLastSavedPaymentMethod: configuration.allowsRemovalOfLastSavedPaymentMethod, paymentMethodRemove: paymentMethodRemove, - isTestMode: configuration.apiClient.isTestmode + isTestMode: configuration.apiClient.isTestmode, + defaultSPMNavigation: configuration.defaultSPMNavigation ), appearance: configuration.appearance, cbcEligible: cbcEligible, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift index 67f6785c5df..e4a7c2b8ccd 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSheetConfiguration.swift @@ -81,6 +81,11 @@ extension CustomerSheet { /// Note: Card brand filtering is not currently supported by Link. @_spi(CardBrandFilteringBeta) public var cardBrandAcceptance: PaymentSheet.CardBrandAcceptance = .all + /// This is an experimental feature that may be removed at any time. + /// If true, when editing, cards and us bank accounts will have the edit icon, users cannot remove them from the list view screen. + /// If false (default), only cbc eligible cards can be edited and users can remove payment methods from the list screen. + @_spi(DefaultSPMNavigation) public var defaultSPMNavigation = false + public init () { } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift index c9a407e8627..d658de8de0c 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift @@ -188,12 +188,14 @@ extension TextFieldElement { // MARK: - CVC Configuration extension TextFieldElement { struct CVCConfiguration: TextFieldElementConfiguration { - init(defaultValue: String? = nil, cardBrandProvider: @escaping () -> (STPCardBrand)) { + init(defaultValue: String? = nil, cardBrandProvider: @escaping () -> (STPCardBrand), isEditable: Bool = true) { self.defaultValue = defaultValue self.cardBrandProvider = cardBrandProvider + self.isEditable = isEditable } let defaultValue: String? + let isEditable: Bool let cardBrandProvider: () -> (STPCardBrand) var label = String.Localized.cvc let disallowedCharacters: CharacterSet = .stp_invertedAsciiDigit @@ -227,14 +229,16 @@ extension TextFieldElement { // MARK: - Expiry Date Configuration extension TextFieldElement { struct ExpiryDateConfiguration: TextFieldElementConfiguration { - init(defaultValue: String? = nil) { + init(defaultValue: String? = nil, isEditable: Bool = true) { self.defaultValue = defaultValue + self.isEditable = isEditable } let label: String = String.Localized.mm_yy let accessibilityLabel: String = String.Localized.expiration_date_accessibility_label let disallowedCharacters: CharacterSet = .stp_invertedAsciiDigit let defaultValue: String? + let isEditable: Bool func keyboardProperties(for text: String) -> KeyboardProperties { return .init(type: .asciiCapableNumberPad, textContentType: nil, autocapitalization: .none) } @@ -245,6 +249,7 @@ extension TextFieldElement { enum Error: TextFieldValidationError { case empty case incomplete + case expired case invalidMonth case invalid @@ -252,7 +257,7 @@ extension TextFieldElement { switch self { case .empty: return false case .incomplete: return !isUserEditing - case .invalidMonth, .invalid: return true + case .expired, .invalidMonth, .invalid: return true } } @@ -262,6 +267,8 @@ extension TextFieldElement { return "" case .incomplete: return String.Localized.your_cards_expiration_date_is_incomplete + case .expired: + return String.Localized.your_card_has_expired case .invalidMonth: return String.Localized.your_cards_expiration_month_is_invalid case .invalid: @@ -288,7 +295,12 @@ extension TextFieldElement { } // Is the date expired? guard let expiryDate = CardExpiryDate(text), !expiryDate.expired() else { - return .invalid(Error.invalid) + if !isEditable { + return .invalid(Error.expired) + } + else { + return .invalid(Error.invalid) + } } return .valid default: @@ -314,7 +326,7 @@ extension TextFieldElement { // MARK: Last four configuration extension TextFieldElement { struct LastFourConfiguration: TextFieldElementConfiguration { - let label = String.Localized.card_brand + let label = String.Localized.card_number let lastFour: String let isEditable = false let cardBrandDropDown: DropdownFieldElement diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+Internal.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+Internal.swift index a0b436ba829..5a9c5548aa7 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+Internal.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+Internal.swift @@ -135,18 +135,21 @@ extension EmbeddedPaymentElement: EmbeddedPaymentMethodsViewDelegate { self.formViewController = formViewController } func presentSavedPaymentMethods(selectedSavedPaymentMethod: STPPaymentMethod?) { - // Special case, only 1 card remaining but is co-branded, skip showing the list and show update view controller + // Special case, only 1 card remaining but is co-branded (or defaultSPM), skip showing the list and show update view controller if savedPaymentMethods.count == 1, let paymentMethod = savedPaymentMethods.first, - paymentMethod.isCoBrandedCard, - elementsSession.isCardBrandChoiceEligible { + (paymentMethod.isCoBrandedCard && + elementsSession.isCardBrandChoiceEligible) || configuration.defaultSPM != .off { let updateViewController = UpdateCardViewController(paymentMethod: paymentMethod, removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, appearance: configuration.appearance, hostedSurface: .paymentSheet, + canEditCard: paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible, canRemoveCard: configuration.allowsRemovalOfLastSavedPaymentMethod && elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(), isTestMode: configuration.apiClient.isTestmode, - cardBrandFilter: configuration.cardBrandFilter) + cardBrandFilter: configuration.cardBrandFilter, + defaultSPM: configuration.defaultSPM + ) updateViewController.delegate = self let bottomSheetVC = bottomSheetController(with: updateViewController) presentingViewController?.presentAsBottomSheet(bottomSheetVC, appearance: configuration.appearance) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementConfiguration.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementConfiguration.swift index 6268531bbec..aa3aac7ab5c 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElementConfiguration.swift @@ -131,6 +131,12 @@ extension EmbeddedPaymentElement { /// Note: Card brand filtering is not currently supported by Link. @_spi(CardBrandFilteringBeta) public var cardBrandAcceptance: PaymentSheet.CardBrandAcceptance = .all + /// This is an experimental feature that may be removed at any time. + /// If allowsDefaultSPM, when editing, cards and US bank accounts will have the edit icon, users cannot remove them from the list screen, and users can set default payment method. + /// If navigationOnly, when editing, cards and US bank accounts will have the edit icon and users cannot remove them from the list screen. + /// If off (default), only cbc eligible cards can be edited and users can remove payment methods from the list screen. + @_spi(DefaultSPM) public var defaultSPM: PaymentSheet.DefaultSPM = .off + /// The view can display payment methods like “Card” that, when tapped, open a form sheet where customers enter their payment method details. The sheet has a button at the bottom. `FormSheetAction` enumerates the actions the button can perform. public enum FormSheetAction { /// The button says “Pay” or “Setup”. When tapped, we confirm the payment or setup in the form sheet. diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentElementConfiguration.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentElementConfiguration.swift index 737b24950ee..d9244539486 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentElementConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentElementConfiguration.swift @@ -37,6 +37,7 @@ protocol PaymentElementConfiguration: PaymentMethodRequirementProvider { var cardBrandAcceptance: PaymentSheet.CardBrandAcceptance { get set } var analyticPayload: [String: Any] { get } var disableWalletPaymentMethodFiltering: Bool { get set } + var defaultSPM: PaymentSheet.DefaultSPM { get set } } extension PaymentElementConfiguration { diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift index 839f1f327b3..f56b678296b 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetConfiguration.swift @@ -210,6 +210,12 @@ extension PaymentSheet { /// Note: This is only a client-side solution. /// Note: Card brand filtering is not currently supported by Link. @_spi(CardBrandFilteringBeta) public var cardBrandAcceptance: PaymentSheet.CardBrandAcceptance = .all + + + /// This is an experimental feature that may be removed at any time. + /// If true, when editing, cards and us bank accounts will have the edit icon, users cannot remove them from the list view screen. + /// If false (default), only cbc eligible cards can be edited and users can remove payment methods from the list screen. + @_spi(DefaultSPM) public var defaultSPM: PaymentSheet.DefaultSPM = .off } /// Defines the layout orientations available for displaying payment methods in PaymentSheet. @@ -562,3 +568,15 @@ extension PaymentSheet.CustomerConfiguration { case disallowed(brands: [BrandCategory]) } } + +@_spi(DefaultSPM) extension PaymentSheet { + /// Options to block certain card brands on the client + public enum DefaultSPM: Equatable { + /// DefaultSPM navigation and allows setting default SPM + case allowsDefaultSPM + /// DefaultSPM navigation + case navigationOnly + /// Existing navigation + case off + } +} 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 cf109a86cf2..83b42d00415 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift @@ -112,12 +112,18 @@ extension SavedPaymentMethodCollectionView { var cbcEligible: Bool = false var allowsPaymentMethodRemoval: Bool = true + var defaultSPM: PaymentSheet.DefaultSPM = .off /// 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 /// the cell should be editable. Otherwise, it should be just removable. var shouldAllowEditing: Bool { - return (viewModel?.isCoBrandedCard ?? false) && cbcEligible + switch defaultSPM { + case .allowsDefaultSPM, .navigationOnly: + return viewModel?.savedPaymentMethod?.type == STPPaymentMethodType.card + default: + return (viewModel?.isCoBrandedCard ?? false) && cbcEligible + } } // MARK: - UICollectionViewCell @@ -210,13 +216,14 @@ extension SavedPaymentMethodCollectionView { // MARK: - Internal Methods - func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: Bool, allowsPaymentMethodRemoval: Bool) { + func setViewModel(_ viewModel: SavedPaymentOptionsViewController.Selection, cbcEligible: Bool, allowsPaymentMethodRemoval: Bool, defaultSPM: PaymentSheet.DefaultSPM = .off) { paymentMethodLogo.isHidden = false plus.isHidden = true shadowRoundedRectangle.isHidden = false self.viewModel = viewModel self.cbcEligible = cbcEligible self.allowsPaymentMethodRemoval = allowsPaymentMethodRemoval + self.defaultSPM = defaultSPM update() } 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 9a1718348bd..2c915ab86f8 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift @@ -103,6 +103,7 @@ class SavedPaymentOptionsViewController: UIViewController { let isTestMode: Bool let allowsRemovalOfLastSavedPaymentMethod: Bool let allowsRemovalOfPaymentMethods: Bool + let defaultSPM: PaymentSheet.DefaultSPM } // MARK: - Internal Properties @@ -503,7 +504,7 @@ extension SavedPaymentOptionsViewController: UICollectionViewDataSource, UIColle stpAssertionFailure() return UICollectionViewCell() } - cell.setViewModel(viewModel, cbcEligible: cbcEligible, allowsPaymentMethodRemoval: self.configuration.allowsRemovalOfPaymentMethods) + cell.setViewModel(viewModel, cbcEligible: cbcEligible, allowsPaymentMethodRemoval: self.configuration.allowsRemovalOfPaymentMethods, defaultSPM: self.configuration.defaultSPM) cell.delegate = self cell.isRemovingPaymentMethods = self.collectionView.isRemovingPaymentMethods cell.appearance = appearance @@ -570,9 +571,12 @@ extension SavedPaymentOptionsViewController: PaymentOptionCellDelegate { removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, appearance: appearance, hostedSurface: .paymentSheet, + canEditCard: paymentMethod.isCoBrandedCard && cbcEligible, canRemoveCard: configuration.allowsRemovalOfPaymentMethods && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod), isTestMode: configuration.isTestMode, - cardBrandFilter: paymentSheetConfiguration.cardBrandFilter) + cardBrandFilter: paymentSheetConfiguration.cardBrandFilter, + defaultSPM: configuration.defaultSPM + ) editVc.delegate = self self.bottomSheetController?.pushContentViewController(editVc) } @@ -666,6 +670,10 @@ extension SavedPaymentOptionsViewController: UpdateCardViewControllerDelegate { let updatedViewModel: Selection = .saved(paymentMethod: updatedPaymentMethod) viewModels[row] = updatedViewModel + // Update savedPaymentMethods + if let row = self.savedPaymentMethods.firstIndex(where: { $0.stripeId == updatedPaymentMethod.stripeId }) { + self.savedPaymentMethods[row] = updatedPaymentMethod + } collectionView.reloadData() _ = viewController.bottomSheetController?.popContentViewController() } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift index e8cb6d24e4a..25ac7788dd1 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift @@ -48,8 +48,14 @@ class VerticalSavedPaymentMethodsViewController: UIViewController { // If we are entering edit mode, put all buttons in an edit state, otherwise put back in their previous state if isEditingPaymentMethods { - paymentMethodRows.forEach { $0.state = .editing(allowsRemoval: canRemovePaymentMethods, - allowsUpdating: $0.paymentMethod.isCoBrandedCard && isCBCEligible) } + switch configuration.defaultSPM { + case .allowsDefaultSPM, .navigationOnly: + paymentMethodRows.forEach { $0.state = .editing(allowsRemoval: false, + allowsUpdating: $0.paymentMethod.type == .card) } + case .off: + paymentMethodRows.forEach { $0.state = .editing(allowsRemoval: canRemovePaymentMethods, + allowsUpdating: $0.paymentMethod.isCoBrandedCard && isCBCEligible) } + } } else if oldValue { // If we are exiting edit mode restore previous selected states paymentMethodRows.forEach { $0.state = $0.previousSelectedState } @@ -165,10 +171,15 @@ class VerticalSavedPaymentMethodsViewController: UIViewController { self.paymentMethodRemove = elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet() self.isCBCEligible = elementsSession.isCardBrandChoiceEligible self.analyticsHelper = analyticsHelper - // Put in remove only mode and don't show the option to update PMs if: - // 1. We only have 1 payment method - // 2. The customer can't update the card brand - self.isRemoveOnlyMode = paymentMethods.count == 1 && (!paymentMethods[0].isCoBrandedCard || !isCBCEligible) + if configuration.defaultSPM != .off { + self.isRemoveOnlyMode = false + } + else { + // Put in remove only mode and don't show the option to update PMs if: + // 1. We only have 1 payment method + // 2. The customer can't update the card brand + self.isRemoveOnlyMode = paymentMethods.count == 1 && (!paymentMethods[0].isCoBrandedCard || !isCBCEligible) + } super.init(nibName: nil, bundle: nil) self.paymentMethodRows = buildPaymentMethodRows(paymentMethods: paymentMethods) setInitialState(selectedPaymentMethod: selectedPaymentMethod) @@ -336,9 +347,12 @@ extension VerticalSavedPaymentMethodsViewController: SavedPaymentMethodRowButton removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, appearance: configuration.appearance, hostedSurface: .paymentSheet, + canEditCard: paymentMethod.isCoBrandedCard && isCBCEligible, canRemoveCard: canRemovePaymentMethods, isTestMode: configuration.apiClient.isTestmode, - cardBrandFilter: configuration.cardBrandFilter) + cardBrandFilter: configuration.cardBrandFilter, + defaultSPM: configuration.defaultSPM + ) updateViewController.delegate = self self.updateViewController = updateViewController diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift index 2ac2dc0376e..da1e013a74e 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetFlowControllerViewController.swift @@ -206,7 +206,8 @@ class PaymentSheetFlowControllerViewController: UIViewController, FlowController isCVCRecollectionEnabled: false, isTestMode: configuration.apiClient.isTestmode, allowsRemovalOfLastSavedPaymentMethod: configuration.allowsRemovalOfLastSavedPaymentMethod, - allowsRemovalOfPaymentMethods: elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet() + allowsRemovalOfPaymentMethods: elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(), + defaultSPM: configuration.defaultSPM ), paymentSheetConfiguration: configuration, intent: intent, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift index 00983796534..a92f72389e3 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift @@ -566,18 +566,20 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo @objc func presentManageScreen() { error = nil - // Special case, only 1 card remaining but is co-branded, skip showing the list and show update view controller + // Special case, only 1 card remaining but is co-branded (or defaultSPM), skip showing the list and show update view controller if savedPaymentMethods.count == 1, let paymentMethod = savedPaymentMethods.first, - paymentMethod.isCoBrandedCard, - elementsSession.isCardBrandChoiceEligible { + (paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible) || configuration.defaultSPM != .off { let updateViewController = UpdateCardViewController(paymentMethod: paymentMethod, removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, appearance: configuration.appearance, hostedSurface: .paymentSheet, + canEditCard: paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible, canRemoveCard: configuration.allowsRemovalOfLastSavedPaymentMethod && elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(), isTestMode: configuration.apiClient.isTestmode, - cardBrandFilter: configuration.cardBrandFilter) + cardBrandFilter: configuration.cardBrandFilter, + defaultSPM: configuration.defaultSPM + ) updateViewController.delegate = self bottomSheetController?.pushContentViewController(updateViewController) return diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetViewController.swift index 885aa47369c..43477aa3e67 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetViewController.swift @@ -174,7 +174,8 @@ class PaymentSheetViewController: UIViewController, PaymentSheetViewControllerPr isCVCRecollectionEnabled: isCVCRecollectionEnabled, isTestMode: configuration.apiClient.isTestmode, allowsRemovalOfLastSavedPaymentMethod: configuration.allowsRemovalOfLastSavedPaymentMethod, - allowsRemovalOfPaymentMethods: loadResult.elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet() + allowsRemovalOfPaymentMethods: loadResult.elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(), + defaultSPM: configuration.defaultSPM ), paymentSheetConfiguration: configuration, intent: intent, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdateCardViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdateCardViewController.swift index adb1169258b..d8b53e46b22 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdateCardViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdateCardViewController.swift @@ -28,8 +28,10 @@ final class UpdateCardViewController: UIViewController { private let removeSavedPaymentMethodMessage: String? private let isTestMode: Bool private let hostedSurface: HostedSurface + private let canEditCard: Bool private let canRemoveCard: Bool private let cardBrandFilter: CardBrandFilter + private let defaultSPM: PaymentSheet.DefaultSPM private var latestError: Error? { didSet { @@ -51,33 +53,52 @@ final class UpdateCardViewController: UIViewController { // MARK: Views lazy var formStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [headerLabel, cardSection.view, updateButton, deleteButton, errorLabel]) + let cardDetails = UIStackView(arrangedSubviews: [cardSection.view, notEditableDetailsLabel]) + cardDetails.axis = .vertical + cardDetails.setCustomSpacing(8, after: cardSection.view) // custom spacing from figma + let manageSection = UIStackView(arrangedSubviews: [cardDetails, defaultCheckbox.view]) + manageSection.axis = .vertical + manageSection.setCustomSpacing(PaymentSheetUI.defaultPadding, after: cardDetails) // custom spacing from figma + let stackView = UIStackView(arrangedSubviews: [headerLabel, manageSection, updateButton, deleteButton, errorLabel]) stackView.isLayoutMarginsRelativeArrangement = true stackView.axis = .vertical - stackView.setCustomSpacing(PaymentSheetUI.defaultPadding - 4, after: headerLabel) // custom spacing from figma - stackView.setCustomSpacing(32, after: cardSection.view) // custom spacing from figma - stackView.setCustomSpacing(10, after: updateButton) // custom spacing from figma + stackView.setCustomSpacing(PaymentSheetUI.defaultPadding, after: headerLabel) // custom spacing from figma + stackView.setCustomSpacing(PaymentSheetUI.defaultPadding + 12, after: manageSection) // custom spacing from figma + stackView.setCustomSpacing(PaymentSheetUI.defaultPadding - 4, after: updateButton) // custom spacing from figma return stackView }() private lazy var headerLabel: UILabel = { let label = PaymentSheetUI.makeHeaderLabel(appearance: appearance) - label.text = .Localized.update_card_brand + label.text = .Localized.manage_card return label }() private lazy var updateButton: ConfirmButton = { - return ConfirmButton(state: .disabled, callToAction: .custom(title: .Localized.update), appearance: appearance, didTap: { [weak self] in + let button = ConfirmButton(state: .disabled, callToAction: .custom(title: .Localized.save), appearance: appearance, didTap: { [weak self] in Task { await self?.updateCard() } }) + button.isHidden = !canEditCard && defaultSPM != .allowsDefaultSPM + return button }() private lazy var deleteButton: UIButton = { let button = UIButton(type: .custom) + if #available(iOS 15.0, *) { + var configuration = UIButton.Configuration.bordered() + configuration.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 16, bottom: 10, trailing: 16) + configuration.baseBackgroundColor = .clear + button.configuration = configuration + } else { + button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16) + } button.setTitleColor(appearance.colors.danger, for: .normal) - button.setTitle(.Localized.remove_card, for: .normal) + button.layer.borderColor = appearance.colors.danger.cgColor + button.layer.borderWidth = appearance.primaryButton.borderWidth + button.layer.cornerRadius = appearance.cornerRadius + button.setTitle(.Localized.remove, for: .normal) button.titleLabel?.textAlignment = .center button.titleLabel?.font = appearance.scaledFont(for: appearance.font.base.medium, style: .callout, maximumPointSize: 25) button.titleLabel?.adjustsFontForContentSizeCategory = true @@ -86,6 +107,28 @@ final class UpdateCardViewController: UIViewController { return button }() + private lazy var notEditableDetailsLabel: UITextView = { + let label = ElementsUI.makeSmallFootnote(theme: appearance.asElementsTheme) + label.text = .Localized.card_details_cannot_be_changed + if defaultSPM != .off { + if canEditCard { + label.isHidden = true + } + else { + switch expiryDateElement.validationState { + case .valid: + label.isHidden = false + default: + label.isHidden = true + } + } + } + else { + label.isHidden = true + } + return label + }() + private lazy var errorLabel: UILabel = { let label = ElementsUI.makeErrorLabel(theme: appearance.asElementsTheme) label.isHidden = true @@ -123,24 +166,51 @@ final class UpdateCardViewController: UIViewController { return cardBrandDropDown }() + private lazy var expiryDateElement: TextFieldElement = { + let expiryDate = CardExpiryDate(month: paymentMethod.card?.expMonth ?? 0, year: paymentMethod.card?.expYear ?? 0) + let expiryDateElement = TextFieldElement.ExpiryDateConfiguration(defaultValue: expiryDate.displayString, isEditable: false).makeElement(theme: appearance.asElementsTheme) + return expiryDateElement + + }() + + private lazy var cvcElement: TextFieldElement = { + let cardBrandProvider = { [weak self] in + self?.paymentMethod.card?.brand ?? .unknown + } + let cvcConfiguration = TextFieldElement.CVCConfiguration(defaultValue: String(repeating: "•", count: Int(STPCardValidator.maxCVCLength(for: cardBrandProvider()))), cardBrandProvider: cardBrandProvider, isEditable: false) + let cvcElement = cvcConfiguration.makeElement(theme: appearance.asElementsTheme) + return cvcElement + + }() + private lazy var cardSection: SectionElement = { let allSubElements: [Element?] = [ - panElement, SectionElement.HiddenElement(cardBrandDropDown), + panElement, + SectionElement.HiddenElement(cardBrandDropDown), + SectionElement.MultiElementRow([expiryDateElement, cvcElement]) ] - let section = SectionElement(elements: allSubElements.compactMap { $0 }, theme: appearance.asElementsTheme) section.delegate = self return section }() + private lazy var defaultCheckbox: CheckboxElement = { + let checkbox = CheckboxElement(theme: appearance.asElementsTheme, label: "Set as default payment method.", isSelectedByDefault: false) + checkbox.view.isHidden = defaultSPM != .allowsDefaultSPM + return checkbox + }() + // MARK: Overrides init(paymentMethod: STPPaymentMethod, removeSavedPaymentMethodMessage: String?, appearance: PaymentSheet.Appearance, hostedSurface: HostedSurface, + canEditCard: Bool, canRemoveCard: Bool, isTestMode: Bool, - cardBrandFilter: CardBrandFilter = .default) { + cardBrandFilter: CardBrandFilter = .default, + defaultSPM: PaymentSheet.DefaultSPM + ) { self.paymentMethod = paymentMethod self.removeSavedPaymentMethodMessage = removeSavedPaymentMethodMessage self.appearance = appearance @@ -148,7 +218,8 @@ final class UpdateCardViewController: UIViewController { self.isTestMode = isTestMode self.canRemoveCard = canRemoveCard self.cardBrandFilter = cardBrandFilter - + self.canEditCard = canEditCard + self.defaultSPM = defaultSPM super.init(nibName: nil, bundle: nil) } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift index bdba1b9a487..058e857029b 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerSnapshotTests.swift @@ -7,7 +7,7 @@ import StripeCoreTestUtils @_spi(STP) @testable import StripePayments -@_spi(STP) @testable import StripePaymentSheet +@_spi(STP) @_spi(DefaultSPM) @testable import StripePaymentSheet @testable import StripePaymentsTestUtils @_spi(STP) @testable import StripeUICore import XCTest @@ -32,7 +32,7 @@ final class SavedPaymentOptionsViewControllerSnapshotTests: STPSnapshotTestCase STPPaymentMethod._testUSBankAccount(), STPPaymentMethod._testSEPA(), ] - let config = SavedPaymentOptionsViewController.Configuration(customerID: "cus_123", showApplePay: true, showLink: true, removeSavedPaymentMethodMessage: nil, merchantDisplayName: "Test Merchant", isCVCRecollectionEnabled: false, isTestMode: false, allowsRemovalOfLastSavedPaymentMethod: false, allowsRemovalOfPaymentMethods: true) + let config = SavedPaymentOptionsViewController.Configuration(customerID: "cus_123", showApplePay: true, showLink: true, removeSavedPaymentMethodMessage: nil, merchantDisplayName: "Test Merchant", isCVCRecollectionEnabled: false, isTestMode: false, allowsRemovalOfLastSavedPaymentMethod: false, allowsRemovalOfPaymentMethods: true, defaultSPM: .off) let intent = Intent.deferredIntent(intentConfig: .init(mode: .payment(amount: 0, currency: "USD", setupFutureUsage: nil, captureMethod: .automatic), confirmHandler: { _, _, _ in })) let sut = SavedPaymentOptionsViewController(savedPaymentMethods: paymentMethods, configuration: config, diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerTests.swift index 9dec04ccc72..1bd75fe2e0e 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/SavedPaymentOptionsViewControllerTests.swift @@ -3,7 +3,7 @@ // StripePaymentSheetTests // -@testable import StripePaymentSheet +@_spi(STP) @_spi(DefaultSPM) @testable import StripePaymentSheet import XCTest class SavedPaymentOptionsViewControllerTests: XCTestCase { @@ -294,7 +294,7 @@ class SavedPaymentOptionsViewControllerTests: XCTestCase { } // MARK: Helpers - func savedPaymentOptionsConfig(allowsRemovalOfLastSavedPaymentMethod: Bool, allowsRemovalOfPaymentMethods: Bool) -> SavedPaymentOptionsViewController.Configuration { + func savedPaymentOptionsConfig(allowsRemovalOfLastSavedPaymentMethod: Bool, allowsRemovalOfPaymentMethods: Bool, defaultSPM: PaymentSheet.DefaultSPM = .off) -> SavedPaymentOptionsViewController.Configuration { return SavedPaymentOptionsViewController.Configuration(customerID: "cus_123", showApplePay: true, showLink: true, @@ -303,7 +303,8 @@ class SavedPaymentOptionsViewControllerTests: XCTestCase { isCVCRecollectionEnabled: true, isTestMode: true, allowsRemovalOfLastSavedPaymentMethod: allowsRemovalOfLastSavedPaymentMethod, - allowsRemovalOfPaymentMethods: allowsRemovalOfPaymentMethods) + allowsRemovalOfPaymentMethods: allowsRemovalOfPaymentMethods, + defaultSPM: defaultSPM) } func savedPaymentOptionsController(_ configuration: SavedPaymentOptionsViewController.Configuration, diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdateCardViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdateCardViewControllerSnapshotTests.swift index 1a565270a2f..3ebd3a37fb0 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdateCardViewControllerSnapshotTests.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdateCardViewControllerSnapshotTests.swift @@ -36,13 +36,16 @@ final class UpdateCardViewControllerSnapshotTests: STPSnapshotTestCase { _test_UpdateCardViewController(darkMode: false, isEmbeddedSingleCard: true, appearance: ._testMSPaintTheme) } - func _test_UpdateCardViewController(darkMode: Bool, isEmbeddedSingleCard: Bool = false, appearance: PaymentSheet.Appearance = .default) { + func _test_UpdateCardViewController(darkMode: Bool, canEditCard: Bool = true, isEmbeddedSingleCard: Bool = false, appearance: PaymentSheet.Appearance = .default) { let sut = UpdateCardViewController(paymentMethod: STPFixtures.paymentMethod(), removeSavedPaymentMethodMessage: "Test removal string", appearance: appearance, hostedSurface: .paymentSheet, + canEditCard: canEditCard, canRemoveCard: true, - isTestMode: false) + isTestMode: false, + defaultSPM: .off + ) let bottomSheet: BottomSheetViewController if isEmbeddedSingleCard { bottomSheet = BottomSheetViewController(contentViewController: sut, appearance: appearance, isTestMode: true, didCancelNative3DS2: {}) diff --git a/StripePaymentsUI/StripePaymentsUI/Resources/Localizations/en.lproj/Localizable.strings b/StripePaymentsUI/StripePaymentsUI/Resources/Localizations/en.lproj/Localizable.strings index 21c368d816b..9109e3fa319 100644 --- a/StripePaymentsUI/StripePaymentsUI/Resources/Localizations/en.lproj/Localizable.strings +++ b/StripePaymentsUI/StripePaymentsUI/Resources/Localizations/en.lproj/Localizable.strings @@ -70,6 +70,9 @@ /* Error when the user hasn't allowed the current app to access the camera when scanning a payment card. 'Settings' is the localized name of the iOS Settings app. */ "To scan your card, allow camera access in Settings." = "To scan your card, allow camera access in Settings."; +/* Error message for card details form when expiration date has passed */ +"Your card has expired." = "Your card has expired."; + /* Error message for card form when card number is incomplete */ "Your card number is incomplete." = "Your card number is incomplete."; diff --git a/StripePaymentsUI/StripePaymentsUI/Source/Helpers/String+Localized.swift b/StripePaymentsUI/StripePaymentsUI/Source/Helpers/String+Localized.swift index d363e4e6625..2caf73ceab6 100644 --- a/StripePaymentsUI/StripePaymentsUI/Source/Helpers/String+Localized.swift +++ b/StripePaymentsUI/StripePaymentsUI/Source/Helpers/String+Localized.swift @@ -102,6 +102,13 @@ extension String.Localized { ) } + @_spi(STP) public static var your_card_has_expired: String { + STPLocalizedString( + "Your card has expired.", + "Error message for card details form when expiration date has passed" + ) + } + @_spi(STP) public static var your_cards_expiration_date_is_incomplete: String { STPLocalizedString( "Your card's expiration date is incomplete.", diff --git a/StripeUICore/StripeUICore/Source/Elements/ElementsUI.swift b/StripeUICore/StripeUICore/Source/Elements/ElementsUI.swift index d0581fb613b..784fcb6b689 100644 --- a/StripeUICore/StripeUICore/Source/Elements/ElementsUI.swift +++ b/StripeUICore/StripeUICore/Source/Elements/ElementsUI.swift @@ -44,6 +44,17 @@ import UIKit return label } + public static func makeSmallFootnote(theme: ElementsAppearance) -> UITextView { + let textView = UITextView() + textView.isScrollEnabled = false + textView.isEditable = false + textView.font = theme.fonts.smallFootnote + textView.backgroundColor = .clear + textView.textColor = theme.colors.secondaryText + textView.linkTextAttributes = [.foregroundColor: theme.colors.primary] + return textView + } + public static func makeNoticeTextField(theme: ElementsAppearance) -> UITextView { let textView = UITextView() textView.isScrollEnabled = false @@ -88,6 +99,7 @@ import UIKit withTextStyle: .caption1, maximumPointSize: 20) public var footnote = UIFont.preferredFont(forTextStyle: .footnote, weight: .regular, maximumPointSize: 20) + public var smallFootnote = UIFont.preferredFont(forTextStyle: .footnote, weight: .medium, maximumPointSize: 10) public var footnoteEmphasis = UIFont.preferredFont(forTextStyle: .footnote, weight: .medium, maximumPointSize: 20) } diff --git a/StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldElement.swift b/StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldElement.swift index 8e7797e8f2b..1bf06dca8a4 100644 --- a/StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldElement.swift +++ b/StripeUICore/StripeUICore/Source/Elements/TextField/TextFieldElement.swift @@ -28,7 +28,12 @@ import UIKit } } public private(set) lazy var text: String = { - sanitize(text: configuration.defaultValue ?? "") + if configuration.isEditable { + sanitize(text: configuration.defaultValue ?? "") + } + else { + configuration.defaultValue ?? "" + } }() public private(set) var isEditing: Bool = false private(set) var didReceiveAutofill: Bool = false @@ -109,7 +114,12 @@ import UIKit /// Call this to manually set the text of the text field. public func setText(_ text: String) { - self.text = sanitize(text: text) + if configuration.isEditable { + self.text = sanitize(text: text) + } + else { + self.text = text + } // Since we're setting the text manually, disable any previous autofill didReceiveAutofill = false diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerAppearance@3x.png index d947c8737ce..99038847f8c 100644 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerAppearance@3x.png and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerDarkMode@3x.png index 29121406ee0..7182a1249f3 100644 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerDarkMode@3x.png and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerLightMode@3x.png index b0f13a5ad1e..a5e71bfc06e 100644 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerLightMode@3x.png and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdateCardViewControllerLightMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerAppearance@3x.png index 9822e5a4c38..74c9184b249 100644 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerAppearance@3x.png and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerDarkMode@3x.png index b4859770b25..5025120f40b 100644 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerDarkMode@3x.png and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerLightMode@3x.png index 5e75ac70264..71b35b43280 100644 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerLightMode@3x.png and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdateCardViewControllerSnapshotTests/test_UpdateCardViewControllerLightMode@3x.png differ