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

Add ability to toggle edit mode for vertical mode #3576

Merged
merged 21 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@
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 */; };
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 */; };
Expand Down Expand Up @@ -433,7 +432,6 @@
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>"; };
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>"; };
Expand Down Expand Up @@ -836,7 +834,6 @@
children = (
61CBE6652BED9749005F7FEB /* VerticalSavedPaymentMethodsViewController.swift */,
6198AA6D2BED1C5A00F39D3E /* PaymentMethodRowButton.swift */,
6198AA6B2BED1AC000F39D3E /* CheckmarkCircleView.swift */,
);
path = "Vertical Saved Payment Method Screen";
sourceTree = "<group>";
Expand Down Expand Up @@ -1704,7 +1701,6 @@
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 @@ -122,6 +122,9 @@
/* iDEAL bank section title for iDEAL form entry. */
"iDEAL Bank" = "iDEAL Bank";

/* Title shown above a view containing the customer's payment methods that they can delete or update */
"Manage payment methods" = "Manage payment methods";

/* Title shown above a carousel containing the customer's payment methods */
"Manage your payment methods" = "Manage your payment methods";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,11 @@ extension String.Localized {
"Select your payment method",
"Title shown above a carousel containing the customer's payment methods")
}

static var manage_payment_methods: String {
STPLocalizedString(
"Manage payment methods",
"Title shown above a view containing the customer's payment methods that they can delete or update"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ class CustomerSavedPaymentMethodsViewController: UIViewController {
self.navigationBar.additionalButton.removeTarget(
self, action: #selector(didSelectEditSavedPaymentMethodsButton),
for: .touchUpInside)
return shouldShowPaymentMethodCarousel ? .back : .close(showAdditionalButton: false)
return shouldShowPaymentMethodCarousel ? .back(showAdditionalButton: false) : .close(showAdditionalButton: false)
}
}())
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import UIKit

protocol PaymentMethodRowButtonDelegate: AnyObject {
func didSelectButton(_ button: PaymentMethodRowButton)
// TODO(porter) Add did delete and did update
func didSelectRemoveButton(_ button: PaymentMethodRowButton)
func didSelectEditButton(_ button: PaymentMethodRowButton)
}

