diff --git a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj index 4844d0aaadd..e18609187f3 100644 --- a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj +++ b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj @@ -111,9 +111,11 @@ 6103F2BC2BE45990002D67F8 /* SavedPaymentMethodManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6103F2BB2BE45990002D67F8 /* SavedPaymentMethodManager.swift */; }; 614A8AE72BE53C6900E8688B /* SavedPaymentMethodManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6103F2BD2BE53737002D67F8 /* SavedPaymentMethodManagerTest.swift */; }; 6151DDC02B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */; }; - 619E5B972BEA84630040647A /* VerticalSavedPaymentOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E5B962BEA84630040647A /* VerticalSavedPaymentOptionsViewController.swift */; }; - 619E5B9A2BEA89D90040647A /* VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 619E5B982BEA891E0040647A /* VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift */; }; + 6198AA6C2BED1AC000F39D3E /* CheckmarkCircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6198AA6B2BED1AC000F39D3E /* CheckmarkCircleView.swift */; }; + 6198AA6E2BED1C5A00F39D3E /* PaymentMethodRowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6198AA6D2BED1C5A00F39D3E /* PaymentMethodRowButton.swift */; }; 61C0D3B8C63EB4558AB74A7E /* StripePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A1C7CFA5C9C1A8A73CFA1C0 /* StripePayments.framework */; }; + 61CB0BD02BED985100E24A4C /* VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CBE6672BED97EE005F7FEB /* VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift */; }; + 61CBE6662BED9749005F7FEB /* VerticalSavedPaymentMethodsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CBE6652BED9749005F7FEB /* VerticalSavedPaymentMethodsViewController.swift */; }; 623C2D9F87929D6DA9C09E23 /* STPCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39B31D0B890A4F8E4819B15 /* STPCameraView.swift */; }; 630A3B22BC5C176928538511 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B7188D37BDE69B56D8223046 /* Main.storyboard */; }; 648FDD85FD6ECDA1BBC71D45 /* CustomerSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83FA9C54257694EC0F205A5C /* CustomerSheet.swift */; }; @@ -431,8 +433,10 @@ 6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCardViewControllerSnapshotTests.swift; sourceTree = ""; }; 617C44F9338DE2E93E318291 /* PayWithLinkWebController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayWithLinkWebController.swift; sourceTree = ""; }; 6193FC5E14E1EC459E31B5F4 /* SheetNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetNavigationButton.swift; sourceTree = ""; }; - 619E5B962BEA84630040647A /* VerticalSavedPaymentOptionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentOptionsViewController.swift; sourceTree = ""; }; - 619E5B982BEA891E0040647A /* VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift; sourceTree = ""; }; + 6198AA6B2BED1AC000F39D3E /* CheckmarkCircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkCircleView.swift; sourceTree = ""; }; + 6198AA6D2BED1C5A00F39D3E /* PaymentMethodRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodRowButton.swift; sourceTree = ""; }; + 61CBE6652BED9749005F7FEB /* VerticalSavedPaymentMethodsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentMethodsViewController.swift; sourceTree = ""; }; + 61CBE6672BED97EE005F7FEB /* VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift; sourceTree = ""; }; 62CE362B80042827F47ABC3F /* AffirmCopyLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffirmCopyLabel.swift; sourceTree = ""; }; 64C8F350CDB5A29F62E86592 /* FlowControllerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowControllerStateTests.swift; sourceTree = ""; }; 64D658AC15478BF1E0A76B9D /* TestModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestModeView.swift; sourceTree = ""; }; @@ -827,6 +831,16 @@ name = Products; sourceTree = ""; }; + 6107484B2BED198700BC2CD6 /* Vertical Saved Payment Method Screen */ = { + isa = PBXGroup; + children = ( + 61CBE6652BED9749005F7FEB /* VerticalSavedPaymentMethodsViewController.swift */, + 6198AA6D2BED1C5A00F39D3E /* PaymentMethodRowButton.swift */, + 6198AA6B2BED1AC000F39D3E /* CheckmarkCircleView.swift */, + ); + path = "Vertical Saved Payment Method Screen"; + sourceTree = ""; + }; 620D4E5B9EE4B529E708F5D9 /* CustomerAdapter */ = { isa = PBXGroup; children = ( @@ -938,7 +952,7 @@ children = ( 5AA26FF00FD57F6AA1A7CB83 /* SavedPaymentMethodCollectionView.swift */, 2B3ECDF6CF9AABD573F86CA2 /* SavedPaymentOptionsViewController.swift */, - 619E5B962BEA84630040647A /* VerticalSavedPaymentOptionsViewController.swift */, + 6107484B2BED198700BC2CD6 /* Vertical Saved Payment Method Screen */, ); path = "Saved Payment Method Screen"; sourceTree = ""; @@ -1323,7 +1337,7 @@ C1AED4473AD4C07D461E9E48 /* SavedPaymentOptionsViewControllerSnapshotTests.swift */, BA3902DA8D647E934686CB5C /* SepaMandateViewControllerSnapshotTest.swift */, 6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */, - 619E5B982BEA891E0040647A /* VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift */, + 61CBE6672BED97EE005F7FEB /* VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift */, B68CB9622B0D2169006ACDB1 /* STPAPIClient+PaymentSheetTest.swift */, D16926577504D37992F8917E /* STPApplePayContext+PaymentSheetTest.swift */, E09C073021CE89593466548C /* STPCardBrandChoiceTest.swift */, @@ -1560,6 +1574,7 @@ files = ( D90987C72BBD76D20D60CE04 /* DictionaryTests.swift in Sources */, E5C7667A08EA85C0FF12523D /* AddPaymentMethodViewControllerSnapshotTests.swift in Sources */, + 61CB0BD02BED985100E24A4C /* VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift in Sources */, 8180BC3615767F896E2F9355 /* AddressViewControllerSnapshotTests.swift in Sources */, 37F750E1C99D6257E845A66E /* BacsDDMandateViewSnapshotTests.swift in Sources */, 694A3B36AC19FC1F87EF0CB1 /* CustomerSheetPaymentMethodAvailabilityTests.swift in Sources */, @@ -1596,7 +1611,6 @@ EEC6283DB21D04AD5B77F9D2 /* STPApplePayContext+PaymentSheetTest.swift in Sources */, 714FBCA75296C291FDB3B345 /* STPCardBrandChoiceTest.swift in Sources */, E0E47773D3C0B432E26AA457 /* STPElementsSessionTest.swift in Sources */, - 619E5B9A2BEA89D90040647A /* VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift in Sources */, 29C98FB712F3FB987CBE18B0 /* STPFixtures+PaymentSheet.swift in Sources */, 45D9849E9B36C56E15EAAE0A /* SavedPaymentOptionsViewControllerSnapshotTests.swift in Sources */, 9787A622B527C1AD96A73827 /* SepaMandateViewControllerSnapshotTest.swift in Sources */, @@ -1634,11 +1648,11 @@ 40806EF506CB719299FC90CC /* STPLocalizedString.swift in Sources */, 71132CE036C3EE0655ECD2DB /* STPStringUtils.swift in Sources */, 1AF3BBA86D643AAF26CD0E2B /* StripePaymentSheet+Exports.swift in Sources */, - 619E5B972BEA84630040647A /* VerticalSavedPaymentOptionsViewController.swift in Sources */, B6859A882BE54CD30018E06C /* PaymentSheetVerticalViewController.swift in Sources */, 73F3E8DCF2314972A162B2A3 /* StripePaymentSheetBundleLocator.swift in Sources */, 73EE441CF71707651109CE19 /* ConsumerSession+LookupResponse.swift in Sources */, 311AC53D6C76953E9B70148A /* ConsumerSession+PublishableKey.swift in Sources */, + 6198AA6E2BED1C5A00F39D3E /* PaymentMethodRowButton.swift in Sources */, 2BEA2A103AD3EE94D60A06D4 /* ConsumerSession.swift in Sources */, 6BA8D3362B0C1F9B008C51FF /* CVCReconfirmationViewController.swift in Sources */, A4FF52567582E9774AE13348 /* PaymentDetails.swift in Sources */, @@ -1658,6 +1672,7 @@ 258A75AF2E5393186C8850CA /* LinkEmailElement.swift in Sources */, EDE71E0BEDD94FB1101F3C10 /* FormElement+Link.swift in Sources */, C346B534D57A952D4415ADFD /* Intent+Link.swift in Sources */, + 61CBE6662BED9749005F7FEB /* VerticalSavedPaymentMethodsViewController.swift in Sources */, 29C91CE046099B86D8DCF310 /* STPAnalyticsClient+Link.swift in Sources */, 93FB7933528A45350593D3EC /* UIColor+Link.swift in Sources */, 11C23605F97D2DB6F171843E /* LinkUI.swift in Sources */, @@ -1689,6 +1704,7 @@ F42DEC1850964E75ACAC29AB /* CustomerSheet+API.swift in Sources */, 50C68C68B007A926BE99B2B8 /* CustomerSheet+PaymentMethodAvailability.swift in Sources */, DB8A4C5FC11D0EED55E8C975 /* CustomerSheet+SwiftUI.swift in Sources */, + 6198AA6C2BED1AC000F39D3E /* CheckmarkCircleView.swift in Sources */, 648FDD85FD6ECDA1BBC71D45 /* CustomerSheet.swift in Sources */, 9E77F1E9F801AE970F1A5BE1 /* CustomerSheetConfiguration.swift in Sources */, AB8E1556F008083257A99E91 /* CustomerSheetError.swift in Sources */, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentOption+Images.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentOption+Images.swift index e9a1c828dc3..c8671dd6a53 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentOption+Images.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentOption+Images.swift @@ -97,6 +97,24 @@ extension STPPaymentMethod { return makeIcon() } } + + /// Returns an image to display inside a row representing the given payment option in the saved PM row view + func makeSavedPaymentMethodRowImage() -> UIImage { + switch type { + case .card: + let cardBrand = card?.preferredDisplayBrand ?? .unknown + return STPImageLibrary.cardBrandImage(for: cardBrand) + case .USBankAccount: + return PaymentSheetImageLibrary.bankIcon( + for: PaymentSheetImageLibrary.bankIconCode(for: usBankAccount?.bankName) + ) + case .SEPADebit: + return Image.pm_type_sepa.makeImage().withRenderingMode(.alwaysOriginal) + default: + assertionFailure("\(type) not supported for saved PMs") + return makeIcon() + } + } } extension STPPaymentMethodParams { diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/CheckmarkCircleView.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/CheckmarkCircleView.swift new file mode 100644 index 00000000000..7a473d2e958 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/CheckmarkCircleView.swift @@ -0,0 +1,53 @@ +// +// CheckmarkCircleView.swift +// StripePaymentSheet +// +// Created by Nick Porter on 5/9/24. +// + +import Foundation +import UIKit + +/// Draws a circle with the desired fill color with a white checkmark in the center +final class CheckmarkCircleView: UIView { + + let checkmarkColor: UIColor = .white + let fillColor: UIColor + + init(fillColor: UIColor) { + self.fillColor = fillColor + super.init(frame: .zero) + self.backgroundColor = .clear + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override var intrinsicContentSize: CGSize { + return CGSize(width: 20, height: 20) + } + + override func draw(_ rect: CGRect) { + super.draw(rect) + drawCircle() + drawCheckmark() + } + + private func drawCircle() { + let path = UIBezierPath(ovalIn: CGRect(origin: .zero, size: intrinsicContentSize)) + fillColor.setFill() + path.fill() + } + + private func drawCheckmark() { + let path = UIBezierPath() + path.lineWidth = max(2, intrinsicContentSize.width * 0.06) + path.move(to: CGPoint(x: intrinsicContentSize.width * 0.28, y: intrinsicContentSize.height * 0.53)) + path.addLine(to: CGPoint(x: intrinsicContentSize.width * 0.42, y: intrinsicContentSize.height * 0.66)) + path.addLine(to: CGPoint(x: intrinsicContentSize.width * 0.72, y: intrinsicContentSize.height * 0.36)) + + checkmarkColor.setStroke() + path.stroke() + } +} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/PaymentMethodRowButton.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/PaymentMethodRowButton.swift new file mode 100644 index 00000000000..ba6864be560 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/PaymentMethodRowButton.swift @@ -0,0 +1,128 @@ +// +// PaymentMethodRowButton.swift +// StripePaymentSheet +// +// Created by Nick Porter on 5/9/24. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +protocol PaymentMethodRowButtonDelegate: AnyObject { + func didSelectButton(_ button: PaymentMethodRowButton) + // TODO(porter) Add did delete and did update +} + +final class PaymentMethodRowButton: UIView { + + struct ViewModel { + let appearance: PaymentSheet.Appearance + let text: String + let image: UIImage + // TODO(porter) Add can remove and can update + } + + // MARK: Internal properties + // TODO(porter) Maybe expand this into an enum of (selected, unselected, editing) state + var isSelected: Bool { + get { + return shadowRoundedRect.isSelected + } + + set { + shadowRoundedRect.isSelected = newValue + circleView.alpha = newValue ? 1.0 : 0.0 + } + } + + weak var delegate: PaymentMethodRowButtonDelegate? + + // MARK: Private properties + private let viewModel: ViewModel + + // MARK: Private views + + private lazy var paymentMethodImageView: UIImageView = { + let imageView = UIImageView(image: viewModel.image) + imageView.contentMode = .scaleAspectFit + // TODO(porter) Do we want to round the corners? + return imageView + }() + + private lazy var label: UILabel = { + let label = UILabel() + label.text = viewModel.text + label.font = viewModel.appearance.scaledFont(for: viewModel.appearance.font.base.medium, + style: .callout, + maximumPointSize: 25) + label.adjustsFontForContentSizeCategory = true + return label + }() + + private lazy var circleView: CheckmarkCircleView = { + let circleView = CheckmarkCircleView(fillColor: viewModel.appearance.colors.primary) + circleView.alpha = 0.0 + return circleView + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [paymentMethodImageView, label, UIView.spacerView, circleView]) + stackView.axis = .horizontal + stackView.alignment = .center + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.directionalLayoutMargins = .init(top: 12, // Hardcoded from figma + leading: PaymentSheetUI.defaultPadding, + bottom: 12, + trailing: PaymentSheetUI.defaultPadding) + stackView.isLayoutMarginsRelativeArrangement = true + stackView.setCustomSpacing(12, after: paymentMethodImageView) // Hardcoded from figma + return stackView + }() + + private lazy var shadowRoundedRect: ShadowedRoundedRectangle = { + let shadowRoundedRect = ShadowedRoundedRectangle(appearance: viewModel.appearance) + shadowRoundedRect.translatesAutoresizingMaskIntoConstraints = false + shadowRoundedRect.addAndPinSubview(stackView) + return shadowRoundedRect + }() + + init(viewModel: ViewModel) { + self.viewModel = viewModel + super.init(frame: .zero) + + addAndPinSubview(shadowRoundedRect) + NSLayoutConstraint.activate([ + paymentMethodImageView.heightAnchor.constraint(equalToConstant: 20), // Hardcoded from figma + paymentMethodImageView.widthAnchor.constraint(equalToConstant: 25), + ]) + // TODO(porter) accessibility? + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap)) + addGestureRecognizer(tapGesture) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Tap handlers + @objc private func handleTap() { + shadowRoundedRect.isSelected = true + circleView.alpha = 1.0 + delegate?.didSelectButton(self) + } + +} + +// MARK: Helper extensions +extension UIView { + static var spacerView: UIView { + let view = UIView() + view.isUserInteractionEnabled = false + view.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal) + view.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal) + return view + } +} 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 new file mode 100644 index 00000000000..d1defd75007 --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/Vertical Saved Payment Method Screen/VerticalSavedPaymentMethodsViewController.swift @@ -0,0 +1,135 @@ +// +// VerticalSavedPaymentMethodsViewController.swift +// StripePaymentSheet +// +// Created by Nick Porter on 5/7/24. +// + +import Foundation +@_spi(STP) import StripeCore +@_spi(STP) import StripePaymentsUI +@_spi(STP) import StripeUICore +import UIKit + +protocol VerticalSavedPaymentMethodsViewControllerDelegate: AnyObject { + func didSelectPaymentMethod(_ paymentMethod: STPPaymentMethod) +} + +/// A view controller that shows a list of saved payment methods in a vertical orientation +class VerticalSavedPaymentMethodsViewController: UIViewController { + + private let configuration: PaymentSheet.Configuration + private let paymentMethods: [STPPaymentMethod] + + // MARK: Internal properties + weak var delegate: VerticalSavedPaymentMethodsViewControllerDelegate? + + // MARK: - UI properties + + lazy var navigationBar: SheetNavigationBar = { + let navBar = SheetNavigationBar(isTestMode: configuration.apiClient.isTestmode, + appearance: configuration.appearance) + navBar.setStyle(.back) + navBar.delegate = self + return navBar + }() + + private lazy var headerLabel: UILabel = { + let label = PaymentSheetUI.makeHeaderLabel(appearance: configuration.appearance) + let nonCardPaymentMethods = paymentMethods.filter({ $0.type != .card }) + label.text = nonCardPaymentMethods.isEmpty ? .Localized.select_card : .Localized.select_payment_method + return label + }() + + private lazy var paymentMethodRows: [(paymentMethod: STPPaymentMethod, button: PaymentMethodRowButton)] = { + return paymentMethods.map { paymentMethod in + let button = PaymentMethodRowButton(viewModel: .init(appearance: configuration.appearance, + text: paymentMethod.paymentSheetLabel, + image: paymentMethod.makeSavedPaymentMethodRowImage())) + button.delegate = self + return (paymentMethod, button) + } + }() + + private lazy var stackView: UIStackView = { + let stackView = UIStackView(arrangedSubviews: [headerLabel] + paymentMethodRows.map { $0.button }) + stackView.directionalLayoutMargins = PaymentSheetUI.defaultMargins + stackView.isLayoutMarginsRelativeArrangement = true + stackView.axis = .vertical + stackView.spacing = 12 + stackView.setCustomSpacing(16, after: headerLabel) + return stackView + }() + + init(configuration: PaymentSheet.Configuration, paymentMethods: [STPPaymentMethod]) { + self.configuration = configuration + self.paymentMethods = paymentMethods + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = configuration.appearance.colors.background + configuration.style.configure(self) + // TODO(porter) Pipe in selected payment method, default to selecting first for now + paymentMethodRows.first?.button.isSelected = true + view.addAndPinSubviewToSafeArea(stackView, insets: PaymentSheetUI.defaultSheetMargins) + } +} + +// MARK: - BottomSheetContentViewController +extension VerticalSavedPaymentMethodsViewController: BottomSheetContentViewController { + var allowsDragToDismiss: Bool { + return true + } + + func didTapOrSwipeToDismiss() { + dismiss(animated: true) + } + + var requiresFullScreen: Bool { + return false + } + + func didFinishAnimatingHeight() { + // no-op + } +} + +// MARK: - SheetNavigationBarDelegate +extension VerticalSavedPaymentMethodsViewController: SheetNavigationBarDelegate { + func sheetNavigationBarDidClose(_ sheetNavigationBar: SheetNavigationBar) { + // no-op we are in 'back' style mode + } + + func sheetNavigationBarDidBack(_ sheetNavigationBar: SheetNavigationBar) { + _ = bottomSheetController?.popContentViewController() + } +} + +// MARK: - PaymentMethodRowButtonDelegate +extension VerticalSavedPaymentMethodsViewController: PaymentMethodRowButtonDelegate { + func didSelectButton(_ button: PaymentMethodRowButton) { + guard let paymentMethod = paymentMethodRows.first(where: { $0.button === button })?.paymentMethod else { + // TODO(porter) Handle error - no matching payment method found + return + } + + // Deselect previous button + paymentMethodRows.first { $0.button != button && $0.button.isSelected }?.button.isSelected = false + + // Disable interaction to prevent double selecting since we will be dismissing soon + self.view.isUserInteractionEnabled = false + + // Give time for new selected row to show it has been selected before dismissing + // Makes UX feel a little nicer + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in + _ = self?.bottomSheetController?.popContentViewController() + self?.delegate?.didSelectPaymentMethod(paymentMethod) + } + } +} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/VerticalSavedPaymentOptionsViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/VerticalSavedPaymentOptionsViewController.swift deleted file mode 100644 index ce26e552c45..00000000000 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Saved Payment Method Screen/VerticalSavedPaymentOptionsViewController.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// VerticalSavedPaymentOptionsViewController.swift -// StripePaymentSheet -// -// Created by Nick Porter on 5/7/24. -// - -import Foundation -@_spi(STP) import StripeCore -@_spi(STP) import StripeUICore -import UIKit - -/// A view controller that shows a list of saved payment methods in a vertical orientation -class VerticalSavedPaymentOptionsViewController: UIViewController { - - private let configuration: PaymentSheet.Configuration - - // MARK: - UI properties - - lazy var navigationBar: SheetNavigationBar = { - let navBar = SheetNavigationBar(isTestMode: configuration.apiClient.isTestmode, - appearance: configuration.appearance) - navBar.setStyle(.back) - navBar.delegate = self - return navBar - }() - - private lazy var headerLabel: UILabel = { - let label = PaymentSheetUI.makeHeaderLabel(appearance: configuration.appearance) - label.text = .Localized.select_payment_method - return label - }() - - private lazy var stackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [headerLabel]) - stackView.directionalLayoutMargins = PaymentSheetUI.defaultMargins - stackView.isLayoutMarginsRelativeArrangement = true - stackView.axis = .vertical - stackView.spacing = PaymentSheetUI.defaultPadding - return stackView - }() - - init(configuration: PaymentSheet.Configuration) { - self.configuration = configuration - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = configuration.appearance.colors.background - configuration.style.configure(self) - - view.addAndPinSubviewToSafeArea(stackView, insets: PaymentSheetUI.defaultSheetMargins) - } -} - -// MARK: - BottomSheetContentViewController -extension VerticalSavedPaymentOptionsViewController: BottomSheetContentViewController { - var allowsDragToDismiss: Bool { - // TODO - return true - } - - func didTapOrSwipeToDismiss() { - dismiss(animated: true) - } - - var requiresFullScreen: Bool { - // TODO - return false - } - - func didFinishAnimatingHeight() { - // no-op - } -} - -// MARK: - SheetNavigationBarDelegate -extension VerticalSavedPaymentOptionsViewController: SheetNavigationBarDelegate { - func sheetNavigationBarDidClose(_ sheetNavigationBar: SheetNavigationBar) { - // no-op we are in 'back' style mode - } - - func sheetNavigationBarDidBack(_ sheetNavigationBar: SheetNavigationBar) { - _ = bottomSheetController?.popContentViewController() - } -} diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift index 1a9227ecbbe..c5a553b3b1b 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/ViewControllers/PaymentSheetVerticalViewController.swift @@ -66,7 +66,10 @@ class PaymentSheetVerticalViewController: UIViewController, FlowControllerViewCo // TOOD(porter) Remove/rename @objc func presentManageScreen() { - bottomSheetController?.pushContentViewController(VerticalSavedPaymentOptionsViewController(configuration: configuration)) + let vc = VerticalSavedPaymentMethodsViewController(configuration: configuration, + paymentMethods: loadResult.savedPaymentMethods) + vc.delegate = self + bottomSheetController?.pushContentViewController(vc) // TODO(porter) Set delegate } } @@ -96,3 +99,10 @@ extension PaymentSheetVerticalViewController: BottomSheetContentViewController { // no-op } } + +extension PaymentSheetVerticalViewController: VerticalSavedPaymentMethodsViewControllerDelegate { + func didSelectPaymentMethod(_ paymentMethod: STPPaymentMethod) { + // TODO + print("Selected payment method with id: \(paymentMethod.stripeId)") + } +} diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift new file mode 100644 index 00000000000..e51bcffdeaf --- /dev/null +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift @@ -0,0 +1,48 @@ +// +// VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift +// StripePaymentSheet +// +// Created by Nick Porter on 5/7/24. +// + +import StripeCoreTestUtils +@_spi(STP) @testable import StripePaymentSheet +@testable import StripePaymentsTestUtils +import XCTest + +final class VerticalSavedPaymentMethodsViewControllerSnapshotTests: STPSnapshotTestCase { + + func test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDarkMode() { + _test_VerticalSavedPaymentMethodsViewControllerSnapshotTests(darkMode: true) + } + + func test_VerticalSavedPaymentMethodsViewControllerSnapshotTestsLightMode() { + _test_VerticalSavedPaymentMethodsViewControllerSnapshotTests(darkMode: false) + } + + func test_VerticalSavedPaymentMethodsViewControllerSnapshotTestsAppearance() { + _test_VerticalSavedPaymentMethodsViewControllerSnapshotTests(darkMode: false, appearance: ._testMSPaintTheme) + } + + func _test_VerticalSavedPaymentMethodsViewControllerSnapshotTests(darkMode: Bool, appearance: PaymentSheet.Appearance = .default) { + var configuration = PaymentSheet.Configuration() + configuration.appearance = appearance + let sut = VerticalSavedPaymentMethodsViewController(configuration: configuration, paymentMethods: generatePaymentMethods()) + let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 428, height: 500)) + testWindow.isHidden = false + if darkMode { + testWindow.overrideUserInterfaceStyle = .dark + } + testWindow.rootViewController = sut + sut.view.autosizeHeight(width: 375) + STPSnapshotVerifyView(sut.view) + } + + private func generatePaymentMethods() -> [STPPaymentMethod] { + return [STPFixtures.paymentMethod(), + STPFixtures.usBankAccountPaymentMethod(), + STPFixtures.usBankAccountPaymentMethod(bankName: "BANK OF AMERICA"), + STPFixtures.usBankAccountPaymentMethod(bankName: "STRIPE"), + STPFixtures.sepaDebitPaymentMethod(), ] + } +} diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift deleted file mode 100644 index 47be6f7a237..00000000000 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift -// StripePaymentSheet -// -// Created by Nick Porter on 5/7/24. -// - -import StripeCoreTestUtils -@_spi(STP) @testable import StripePaymentSheet -@testable import StripePaymentsTestUtils -import XCTest - -final class VerticalSavedPaymentOptionsViewControllerSnapshotTests: STPSnapshotTestCase { - - func test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDarkMode() { - _test_VerticalSavedPaymentOptionsViewControllerSnapshotTests(darkMode: true) - } - - func test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsLightMode() { - _test_VerticalSavedPaymentOptionsViewControllerSnapshotTests(darkMode: false) - } - - func test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsAppearance() { - _test_VerticalSavedPaymentOptionsViewControllerSnapshotTests(darkMode: false, appearance: ._testMSPaintTheme) - } - - func _test_VerticalSavedPaymentOptionsViewControllerSnapshotTests(darkMode: Bool, appearance: PaymentSheet.Appearance = .default) { - var configuration = PaymentSheet.Configuration() - configuration.appearance = appearance - let sut = VerticalSavedPaymentOptionsViewController(configuration: configuration) - let testWindow = UIWindow(frame: CGRect(x: 0, y: 0, width: 428, height: 500)) - testWindow.isHidden = false - if darkMode { - testWindow.overrideUserInterfaceStyle = .dark - } - testWindow.rootViewController = sut - sut.view.autosizeHeight(width: 375) - STPSnapshotVerifyView(sut.view) - } -} diff --git a/StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/SEPADebitPaymentMethod.json b/StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/SEPADebitPaymentMethod.json new file mode 100644 index 00000000000..24b291c4697 --- /dev/null +++ b/StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/SEPADebitPaymentMethod.json @@ -0,0 +1,30 @@ +{ + "id": "pm_123456789", + "object": "payment_method", + "sepa_debit": { + "bank_code": "DE86213522400189569728", + "country": "DE", + "fingerprint": "j9brhn8f41Dh3As", + "last4": "1234", + "mandate_reference": "MR123", + "mandate_url": "https://stripe.com/mandate" + }, + "billing_details": { + "address": { + "city": "Berlin", + "country": "DE", + "line1": "Straße des 17. Juni", + "line2": "135", + "postal_code": "10623", + "state": "Berlin" + }, + "email": "test@test.com", + "name": "John Doe", + "phone": "1231231234" + }, + "created": 1579820912, + "customer": "cus_123456789", + "livemode": true, + "metadata": {}, + "type": "sepa_debit" +} \ No newline at end of file diff --git a/StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/USBankAccountPaymentMethod.json b/StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/USBankAccountPaymentMethod.json new file mode 100644 index 00000000000..316ef53c3ee --- /dev/null +++ b/StripePayments/StripePaymentsObjcTestUtils/Resources/Mock Files/USBankAccountPaymentMethod.json @@ -0,0 +1,31 @@ +{ + "id": "pm_123456789", + "object": "payment_method", + "us_bank_account": { + "country": "US", + "bank_name": "TD BANK", + "fingerprint": "Ec5gh5t1Ord8i7DYa", + "last4": "6789", + "routing_number": "123456789", + "account_holder_name": "John Doe", + "account_holder_type": "individual", + "account_type": "checking" + }, + "billing_details": { + "address": { + "city": "New York", + "country": "US", + "line1": "123 Main St", + "line2": "", + "postal_code": "10001", + "state": "NY" + }, + "email": "test@test.com", + "name": "John Doe", + "phone": "1231231234" + }, + "created": 1599420912, + "customer": "cus_123456789", + "livemode": true, + "type": "us_bank_account" +} diff --git a/StripePayments/StripePaymentsTestUtils/STPFixtures+Swift.swift b/StripePayments/StripePaymentsTestUtils/STPFixtures+Swift.swift index a2b147f4ff8..ad9e3e2c4c0 100644 --- a/StripePayments/StripePaymentsTestUtils/STPFixtures+Swift.swift +++ b/StripePayments/StripePaymentsTestUtils/STPFixtures+Swift.swift @@ -46,6 +46,21 @@ public extension STPFixtures { } return STPPaymentIntent.decodedObject(fromAPIResponse: apiResponse)! } + + static func usBankAccountPaymentMethod(bankName: String? = nil) -> STPPaymentMethod { + var json = STPTestUtils.jsonNamed("USBankAccountPaymentMethod") as? [String: Any] + if let bankName = bankName { + var usBankAccountData = json?["us_bank_account"] as? [String: Any] + usBankAccountData?["bank_name"] = bankName + json?["us_bank_account"] = usBankAccountData + } + return STPPaymentMethod.decodedObject(fromAPIResponse: json)! + } + + static func sepaDebitPaymentMethod() -> STPPaymentMethod { + let json = STPTestUtils.jsonNamed("SEPADebitPaymentMethod") + return STPPaymentMethod.decodedObject(fromAPIResponse: json)! + } } public extension STPPaymentMethodParams { diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentMethodsViewControllerSnapshotTestsAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentMethodsViewControllerSnapshotTestsAppearance@3x.png new file mode 100644 index 00000000000..d6a0fba1ab1 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentMethodsViewControllerSnapshotTestsAppearance@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentMethodsViewControllerSnapshotTestsLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentMethodsViewControllerSnapshotTestsLightMode@3x.png new file mode 100644 index 00000000000..8c4cb30eca8 Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentMethodsViewControllerSnapshotTestsLightMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDarkMode@3x.png new file mode 100644 index 00000000000..5285992a44b Binary files /dev/null and b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentMethodsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDarkMode@3x.png differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsAppearance@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsAppearance@3x.png deleted file mode 100644 index 8cb3a796ec1..00000000000 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsAppearance@3x.png and /dev/null differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDarkMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDarkMode@3x.png deleted file mode 100644 index 53369472bc8..00000000000 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsDarkMode@3x.png and /dev/null differ diff --git a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsLightMode@3x.png b/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsLightMode@3x.png deleted file mode 100644 index a0bf003bf88..00000000000 Binary files a/Tests/ReferenceImages_64/StripePaymentSheetTests.VerticalSavedPaymentOptionsViewControllerSnapshotTests/test_VerticalSavedPaymentOptionsViewControllerSnapshotTestsLightMode@3x.png and /dev/null differ