Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show payment methods in vertical saved list #3573

Merged
merged 23 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -431,8 +433,10 @@
6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCardViewControllerSnapshotTests.swift; sourceTree = "<group>"; };
617C44F9338DE2E93E318291 /* PayWithLinkWebController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayWithLinkWebController.swift; sourceTree = "<group>"; };
6193FC5E14E1EC459E31B5F4 /* SheetNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetNavigationButton.swift; sourceTree = "<group>"; };
619E5B962BEA84630040647A /* VerticalSavedPaymentOptionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentOptionsViewController.swift; sourceTree = "<group>"; };
619E5B982BEA891E0040647A /* VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentOptionsViewControllerSnapshotTests.swift; sourceTree = "<group>"; };
6198AA6B2BED1AC000F39D3E /* CheckmarkCircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkCircleView.swift; sourceTree = "<group>"; };
6198AA6D2BED1C5A00F39D3E /* PaymentMethodRowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodRowButton.swift; sourceTree = "<group>"; };
61CBE6652BED9749005F7FEB /* VerticalSavedPaymentMethodsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentMethodsViewController.swift; sourceTree = "<group>"; };
61CBE6672BED97EE005F7FEB /* VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalSavedPaymentMethodsViewControllerSnapshotTests.swift; sourceTree = "<group>"; };
62CE362B80042827F47ABC3F /* AffirmCopyLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffirmCopyLabel.swift; sourceTree = "<group>"; };
64C8F350CDB5A29F62E86592 /* FlowControllerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowControllerStateTests.swift; sourceTree = "<group>"; };
64D658AC15478BF1E0A76B9D /* TestModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestModeView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -827,6 +831,16 @@
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
620D4E5B9EE4B529E708F5D9 /* CustomerAdapter */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -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 = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment on lines +101 to +102
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not reuse makeSavedPaymentMethodCellImage?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makeSavedPaymentMethodCellImage uses carousel images for card brands and for SEPA. We need the images that are used in the PAN field for card brand rather than the carousel images. Here the diff if we use makeSavedPaymentMethodCellImage, notice visa & SEPA look a little different than what is laid out in the mocks. The differences would be more sark in dark mode but I don't think we have mocks for dark mode.
CleanShot 2024-05-09 at 17 57 13

Mock

CleanShot 2024-05-09 at 17 58 11

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh right, we have 'carousel' versions of these. I guess we can delete those once we finish LPM visibility and the horizontal saved pm carousel goes away.

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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omitted snapshot tests for this class and relying on the VerticalSavedPaymentOptionsViewControllerSnapshotTests. Can add them if we feel they will be useful.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I don't think this needs its own tests


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()
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, omitted snapshot tests for this view and relied on VerticalSavedPaymentOptionsViewControllerSnapshotTests.


struct ViewModel {
let appearance: PaymentSheet.Appearance
let text: String
let image: UIImage
// TODO(porter) Add can remove and can update
}
Comment on lines +21 to +26
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the idea behind putting this in a struct ViewModel rather than just making these properties on the PaymentMethodRowButton class directly?

Copy link
Collaborator Author

@porter-stripe porter-stripe May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not totally needed right now, but I anticipate this will scale better and am hoping in the future to create view models for PaymentMethodRowButton from a STPPaymentMethod. Mainly trying to keep the size of PaymentMethodRowButton.init down and offload the injection building somewhere else. Not totally needed but kinda like the direction.

extension STPPaymentMethod {
   var rowButtonViewModel: PaymentMethodRowButton.ViewModel {...}
}

// Somewhere else
let rowButtons = savedPaymentMethods.map{PaymentMethodRowButton(appearance: appearance, viewModel: $0.rowButtonViewModel)}


// 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
}
}
Loading
Loading