final class PaymentMethodRowButton: UIView {
Expand All @@ -25,16 +26,42 @@ final class PaymentMethodRowButton: UIView {
// TODO(porter) Add can remove and can update
}

enum State {
case selected
case unselected
case editing
}

// MARK: Internal properties
// TODO(porter) Maybe expand this into an enum of (selected, unselected, editing) state
var state: State = .unselected {
didSet {
previousState = oldValue

selectionTapGesture.isEnabled = !isEditing
shadowRoundedRect.isSelected = isSelected
circleView.isHidden = !isSelected
editButton.isHidden = !isEditing // TODO(porter) only show if we can edit
removeButton.isHidden = !isEditing // TOOD(porter) only show if we can remove
}
}

private(set) var previousState: State = .unselected

var isSelected: Bool {
get {
return shadowRoundedRect.isSelected
switch state {
case .selected:
return true
case .unselected, .editing:
return false
}
}

set {
shadowRoundedRect.isSelected = newValue
circleView.alpha = newValue ? 1.0 : 0.0
var isEditing: Bool {
switch state {
case .selected, .unselected:
return false
case .editing:
return true
}
}

Expand Down Expand Up @@ -62,14 +89,31 @@ final class PaymentMethodRowButton: UIView {
return label
}()

private lazy var circleView: CheckmarkCircleView = {
let circleView = CheckmarkCircleView(fillColor: viewModel.appearance.colors.primary)
circleView.alpha = 0.0
private lazy var circleView: CircularButton = {
let circleView = CircularButton(style: .check, iconColor: .white)
circleView.backgroundColor = viewModel.appearance.colors.primary
circleView.isHidden = true
return circleView
}()

lazy var removeButton: CircularButton = {
let removeButton = CircularButton(style: .remove, iconColor: .white)
removeButton.backgroundColor = viewModel.appearance.colors.danger
removeButton.isHidden = true
removeButton.addTarget(self, action: #selector(handleRemoveButtonTapped), for: .touchUpInside)
return removeButton
}()

private lazy var editButton: CircularButton = {
let editButton = CircularButton(style: .edit, iconColor: .white)
editButton.backgroundColor = viewModel.appearance.colors.icon
editButton.isHidden = true
editButton.addTarget(self, action: #selector(handleEditButtonTapped), for: .touchUpInside)
return editButton
}()

private lazy var stackView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [paymentMethodImageView, label, UIView.spacerView, circleView])
let stackView = UIStackView(arrangedSubviews: [paymentMethodImageView, label, UIView.spacerView, circleView, editButton, removeButton])
stackView.axis = .horizontal
stackView.alignment = .center
stackView.translatesAutoresizingMaskIntoConstraints = false
Expand All @@ -78,7 +122,8 @@ final class PaymentMethodRowButton: UIView {
bottom: 12,
trailing: PaymentSheetUI.defaultPadding)
stackView.isLayoutMarginsRelativeArrangement = true
stackView.setCustomSpacing(12, after: paymentMethodImageView) // Hardcoded from figma
stackView.spacing = 12 // Hardcoded from figma

return stackView
}()

Expand All @@ -89,6 +134,10 @@ final class PaymentMethodRowButton: UIView {
return shadowRoundedRect
}()

private lazy var selectionTapGesture: UITapGestureRecognizer = {
return UITapGestureRecognizer(target: self, action: #selector(handleSelectionTap))
}()

init(viewModel: ViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
Expand All @@ -99,21 +148,27 @@ final class PaymentMethodRowButton: UIView {
paymentMethodImageView.widthAnchor.constraint(equalToConstant: 25),
])
// TODO(porter) accessibility?
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
addGestureRecognizer(tapGesture)
addGestureRecognizer(selectionTapGesture)
}

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
@objc private func handleSelectionTap() {
state = .selected
delegate?.didSelectButton(self)
}

@objc private func handleEditButtonTapped() {
delegate?.didSelectEditButton(self)
}

@objc private func handleRemoveButtonTapped() {
delegate?.didSelectRemoveButton(self)
}

}

// MARK: Helper extensions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,31 @@ class VerticalSavedPaymentMethodsViewController: UIViewController {
private let configuration: PaymentSheet.Configuration
private let paymentMethods: [STPPaymentMethod]

private var isEditingPaymentMethods: Bool = false {
didSet {
let additionalButtonTitle = isEditingPaymentMethods ? UIButton.doneButtonTitle : UIButton.editButtonTitle
navigationBar.additionalButton.setTitle(additionalButtonTitle, for: .normal)
headerLabel.text = headerText

// If we are entering edit mode, put all buttons in an edit state, otherwise put back in their previous state
if isEditingPaymentMethods {
paymentMethodRows.map { $0.button }.forEach { $0.state = .editing }
} else {
paymentMethodRows.map { $0.button }.forEach { $0.state = $0.previousState }
}
// TODO(porter) Handle case where we delete the selected card
}
}

private var headerText: String {
if isEditingPaymentMethods {
return .Localized.manage_payment_methods
}

let nonCardPaymentMethods = paymentMethods.filter({ $0.type != .card })
return nonCardPaymentMethods.isEmpty ? .Localized.select_card : .Localized.select_payment_method
}

// MARK: Internal properties
weak var delegate: VerticalSavedPaymentMethodsViewControllerDelegate?

Expand All @@ -29,15 +54,19 @@ class VerticalSavedPaymentMethodsViewController: UIViewController {
lazy var navigationBar: SheetNavigationBar = {
let navBar = SheetNavigationBar(isTestMode: configuration.apiClient.isTestmode,
appearance: configuration.appearance)
navBar.setStyle(.back)
// TODO(porter) Only show edit button if we should
navBar.setStyle(.back(showAdditionalButton: true))
navBar.delegate = self
navBar.additionalButton.setTitle(UIButton.editButtonTitle, for: .normal)
navBar.additionalButton.accessibilityIdentifier = "edit_saved_button"
navBar.additionalButton.titleLabel?.adjustsFontForContentSizeCategory = true
navBar.additionalButton.addTarget(self, action: #selector(didSelectEditSavedPaymentMethodsButton), for: .touchUpInside)
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
label.text = headerText
return label
}()

Expand Down Expand Up @@ -76,8 +105,12 @@ class VerticalSavedPaymentMethodsViewController: UIViewController {
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)
paymentMethodRows.first?.button.state = .selected
view.addAndPinSubview(stackView, insets: PaymentSheetUI.defaultSheetMargins)
}

@objc func didSelectEditSavedPaymentMethodsButton() {
isEditingPaymentMethods = !isEditingPaymentMethods
}
}

Expand Down Expand Up @@ -113,17 +146,23 @@ extension VerticalSavedPaymentMethodsViewController: SheetNavigationBarDelegate

// MARK: - PaymentMethodRowButtonDelegate
extension VerticalSavedPaymentMethodsViewController: PaymentMethodRowButtonDelegate {

private func paymentMethod(from button: PaymentMethodRowButton) -> STPPaymentMethod? {
return paymentMethodRows.first(where: { $0.button === button })?.paymentMethod
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Picking up this convo #3573 (comment)

I understand your point but IMO it's worth making this payment method a property of PaymentMethodRowButton, it lets you get rid of this funny search and removes the possibility that the STPPaymentMethod is nil.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

👍, refactored out the ViewModel and instead inject a STPPaymentMethod.


func didSelectButton(_ button: PaymentMethodRowButton) {
guard let paymentMethod = paymentMethodRows.first(where: { $0.button === button })?.paymentMethod else {
guard let paymentMethod = paymentMethod(from: button) 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
paymentMethodRows.first { $0.button != button && $0.button.isSelected }?.button.state = .unselected

// Disable interaction to prevent double selecting since we will be dismissing soon
self.view.isUserInteractionEnabled = false
self.navigationBar.isUserInteractionEnabled = false // Tint buttons in the nav bar to look disabled
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure I understand this comment, is it supposed to be a TODO?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, maybe a bad/un-needed comment but the main rational behind disabling the interaction of the nav bar is to give the disabled look to the buttons in the nav bar.

Right after selecting:
CleanShot 2024-05-14 at 14 35 28

Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't the main rationale to disable interaction? You wouldn't want to be able to edit while we are in the middle of transitioning back to the main screen.


// Give time for new selected row to show it has been selected before dismissing
// Makes UX feel a little nicer
Expand All @@ -132,4 +171,22 @@ extension VerticalSavedPaymentMethodsViewController: PaymentMethodRowButtonDeleg
self?.delegate?.didSelectPaymentMethod(paymentMethod)
}
}

func didSelectRemoveButton(_ button: PaymentMethodRowButton) {
guard let paymentMethod = paymentMethod(from: button) else {
// TODO(porter) Handle error - no matching payment method found
return
}

print("Remove payment method with id: \(paymentMethod.stripeId)")
}

func didSelectEditButton(_ button: PaymentMethodRowButton) {
guard let paymentMethod = paymentMethod(from: button) else {
// TODO(porter) Handle error - no matching payment method found
return
}

print("Edit payment method with id: \(paymentMethod.stripeId)")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class BottomSheet3DS2ViewController: UIViewController {
lazy var navigationBar: SheetNavigationBar = {
let navBar = SheetNavigationBar(isTestMode: isTestMode,
appearance: appearance)
navBar.setStyle(.back)
navBar.setStyle(.back(showAdditionalButton: false))
navBar.delegate = self
return navBar
}()
Expand Down
Loading
Loading