diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift index 84586887985..e103bc1287f 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/CustomerSheetUITest.swift @@ -487,7 +487,7 @@ class CustomerSheetUITest: XCTestCase { // circularEditButton shows up in the view hierarchy, but it's not actually on the screen or tappable so we scroll a little let startCoordinate = app.collectionViews.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.9, dy: 0.99)) startCoordinate.press(forDuration: 0.1, thenDragTo: app.collectionViews.firstMatch.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.99))) - XCTAssertTrue(app.buttons.matching(identifier: "CircularButton.Edit").firstMatch.waitForExistenceAndTap()) + XCTAssertTrue(app.buttons.matching(identifier: "CircularButton.Edit").element(boundBy: 1).waitForExistenceAndTap()) XCTAssertTrue(app.otherElements.matching(identifier: "Card Brand Dropdown").firstMatch.waitForExistenceAndTap()) app.pickerWheels.firstMatch.selectNextOption() app.toolbars.buttons["Done"].tap() @@ -496,7 +496,7 @@ class CustomerSheetUITest: XCTestCase { XCTAssertTrue(app.buttons["Done"].waitForExistence(timeout: 3)) XCTAssertEqual(app.images.matching(identifier: "carousel_card_visa").count, 2) - app.buttons.matching(identifier: "CircularButton.Edit").element(boundBy: 1).waitForExistenceAndTap() + app.buttons.matching(identifier: "CircularButton.Edit").element(boundBy: 2).waitForExistenceAndTap() app.buttons["Remove"].waitForExistenceAndTap() app.alerts.buttons["Remove"].waitForExistenceAndTap() XCTAssertTrue(app.buttons["Done"].waitForExistence(timeout: 3)) diff --git a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj index ca60ca10ceb..ffb6c96f038 100644 --- a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj +++ b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj @@ -161,7 +161,7 @@ 614068E22CB0BF10003D2F12 /* EmbeddedPaymentMethodsViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614068E12CB0BF10003D2F12 /* EmbeddedPaymentMethodsViewTests.swift */; }; 6141C5072C0A47A700E81735 /* RightAccessoryButtonTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6141C5062C0A47A700E81735 /* RightAccessoryButtonTest.swift */; }; 614A8AE72BE53C6900E8688B /* SavedPaymentMethodManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6103F2BD2BE53737002D67F8 /* SavedPaymentMethodManagerTest.swift */; }; - 6151DDC02B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */; }; + 6151DDC02B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6151DDBF2B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift */; }; 615AADAF2CB97A2000D0AED9 /* STPCardValidator+BrandFiltering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615AADAE2CB97A2000D0AED9 /* STPCardValidator+BrandFiltering.swift */; }; 615AADB12CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615AADB02CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift */; }; 615C2C502CBDBA61003F0173 /* EmbeddedFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615C2C4F2CBDBA61003F0173 /* EmbeddedFormViewController.swift */; }; @@ -253,6 +253,9 @@ 9E77F1E9F801AE970F1A5BE1 /* CustomerSheetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7768E4377A7D7495A02655D /* CustomerSheetConfiguration.swift */; }; 9F750611C4E8EAABE9F0B460 /* CustomerSheetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED7ABCE56539213DCE501C54 /* CustomerSheetTests.swift */; }; A1AC7034E778F81B8758A653 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 20076FBD56C42E259EF62F2B /* OHHTTPStubsSwift */; }; + A36949C52CE7D72300739E5D /* UpdatePaymentMethodViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36949C42CE7D72300739E5D /* UpdatePaymentMethodViewModel.swift */; }; + A3B2F2B22CEE689C00C0E88C /* TextFieldElement+USBankAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B2F2B12CEE689C00C0E88C /* TextFieldElement+USBankAccount.swift */; }; + A3B2F2B42CEE882D00C0E88C /* SavedPaymentMethodFormFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B2F2B32CEE882D00C0E88C /* SavedPaymentMethodFormFactory.swift */; }; A4CD99B2032CBFA7F957B1B8 /* String+AutoComplete.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982014B36F49D902CD04AF5C /* String+AutoComplete.swift */; }; A4FF52567582E9774AE13348 /* PaymentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981F958E99945A0318D47BBF /* PaymentDetails.swift */; }; A59432E765A72CEE2C36E0EF /* PaymentSheetFormFactory+Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CF41D601B198DC37337940C /* PaymentSheetFormFactory+Card.swift */; }; @@ -265,7 +268,7 @@ ABC3A7CF6D5B21D0C9684A09 /* LinkPopupURLParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E4212F4A865B5AB5D72F99 /* LinkPopupURLParserTests.swift */; }; ABE13E65678673EC4DE14EF4 /* PaymentSheetLinkAccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0901281208BEB220D0B491A8 /* PaymentSheetLinkAccountTests.swift */; }; AE8EF3966E7BABDFC3B426ED /* PaymentSheet+DashboardConfirmParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82C21D5722BDEB8BAA71F69F /* PaymentSheet+DashboardConfirmParamsTest.swift */; }; - AF0D609C28A8B0ECD11FD539 /* UpdateCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0E71D9DEA93036E4C87CEC /* UpdateCardViewController.swift */; }; + AF0D609C28A8B0ECD11FD539 /* UpdatePaymentMethodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF0E71D9DEA93036E4C87CEC /* UpdatePaymentMethodViewController.swift */; }; B2979A0740F8730FC14DFEC1 /* BottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73853A55045C53AF2BBB8489 /* BottomSheetViewController.swift */; }; B306EA3F66D07CCABF17CB9C /* LinkInlineSignupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7910B57E6FD99F2AFCA4DAC2 /* LinkInlineSignupViewModel.swift */; }; B4679C9095BCD53CCC2C7D25 /* StripeCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E41AA4E90E5BB28D588FDE51 /* StripeCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -578,7 +581,7 @@ 6139AA50F07A1E2AC7E9827F /* AUBECSMandate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AUBECSMandate.swift; sourceTree = ""; }; 614068E12CB0BF10003D2F12 /* EmbeddedPaymentMethodsViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedPaymentMethodsViewTests.swift; sourceTree = ""; }; 6141C5062C0A47A700E81735 /* RightAccessoryButtonTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RightAccessoryButtonTest.swift; sourceTree = ""; }; - 6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCardViewControllerSnapshotTests.swift; sourceTree = ""; }; + 6151DDBF2B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePaymentMethodViewControllerSnapshotTests.swift; sourceTree = ""; }; 615AADAE2CB97A2000D0AED9 /* STPCardValidator+BrandFiltering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPCardValidator+BrandFiltering.swift"; sourceTree = ""; }; 615AADB02CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPCardValidator+BrandFilteringTest.swift"; sourceTree = ""; }; 615C2C4F2CBDBA61003F0173 /* EmbeddedFormViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedFormViewController.swift; sourceTree = ""; }; @@ -677,7 +680,10 @@ 9E3905FE9F40E82EAEF49CD2 /* ro-RO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ro-RO"; path = "ro-RO.lproj/Localizable.strings"; sourceTree = ""; }; A09CCD0ED44336B23450A995 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; A1928BE9DFF116368B1A19DC /* LinkCookieKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkCookieKey.swift; sourceTree = ""; }; + A36949C42CE7D72300739E5D /* UpdatePaymentMethodViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePaymentMethodViewModel.swift; sourceTree = ""; }; A39F7EBA2E9E3CE55E7AADC9 /* STPFixtures+PaymentSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPFixtures+PaymentSheet.swift"; sourceTree = ""; }; + A3B2F2B12CEE689C00C0E88C /* TextFieldElement+USBankAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextFieldElement+USBankAccount.swift"; sourceTree = ""; }; + A3B2F2B32CEE882D00C0E88C /* SavedPaymentMethodFormFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedPaymentMethodFormFactory.swift; sourceTree = ""; }; A49D0A50ECFA7A4A5FC40878 /* ca-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ca-ES"; path = "ca-ES.lproj/Localizable.strings"; sourceTree = ""; }; A5012364ED0F2EEC6EC2AB52 /* PaymentMethodElementWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodElementWrapper.swift; sourceTree = ""; }; A5E8DD8761B4C52B143038C4 /* LinkInlineSignupElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkInlineSignupElement.swift; sourceTree = ""; }; @@ -741,7 +747,7 @@ BE92D55DA4B4D449734B2917 /* BacsDDMandateViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacsDDMandateViewSnapshotTests.swift; sourceTree = ""; }; BEF72D6FB252D340AF1854FE /* AddressViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressViewController.swift; sourceTree = ""; }; BF0319C54AA8B74DEB6881F0 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; - BF0E71D9DEA93036E4C87CEC /* UpdateCardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCardViewController.swift; sourceTree = ""; }; + BF0E71D9DEA93036E4C87CEC /* UpdatePaymentMethodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePaymentMethodViewController.swift; sourceTree = ""; }; C1AED4473AD4C07D461E9E48 /* SavedPaymentOptionsViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedPaymentOptionsViewControllerSnapshotTests.swift; sourceTree = ""; }; C2224DF2C85F86C680B5078F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; C3C1A5F36075EAEA5A413DC5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -850,6 +856,7 @@ isa = PBXGroup; children = ( 47C5DB8C01BA7137369C8B4D /* TextFieldElement+Card.swift */, + A3B2F2B12CEE689C00C0E88C /* TextFieldElement+USBankAccount.swift */, 570931B897DCCAC0F55FB6E3 /* TextFieldElement+IBAN.swift */, ); path = TextField; @@ -1175,7 +1182,9 @@ 101DFBD8D19B7B182CDD8882 /* PollingViewController.swift */, 26C092533C67D8C7FDE12742 /* PollingViewModel.swift */, BBB56BACEB0ADD42882632CB /* SepaMandateViewController.swift */, - BF0E71D9DEA93036E4C87CEC /* UpdateCardViewController.swift */, + BF0E71D9DEA93036E4C87CEC /* UpdatePaymentMethodViewController.swift */, + A36949C42CE7D72300739E5D /* UpdatePaymentMethodViewModel.swift */, + A3B2F2B32CEE882D00C0E88C /* SavedPaymentMethodFormFactory.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1682,7 +1691,7 @@ 615AADB02CB97A9400D0AED9 /* STPCardValidator+BrandFilteringTest.swift */, AC0DBA7D63BAD0182695E436 /* Stubbed */, D00C7F5905759525C9BF8BD4 /* TextFieldElement+CardTest.swift */, - 6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */, + 6151DDBF2B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift */, B6F4C5F72CADB0CB00AF3767 /* USBankAccountPaymentMethodElementTest.swift */, 316B33112B5F171C0008D2E5 /* UserDefaults+StripePaymentSheetTest.swift */, B65FE7082BED33EA009A73FC /* VerticalPaymentMethodListViewControllerSnapshotTest.swift */, @@ -2013,7 +2022,7 @@ 6141C5072C0A47A700E81735 /* RightAccessoryButtonTest.swift in Sources */, B64FEF122C0FAC1E00F7CA26 /* PaymentSheetVerticalViewControllerTest.swift in Sources */, 4A1A0A542B824C830A200BE0 /* StubbedBackend.swift in Sources */, - 6151DDC02B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift in Sources */, + 6151DDC02B14FDCF00ED4F7E /* UpdatePaymentMethodViewControllerSnapshotTests.swift in Sources */, 34CF08CBC636F596B8BA4C12 /* TextFieldElement+CardTest.swift in Sources */, 52B734BA0B91706F37025523 /* STPAnalyticsClient+PaymentSheetTests.swift in Sources */, AE8EF3966E7BABDFC3B426ED /* PaymentSheet+DashboardConfirmParamsTest.swift in Sources */, @@ -2148,6 +2157,7 @@ 3147CECB2CC1BF550067B5E4 /* LinkPaymentMethodPicker-CellContentView.swift in Sources */, 3147CECC2CC1BF550067B5E4 /* LinkPaymentMethodPicker-Cell.swift in Sources */, 3147CECD2CC1BF550067B5E4 /* LinkPaymentMethodPicker-Header.swift in Sources */, + A36949C52CE7D72300739E5D /* UpdatePaymentMethodViewModel.swift in Sources */, 313D00C82CD9972F00A8E6B0 /* PayWithNativeLinkController.swift in Sources */, 3147CECE2CC1BF550067B5E4 /* LinkPaymentMethodPicker-RadioButton.swift in Sources */, 3147CECF2CC1BF550067B5E4 /* LinkPaymentMethodPicker-AddButton.swift in Sources */, @@ -2189,6 +2199,7 @@ 209FF56603EE6FC381BB58F1 /* FormSpec.swift in Sources */, B63B2CF32BFBEE7B003810F3 /* VerticalPaymentMethodListViewController.swift in Sources */, D592BEF7679F39A4DE26E5AF /* FormSpecProvider.swift in Sources */, + A3B2F2B22CEE689C00C0E88C /* TextFieldElement+USBankAccount.swift in Sources */, D39475E63F8372FFDB0F06EA /* PaymentSheetFormFactory+BLIK.swift in Sources */, B99DA5A48A9EF5E352DDA872 /* PaymentSheetFormFactory+Boleto.swift in Sources */, A59432E765A72CEE2C36E0EF /* PaymentSheetFormFactory+Card.swift in Sources */, @@ -2209,6 +2220,7 @@ 3147CED92CC1BF860067B5E4 /* LinkVerificationController.swift in Sources */, 3147CEDA2CC1BF860067B5E4 /* LinkVerificationViewController-PresentationController.swift in Sources */, 3147CEDB2CC1BF860067B5E4 /* LinkVerificationView.swift in Sources */, + A3B2F2B42CEE882D00C0E88C /* SavedPaymentMethodFormFactory.swift in Sources */, 3147CEDC2CC1BF860067B5E4 /* LinkVerificationView-Header.swift in Sources */, 3147CEDD2CC1BF860067B5E4 /* LinkVerificationView-LogoutView.swift in Sources */, 6103F2BC2BE45990002D67F8 /* SavedPaymentMethodManager.swift in Sources */, @@ -2231,7 +2243,7 @@ 4DDECA1F7EC6B624C00D549E /* PollingViewController.swift in Sources */, 59BE39C4C3992DBB6A698390 /* PollingViewModel.swift in Sources */, F7BCE78B8F782979FD5EE323 /* SepaMandateViewController.swift in Sources */, - AF0D609C28A8B0ECD11FD539 /* UpdateCardViewController.swift in Sources */, + AF0D609C28A8B0ECD11FD539 /* UpdatePaymentMethodViewController.swift in Sources */, D203D701AF400680AF0F82F8 /* AUBECSMandate.swift in Sources */, D792BA37B04E5A3AD30E37CF /* AffirmCopyLabel.swift in Sources */, 9BFC22175CF85F58B8B8792A /* AfterpayPriceBreakdownView.swift in Sources */, diff --git a/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings b/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings index 5a839647ef4..50c3a96d1f0 100644 --- a/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings +++ b/StripePaymentSheet/StripePaymentSheet/Resources/Localizations/en.lproj/Localizable.strings @@ -46,6 +46,9 @@ /* Title for collected bank account information */ "Bank account" = "Bank account"; +/* Text on a screen that indicates bank account details cannot be changed. */ +"Bank account details cannot be changed." = "Bank account details cannot be changed."; + /* Promotional text for Afterpay/Clearpay - the image tag will display the Afterpay or Clearpay logo. This text is displayed in a button that lets the customer pay with Afterpay/Clearpay */ "Buy now or pay later with " = "Buy now or pay later with "; @@ -106,6 +109,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"; @@ -182,6 +188,12 @@ re-entering the security code (CVV/CVC). */ /* Title shown above a view containing the customer's payment methods that they can delete or update */ "Manage payment methods" = "Manage payment methods"; +/* Title shown above a view containing the customer's SEPA debit that they can delete or update */ +"Manage SEPA debit" = "Manage SEPA debit"; + +/* Title shown above a view containing the customer's bank account that they can delete or update */ +"Manage US bank account" = "Manage US bank account"; + /* Title shown above a carousel containing the customer's payment methods */ "Manage your payment methods" = "Manage your payment methods"; @@ -312,6 +324,9 @@ to be saved and used in future checkout sessions. */ /* Title shown above a carousel containing the customer's payment methods */ "Select your payment method" = "Select your payment method"; +/* Text on a screen that indicates SEPA debit details cannot be changed. */ +"SEPA debit details cannot be changed." = "SEPA debit details cannot be changed."; + /* Label for a button or menu item that sets a payment method as default when tapped. */ "Set as default" = "Set as default"; @@ -377,8 +392,8 @@ is not supported by the merchant */ the heading the screen itself. */ "Update card" = "Update card"; -/* Title for a screen for updating a card brand. */ -"Update card brand" = "Update card brand"; +/* Accessibility label for a button that leads to a screen for updating a payment method. */ +"Update payment method" = "Update payment method"; /* Two factor authentication screen heading */ "Use your saved info to check out faster" = "Use your saved info to check out faster"; diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift b/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift index 8ba5088c985..97ed0d3b54f 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Categories/String+Localized.swift @@ -77,6 +77,20 @@ extension String.Localized { STPLocalizedString("Back", "Text for back button") } + static var manage_us_bank_account: String { + STPLocalizedString( + "Manage US bank account", + "Title shown above a view containing the customer's bank account that they can delete or update" + ) + } + + static var manage_sepa_debit: String { + STPLocalizedString( + "Manage SEPA debit", + "Title shown above a view containing the customer's SEPA debit that they can delete or update" + ) + } + static var manage_card: String { STPLocalizedString( "Manage card", @@ -84,6 +98,27 @@ extension String.Localized { ) } + static var bank_account_details_cannot_be_changed: String { + STPLocalizedString( + "Bank account details cannot be changed.", + "Text on a screen that indicates bank account details cannot be changed." + ) + } + + static var sepa_debit_details_cannot_be_changed: String { + STPLocalizedString( + "SEPA debit details cannot be changed.", + "Text on a screen that indicates SEPA debit details cannot be changed." + ) + } + + 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 save: String { STPLocalizedString( "Save", @@ -101,10 +136,12 @@ extension String.Localized { ) } - static var update_card_brand: String { + static var update_payment_method: String { STPLocalizedString( - "Update card brand", - "Title for a screen for updating a card brand." + "Update payment method", + """ + Accessibility label for a button that leads to a screen for updating a payment method. + """ ) } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift index a3ecc1dfc9a..a1554fbcac6 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/CustomerSheet/CustomerSavedPaymentMethodsCollectionViewController.swift @@ -432,14 +432,16 @@ extension CustomerSavedPaymentMethodsCollectionViewController: PaymentOptionCell stpAssertionFailure() return } - - let editVc = UpdateCardViewController(paymentMethod: paymentMethod, + let updateViewModel = UpdatePaymentMethodViewModel(paymentMethod: paymentMethod, + appearance: appearance, + hostedSurface: .customerSheet, + cardBrandFilter: savedPaymentMethodsConfiguration.cardBrandFilter, + canEdit: paymentMethod.isCoBrandedCard && cbcEligible, + canRemove: configuration.paymentMethodRemove && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod)) + let editVc = UpdatePaymentMethodViewController( removeSavedPaymentMethodMessage: savedPaymentMethodsConfiguration.removeSavedPaymentMethodMessage, - appearance: appearance, - hostedSurface: .customerSheet, - canRemoveCard: configuration.paymentMethodRemove && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod), isTestMode: configuration.isTestMode, - cardBrandFilter: savedPaymentMethodsConfiguration.cardBrandFilter) + viewModel: updateViewModel) editVc.delegate = self self.bottomSheetController?.pushContentViewController(editVc) } @@ -519,10 +521,10 @@ extension CustomerSavedPaymentMethodsCollectionViewController: PaymentOptionCell } } -// MARK: - UpdateCardViewControllerDelegate +// MARK: - UpdatePaymentMethodViewControllerDelegate /// :nodoc: -extension CustomerSavedPaymentMethodsCollectionViewController: UpdateCardViewControllerDelegate { - func didUpdate(viewController: UpdateCardViewController, +extension CustomerSavedPaymentMethodsCollectionViewController: UpdatePaymentMethodViewControllerDelegate { + func didUpdate(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod, updateParams: StripePayments.STPPaymentMethodUpdateParams) async throws { guard let row = viewModels.firstIndex(where: { $0.toSavedPaymentOptionsViewControllerSelection().savedPaymentMethod?.stripeId == paymentMethod.stripeId }), @@ -547,7 +549,7 @@ extension CustomerSavedPaymentMethodsCollectionViewController: UpdateCardViewCon _ = viewController.bottomSheetController?.popContentViewController() } - func didRemove(viewController: UpdateCardViewController, + func didRemove(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod) { guard let row = viewModels.firstIndex(where: { $0.toSavedPaymentOptionsViewControllerSelection().savedPaymentMethod?.stripeId == paymentMethod.stripeId }) else { @@ -562,7 +564,7 @@ extension CustomerSavedPaymentMethodsCollectionViewController: UpdateCardViewCon _ = viewController.bottomSheetController?.popContentViewController() } - func didDismiss(viewController: UpdateCardViewController) { + func didDismiss(_: UpdatePaymentMethodViewController) { // No-op } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift index 48e03a09947..b56a63f11b4 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+Card.swift @@ -341,13 +341,13 @@ extension TextFieldElement { let label = String.Localized.card_number let lastFour: String let isEditable = false - let cardBrandDropDown: DropdownFieldElement + let cardBrandDropDown: DropdownFieldElement? private var lastFourFormatted: String { "•••• •••• •••• \(lastFour)" } - init(lastFour: String, cardBrandDropDown: DropdownFieldElement) { + init(lastFour: String, cardBrandDropDown: DropdownFieldElement?) { self.lastFour = lastFour self.cardBrandDropDown = cardBrandDropDown } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+IBAN.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+IBAN.swift index 164f7e10200..40d088fd03e 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+IBAN.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+IBAN.swift @@ -179,6 +179,24 @@ extension TextFieldElement { } } } + + struct LastFourIBANConfiguration: TextFieldElementConfiguration { + let label: String = "IBAN" + let lastFour: String + let isEditable = false + + private var lastFourFormatted: String { + "•••• \(lastFour)" + } + + init(lastFour: String) { + self.lastFour = lastFour + } + + func makeDisplayText(for text: String) -> NSAttributedString { + return NSAttributedString(string: lastFourFormatted) + } + } } private let asciiValueOfA: Int = Int(Character("A").asciiValue!) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+USBankAccount.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+USBankAccount.swift new file mode 100644 index 00000000000..60626755359 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/TextField/TextFieldElement+USBankAccount.swift @@ -0,0 +1,34 @@ +// +// TextFieldElement+USBankAccount.swift +// StripePaymentSheet +// +// Created by Joyce Qin on 11/20/24. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +extension TextFieldElement { + struct USBankNumberConfiguration: TextFieldElementConfiguration { + let label = String.Localized.bank_account + let bankName: String + let lastFour: String + let isEditable = false + + private var lastFourFormatted: String { + "\(bankName) ••••\(lastFour)" + } + + public init(bankName:String, lastFour: String) { + self.bankName = bankName + self.lastFour = lastFour + } + + public func makeDisplayText(for text: String) -> NSAttributedString { + return NSAttributedString(string: lastFourFormatted) + } + } +} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+Internal.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+Internal.swift index 8a60e147922..adac81d1293 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+Internal.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Embedded/EmbeddedPaymentElement+Internal.swift @@ -141,13 +141,16 @@ extension EmbeddedPaymentElement: EmbeddedPaymentMethodsViewDelegate { let paymentMethod = savedPaymentMethods.first, paymentMethod.isCoBrandedCard, elementsSession.isCardBrandChoiceEligible || configuration.alternateUpdatePaymentMethodNavigation { - let updateViewController = UpdateCardViewController(paymentMethod: paymentMethod, + let updateViewModel = UpdatePaymentMethodViewModel(paymentMethod: paymentMethod, + appearance: configuration.appearance, + hostedSurface: .paymentSheet, + cardBrandFilter: configuration.cardBrandFilter, + canEdit: paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible, + canRemove: configuration.allowsRemovalOfLastSavedPaymentMethod && elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet()) + let updateViewController = UpdatePaymentMethodViewController( removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, - appearance: configuration.appearance, - hostedSurface: .paymentSheet, - canRemoveCard: configuration.allowsRemovalOfLastSavedPaymentMethod && elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(), isTestMode: configuration.apiClient.isTestmode, - cardBrandFilter: configuration.cardBrandFilter) + viewModel: updateViewModel) updateViewController.delegate = self let bottomSheetVC = bottomSheetController(with: updateViewController) presentingViewController?.presentAsBottomSheet(bottomSheetVC, appearance: configuration.appearance) @@ -167,9 +170,9 @@ extension EmbeddedPaymentElement: EmbeddedPaymentMethodsViewDelegate { } } -// MARK: UpdateCardViewControllerDelegate -extension EmbeddedPaymentElement: UpdateCardViewControllerDelegate { - func didRemove(viewController: UpdateCardViewController, paymentMethod: StripePayments.STPPaymentMethod) { +// MARK: UpdatePaymentMethodViewControllerDelegate +extension EmbeddedPaymentElement: UpdatePaymentMethodViewControllerDelegate { + func didRemove(viewController: UpdatePaymentMethodViewController, paymentMethod: StripePayments.STPPaymentMethod) { // Detach the payment method from the customer savedPaymentMethodManager.detach(paymentMethod: paymentMethod) analyticsHelper.logSavedPaymentMethodRemoved(paymentMethod: paymentMethod) @@ -184,7 +187,7 @@ extension EmbeddedPaymentElement: UpdateCardViewControllerDelegate { presentingViewController?.dismiss(animated: true) } - func didUpdate(viewController: UpdateCardViewController, + func didUpdate(viewController: UpdatePaymentMethodViewController, paymentMethod: StripePayments.STPPaymentMethod, updateParams: StripePayments.STPPaymentMethodUpdateParams) async throws { let updatedPaymentMethod = try await savedPaymentMethodManager.update(paymentMethod: paymentMethod, with: updateParams) @@ -201,8 +204,8 @@ extension EmbeddedPaymentElement: UpdateCardViewControllerDelegate { accessoryType: accessoryType) presentingViewController?.dismiss(animated: true) } - - func didDismiss(viewController: UpdateCardViewController) { + + func didDismiss(_: UpdatePaymentMethodViewController) { presentingViewController?.dismiss(animated: true) } 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 5acb96ee8ba..84039502938 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentMethodCollectionView.swift @@ -119,7 +119,9 @@ extension SavedPaymentMethodCollectionView { /// the cell should be editable. Otherwise, it should be just removable. var shouldAllowEditing: Bool { if alternateUpdatePaymentMethodNavigation { - return viewModel?.savedPaymentMethod?.type == .card + return UpdatePaymentMethodViewModel.supportedPaymentMethods.contains { type in + viewModel?.savedPaymentMethod?.type == type + } } else { return (viewModel?.isCoBrandedCard ?? false) && 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 ab91bd8f1d0..25b7fba3a56 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/SavedPaymentOptionsViewController.swift @@ -555,14 +555,16 @@ extension SavedPaymentOptionsViewController: PaymentOptionCellDelegate { stpAssertionFailure() return } - - let editVc = UpdateCardViewController(paymentMethod: paymentMethod, + let updateViewModel = UpdatePaymentMethodViewModel(paymentMethod: paymentMethod, + appearance: appearance, + hostedSurface: .paymentSheet, + cardBrandFilter: paymentSheetConfiguration.cardBrandFilter, + canEdit: paymentMethod.isCoBrandedCard && cbcEligible, + canRemove: configuration.allowsRemovalOfPaymentMethods && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod)) + let editVc = UpdatePaymentMethodViewController( removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, - appearance: appearance, - hostedSurface: .paymentSheet, - canRemoveCard: configuration.allowsRemovalOfPaymentMethods && (savedPaymentMethods.count > 1 || configuration.allowsRemovalOfLastSavedPaymentMethod), isTestMode: configuration.isTestMode, - cardBrandFilter: paymentSheetConfiguration.cardBrandFilter) + viewModel: updateViewModel) editVc.delegate = self self.bottomSheetController?.pushContentViewController(editVc) } @@ -632,14 +634,14 @@ extension SavedPaymentOptionsViewController: PaymentOptionCellDelegate { } } -// MARK: - UpdateCardViewControllerDelegate -extension SavedPaymentOptionsViewController: UpdateCardViewControllerDelegate { - func didRemove(viewController: UpdateCardViewController, paymentMethod: STPPaymentMethod) { +// MARK: - UpdatePaymentMethodViewControllerDelegate +extension SavedPaymentOptionsViewController: UpdatePaymentMethodViewControllerDelegate { + func didRemove(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod) { removePaymentMethod(paymentMethod) _ = viewController.bottomSheetController?.popContentViewController() } - func didUpdate(viewController: UpdateCardViewController, + func didUpdate(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod, updateParams: STPPaymentMethodUpdateParams) async throws { guard let row = viewModels.firstIndex(where: { $0.savedPaymentMethod?.stripeId == paymentMethod.stripeId }), @@ -664,7 +666,7 @@ extension SavedPaymentOptionsViewController: UpdateCardViewControllerDelegate { _ = viewController.bottomSheetController?.popContentViewController() } - func didDismiss(viewController: UpdateCardViewController) { + func didDismiss(_: UpdatePaymentMethodViewController) { // No-op } } 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 7ba4605898c..7ac5f85bd81 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 @@ -38,7 +38,7 @@ class VerticalSavedPaymentMethodsViewController: UIViewController { private let isCBCEligible: Bool private let analyticsHelper: PaymentSheetAnalyticsHelper - private var updateViewController: UpdateCardViewController? + private var updateViewController: UpdatePaymentMethodViewController? private var isEditingPaymentMethods: Bool = false { didSet { @@ -50,7 +50,8 @@ class VerticalSavedPaymentMethodsViewController: UIViewController { if isEditingPaymentMethods { paymentMethodRows.forEach { let allowsRemoval = canRemovePaymentMethods - let allowsUpdating = ($0.paymentMethod.isCoBrandedCard && isCBCEligible) || (configuration.alternateUpdatePaymentMethodNavigation && $0.paymentMethod.type == .card) + let paymentMethodType = $0.paymentMethod.type + let allowsUpdating = ($0.paymentMethod.isCoBrandedCard && isCBCEligible) || (configuration.alternateUpdatePaymentMethodNavigation && (UpdatePaymentMethodViewModel.supportedPaymentMethods.contains { type in paymentMethodType == type })) $0.state = .editing(allowsRemoval: allowsRemoval, allowsUpdating: allowsUpdating) } @@ -88,8 +89,8 @@ class VerticalSavedPaymentMethodsViewController: UIViewController { var canEdit: Bool { // We can edit if there are removable or editable payment methods and we are not in remove only mode - // Or, under the new navigation flow, if any of the payment methods are cards - return ((canRemovePaymentMethods || (hasCoBrandedCards && isCBCEligible)) && !isRemoveOnlyMode) || (configuration.alternateUpdatePaymentMethodNavigation && !paymentMethods.filter { $0.type == .card }.isEmpty) + // Or, under the new navigation flow, if any of the payment methods are cards, US bank accounts, or SEPA debit + return ((canRemovePaymentMethods || (hasCoBrandedCards && isCBCEligible)) && !isRemoveOnlyMode) || (configuration.alternateUpdatePaymentMethodNavigation && paymentMethods.contains { UpdatePaymentMethodViewModel.supportedPaymentMethods.contains($0.type) }) } private var selectedPaymentMethod: STPPaymentMethod? { @@ -342,28 +343,30 @@ extension VerticalSavedPaymentMethodsViewController: SavedPaymentMethodRowButton } func didSelectUpdateButton(_ button: SavedPaymentMethodRowButton, with paymentMethod: STPPaymentMethod) { - let updateViewController = UpdateCardViewController(paymentMethod: paymentMethod, + let updateViewModel = UpdatePaymentMethodViewModel(paymentMethod: paymentMethod, + appearance: configuration.appearance, + hostedSurface: .paymentSheet, + cardBrandFilter: configuration.cardBrandFilter, + canEdit: paymentMethod.isCoBrandedCard && isCBCEligible, + canRemove: canRemovePaymentMethods) + let updateViewController = UpdatePaymentMethodViewController( removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, - appearance: configuration.appearance, - hostedSurface: .paymentSheet, - canRemoveCard: canRemovePaymentMethods, isTestMode: configuration.apiClient.isTestmode, - cardBrandFilter: configuration.cardBrandFilter) - + viewModel: updateViewModel) updateViewController.delegate = self self.updateViewController = updateViewController self.bottomSheetController?.pushContentViewController(updateViewController) } } -// MARK: - UpdateCardViewControllerDelegate -extension VerticalSavedPaymentMethodsViewController: UpdateCardViewControllerDelegate { - func didRemove(viewController: UpdateCardViewController, paymentMethod: STPPaymentMethod) { +// MARK: - UpdatePaymentMethodViewControllerDelegate +extension VerticalSavedPaymentMethodsViewController: UpdatePaymentMethodViewControllerDelegate { + func didRemove(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod) { remove(paymentMethod: paymentMethod) _ = viewController.bottomSheetController?.popContentViewController() } - func didUpdate(viewController: UpdateCardViewController, paymentMethod: STPPaymentMethod, updateParams: STPPaymentMethodUpdateParams) async throws { + func didUpdate(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod, updateParams: STPPaymentMethodUpdateParams) async throws { // Update the payment method let updatedPaymentMethod = try await savedPaymentMethodManager.update(paymentMethod: paymentMethod, with: updateParams) @@ -371,7 +374,7 @@ extension VerticalSavedPaymentMethodsViewController: UpdateCardViewControllerDel _ = viewController.bottomSheetController?.popContentViewController() } - func didDismiss(viewController: UpdateCardViewController) { + func didDismiss(_: UpdatePaymentMethodViewController) { // No-op } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift index 795e1ee34bf..f3888aba9cd 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift @@ -573,13 +573,16 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo let paymentMethod = savedPaymentMethods.first, paymentMethod.isCoBrandedCard, elementsSession.isCardBrandChoiceEligible || configuration.alternateUpdatePaymentMethodNavigation { - let updateViewController = UpdateCardViewController(paymentMethod: paymentMethod, + let updateViewModel = UpdatePaymentMethodViewModel(paymentMethod: paymentMethod, + appearance: configuration.appearance, + hostedSurface: .paymentSheet, + cardBrandFilter: configuration.cardBrandFilter, + canEdit: paymentMethod.isCoBrandedCard && elementsSession.isCardBrandChoiceEligible, + canRemove: configuration.allowsRemovalOfLastSavedPaymentMethod && elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet()) + let updateViewController = UpdatePaymentMethodViewController( removeSavedPaymentMethodMessage: configuration.removeSavedPaymentMethodMessage, - appearance: configuration.appearance, - hostedSurface: .paymentSheet, - canRemoveCard: configuration.allowsRemovalOfLastSavedPaymentMethod && elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet(), isTestMode: configuration.apiClient.isTestmode, - cardBrandFilter: configuration.cardBrandFilter) + viewModel: updateViewModel) updateViewController.delegate = self bottomSheetController?.pushContentViewController(updateViewController) return @@ -791,9 +794,9 @@ extension PaymentSheetVerticalViewController: SheetNavigationBarDelegate { } } -// MARK: UpdateCardViewControllerDelegate -extension PaymentSheetVerticalViewController: UpdateCardViewControllerDelegate { - func didRemove(viewController: UpdateCardViewController, paymentMethod: STPPaymentMethod) { +// MARK: UpdatePaymentMethodViewControllerDelegate +extension PaymentSheetVerticalViewController: UpdatePaymentMethodViewControllerDelegate { + func didRemove(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod) { // Detach the payment method from the customer savedPaymentMethodManager.detach(paymentMethod: paymentMethod) analyticsHelper.logSavedPaymentMethodRemoved(paymentMethod: paymentMethod) @@ -806,7 +809,7 @@ extension PaymentSheetVerticalViewController: UpdateCardViewControllerDelegate { _ = viewController.bottomSheetController?.popContentViewController() } - func didUpdate(viewController: UpdateCardViewController, paymentMethod: STPPaymentMethod, updateParams: STPPaymentMethodUpdateParams) async throws { + func didUpdate(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod, updateParams: STPPaymentMethodUpdateParams) async throws { // Update the payment method let updatedPaymentMethod = try await savedPaymentMethodManager.update(paymentMethod: paymentMethod, with: updateParams) @@ -820,7 +823,7 @@ extension PaymentSheetVerticalViewController: UpdateCardViewControllerDelegate { _ = viewController.bottomSheetController?.popContentViewController() } - func didDismiss(viewController: UpdateCardViewController) { + func didDismiss(_: UpdatePaymentMethodViewController) { // No-op } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/SavedPaymentMethodFormFactory.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/SavedPaymentMethodFormFactory.swift new file mode 100644 index 00000000000..29242c79756 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/SavedPaymentMethodFormFactory.swift @@ -0,0 +1,148 @@ +// +// SavedPaymentMethodFormFactory.swift +// StripePaymentSheet +// +// Created by Joyce Qin on 11/20/24. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +protocol SavedPaymentMethodFormFactoryDelegate: AnyObject { + func didUpdate(_: Element, shouldEnableSaveButton: Bool) +} + +class SavedPaymentMethodFormFactory { + let viewModel: UpdatePaymentMethodViewModel + weak var delegate: SavedPaymentMethodFormFactoryDelegate? + + init(viewModel: UpdatePaymentMethodViewModel) { + self.viewModel = viewModel + } + + func makePaymentMethodForm() -> UIView { + switch viewModel.paymentMethod.type { + case .card: + return cardSection.view + case .USBankAccount: + return usBankAccountSection + case .SEPADebit: + return sepaDebitSection + default: + fatalError("Cannot make payment method form for payment method type \(viewModel.paymentMethod.type).") + } + } + + private lazy var cardBrandDropDown: DropdownFieldElement? = { + guard viewModel.paymentMethod.isCoBrandedCard else { return nil } + let cardBrands = viewModel.paymentMethod.card?.networks?.available.map({ STPCard.brand(from: $0) }).filter { viewModel.cardBrandFilter.isAccepted(cardBrand: $0) } ?? [] + let cardBrandDropDown = DropdownFieldElement.makeCardBrandDropdown(cardBrands: Set(cardBrands), + theme: viewModel.appearance.asElementsTheme, + includePlaceholder: false) { [self] in + let selectedCardBrand = viewModel.selectedCardBrand ?? .unknown + let params = ["selected_card_brand": STPCardBrandUtilities.apiValue(from: selectedCardBrand), "cbc_event_source": "edit"] + STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: viewModel.hostedSurface.analyticEvent(for: .openCardBrandDropdown), + params: params) + } didTapClose: { [self] in + let selectedCardBrand = viewModel.selectedCardBrand ?? .unknown + STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: viewModel.hostedSurface.analyticEvent(for: .closeCardBrandDropDown), + params: ["selected_card_brand": STPCardBrandUtilities.apiValue(from: selectedCardBrand)]) + } + + // pre-select current card brand + if let currentCardBrand = viewModel.paymentMethod.card?.preferredDisplayBrand, + let indexToSelect = cardBrandDropDown.items.firstIndex(where: { $0.rawData == STPCardBrandUtilities.apiValue(from: currentCardBrand) }) { + cardBrandDropDown.select(index: indexToSelect, shouldAutoAdvance: false) + viewModel.selectedCardBrand = currentCardBrand + } + return cardBrandDropDown + }() + + private lazy var panElement: TextFieldElement = { + return TextFieldElement.LastFourConfiguration(lastFour: viewModel.paymentMethod.card?.last4 ?? "", cardBrandDropDown: cardBrandDropDown).makeElement(theme: viewModel.appearance.asElementsTheme) + }() + + private lazy var expiryDateElement: TextFieldElement = { + let expiryDate = CardExpiryDate(month: viewModel.paymentMethod.card?.expMonth ?? 0, year: viewModel.paymentMethod.card?.expYear ?? 0) + let expiryDateElement = TextFieldElement.ExpiryDateConfiguration(defaultValue: expiryDate.displayString, isEditable: false).makeElement(theme: viewModel.appearance.asElementsTheme) + return expiryDateElement + }() + + private lazy var cvcElement: TextFieldElement = { + let cvcConfiguration = TextFieldElement.CensoredCVCConfiguration(brand: self.viewModel.paymentMethod.card?.preferredDisplayBrand ?? .unknown) + let cvcElement = cvcConfiguration.makeElement(theme: viewModel.appearance.asElementsTheme) + return cvcElement + }() + + private lazy var cardSection: SectionElement = { + let allSubElements: [Element?] = [ + panElement, + SectionElement.HiddenElement(cardBrandDropDown), + SectionElement.MultiElementRow([expiryDateElement, cvcElement]) + ] + let section = SectionElement(elements: allSubElements.compactMap { $0 }, theme: viewModel.appearance.asElementsTheme) + section.delegate = self + viewModel.errorState = !expiryDateElement.validationState.isValid + return section + }() + + private lazy var usBankAccountSection: UIStackView = { + let nameElement: SectionElement = { + return SectionElement(elements: [TextFieldElement.NameConfiguration(defaultValue: viewModel.paymentMethod.billingDetails?.name, isEditable: false).makeElement(theme: viewModel.appearance.asElementsTheme)]) + }() + let emailElement: SectionElement = { + return SectionElement(elements: [TextFieldElement.EmailConfiguration(defaultValue: viewModel.paymentMethod.billingDetails?.email, isEditable: false).makeElement(theme: viewModel.appearance.asElementsTheme)]) + }() + let bankAccountElement: SectionElement = { + return SectionElement(elements: [TextFieldElement.USBankNumberConfiguration(bankName: viewModel.paymentMethod.usBankAccount?.bankName ?? "Bank name", lastFour: viewModel.paymentMethod.usBankAccount?.last4 ?? "").makeElement(theme: viewModel.appearance.asElementsTheme)]) + }() + let stackView = UIStackView(arrangedSubviews: [nameElement.view, emailElement.view, bankAccountElement.view]) + stackView.isLayoutMarginsRelativeArrangement = true + stackView.axis = .vertical + stackView.setCustomSpacing(8, after: nameElement.view) // custom spacing from figma + stackView.setCustomSpacing(8, after: emailElement.view) // custom spacing from figma + return stackView + }() + + private lazy var sepaDebitSection: UIStackView = { + let nameElement: SectionElement = { + return SectionElement(elements: [TextFieldElement.NameConfiguration(defaultValue: viewModel.paymentMethod.billingDetails?.name, isEditable: false).makeElement(theme: viewModel.appearance.asElementsTheme)]) + }() + let emailElement: SectionElement = { + return SectionElement(elements: [TextFieldElement.EmailConfiguration(defaultValue: viewModel.paymentMethod.billingDetails?.email, isEditable: false).makeElement(theme: viewModel.appearance.asElementsTheme)]) + }() + let ibanElement: SectionElement = { + return SectionElement(elements: [TextFieldElement.LastFourIBANConfiguration(lastFour: viewModel.paymentMethod.sepaDebit?.last4 ?? "0000").makeElement(theme: viewModel.appearance.asElementsTheme)]) + }() + let stackView = UIStackView(arrangedSubviews: [nameElement.view, emailElement.view, ibanElement.view]) + stackView.isLayoutMarginsRelativeArrangement = true + stackView.axis = .vertical + stackView.setCustomSpacing(8, after: nameElement.view) // custom spacing from figma + stackView.setCustomSpacing(8, after: emailElement.view) // custom spacing from figma + return stackView + }() +} + +// MARK: ElementDelegate +extension SavedPaymentMethodFormFactory: ElementDelegate { + func continueToNextField(element: Element) { + // no-op + } + + func didUpdate(element: Element) { + switch viewModel.paymentMethod.type { + case .card: + let selectedBrand = cardBrandDropDown?.selectedItem.rawData.toCardBrand + let currentCardBrand = viewModel.paymentMethod.card?.preferredDisplayBrand ?? .unknown + let shouldBeEnabled = selectedBrand != currentCardBrand && selectedBrand != .unknown + viewModel.selectedCardBrand = selectedBrand + delegate?.didUpdate(_: element, shouldEnableSaveButton: shouldBeEnabled) + default: + break + } + } +} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdateCardViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdateCardViewController.swift deleted file mode 100644 index 6d33e3a33fa..00000000000 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdateCardViewController.swift +++ /dev/null @@ -1,299 +0,0 @@ -// -// UpdateCardViewController.swift -// StripePaymentSheet -// -// Created by Nick Porter on 10/27/23. -// -import Foundation -@_spi(STP) import StripeCore -@_spi(STP) import StripePayments -@_spi(STP) import StripePaymentsUI -@_spi(STP) import StripeUICore -import UIKit - -@MainActor -protocol UpdateCardViewControllerDelegate: AnyObject { - func didRemove(viewController: UpdateCardViewController, paymentMethod: STPPaymentMethod) - func didUpdate(viewController: UpdateCardViewController, - paymentMethod: STPPaymentMethod, - updateParams: STPPaymentMethodUpdateParams) async throws - func didDismiss(viewController: UpdateCardViewController) -} - -/// For internal SDK use only -@objc(STP_Internal_UpdateCardViewController) -final class UpdateCardViewController: UIViewController { - private let appearance: PaymentSheet.Appearance - private let paymentMethod: STPPaymentMethod - private let removeSavedPaymentMethodMessage: String? - private let isTestMode: Bool - private let hostedSurface: HostedSurface - private let canRemoveCard: Bool - private let cardBrandFilter: CardBrandFilter - - private var latestError: Error? { - didSet { - errorLabel.text = latestError?.localizedDescription - errorLabel.isHidden = latestError == nil - } - } - - weak var delegate: UpdateCardViewControllerDelegate? - - // MARK: Navigation bar - internal lazy var navigationBar: SheetNavigationBar = { - let navBar = SheetNavigationBar(isTestMode: isTestMode, - appearance: appearance) - navBar.delegate = self - navBar.setStyle(navigationBarStyle()) - return navBar - }() - - // MARK: Views - lazy var formStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [headerLabel, cardSection.view, 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 - return stackView - }() - - private lazy var headerLabel: UILabel = { - let label = PaymentSheetUI.makeHeaderLabel(appearance: appearance) - label.text = .Localized.manage_card - return label - }() - - private lazy var updateButton: ConfirmButton = { - return ConfirmButton(state: .disabled, callToAction: .custom(title: .Localized.save), appearance: appearance, didTap: { [weak self] in - Task { - await self?.updateCard() - } - }) - }() - - 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.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 - button.addTarget(self, action: #selector(removeCard), for: .touchUpInside) - button.isHidden = !canRemoveCard - return button - }() - - private lazy var errorLabel: UILabel = { - let label = ElementsUI.makeErrorLabel(theme: appearance.asElementsTheme) - label.isHidden = true - return label - }() - - // MARK: Elements - private lazy var panElement: TextFieldElement = { - return TextFieldElement.LastFourConfiguration(lastFour: paymentMethod.card?.last4 ?? "", cardBrandDropDown: cardBrandDropDown).makeElement(theme: appearance.asElementsTheme) - }() - - private lazy var cardBrandDropDown: DropdownFieldElement = { - let cardBrands = paymentMethod.card?.networks?.available.map({ STPCard.brand(from: $0) }).filter { cardBrandFilter.isAccepted(cardBrand: $0) } ?? [] - let cardBrandDropDown = DropdownFieldElement.makeCardBrandDropdown(cardBrands: Set(cardBrands), - theme: appearance.asElementsTheme, - includePlaceholder: false) { [weak self] in - guard let self = self else { return } - let selectedCardBrand = self.cardBrandDropDown.selectedItem.rawData.toCardBrand ?? .unknown - let params = ["selected_card_brand": STPCardBrandUtilities.apiValue(from: selectedCardBrand), "cbc_event_source": "edit"] - STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: self.hostedSurface.analyticEvent(for: .openCardBrandDropdown), - params: params) - } didTapClose: { [weak self] in - guard let self = self else { return } - let selectedCardBrand = self.cardBrandDropDown.selectedItem.rawData.toCardBrand ?? .unknown - STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: self.hostedSurface.analyticEvent(for: .closeCardBrandDropDown), - params: ["selected_card_brand": STPCardBrandUtilities.apiValue(from: selectedCardBrand)]) - } - - // pre-select current card brand - if let currentCardBrand = paymentMethod.card?.preferredDisplayBrand, - let indexToSelect = cardBrandDropDown.items.firstIndex(where: { $0.rawData == STPCardBrandUtilities.apiValue(from: currentCardBrand) }) { - cardBrandDropDown.select(index: indexToSelect, shouldAutoAdvance: false) - } - - 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 cvcConfiguration = TextFieldElement.CensoredCVCConfiguration(brand: self.paymentMethod.card?.preferredDisplayBrand ?? .unknown) - let cvcElement = cvcConfiguration.makeElement(theme: appearance.asElementsTheme) - return cvcElement - - }() - - private lazy var cardSection: SectionElement = { - let allSubElements: [Element?] = [ - panElement, - SectionElement.HiddenElement(cardBrandDropDown), - SectionElement.MultiElementRow([expiryDateElement, cvcElement]) - ] - let section = SectionElement(elements: allSubElements.compactMap { $0 }, theme: appearance.asElementsTheme) - section.delegate = self - return section - }() - - // MARK: Overrides - init(paymentMethod: STPPaymentMethod, - removeSavedPaymentMethodMessage: String?, - appearance: PaymentSheet.Appearance, - hostedSurface: HostedSurface, - canRemoveCard: Bool, - isTestMode: Bool, - cardBrandFilter: CardBrandFilter = .default) { - self.paymentMethod = paymentMethod - self.removeSavedPaymentMethodMessage = removeSavedPaymentMethodMessage - self.appearance = appearance - self.hostedSurface = hostedSurface - self.isTestMode = isTestMode - self.canRemoveCard = canRemoveCard - self.cardBrandFilter = cardBrandFilter - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - // disable swipe to dismiss - isModalInPresentation = true - self.view.backgroundColor = appearance.colors.background - view.addAndPinSubview(formStackView, insets: PaymentSheetUI.defaultSheetMargins) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: hostedSurface.analyticEvent(for: .openCardBrandEditScreen)) - } - - // MARK: Private helpers - private func dismiss() { - guard let bottomVc = parent as? BottomSheetViewController else { return } - STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: hostedSurface.analyticEvent(for: .closeEditScreen)) - _ = bottomVc.popContentViewController() - delegate?.didDismiss(viewController: self) - } - - private func navigationBarStyle() -> SheetNavigationBar.Style { - if let bottomSheet = self.bottomSheetController, - bottomSheet.contentStack.count > 1 { - return .back(showAdditionalButton: false) - } else { - return .close(showAdditionalButton: false) - } - } - - @objc private func removeCard() { - let alertController = UIAlertController.makeRemoveAlertController(paymentMethod: paymentMethod, - removeSavedPaymentMethodMessage: removeSavedPaymentMethodMessage) { [weak self] in - guard let self = self else { return } - self.delegate?.didRemove(viewController: self, paymentMethod: self.paymentMethod) - } - - present(alertController, animated: true, completion: nil) - } - - private func updateCard() async { - guard let selectedBrand = cardBrandDropDown.selectedItem.rawData.toCardBrand, let delegate = delegate else { return } - - view.isUserInteractionEnabled = false - updateButton.update(state: .spinnerWithInteractionDisabled) - - // Create the update card params - let cardParams = STPPaymentMethodCardParams() - cardParams.networks = .init(preferred: STPCardBrandUtilities.apiValue(from: selectedBrand)) - let updateParams = STPPaymentMethodUpdateParams(card: cardParams, billingDetails: nil) - - // Make the API request to update the payment method - do { - try await delegate.didUpdate(viewController: self, paymentMethod: paymentMethod, updateParams: updateParams) - STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: hostedSurface.analyticEvent(for: .updateCardBrand), - params: ["selected_card_brand": STPCardBrandUtilities.apiValue(from: selectedBrand)]) - } catch { - updateButton.update(state: .enabled) - latestError = error - STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: hostedSurface.analyticEvent(for: .updateCardBrandFailed), - error: error, - params: ["selected_card_brand": STPCardBrandUtilities.apiValue(from: selectedBrand)]) - } - view.isUserInteractionEnabled = true - } - -} - -// MARK: BottomSheetContentViewController -extension UpdateCardViewController: BottomSheetContentViewController { - - var allowsDragToDismiss: Bool { - return view.isUserInteractionEnabled - } - - func didTapOrSwipeToDismiss() { - guard view.isUserInteractionEnabled else { - return - } - - dismiss() - } - - var requiresFullScreen: Bool { - return false - } -} - -// MARK: SheetNavigationBarDelegate -extension UpdateCardViewController: SheetNavigationBarDelegate { - - func sheetNavigationBarDidClose(_ sheetNavigationBar: SheetNavigationBar) { - dismiss() - } - - func sheetNavigationBarDidBack(_ sheetNavigationBar: SheetNavigationBar) { - dismiss() - } - -} - -// MARK: ElementDelegate -extension UpdateCardViewController: ElementDelegate { - func continueToNextField(element: Element) { - // no-op - } - - func didUpdate(element: Element) { - latestError = nil // clear error on new input - let selectedBrand = cardBrandDropDown.selectedItem.rawData.toCardBrand - let currentCardBrand = paymentMethod.card?.preferredDisplayBrand ?? .unknown - let shouldBeEnabled = selectedBrand != currentCardBrand && selectedBrand != .unknown - updateButton.update(state: shouldBeEnabled ? .enabled : .disabled) - } -} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdatePaymentMethodViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdatePaymentMethodViewController.swift new file mode 100644 index 00000000000..378e50a3d11 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdatePaymentMethodViewController.swift @@ -0,0 +1,253 @@ +// +// UpdatePaymentMethodViewController.swift +// StripePaymentSheet +// +// Created by Nick Porter on 10/27/23. +// +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +@MainActor +protocol UpdatePaymentMethodViewControllerDelegate: AnyObject { + func didRemove(viewController: UpdatePaymentMethodViewController, paymentMethod: STPPaymentMethod) + func didUpdate(viewController: UpdatePaymentMethodViewController, + paymentMethod: STPPaymentMethod, + updateParams: STPPaymentMethodUpdateParams) async throws + func didDismiss(_: UpdatePaymentMethodViewController) +} + +/// For internal SDK use only +@objc(STP_Internal_UpdatePaymentMethodViewController) +final class UpdatePaymentMethodViewController: UIViewController { + private let removeSavedPaymentMethodMessage: String? + private let isTestMode: Bool + private let viewModel: UpdatePaymentMethodViewModel + + private var latestError: Error? { + didSet { + errorLabel.text = latestError?.localizedDescription + errorLabel.isHidden = latestError == nil + } + } + + weak var delegate: UpdatePaymentMethodViewControllerDelegate? + + // MARK: Navigation bar + internal lazy var navigationBar: SheetNavigationBar = { + let navBar = SheetNavigationBar(isTestMode: isTestMode, + appearance: viewModel.appearance) + navBar.delegate = self + navBar.setStyle(navigationBarStyle()) + return navBar + }() + + // MARK: Views + lazy var formStackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [headerLabel, paymentMethodForm, updateButton, removeButton, errorLabel]) + stackView.isLayoutMarginsRelativeArrangement = true + stackView.axis = .vertical + stackView.setCustomSpacing(16, after: headerLabel) // custom spacing from figma + if let footnoteLabel = footnoteLabel { + stackView.insertArrangedSubview(footnoteLabel, at: 2) + stackView.setCustomSpacing(8, after: paymentMethodForm) // custom spacing from figma + stackView.setCustomSpacing(32, after: footnoteLabel) // custom spacing from figma + } + else { + stackView.setCustomSpacing(32, after: paymentMethodForm) // custom spacing from figma + } + stackView.setCustomSpacing(16, after: updateButton) // custom spacing from figma + return stackView + }() + + private lazy var headerLabel: UILabel = { + let label = PaymentSheetUI.makeHeaderLabel(appearance: viewModel.appearance) + label.text = viewModel.header + return label + }() + + private lazy var updateButton: ConfirmButton = { + let button = ConfirmButton(state: .disabled, callToAction: .custom(title: .Localized.save), appearance: viewModel.appearance, didTap: { [weak self] in + switch self?.viewModel.paymentMethod.type { + case .card: + Task { + await self?.updateCard() + } + default: + fatalError("Updating payment method has not been implemented for \(self?.viewModel.paymentMethod.type ?? .unknown)") + } + }) + button.isHidden = !viewModel.canEdit + return button + }() + + private lazy var removeButton: 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(viewModel.appearance.colors.danger, for: .normal) + button.layer.borderColor = viewModel.appearance.colors.danger.cgColor + button.layer.borderWidth = viewModel.appearance.primaryButton.borderWidth + button.layer.cornerRadius = viewModel.appearance.cornerRadius + button.setTitle(.Localized.remove, for: .normal) + button.titleLabel?.textAlignment = .center + button.titleLabel?.font = viewModel.appearance.scaledFont(for: viewModel.appearance.font.base.medium, style: .callout, maximumPointSize: 25) + button.titleLabel?.adjustsFontForContentSizeCategory = true + button.addTarget(self, action: #selector(removePaymentMethod), for: .touchUpInside) + button.isHidden = !viewModel.canRemove + return button + }() + + private lazy var paymentMethodForm: UIView = { + let form = SavedPaymentMethodFormFactory(viewModel: viewModel) + form.delegate = self + return form.makePaymentMethodForm() + }() + + private lazy var footnoteLabel: UITextView? = { + if viewModel.canEdit || viewModel.errorState { + return nil + } + let label = ElementsUI.makeSmallFootnote(theme: viewModel.appearance.asElementsTheme) + label.text = viewModel.footnote + return label + }() + + private lazy var errorLabel: UILabel = { + let label = ElementsUI.makeErrorLabel(theme: viewModel.appearance.asElementsTheme) + label.isHidden = true + return label + }() + + // MARK: Overrides + init(removeSavedPaymentMethodMessage: String?, + isTestMode: Bool, + viewModel: UpdatePaymentMethodViewModel) { + self.removeSavedPaymentMethodMessage = removeSavedPaymentMethodMessage + self.isTestMode = isTestMode + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + // disable swipe to dismiss + isModalInPresentation = true + self.view.backgroundColor = viewModel.appearance.colors.background + view.addAndPinSubview(formStackView, insets: PaymentSheetUI.defaultSheetMargins) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: viewModel.hostedSurface.analyticEvent(for: .openCardBrandEditScreen)) + } + + // MARK: Private helpers + private func dismiss() { + guard let bottomVc = parent as? BottomSheetViewController else { return } + STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: viewModel.hostedSurface.analyticEvent(for: .closeEditScreen)) + _ = bottomVc.popContentViewController() + delegate?.didDismiss(_: self) + } + + private func navigationBarStyle() -> SheetNavigationBar.Style { + if let bottomSheet = self.bottomSheetController, + bottomSheet.contentStack.count > 1 { + return .back(showAdditionalButton: false) + } else { + return .close(showAdditionalButton: false) + } + } + + @objc private func removePaymentMethod() { + let alertController = UIAlertController.makeRemoveAlertController(paymentMethod: viewModel.paymentMethod, + removeSavedPaymentMethodMessage: removeSavedPaymentMethodMessage) { [weak self] in + guard let self = self else { return } + self.delegate?.didRemove(viewController: self, paymentMethod: self.viewModel.paymentMethod) + } + + present(alertController, animated: true, completion: nil) + } + + private func updateCard() async { + guard let selectedBrand = viewModel.selectedCardBrand, let delegate = delegate else { return } + + view.isUserInteractionEnabled = false + updateButton.update(state: .spinnerWithInteractionDisabled) + + // Create the update card params + let cardParams = STPPaymentMethodCardParams() + cardParams.networks = .init(preferred: STPCardBrandUtilities.apiValue(from: selectedBrand)) + let updateParams = STPPaymentMethodUpdateParams(card: cardParams, billingDetails: nil) + + // Make the API request to update the payment method + do { + try await delegate.didUpdate(viewController: self, paymentMethod: viewModel.paymentMethod, updateParams: updateParams) + STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: viewModel.hostedSurface.analyticEvent(for: .updateCardBrand), + params: ["selected_card_brand": STPCardBrandUtilities.apiValue(from: selectedBrand)]) + } catch { + updateButton.update(state: .enabled) + latestError = error + STPAnalyticsClient.sharedClient.logPaymentSheetEvent(event: viewModel.hostedSurface.analyticEvent(for: .updateCardBrandFailed), + error: error, + params: ["selected_card_brand": STPCardBrandUtilities.apiValue(from: selectedBrand)]) + } + view.isUserInteractionEnabled = true + } + +} + +// MARK: BottomSheetContentViewController +extension UpdatePaymentMethodViewController: BottomSheetContentViewController { + + func didTapOrSwipeToDismiss() { + guard view.isUserInteractionEnabled else { + return + } + + dismiss() + } + + var requiresFullScreen: Bool { + return false + } +} + +// MARK: SheetNavigationBarDelegate +extension UpdatePaymentMethodViewController: SheetNavigationBarDelegate { + + func sheetNavigationBarDidClose(_: SheetNavigationBar) { + dismiss() + } + + func sheetNavigationBarDidBack(_: SheetNavigationBar) { + dismiss() + } + +} + +// MARK: SavedPaymentMethodFormFactoryDelegate +extension UpdatePaymentMethodViewController: SavedPaymentMethodFormFactoryDelegate { + func didUpdate(_: Element, shouldEnableSaveButton: Bool) { + latestError = nil // clear error on new input + switch viewModel.paymentMethod.type { + case .card: + updateButton.update(state: shouldEnableSaveButton ? .enabled : .disabled) + default: + break + } + } +} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdatePaymentMethodViewModel.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdatePaymentMethodViewModel.swift new file mode 100644 index 00000000000..4c0566fdd48 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/UpdatePaymentMethodViewModel.swift @@ -0,0 +1,65 @@ +// +// UpdatePaymentMethodViewModel.swift +// StripePaymentSheet +// +// Created by Joyce Qin on 11/15/24. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +class UpdatePaymentMethodViewModel { + static let supportedPaymentMethods: [STPPaymentMethodType] = [.card, .USBankAccount, .SEPADebit] + + let paymentMethod: STPPaymentMethod + let appearance: PaymentSheet.Appearance + let hostedSurface: HostedSurface + let cardBrandFilter: CardBrandFilter + let canEdit: Bool + let canRemove: Bool + + var selectedCardBrand: STPCardBrand? + var errorState: Bool = false + + lazy var header: String = { + switch paymentMethod.type { + case .card: + return .Localized.manage_card + case .USBankAccount: + return .Localized.manage_us_bank_account + case .SEPADebit: + return .Localized.manage_sepa_debit + default: + fatalError("Updating payment method has not been implemented for \(paymentMethod.type)") + } + }() + + lazy var footnote: String = { + switch paymentMethod.type { + case .card: + return .Localized.card_details_cannot_be_changed + case .USBankAccount: + return .Localized.bank_account_details_cannot_be_changed + case .SEPADebit: + return .Localized.sepa_debit_details_cannot_be_changed + default: + fatalError("Updating payment method has not been implemented for \(paymentMethod.type)") + } + }() + + init(paymentMethod: STPPaymentMethod, appearance: PaymentSheet.Appearance, hostedSurface: HostedSurface, cardBrandFilter: CardBrandFilter = .default, canEdit: Bool, canRemove: Bool) { + guard UpdatePaymentMethodViewModel.supportedPaymentMethods.contains(paymentMethod.type) else { + fatalError("Unsupported payment type \(paymentMethod.type) in UpdatePaymentMethodViewModel") + } + self.paymentMethod = paymentMethod + self.appearance = appearance + self.hostedSurface = hostedSurface + self.cardBrandFilter = cardBrandFilter + self.canEdit = canEdit + self.canRemove = canRemove + } +} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CircularButton.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CircularButton.swift index 266db59219d..3c89305333f 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CircularButton.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Views/CircularButton.swift @@ -103,7 +103,7 @@ class CircularButton: UIControl { accessibilityIdentifier = "CircularButton.Remove" case .edit: imageView.image = Image.icon_edit.makeImage(template: true) - accessibilityLabel = String.Localized.update_card_brand + accessibilityLabel = String.Localized.update_payment_method accessibilityIdentifier = "CircularButton.Edit" } } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift index c7167d4d47b..43ed581fbc5 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPFixtures+PaymentSheet.swift @@ -160,6 +160,9 @@ extension STPPaymentMethod { "last4": "4242", "brand": "visa", "fingerprint": "B8XXs2y2JsVBtB9f", + "networks": ["available": ["visa"]], + "exp_month": "01", + "exp_year": Calendar.current.component(.year, from: Date()) + 1 ], ])! } @@ -183,6 +186,8 @@ extension STPPaymentMethod { "last4": "4242", "brand": brand, "networks": ["available": networks], + "exp_month": "01", + "exp_year": Calendar.current.component(.year, from: Date()) + 1 ], ] if let displayBrand { @@ -209,6 +214,10 @@ extension STPPaymentMethod { ] as [String: Any], "routing_number": "110000000", ] as [String: Any], + "billing_details": [ + "name": "Sam Stripe", + "email": "sam@stripe.com", + ] as [String: Any], ])! } @@ -219,6 +228,10 @@ extension STPPaymentMethod { "sepa_debit": [ "last4": "1234", ], + "billing_details": [ + "name": "Sam Stripe", + "email": "sam@stripe.com", + ] as [String: Any], ])! } } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdateCardViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdateCardViewControllerSnapshotTests.swift deleted file mode 100644 index 1a565270a2f..00000000000 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdateCardViewControllerSnapshotTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// UpdateCardViewControllerSnapshotTests.swift -// StripePaymentSheetTests -// -// Created by Nick Porter on 11/27/23. -// - -import StripeCoreTestUtils -@_spi(STP) @testable import StripePaymentSheet -@testable import StripePaymentsTestUtils -import XCTest - -final class UpdateCardViewControllerSnapshotTests: STPSnapshotTestCase { - - func test_UpdateCardViewControllerDarkMode() { - _test_UpdateCardViewController(darkMode: true) - } - - func test_UpdateCardViewControllerLightMode() { - _test_UpdateCardViewController(darkMode: false) - } - - func test_UpdateCardViewControllerAppearance() { - _test_UpdateCardViewController(darkMode: false, appearance: ._testMSPaintTheme) - } - - func test_EmbeddedSingleCard_UpdateCardViewControllerDarkMode() { - _test_UpdateCardViewController(darkMode: true, isEmbeddedSingleCard: true) - } - - func test_EmbeddedSingleCard_UpdateCardViewControllerLightMode() { - _test_UpdateCardViewController(darkMode: false, isEmbeddedSingleCard: true) - } - - func test_EmbeddedSingleCard_UpdateCardViewControllerAppearance() { - _test_UpdateCardViewController(darkMode: false, isEmbeddedSingleCard: true, appearance: ._testMSPaintTheme) - } - - func _test_UpdateCardViewController(darkMode: Bool, isEmbeddedSingleCard: Bool = false, appearance: PaymentSheet.Appearance = .default) { - let sut = UpdateCardViewController(paymentMethod: STPFixtures.paymentMethod(), - removeSavedPaymentMethodMessage: "Test removal string", - appearance: appearance, - hostedSurface: .paymentSheet, - canRemoveCard: true, - isTestMode: false) - let bottomSheet: BottomSheetViewController - if isEmbeddedSingleCard { - bottomSheet = BottomSheetViewController(contentViewController: sut, appearance: appearance, isTestMode: true, didCancelNative3DS2: {}) - } else { - let stubViewController = StubBottomSheetContentViewController() - bottomSheet = BottomSheetViewController(contentViewController: stubViewController, appearance: appearance, isTestMode: true, didCancelNative3DS2: {}) - bottomSheet.pushContentViewController(sut) - } - bottomSheet.view.autosizeHeight(width: 375) - - let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 375, height: bottomSheet.view.frame.size.height + sut.view.frame.size.height)) - testWindow.isHidden = false - if darkMode { - testWindow.overrideUserInterfaceStyle = .dark - } - testWindow.rootViewController = bottomSheet - STPSnapshotVerifyView(bottomSheet.view) - } -} - -extension UIView { - /// Constrains the view to the given width and autosizes its height. - /// - Parameter width: Resizes the view to this width - /// - Parameter height: Resizes the view to this height - func autosizeHeight(width: CGFloat, height: CGFloat) { - translatesAutoresizingMaskIntoConstraints = false - widthAnchor.constraint(equalToConstant: width).isActive = true - heightAnchor.constraint(equalToConstant: height).isActive = true - setNeedsLayout() - layoutIfNeeded() - frame = .init( - origin: .zero, - size: systemLayoutSizeFitting(CGSize(width: width, height: height)) - ) - } -} diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdatePaymentMethodViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdatePaymentMethodViewControllerSnapshotTests.swift new file mode 100644 index 00000000000..372d0f654ce --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/UpdatePaymentMethodViewControllerSnapshotTests.swift @@ -0,0 +1,162 @@ +// +// UpdatePaymentMethodViewControllerSnapshotTests.swift +// StripePaymentSheetTests +// +// Created by Nick Porter on 11/27/23. +// + +import StripeCoreTestUtils +@_spi(STP) @testable import StripePaymentSheet +@testable import StripePaymentsTestUtils +import XCTest + +final class UpdatePaymentMethodViewControllerSnapshotTests: STPSnapshotTestCase { + + func test_UpdatePaymentMethodViewControllerDarkMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .card, darkMode: true) + } + + func test_UpdatePaymentMethodViewControllerLightMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .card, darkMode: false) + } + + func test_UpdatePaymentMethodViewControllerAppearance() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .card, darkMode: false, appearance: ._testMSPaintTheme) + } + + func test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerDarkMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .card, darkMode: true, isEmbeddedSingle: true) + } + + func test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerLightMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .card, darkMode: false, isEmbeddedSingle: true) + } + + func test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerAppearance() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .card, darkMode: false, isEmbeddedSingle: true, appearance: ._testMSPaintTheme) + } + + func test_UpdatePaymentMethodViewControllerExpiredCard() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .card, darkMode: false, expired: true) + } + + func test_UpdatePaymentMethodViewControllerRemoveOnlyCard() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .card, darkMode: false, canEdit: false) + } + + func test_UpdatePaymentMethodViewControllerUSBankAccountDarkMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .USBankAccount, darkMode: true) + } + + func test_UpdatePaymentMethodViewControllerUSBankAccountLightMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .USBankAccount, darkMode: false) + } + + func test_UpdatePaymentMethodViewControllerUSBankAccountAppearance() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .USBankAccount, darkMode: false, appearance: ._testMSPaintTheme) + } + + func test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerDarkMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .USBankAccount, darkMode: true, isEmbeddedSingle: true) + } + + func test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerLightMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .USBankAccount, darkMode: false, isEmbeddedSingle: true) + } + + func test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerAppearance() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .USBankAccount, darkMode: false, isEmbeddedSingle: true, appearance: ._testMSPaintTheme) + } + + func test_UpdatePaymentMethodViewControllerSEPADebitDarkMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .SEPADebit, darkMode: true) + } + + func test_UpdatePaymentMethodViewControllerSEPADebitLightMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .SEPADebit, darkMode: false) + } + + func test_UpdatePaymentMethodViewControllerSEPADebitAppearance() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .SEPADebit, darkMode: false, appearance: ._testMSPaintTheme) + } + + func test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerDarkMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .SEPADebit, darkMode: true, isEmbeddedSingle: true) + } + + func test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerLightMode() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .SEPADebit, darkMode: false, isEmbeddedSingle: true) + } + + func test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerAppearance() { + _test_UpdatePaymentMethodViewController(paymentMethodType: .SEPADebit, darkMode: false, isEmbeddedSingle: true, appearance: ._testMSPaintTheme) + } + + func _test_UpdatePaymentMethodViewController(paymentMethodType: STPPaymentMethodType, darkMode: Bool, isEmbeddedSingle: Bool = false, appearance: PaymentSheet.Appearance = .default, canEdit: Bool? = nil, canRemove: Bool = true, expired: Bool = false) { + let paymentMethod: STPPaymentMethod = { + switch paymentMethodType { + case .card: + if expired { + return STPFixtures.paymentMethod() + } + else { + if canEdit == false { + return STPPaymentMethod._testCard() + } + else { + return STPPaymentMethod._testCardCoBranded() + } + } + case .USBankAccount: + return STPPaymentMethod._testUSBankAccount() + case .SEPADebit: + return STPPaymentMethod._testSEPA() + default: + fatalError("Updating payment method has not been implemented for type \(paymentMethodType)") + } + }() + let updateViewModel = UpdatePaymentMethodViewModel(paymentMethod: paymentMethod, + appearance: appearance, + hostedSurface: .paymentSheet, + canEdit: canEdit ?? paymentMethod.isCoBrandedCard, + canRemove: canRemove) + let sut = UpdatePaymentMethodViewController( + removeSavedPaymentMethodMessage: "Test removal string", + isTestMode: false, + viewModel: updateViewModel) + let bottomSheet: BottomSheetViewController + if isEmbeddedSingle { + bottomSheet = BottomSheetViewController(contentViewController: sut, appearance: appearance, isTestMode: true, didCancelNative3DS2: {}) + } else { + let stubViewController = StubBottomSheetContentViewController() + bottomSheet = BottomSheetViewController(contentViewController: stubViewController, appearance: appearance, isTestMode: true, didCancelNative3DS2: {}) + bottomSheet.pushContentViewController(sut) + } + bottomSheet.view.autosizeHeight(width: 375) + + let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 375, height: bottomSheet.view.frame.size.height + sut.view.frame.size.height)) + testWindow.isHidden = false + if darkMode { + testWindow.overrideUserInterfaceStyle = .dark + } + testWindow.rootViewController = bottomSheet + STPSnapshotVerifyView(bottomSheet.view) + } +} + +extension UIView { + /// Constrains the view to the given width and autosizes its height. + /// - Parameter width: Resizes the view to this width + /// - Parameter height: Resizes the view to this height + func autosizeHeight(width: CGFloat, height: CGFloat) { + translatesAutoresizingMaskIntoConstraints = false + widthAnchor.constraint(equalToConstant: width).isActive = true + heightAnchor.constraint(equalToConstant: height).isActive = true + setNeedsLayout() + layoutIfNeeded() + frame = .init( + origin: .zero, + size: systemLayoutSizeFitting(CGSize(width: width, height: height)) + ) + } +} 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/Factories/TextFieldElement+Factory.swift b/StripeUICore/StripeUICore/Source/Elements/Factories/TextFieldElement+Factory.swift index fbdad2ba518..2628dada482 100644 --- a/StripeUICore/StripeUICore/Source/Elements/Factories/TextFieldElement+Factory.swift +++ b/StripeUICore/StripeUICore/Source/Elements/Factories/TextFieldElement+Factory.swift @@ -22,6 +22,7 @@ import UIKit public let defaultValue: String? public let label: String public let isOptional: Bool + public let isEditable: Bool private var textContentType: UITextContentType { switch type { case .given: @@ -34,7 +35,7 @@ import UIKit } /// - Parameter label: If `nil`, defaults to a string on the `type` e.g. "Name" - public init(type: NameType = .full, defaultValue: String?, label: String? = nil, isOptional: Bool = false) { + public init(type: NameType = .full, defaultValue: String?, label: String? = nil, isOptional: Bool = false, isEditable: Bool = true) { self.type = type self.defaultValue = defaultValue if let label = label { @@ -43,6 +44,7 @@ import UIKit self.label = Self.label(for: type) } self.isOptional = isOptional + self.isEditable = isEditable } public func keyboardProperties(for text: String) -> TextFieldElement.KeyboardProperties { @@ -73,14 +75,16 @@ import UIKit public let label = String.Localized.email public let defaultValue: String? public let isOptional: Bool + public let isEditable: Bool public let disallowedCharacters: CharacterSet = .whitespacesAndNewlines let invalidError = Error.invalid( localizedDescription: String.Localized.invalid_email ) - init(defaultValue: String? = nil, isOptional: Bool = false) { + public init(defaultValue: String? = nil, isOptional: Bool = false, isEditable: Bool = true) { self.defaultValue = defaultValue self.isOptional = isOptional + self.isEditable = isEditable } public func validate(text: String, isOptional: Bool) -> ValidationState { @@ -239,4 +243,5 @@ import UIKit } } } + } diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerAppearance@3x.png new file mode 100644 index 00000000000..46493c23a65 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerDarkMode@3x.png new file mode 100644 index 00000000000..2f0f588aaa2 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerLightMode@3x.png new file mode 100644 index 00000000000..017f187cd1b Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleCard_UpdatePaymentMethodViewControllerLightMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerAppearance@3x.png new file mode 100644 index 00000000000..dc41b6a166d Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerDarkMode@3x.png new file mode 100644 index 00000000000..ea8535aadf4 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerLightMode@3x.png new file mode 100644 index 00000000000..4a4d5ceca6e Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleSEPADebit_UpdatePaymentMethodViewControllerLightMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerAppearance@3x.png new file mode 100644 index 00000000000..6ba2b9e4103 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerDarkMode@3x.png new file mode 100644 index 00000000000..13037ceb1ef Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerLightMode@3x.png new file mode 100644 index 00000000000..5168e7f1322 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_EmbeddedSingleUSBankAccount_UpdatePaymentMethodViewControllerLightMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerAppearance@3x.png new file mode 100644 index 00000000000..841b244527c Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerDarkMode@3x.png new file mode 100644 index 00000000000..b846a96fe3c Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerExpiredCard@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerExpiredCard@3x.png new file mode 100644 index 00000000000..eee6ca52f3b Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerExpiredCard@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerLightMode@3x.png new file mode 100644 index 00000000000..b0d226f4c7e Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerLightMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerRemoveOnlyCard@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerRemoveOnlyCard@3x.png new file mode 100644 index 00000000000..b9362fc1ca5 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerRemoveOnlyCard@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitAppearance@3x.png new file mode 100644 index 00000000000..da4c646dd3e Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitDarkMode@3x.png new file mode 100644 index 00000000000..e68fa3f4fc0 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitLightMode@3x.png new file mode 100644 index 00000000000..fecbf29e6c9 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerSEPADebitLightMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountAppearance@3x.png new file mode 100644 index 00000000000..a6420d146c1 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountDarkMode@3x.png new file mode 100644 index 00000000000..d13a65c3f57 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountLightMode@3x.png new file mode 100644 index 00000000000..254d5852161 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.UpdatePaymentMethodViewControllerSnapshotTests/test_UpdatePaymentMethodViewControllerUSBankAccountLightMode@3x.png differ