diff --git a/Example/UI Examples/UI Examples.xcodeproj/project.pbxproj b/Example/UI Examples/UI Examples.xcodeproj/project.pbxproj index fa0d606b1e0..fbf1e31aa70 100644 --- a/Example/UI Examples/UI Examples.xcodeproj/project.pbxproj +++ b/Example/UI Examples/UI Examples.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 55; objects = { /* Begin PBXBuildFile section */ @@ -14,6 +14,7 @@ 3B902170875FA481C136290D /* StripePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D7D4CC026D81D7E26EDA474B /* StripePayments.framework */; }; 3BCB2193E00E9B8766E93AC9 /* Stripe.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 753BA90BA35AA6FBDAA07FBE /* Stripe.framework */; }; 3D7A0D5F4391825F36AA42E7 /* StripeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F16D8E3EDF1E3CCBF5FE6F36 /* StripeCore.framework */; }; + 5216B23B2CAEBB57007F271A /* CardFormScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5216B23A2CAEBB57007F271A /* CardFormScannerViewController.swift */; }; 546290D05670988D27885299 /* StripeUICore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A7973A50BA0AE6BE3911FCE8 /* StripeUICore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 566876172C33FCC7ACB66754 /* MockCustomerContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931AF426917A2801C4205F00 /* MockCustomerContext.swift */; }; 5B4F3271CE029F9AAB2ABC7F /* StripePayments.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D7D4CC026D81D7E26EDA474B /* StripePayments.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -65,6 +66,7 @@ 33045CD268B7E20221AEF345 /* UIExamples.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIExamples.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3E29FB4EC02BA5C29D8A7453 /* Project-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Project-Debug.xcconfig"; sourceTree = ""; }; 460B319B13534E7889F7DADE /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 5216B23A2CAEBB57007F271A /* CardFormScannerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardFormScannerViewController.swift; sourceTree = ""; }; 592779C24B55133469B657A2 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; 5C2F92F71B779415739E02D5 /* SwiftUICardFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUICardFormView.swift; sourceTree = ""; }; 69382A94399A814B97E6BF13 /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = ""; }; @@ -143,6 +145,7 @@ 69382A94399A814B97E6BF13 /* BrowseViewController.swift */, 1294FDC8AE5C19800151F829 /* CardFieldViewController.swift */, D0EE0C28786C46F7AD5678C4 /* CardFormViewController.swift */, + 5216B23A2CAEBB57007F271A /* CardFormScannerViewController.swift */, 931AF426917A2801C4205F00 /* MockCustomerContext.swift */, EFEAD84E710812643F204F1E /* PaymentMethodMessagingViewController.swift */, 5C2F92F71B779415739E02D5 /* SwiftUICardFormView.swift */, @@ -240,8 +243,6 @@ 60F7610314405547AB6416E8 /* Project object */ = { isa = PBXProject; attributes = { - TargetAttributes = { - }; }; buildConfigurationList = 6E231E366F944087F8DD657F /* Build configuration list for PBXProject "UI Examples" */; compatibilityVersion = "Xcode 13.0"; @@ -288,6 +289,7 @@ buildActionMask = 2147483647; files = ( B0542D0E99812D6D2F595CAF /* AUBECSDebitFormViewController.swift in Sources */, + 5216B23B2CAEBB57007F271A /* CardFormScannerViewController.swift in Sources */, B881DC1D937D301C7B35B86C /* AppDelegate.swift in Sources */, A61E99A31C25F9EE54CB3465 /* BrowseViewController.swift in Sources */, 7251058865ACA98C98273875 /* CardFieldViewController.swift in Sources */, diff --git a/Example/UI Examples/UI Examples/Source/BrowseViewController.swift b/Example/UI Examples/UI Examples/Source/BrowseViewController.swift index a9ada5fcbdf..51d1eb4b684 100644 --- a/Example/UI Examples/UI Examples/Source/BrowseViewController.swift +++ b/Example/UI Examples/UI Examples/Source/BrowseViewController.swift @@ -29,38 +29,41 @@ class BrowseViewController: UITableViewController, STPAddCardViewControllerDeleg case STPCardFormViewController case STPCardFormViewControllerCBC case SwiftUICardFormViewController + case STPCardFormScannerViewController case PaymentMethodMessagingView case ChangeTheme var title: String { switch self { - case .STPPaymentCardTextField: return "Card Field" - case .STPPaymentCardTextFieldWithCBC: return "Card Field (CBC)" - case .STPPaymentOptionsViewController: return "Payment Option Picker" - case .STPPaymentOptionsFPXViewController: return "Payment Option Picker (With FPX)" - case .STPShippingInfoViewController: return "Shipping Info Form" - case .STPAUBECSFormViewController: return "AU BECS Form" - case .STPCardFormViewController: return "Card Form" - case .STPCardFormViewControllerCBC: return "Card Form (CBC)" - case .SwiftUICardFormViewController: return "Card Form (SwiftUI)" - case .PaymentMethodMessagingView: return "Payment Method Messaging View" - case .ChangeTheme: return "Change Theme" + case .STPPaymentCardTextField: "Card Field" + case .STPPaymentCardTextFieldWithCBC: "Card Field (CBC)" + case .STPPaymentOptionsViewController: "Payment Option Picker" + case .STPPaymentOptionsFPXViewController: "Payment Option Picker (With FPX)" + case .STPShippingInfoViewController: "Shipping Info Form" + case .STPAUBECSFormViewController: "AU BECS Form" + case .STPCardFormViewController: "Card Form" + case .STPCardFormViewControllerCBC: "Card Form (CBC)" + case .SwiftUICardFormViewController: "Card Form (SwiftUI)" + case .STPCardFormScannerViewController: "Card Form Scanner" + case .PaymentMethodMessagingView: "Payment Method Messaging View" + case .ChangeTheme: "Change Theme" } } var detail: String { switch self { - case .STPPaymentCardTextField: return "STPPaymentCardTextField" - case .STPPaymentCardTextFieldWithCBC: return "STPPaymentCardTextField" - case .STPPaymentOptionsViewController: return "STPPaymentOptionsViewController" - case .STPPaymentOptionsFPXViewController: return "STPPaymentOptionsViewController" - case .STPShippingInfoViewController: return "STPShippingInfoViewController" - case .STPAUBECSFormViewController: return "STPAUBECSFormViewController" - case .STPCardFormViewController: return "STPCardFormViewController" - case .STPCardFormViewControllerCBC: return "STPCardFormViewController (CBC)" - case .SwiftUICardFormViewController: return "STPCardFormView.Representable" - case .PaymentMethodMessagingView: return "PaymentMethodMessagingView" - case .ChangeTheme: return "" + case .STPPaymentCardTextField: "STPPaymentCardTextField" + case .STPPaymentCardTextFieldWithCBC: "STPPaymentCardTextField" + case .STPPaymentOptionsViewController: "STPPaymentOptionsViewController" + case .STPPaymentOptionsFPXViewController: "STPPaymentOptionsViewController" + case .STPShippingInfoViewController: "STPShippingInfoViewController" + case .STPAUBECSFormViewController: "STPAUBECSFormViewController" + case .STPCardFormViewController: "STPCardFormViewController" + case .STPCardFormViewControllerCBC: "STPCardFormViewController (CBC)" + case .SwiftUICardFormViewController: "STPCardFormView.Representable" + case .STPCardFormScannerViewController: "STPCardFormScannerViewController" + case .PaymentMethodMessagingView: "PaymentMethodMessagingView" + case .ChangeTheme: "" } } } @@ -179,17 +182,22 @@ class BrowseViewController: UITableViewController, STPAddCardViewControllerDeleg let navigationController = UINavigationController(rootViewController: viewController) navigationController.navigationBar.stp_theme = theme present(navigationController, animated: true, completion: nil) - case .ChangeTheme: - let navigationController = UINavigationController( - rootViewController: self.themeViewController) - present(navigationController, animated: true, completion: nil) case .SwiftUICardFormViewController: let controller = UIHostingController(rootView: SwiftUICardFormView()) present(controller, animated: true, completion: nil) + case .STPCardFormScannerViewController: + let viewController = CardFormScannerViewController() + let navigationController = UINavigationController(rootViewController: viewController) + navigationController.navigationBar.stp_theme = theme + present(navigationController, animated: true, completion: nil) case .PaymentMethodMessagingView: let vc = PaymentMethodMessagingViewController() let navigationController = UINavigationController(rootViewController: vc) present(navigationController, animated: true, completion: nil) + case .ChangeTheme: + let navigationController = UINavigationController( + rootViewController: self.themeViewController) + present(navigationController, animated: true, completion: nil) } } diff --git a/Example/UI Examples/UI Examples/Source/CardFormScannerViewController.swift b/Example/UI Examples/UI Examples/Source/CardFormScannerViewController.swift new file mode 100644 index 00000000000..65b1acd5d0b --- /dev/null +++ b/Example/UI Examples/UI Examples/Source/CardFormScannerViewController.swift @@ -0,0 +1,59 @@ +// +// CardFormScannerViewController.swift +// UI Examples +// +// Copyright © 2024 Stripe. All rights reserved. +// + +import Stripe +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +class CardFormScannerViewController: UIViewController { + var alwaysEnableCBC: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + title = "Card Form Scanner" + view.backgroundColor = .systemBackground + + let handler: (Error?) -> () = { [weak self] error in + guard let error else { return } + let alertController = UIAlertController( + title: error.localizedDescription, + message: (error as NSError).localizedFailureReason, + preferredStyle: .alert + ) + alertController.addAction( + UIAlertAction( + title: String.Localized.ok, + style: .cancel, + handler: nil + ) + ) + self?.present(alertController, animated: true) + } + + let cardForm = STPCardFormScannerView(style: .standard, handleError: handler) + cardForm.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(cardForm) + + NSLayoutConstraint.activate([ + cardForm.leadingAnchor.constraint(equalToSystemSpacingAfter: view.safeAreaLayoutGuide.leadingAnchor, multiplier: 4), + view.safeAreaLayoutGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: cardForm.trailingAnchor, multiplier: 4), + cardForm.centerXAnchor.constraint(equalTo: view.centerXAnchor), + + cardForm.topAnchor.constraint(greaterThanOrEqualToSystemSpacingBelow: view.safeAreaLayoutGuide.topAnchor, multiplier: 1), + view.safeAreaLayoutGuide.bottomAnchor.constraint(greaterThanOrEqualToSystemSpacingBelow: cardForm.bottomAnchor, multiplier: 1), + cardForm.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + + navigationItem.leftBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, target: self, action: #selector(done)) + } + + @objc func done() { + dismiss(animated: true, completion: nil) + } +} diff --git a/Stripe/Stripe.xcodeproj/project.pbxproj b/Stripe/Stripe.xcodeproj/project.pbxproj index 1e6707b9a69..a70da136a1b 100644 --- a/Stripe/Stripe.xcodeproj/project.pbxproj +++ b/Stripe/Stripe.xcodeproj/project.pbxproj @@ -120,6 +120,7 @@ 5170651536332C4842E9D009 /* STPPaymentMethodBoletoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E371E9B3B2E343FE954531C /* STPPaymentMethodBoletoTests.swift */; }; 51D515315F02D4C03BA12366 /* UserDefaults+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0E131538728BC4802627B1 /* UserDefaults+StripeTest.swift */; }; 5212C7875C07F9BF16AFD98D /* STPAPIClient+PushProvisioning.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807FF966F1DE05F3496B817B /* STPAPIClient+PushProvisioning.swift */; }; + 5216B23D2CB0435B007F271A /* STPCardFormScannerViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5216B23C2CB0435B007F271A /* STPCardFormScannerViewSnapshotTests.swift */; }; 524AE1978E0A4490D1C390C5 /* CustomerAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C866064B3482878A69892F /* CustomerAdapterTests.swift */; }; 5302F9246A4A6381CB4FB874 /* StripePaymentsTestUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 77247622AB08FEF48CA0DC26 /* StripePaymentsTestUtils.framework */; }; 5370700ED1F630E8261507D3 /* STPBankAccountParamsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 967C784618A074FF021B3089 /* STPBankAccountParamsTest.swift */; }; @@ -262,7 +263,6 @@ B8ED1F697519A6FCD3D79431 /* STPPaymentMethodGiropayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DB466066B47C0F716E474 /* STPPaymentMethodGiropayTests.swift */; }; B917BF282C84507292112B9D /* STPCardBINMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E1C5E08678292561255B1C5 /* STPCardBINMetadataTests.swift */; }; B98D71ED9ACC2E1B47372F53 /* NSDecimalNumber+StripeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20552E792B8E7BA15821AB5D /* NSDecimalNumber+StripeTest.swift */; }; - BAFD06E994739E1C38DFFBBC /* STPCardScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69BD038947E8E2376A0D240B /* STPCardScanner.swift */; }; BB46077C256C26418420F240 /* STPAddCardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA793904C7B2D3AA0A4D5EFB /* STPAddCardViewController.swift */; }; BBB734F006FAD749678B87D1 /* STPPaymentMethodRevolutPayParamsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE7BEADD3824A06C2994854 /* STPPaymentMethodRevolutPayParamsTests.swift */; }; BC6912C0DE15008C8D8C303C /* STPFloatingPlaceholderTextFieldSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7385193226663A5B79E69ED /* STPFloatingPlaceholderTextFieldSnapshotTests.swift */; }; @@ -328,7 +328,6 @@ DCF615643A22D0A7B739547C /* Stripe+Exports.swift in Sources */ = {isa = PBXBuildFile; fileRef = B70DF0B659009041F485EE0F /* Stripe+Exports.swift */; }; DD16FC7ABCA7817794ECC407 /* STPThreeDSSelectionCustomizationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C23D612FD5AD7772E1B30DCC /* STPThreeDSSelectionCustomizationTest.swift */; }; DD8E2B99BAE917F83258DC35 /* STPPaymentMethodOptionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E2638F7AA0906914117C2D5 /* STPPaymentMethodOptionsTest.swift */; }; - DDBF5AAE607C698618DDE865 /* STPCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61F8308B7250B642D19827D8 /* STPCameraView.swift */; }; DE23FEF74E860620A334FDF5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 884C01B087B4D820395BD374 /* Main.storyboard */; }; DF73457BF349BC962A6AC502 /* STPCoreScrollViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1525AF65BDEF691F8BCBE8 /* STPCoreScrollViewController.swift */; }; DF85F5EC6E16CAD21491891A /* AnalyticsHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61AF6E95FE0DD913204CAB32 /* AnalyticsHelperTests.swift */; }; @@ -591,6 +590,7 @@ 51408DE266D0345784ADD4FA /* STPThreeDSNavigationBarCustomizationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPThreeDSNavigationBarCustomizationTest.swift; sourceTree = ""; }; 51BD2CE41E4F0CF648F44E4A /* TextFieldElement+IBANTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextFieldElement+IBANTest.swift"; sourceTree = ""; }; 51E62BB62EA9B782778CA880 /* STPStackViewWithSeparatorSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPStackViewWithSeparatorSnapshotTests.swift; sourceTree = ""; }; + 5216B23C2CB0435B007F271A /* STPCardFormScannerViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardFormScannerViewSnapshotTests.swift; sourceTree = ""; }; 52F8AEC50D4623F80F04A533 /* StripeApplePay.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = StripeApplePay.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 533538E3EB92E326CCB95506 /* STPPaymentMethodCardWalletMasterpassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodCardWalletMasterpassTest.swift; sourceTree = ""; }; 53C5AB22D6328E85A6DDF663 /* STPPaymentMethodParams+BasicUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPPaymentMethodParams+BasicUI.swift"; sourceTree = ""; }; @@ -617,7 +617,6 @@ 61E1CA1E2BD6B72800A421AE /* STPPaymentMethodMultibancoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodMultibancoTests.swift; sourceTree = ""; }; 61E1CA202BD6B78500A421AE /* STPPaymentMethodMultibancoParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodMultibancoParamsTests.swift; sourceTree = ""; }; 61E1CA262BD6BED600A421AE /* STPIntentActionMultibancoDisplayDetailsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPIntentActionMultibancoDisplayDetailsTest.swift; sourceTree = ""; }; - 61F8308B7250B642D19827D8 /* STPCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCameraView.swift; sourceTree = ""; }; 6215A9BF343775B1BD0F62AF /* STPPaymentOptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentOptionTableViewCell.swift; sourceTree = ""; }; 6223E57D3A198F956A37ED89 /* STPNumericDigitInputTextFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPNumericDigitInputTextFormatterTests.swift; sourceTree = ""; }; 63114D0EAAE2606732DF5AA0 /* STPSourceCardDetailsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceCardDetailsTest.swift; sourceTree = ""; }; @@ -629,7 +628,6 @@ 683F7735569D22CBEC9CA2E6 /* STPPaymentHandlerFunctionalTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = STPPaymentHandlerFunctionalTest.m; sourceTree = ""; }; 6887F19BB9804BF45FD703FF /* STPPushProvisioningContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPushProvisioningContext.swift; sourceTree = ""; }; 6955B3A3353F8442E4FBBBF6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 69BD038947E8E2376A0D240B /* STPCardScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardScanner.swift; sourceTree = ""; }; 6A9E7B637A8747431B38FD1D /* STPCardValidationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardValidationState.swift; sourceTree = ""; }; 6B7A947152A728EB2CBC4DB2 /* STPSourceReceiverTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPSourceReceiverTest.swift; sourceTree = ""; }; 6BA4B9192BF433B200D1F21D /* STPPaymentMethodMobilePayParamsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPaymentMethodMobilePayParamsTests.swift; sourceTree = ""; }; @@ -962,9 +960,7 @@ 458F8576215E0F8ECE1D74CE /* STPBankSelectionTableViewCell.swift */, 81352A0CBE46A59E6B1A712E /* STPBankSelectionViewController.swift */, E3B42EBAC0DC7ED0D9200DB7 /* STPBlocks.swift */, - 61F8308B7250B642D19827D8 /* STPCameraView.swift */, 3E5FB20B2BEFC00D54FDD87D /* STPCard+BasicUI.swift */, - 69BD038947E8E2376A0D240B /* STPCardScanner.swift */, 0B91C4D5B93FF71C61B140F1 /* STPCardScannerTableViewCell.swift */, 6A9E7B637A8747431B38FD1D /* STPCardValidationState.swift */, 2D1525AF65BDEF691F8BCBE8 /* STPCoreScrollViewController.swift */, @@ -1198,6 +1194,7 @@ 40BB87E28719FE0C6B946BB5 /* STPCardExpiryInputTextFieldFormatterTests.swift */, 0C75157665428685C7A4FD20 /* STPCardExpiryInputTextFieldSnapshotTests.swift */, 4418164D75002AE6A0273176 /* STPCardExpiryInputTextFieldValidatorTests.swift */, + 5216B23C2CB0435B007F271A /* STPCardFormScannerViewSnapshotTests.swift */, 1D983E089196152DA1C69469 /* STPCardFormViewSnapshotTests.swift */, FF5E08A1651D9DFE502DA021 /* STPCardFormViewTests.swift */, D38184A7CD27B978DFA30E69 /* STPCardFunctionalTest.swift */, @@ -1643,9 +1640,7 @@ CEF318C74D2E44C78EF85306 /* STPBankSelectionTableViewCell.swift in Sources */, 172D96526023A80534D54CC0 /* STPBankSelectionViewController.swift in Sources */, A0AA0B8AEF5B429858D71F6B /* STPBlocks.swift in Sources */, - DDBF5AAE607C698618DDE865 /* STPCameraView.swift in Sources */, 66065B1D65D7D5502D4E2F2B /* STPCard+BasicUI.swift in Sources */, - BAFD06E994739E1C38DFFBBC /* STPCardScanner.swift in Sources */, 2FFA7C2D1C7337FDB4C608A5 /* STPCardScannerTableViewCell.swift in Sources */, 2F18A1903244E144C7802E09 /* STPCardValidationState.swift in Sources */, DF73457BF349BC962A6AC502 /* STPCoreScrollViewController.swift in Sources */, @@ -1736,6 +1731,7 @@ 524AE1978E0A4490D1C390C5 /* CustomerAdapterTests.swift in Sources */, 61E1CA1F2BD6B72800A421AE /* STPPaymentMethodMultibancoTests.swift in Sources */, 0DFA17378D894C70D72C9F62 /* Error+PaymentSheetTests.swift in Sources */, + 5216B23D2CB0435B007F271A /* STPCardFormScannerViewSnapshotTests.swift in Sources */, C9E66A22494C02050AE34A9B /* FBSnapshotTestCase+STPViewControllerLoading.swift in Sources */, 35C1CF73701EECC7DB6AB722 /* FormSpecProviderTest.swift in Sources */, ACF6CFE0F8B88FDBBB16968C /* FraudDetectionDataTest.swift in Sources */, diff --git a/Stripe/StripeiOS/Source/STPAddCardViewController.swift b/Stripe/StripeiOS/Source/STPAddCardViewController.swift index 853908fd4ba..3a2832c1fd6 100644 --- a/Stripe/StripeiOS/Source/STPAddCardViewController.swift +++ b/Stripe/StripeiOS/Source/STPAddCardViewController.swift @@ -872,7 +872,7 @@ public class STPAddCardViewController: STPCoreTableViewController, STPAddressVie static let cardScannerKSTPCardScanAnimationTime: TimeInterval = 0.04 @available(macCatalyst 14.0, *) - func cardScanner( + public func cardScanner( _ scanner: STPCardScanner, didFinishWith cardParams: STPPaymentMethodCardParams?, error: Error? diff --git a/Stripe/StripeiOSTests/STPCardFormScannerViewSnapshotTests.swift b/Stripe/StripeiOSTests/STPCardFormScannerViewSnapshotTests.swift new file mode 100644 index 00000000000..fb29056ec02 --- /dev/null +++ b/Stripe/StripeiOSTests/STPCardFormScannerViewSnapshotTests.swift @@ -0,0 +1,176 @@ +// +// STPCardFormScannerViewSnapshotTests.swift +// StripeiOS Tests +// +// Copyright © 2024 Stripe, Inc. All rights reserved. +// + +import iOSSnapshotTestCase +import StripeCoreTestUtils + +@testable@_spi(STP) import Stripe +@testable@_spi(STP) import StripeCore +@testable@_spi(STP) import StripePayments +@testable@_spi(STP) import StripePaymentSheet +@testable@_spi(STP) import StripePaymentsUI + +class STPCardFormScannerViewSnapshotTests: STPSnapshotTestCase { + class MockCardScanner: StripePaymentsUI.STPCardScanner { + override func start() {} + override func stop() {} + } + + func testWithFullBillingDetailsWithScanner() { + let formScannerView = STPCardFormScannerView(MockCardScanner(), style: .standard, handleError: nil) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 400)) + formScannerView.startScanCard() + + let exp = expectation(description: "Waiting for layout") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.STPSnapshotVerifyView(formScannerView) + exp.fulfill() + } + waitForExpectations(timeout: 1.0) + } + + func testEmpty() { + let formScannerView = STPCardFormScannerView(billingAddressCollection: .automatic) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formScannerView) + } + + func testIncomplete() { + let formScannerView = STPCardFormScannerView(billingAddressCollection: .automatic) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 265)) + + formScannerView.numberField.text = "4242" + formScannerView.numberField.textDidChange() + formScannerView.cvcField.text = "123" + formScannerView.cvcField.textDidChange() + + STPSnapshotVerifyView(formScannerView) + } + + // valid expiration date will change over time so we just test without it + func testCompleteWithoutExpiry() { + let formScannerView = STPCardFormScannerView(billingAddressCollection: .automatic) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + formScannerView.numberField.text = "4242424242424242" + formScannerView.numberField.textDidChange() + formScannerView.cvcField.text = "123" + formScannerView.cvcField.textDidChange() + formScannerView.postalCodeField.text = "12345" + + STPSnapshotVerifyView(formScannerView) + } + + func testEmptyHiddenPostalCode() { + let formScannerView = STPCardFormScannerView(billingAddressCollection: .automatic) + formScannerView.countryCode = "AE" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formScannerView) + } + + func testWithFullBillingDetails() { + let formScannerView = STPCardFormScannerView(billingAddressCollection: .required) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 400)) + + STPSnapshotVerifyView(formScannerView) + } + + // MARK: - Standalone + + func testDefaultStandalone() { + let formScannerView = STPCardFormScannerView() + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formScannerView) + } + + func testBorderlessStandalone() { + let formScannerView = STPCardFormScannerView(style: .borderless) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formScannerView) + } + + func testCustomBackgroundStandalone() { + let formScannerView = STPCardFormScannerView() + formScannerView.countryCode = "US" + formScannerView.backgroundColor = .green + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formScannerView) + } + + func testCustomBackgroundDisabledColorStandalone() { + let formScannerView = STPCardFormScannerView() + formScannerView.countryCode = "US" + formScannerView.disabledBackgroundColor = .green + formScannerView.isUserInteractionEnabled = false + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + STPSnapshotVerifyView(formScannerView) + } + + func testBorderlessStandaloneIncomplete() { + let formScannerView = STPCardFormScannerView(style: .borderless) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + formScannerView.numberField.text = "4242" + formScannerView.numberField.textDidChange() + formScannerView.cvcField.text = "123" + formScannerView.cvcField.textDidChange() + + STPSnapshotVerifyView(formScannerView) + } + + func testCBC() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let formScannerView = STPCardFormScannerView(billingAddressCollection: .automatic, cbcEnabledOverride: true) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + formScannerView.numberField.text = "4973019750239993" + formScannerView.numberField.textDidChange() + formScannerView.cvcField.text = "123" + formScannerView.cvcField.textDidChange() + formScannerView.postalCodeField.text = "12345" + let exp = expectation(description: "Wait for CBC load") + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.STPSnapshotVerifyView(formScannerView) + exp.fulfill() + } + waitForExpectations(timeout: 3.0) + } + + func testCBCPreselectVisa() { + STPAPIClient.shared.publishableKey = STPTestingDefaultPublishableKey + let formScannerView = STPCardFormScannerView(billingAddressCollection: .automatic, cbcEnabledOverride: true) + formScannerView.countryCode = "US" + formScannerView.frame = CGRect(origin: .zero, size: CGSize(width: 300, height: 225)) + + formScannerView.numberField.text = "4973019750239993" + formScannerView.numberField.textDidChange() + formScannerView.cvcField.text = "123" + formScannerView.cvcField.textDidChange() + formScannerView.postalCodeField.text = "12345" + formScannerView.preferredNetworks = [.visa] + let exp = expectation(description: "Wait for CBC load") + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { + self.STPSnapshotVerifyView(formScannerView) + exp.fulfill() + } + waitForExpectations(timeout: 3.0) + } +} diff --git a/StripePaymentsUI/StripePaymentsUI.xcodeproj/project.pbxproj b/StripePaymentsUI/StripePaymentsUI.xcodeproj/project.pbxproj index fd386ad3b49..c65e0bb9312 100644 --- a/StripePaymentsUI/StripePaymentsUI.xcodeproj/project.pbxproj +++ b/StripePaymentsUI/StripePaymentsUI.xcodeproj/project.pbxproj @@ -25,6 +25,9 @@ 4F2317E3CD41D6D903E331C2 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFE846D5129FB85A61ADB219 /* XCTest.framework */; }; 4FE3B76C2BF8F46FF3FFAAA0 /* STPCardLoadingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C911731DD279086059CA23E /* STPCardLoadingIndicator.swift */; }; 52014012018F766C62311660 /* STPMultiFormTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BD968A1A81BAD59B63E156E /* STPMultiFormTextField.swift */; }; + 5216B2362CADECC0007F271A /* STPCardScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5216B2352CADECC0007F271A /* STPCardScanner.swift */; }; + 5216B2372CADECCA007F271A /* STPCardFormScannerView .swift in Sources */ = {isa = PBXBuildFile; fileRef = 527B5D162CA593C900478060 /* STPCardFormScannerView .swift */; }; + 5216B2392CADED48007F271A /* STPCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5216B2382CADED48007F271A /* STPCameraView.swift */; }; 528BE8DF6DD797D45501886F /* au_becs_bsb.json in Resources */ = {isa = PBXBuildFile; fileRef = 9801336237230E00A8D8F6ED /* au_becs_bsb.json */; }; 5324CDE826773FD808B5F5A3 /* STPCardNumberInputTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 778ECC1469214B0535BA8515 /* STPCardNumberInputTextField.swift */; }; 54069B3F36BE51B117AE66DE /* UIButton+Stripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB967723AE164AD7FC104111 /* UIButton+Stripe.swift */; }; @@ -152,6 +155,9 @@ 4CE7F2BE40869D265B4E2501 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 500CBFBF3B9888148350E834 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; 521054B1E6486BD4F285F830 /* STPStringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPStringUtils.swift; sourceTree = ""; }; + 5216B2352CADECC0007F271A /* STPCardScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardScanner.swift; sourceTree = ""; }; + 5216B2382CADED48007F271A /* STPCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCameraView.swift; sourceTree = ""; }; + 527B5D162CA593C900478060 /* STPCardFormScannerView .swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "STPCardFormScannerView .swift"; sourceTree = ""; }; 55847CB08B388547F06612D1 /* cs-CZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "cs-CZ"; path = "cs-CZ.lproj/Localizable.strings"; sourceTree = ""; }; 558D20EEBB72368213CD701D /* sl-SI */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sl-SI"; path = "sl-SI.lproj/Localizable.strings"; sourceTree = ""; }; 5B79A953FFD647CFC46A7C81 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; }; @@ -431,6 +437,9 @@ 153419E931FD1C7C7241A308 /* PaymentMethodMessagingView+Configuration.swift */, 978DC21B087406C15D764B27 /* STPAUBECSDebitFormView.swift */, B30F4593F849D1B9395B94FA /* STPCardFormView.swift */, + 527B5D162CA593C900478060 /* STPCardFormScannerView .swift */, + 5216B2352CADECC0007F271A /* STPCardScanner.swift */, + 5216B2382CADED48007F271A /* STPCameraView.swift */, 75BA4436655DE117219F0C47 /* STPCardFormView+SwiftUI.swift */, E87D9A22F5475D1F8058E38D /* STPFloatingPlaceholderTextField.swift */, A8165DB2F2C8B257ACF4015C /* STPFormTextFieldContainer.swift */, @@ -688,6 +697,7 @@ A4A9946052C4A1BCCBA6D508 /* STPPostalCodeValidator.swift in Sources */, 9CCE00DDB77E0B1E5BC30372 /* STPPromise.swift in Sources */, 381E12068537365DC3423575 /* STPStringUtils.swift in Sources */, + 5216B2392CADED48007F271A /* STPCameraView.swift in Sources */, 14B5DCA6C906F6D60551ECAB /* String+Localized.swift in Sources */, A915992666D7877A26E42231 /* StripePayments+Export.swift in Sources */, EB866272BEDCA262181F87B9 /* StripePaymentsBundleLocator.swift in Sources */, @@ -707,6 +717,7 @@ 5C86B4020AAFB78DFD920446 /* STPCardNumberInputTextFieldFormatter.swift in Sources */, 1B6270ED136F4C0657A87FB3 /* STPCardNumberInputTextFieldValidator.swift in Sources */, 85500DE65102C3B3106B05E4 /* STPPostalCodeInputTextField.swift in Sources */, + 5216B2362CADECC0007F271A /* STPCardScanner.swift in Sources */, A41B0A2A37AD28D28D9577CA /* STPPostalCodeInputTextFieldFormatter.swift in Sources */, 92F147E82F5A7ADF9C9BC4CE /* STPPostalCodeInputTextFieldValidator.swift in Sources */, C805055D566F42647F79131B /* STPCountryPickerInputField.swift in Sources */, @@ -736,6 +747,7 @@ 52014012018F766C62311660 /* STPMultiFormTextField.swift in Sources */, 59775D5885338AB20F13E388 /* STPPaymentCardTextField+SwiftUI.swift in Sources */, E193F62029BD7821D8B6950E /* STPPaymentCardTextField.swift in Sources */, + 5216B2372CADECCA007F271A /* STPCardFormScannerView .swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Stripe/StripeiOS/Source/STPCameraView.swift b/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCameraView.swift similarity index 93% rename from Stripe/StripeiOS/Source/STPCameraView.swift rename to StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCameraView.swift index 17fa42470b5..aa4d8bd976c 100644 --- a/Stripe/StripeiOS/Source/STPCameraView.swift +++ b/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCameraView.swift @@ -1,6 +1,6 @@ // // STPCameraView.swift -// StripeiOS +// StripePaymentsUI // // Created by David Estes on 8/17/20. // Copyright © 2020 Stripe, Inc. All rights reserved. @@ -10,7 +10,7 @@ import AVFoundation import UIKit @available(macCatalyst 14.0, *) -class STPCameraView: UIView { +public class STPCameraView: UIView { private var flashLayer: CALayer? var captureSession: AVCaptureSession? { @@ -50,7 +50,7 @@ class STPCameraView: UIView { }) } - override init( + public override init( frame: CGRect ) { super.init(frame: frame) @@ -65,7 +65,7 @@ class STPCameraView: UIView { videoPreviewLayer.videoGravity = .resizeAspectFill } - override class var layerClass: AnyClass { + public override class var layerClass: AnyClass { return AVCaptureVideoPreviewLayer.self } diff --git a/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormScannerView .swift b/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormScannerView .swift new file mode 100644 index 00000000000..ed2f366d988 --- /dev/null +++ b/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormScannerView .swift @@ -0,0 +1,318 @@ +// +// STPCardFormScannerView.swift +// StripePaymentsUI +// +// Copyright © 2024 Stripe, Inc. All rights reserved. +// + +import AVFoundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePayments +@_spi(STP) import StripeUICore +import UIKit + +/// `STPCardFormScannerView ` provides a multiline interface for users to input their +/// credit card details as well as billing postal code and provides an interface to access +/// the created `STPPaymentMethodParams`. +/// `STPCardFormView` includes both the input fields as well as an error label that +/// is displayed when invalid input is detected. +public class STPCardFormScannerView: STPCardFormView, STPCardScannerDelegate { + @available(macCatalyst 14.0, *) private var cardScanner: STPCardScanner? { + get { + _cardScanner as? STPCardScanner + } + set { + _cardScanner = newValue + } + } + + /// Storage for `cardScanner`. + private var _cardScanner: NSObject? + private(set) weak var cameraView: STPCameraView? + + private var scannerCompleteAnimationTimer: Timer? + private static let cardScannerKSTPCardScanAnimationTime: TimeInterval = 0.04 + private static let cardSizeRatio: CGFloat = 2.125 / 3.370 // ID-1 card size (in inches) + private var _isScanning = false + private var isScanning: Bool { + get { + _isScanning + } + set(isScanning) { + if _isScanning == isScanning { + return + } + _isScanning = isScanning + switch _isScanning { + case true: + showCardScanner() + cardScanner?.start() + case false: + hideCardScanner() + cardScanner?.stop() + } + } + } + + private var handleError: ((_ error: Error?) -> Void)? + private lazy var hideHeightConstraint = cameraView?.heightAnchor + .constraint(equalToConstant: 0) ?? NSLayoutConstraint() + private lazy var showHeightConstraint = cameraView?.heightAnchor.constraint( + equalTo: cameraView?.widthAnchor ?? NSLayoutDimension(), + multiplier: STPCardFormScannerView.cardSizeRatio + ) ?? NSLayoutConstraint() + private var sectionAccessoryButton: UIButton? + + @objc public convenience init( + style: STPCardFormViewStyle = .standard + ) { + self.init( + billingAddressCollection: .automatic, + style: style, + prefillDetails: nil + ) + } + + @_spi(STP) public convenience init( + billingAddressCollection: BillingAddressCollectionLevel, + style: STPCardFormViewStyle = .standard, + postalCodeRequirement: STPPostalCodeRequirement = .standard, + prefillDetails: PrefillDetails? = nil, + inputMode: STPCardNumberInputTextField.InputMode = .standard, + cbcEnabledOverride: Bool? = nil, + sectionTitle: String? = nil, + sectionAccessoryButton: UIButton? = nil + ) { + self.init( + numberField: STPCardNumberInputTextField( + inputMode: inputMode, + prefillDetails: prefillDetails, + cbcEnabledOverride: cbcEnabledOverride + ), + cvcField: STPCardCVCInputTextField(prefillDetails: prefillDetails), + expiryField: STPCardExpiryInputTextField(prefillDetails: prefillDetails), + billingAddressSubForm: BillingAddressSubForm( + billingAddressCollection: billingAddressCollection, + postalCodeRequirement: postalCodeRequirement + ), + style: style, + postalCodeRequirement: postalCodeRequirement, + prefillDetails: prefillDetails, + inputMode: inputMode, + sectionTitle: sectionTitle, + sectionAccessoryButton: sectionAccessoryButton + ) + } + + required init( + numberField: STPCardNumberInputTextField, + cvcField: STPCardCVCInputTextField, + expiryField: STPCardExpiryInputTextField, + billingAddressSubForm: BillingAddressSubForm, + style: STPCardFormViewStyle = .standard, + postalCodeRequirement: STPPostalCodeRequirement = .standard, + prefillDetails: PrefillDetails? = nil, + inputMode: STPCardNumberInputTextField.InputMode = .standard, + sectionTitle: String? = nil, + sectionAccessoryButton: UIButton? = nil + ) { + Self.stp_analyticsIdentifier = "STPCardFormScannerView" + + let sectionAccessoryButton = UIButton(type: .system) + + super.init( + numberField: numberField, + cvcField: cvcField, + expiryField: expiryField, + billingAddressSubForm: billingAddressSubForm, + style: style, + postalCodeRequirement: postalCodeRequirement, + prefillDetails: prefillDetails, + inputMode: inputMode, + sectionTitle: sectionTitle, + sectionAccessoryButton: sectionAccessoryButton + ) + cardScanner = cardScanner + handleError = nil + self.sectionAccessoryButton = sectionAccessoryButton + + sectionAccessoryButton.contentHorizontalAlignment = .right + sectionAccessoryButton.titleLabel?.numberOfLines = 0 + sectionAccessoryButton.titleLabel?.lineBreakMode = .byWordWrapping + sectionAccessoryButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + sectionAccessoryButton.contentEdgeInsets = .zero + sectionAccessoryButton.setTitle( + String.Localized.scan_card_title_capitalization, + for: .normal + ) + sectionAccessoryButton.addTarget(self, action: #selector(toggleScanCard), for: .touchUpInside) + + numberField.addTarget(self, action: #selector(didTapFieldGesture), for: .touchDown) + expiryField.addTarget(self, action: #selector(didTapFieldGesture), for: .touchDown) + cvcField.addTarget(self, action: #selector(didTapFieldGesture), for: .touchDown) + countryField.addTarget(self, action: #selector(didTapFieldGesture), for: .touchDown) + postalCodeField.addTarget(self, action: #selector(didTapFieldGesture), for: .touchDown) + + let cameraView = STPCameraView(frame: .zero) + vStack.distribution = .equalCentering + vStack.addArrangedSubview(cameraView) + self.cameraView = cameraView + self.cameraView?.backgroundColor = UIColor.black + self.cameraView?.translatesAutoresizingMaskIntoConstraints = false + cameraView.setContentHuggingPriority(.defaultLow, for: .horizontal) + addConstraints( + [ + showHeightConstraint, + hideHeightConstraint, + cameraView.widthAnchor.constraint(equalTo: widthAnchor), + ] + ) + showHeightConstraint.isActive = false + hideHeightConstraint.isActive = true + setUpCardScanningIfAvailable() + } + + /// Public initializer for `STPCardFormScannerView`. + /// @param style The visual style to use for this instance. @see STPCardFormViewStyle + @objc public convenience init( + _ cardScanner: STPCardScanner? = nil, + style: STPCardFormViewStyle = .standard, + handleError: ((_ error: Error?) -> Void)? + ) { + self.init( + billingAddressCollection: .automatic, + style: style, + prefillDetails: nil, + sectionTitle: STPPaymentMethodType.card.displayName + ) + self.cardScanner = cardScanner + self.handleError = nil + } + + @available(*, unavailable) required init?( + coder: NSCoder + ) { + fatalError("init(coder:) has not been implemented") + } + + required init( + sections: [Section] + ) { + fatalError("init(sections:) has not been implemented") + } + + @objc private func didTapFieldGesture() { + stopScanCard() + } + + func setUpCardScanningIfAvailable() { + if #available(macCatalyst 14.0, *) { + if !STPCardScanner.cardScanningAvailable { + return + } + let cardScanner = STPCardScanner(delegate: self) + cardScanner.cameraView = cameraView + self.cardScanner = (self.cardScanner == nil) ? cardScanner : nil + } + } + + @available(macCatalyst 14.0, *) + @objc func toggleScanCard() { + isScanning.toggle() + } + + @available(macCatalyst 14.0, *) + @objc func startScanCard() { + isScanning = true + } + + @available(macCatalyst 14.0, *) + @objc func stopScanCard() { + isScanning = false + } + + private func showCardScanner() { + DispatchQueue.main.async { + NSLayoutConstraint.deactivate([self.hideHeightConstraint]) + NSLayoutConstraint.activate([self.showHeightConstraint]) + UIView.animate(withDuration: 0.2) { + self.layoutIfNeeded() + } + } + sectionAccessoryButton?.setTitle( + String.Localized.close, + for: .normal + ) + endEditing(true) + } + + private func hideCardScanner() { + DispatchQueue.main.async { + NSLayoutConstraint.deactivate([self.showHeightConstraint]) + NSLayoutConstraint.activate([self.hideHeightConstraint]) + UIView.animate(withDuration: 0.2) { + self.layoutIfNeeded() + } + } + sectionAccessoryButton?.setTitle( + String.Localized.scan_card_title_capitalization, + for: .normal + ) + } + + // MARK: - STPCardScanner + + /// :nodoc: + @available(macCatalyst 14.0, *) public func cardScanner( + _ scanner: STPCardScanner, + didFinishWith cardParams: STPPaymentMethodCardParams?, + error: Error? + ) { + if error != nil { + isScanning = false + handleError?(error) + } + if let cardParams { + isUserInteractionEnabled = false + var i = 0 + scannerCompleteAnimationTimer = Timer.scheduledTimer( + withTimeInterval: Self.cardScannerKSTPCardScanAnimationTime, + repeats: true, + block: { timer in + i += 1 + let newParams = STPPaymentMethodCardParams() + guard let number = cardParams.number else { + timer.invalidate() + self.isUserInteractionEnabled = false + return + } + if i < number.count { + newParams.number = String( + number[...number.index(number.startIndex, offsetBy: i)] + ) + } else { + newParams.number = number + } + self.cardParams = STPPaymentMethodParams( + card: newParams, + billingDetails: nil, + metadata: nil + ) + if i > number.count { + self.cardParams = + STPPaymentMethodParams( + card: cardParams, + billingDetails: nil, + metadata: nil + ) + self.isScanning = false + timer.invalidate() + self.isUserInteractionEnabled = true + } + } + ) + } else { + isScanning = false + } + } +} diff --git a/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormView.swift b/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormView.swift index 3b6ac53694a..ccc7be517e7 100644 --- a/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormView.swift +++ b/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardFormView.swift @@ -319,7 +319,9 @@ public class STPCardFormView: STPFormView { postalCodeRequirement: STPPostalCodeRequirement = .standard, prefillDetails: PrefillDetails? = nil, inputMode: STPCardNumberInputTextField.InputMode = .standard, - cbcEnabledOverride: Bool? = nil + cbcEnabledOverride: Bool? = nil, + sectionTitle: String? = nil, + sectionAccessoryButton: UIButton? = nil ) { self.init( numberField: STPCardNumberInputTextField( @@ -336,7 +338,9 @@ public class STPCardFormView: STPFormView { style: style, postalCodeRequirement: postalCodeRequirement, prefillDetails: prefillDetails, - inputMode: inputMode + inputMode: inputMode, + sectionTitle: sectionTitle, + sectionAccessoryButton: sectionAccessoryButton ) } @@ -348,7 +352,9 @@ public class STPCardFormView: STPFormView { style: STPCardFormViewStyle = .standard, postalCodeRequirement: STPPostalCodeRequirement = .standard, prefillDetails: PrefillDetails? = nil, - inputMode: STPCardNumberInputTextField.InputMode = .standard + inputMode: STPCardNumberInputTextField.InputMode = .standard, + sectionTitle: String? = nil, + sectionAccessoryButton: UIButton? = nil ) { self.numberField = numberField self.cvcField = cvcField @@ -370,8 +376,8 @@ public class STPCardFormView: STPFormView { let cardParamsSection = STPFormView.Section( rows: rows, - title: nil, - accessoryButton: nil + title: sectionTitle, + accessoryButton: sectionAccessoryButton ) super.init( diff --git a/Stripe/StripeiOS/Source/STPCardScanner.swift b/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardScanner.swift similarity index 97% rename from Stripe/StripeiOS/Source/STPCardScanner.swift rename to StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardScanner.swift index c55e104b467..9a694d30de7 100644 --- a/Stripe/StripeiOS/Source/STPCardScanner.swift +++ b/StripePaymentsUI/StripePaymentsUI/Source/UI Components/STPCardScanner.swift @@ -1,6 +1,6 @@ // // STPCardScanner.swift -// StripeiOS +// StripePaymentsUI // // Created by David Estes on 8/17/20. // Copyright © 2020 Stripe, Inc. All rights reserved. @@ -10,7 +10,6 @@ import AVFoundation import Foundation @_spi(STP) import StripeCore @_spi(STP) import StripePayments -@_spi(STP) import StripePaymentsUI import UIKit import Vision @@ -20,7 +19,7 @@ enum STPCardScannerError: Int { } @available(macCatalyst 14.0, *) -@objc protocol STPCardScannerDelegate: NSObjectProtocol { +@objc public protocol STPCardScannerDelegate: NSObjectProtocol { @objc(cardScanner:didFinishWithCardParams:error:) func cardScanner( _ scanner: STPCardScanner, didFinishWith cardParams: @@ -30,7 +29,7 @@ enum STPCardScannerError: Int { @available(macCatalyst 14.0, *) @objc(STPCardScanner_legacy) -class STPCardScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { +public class STPCardScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { // iOS will kill the app if it tries to request the camera without an NSCameraUsageDescription static let cardScanningAvailableCameraHasUsageDescription = { return @@ -38,7 +37,7 @@ class STPCardScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { || Bundle.main.localizedInfoDictionary?["NSCameraUsageDescription"] != nil) }() - static var cardScanningAvailable: Bool { + public static var cardScanningAvailable: Bool { // Always allow in tests: if NSClassFromString("XCTest") != nil { return true @@ -46,7 +45,7 @@ class STPCardScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { return cardScanningAvailableCameraHasUsageDescription } - weak var cameraView: STPCameraView? + public weak var cameraView: STPCameraView? var feedbackGenerator: UINotificationFeedbackGenerator? @@ -87,17 +86,17 @@ class STPCardScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { } } - override init() { + public override init() { } - init(delegate: STPCardScannerDelegate?) { + public init(delegate: STPCardScannerDelegate?) { super.init() self.delegate = delegate captureSessionQueue = DispatchQueue(label: "com.stripe.CardScanning.CaptureSessionQueue") deviceOrientation = UIDevice.current.orientation } - func start() { + public func start() { if isScanning { return } @@ -127,7 +126,7 @@ class STPCardScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { }) } - func stop() { + public func stop() { stopWithError(nil) } @@ -256,7 +255,7 @@ class STPCardScanner: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate { } // MARK: Processing - func captureOutput( + public func captureOutput( _ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection @@ -483,6 +482,6 @@ let STPCardScannerErrorDomain = "STPCardScannerErrorDomain" /// :nodoc: @available(macCatalyst 14.0, *) -extension STPCardScanner: STPAnalyticsProtocol { - static var stp_analyticsIdentifier = "STPCardScanner" +@_spi(STP) extension STPCardScanner: STPAnalyticsProtocol { + public static var stp_analyticsIdentifier = "STPCardScanner" } diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testBorderlessStandalone@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testBorderlessStandalone@3x.png new file mode 100644 index 00000000000..339bc79c834 Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testBorderlessStandalone@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testBorderlessStandaloneIncomplete@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testBorderlessStandaloneIncomplete@3x.png new file mode 100644 index 00000000000..36efd4bb8b7 Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testBorderlessStandaloneIncomplete@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCBC@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCBC@3x.png new file mode 100644 index 00000000000..c80357f45bc Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCBC@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCBCPreselectVisa@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCBCPreselectVisa@3x.png new file mode 100644 index 00000000000..a94581d0af2 Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCBCPreselectVisa@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCompleteWithoutExpiry@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCompleteWithoutExpiry@3x.png new file mode 100644 index 00000000000..3cc3e67399b Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCompleteWithoutExpiry@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCustomBackgroundDisabledColorStandalone@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCustomBackgroundDisabledColorStandalone@3x.png new file mode 100644 index 00000000000..0a5acd94103 Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCustomBackgroundDisabledColorStandalone@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCustomBackgroundStandalone@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCustomBackgroundStandalone@3x.png new file mode 100644 index 00000000000..8bfdffae5a6 Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testCustomBackgroundStandalone@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testDefaultStandalone@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testDefaultStandalone@3x.png new file mode 100644 index 00000000000..bac4dfe12fb Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testDefaultStandalone@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testEmpty@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testEmpty@3x.png new file mode 100644 index 00000000000..bac4dfe12fb Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testEmpty@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testEmptyHiddenPostalCode@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testEmptyHiddenPostalCode@3x.png new file mode 100644 index 00000000000..ed15ed87f4f Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testEmptyHiddenPostalCode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testIncomplete@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testIncomplete@3x.png new file mode 100644 index 00000000000..54d43cd3503 Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testIncomplete@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testWithFullBillingDetails@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testWithFullBillingDetails@3x.png new file mode 100644 index 00000000000..3e2c0985279 Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testWithFullBillingDetails@3x.png differ diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testWithFullBillingDetailsWithScanner@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testWithFullBillingDetailsWithScanner@3x.png new file mode 100644 index 00000000000..06185f615fb Binary files /dev/null and b/Tests/ReferenceImages_64/StripeiOS_Tests.STPCardFormScannerViewSnapshotTests/testWithFullBillingDetailsWithScanner@3x.png differ