-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[ 💳 Native Checkout ] Refresh Card List #835
Conversation
…card-list-refresh
Generated by 🚫 Danger |
…card-list-refresh
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey I have some suggestions, let me know if you want to pair.
@@ -67,6 +74,9 @@ final class PledgeCreditCardView: UIView { | |||
_ = self.selectButton | |||
|> cardSelectButtonStyle |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have cardSelectButtonStyle
as the default style and emit a new style from the VM based on whether or not the card is selected, similar to how we've done it in RewardCardContainerViewModel
. Let's also add a cardSelectedButtonStyle
and use the mixDarker
/mixLighter
to get the highlighted/disabled backgroundColor
s.
This selected color looks like it's disabled though:
We should also emit the button title from the VM so that we can test it.
self.viewModel.inputs.configureWith(creditCard: value, isNew: isNew) | ||
} | ||
|
||
public func updateButtonState(_ selected: Bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably won't need this function if we're emitting the button style and title from the VM.
func didSelectCard(_ cardView: PledgeCreditCardView) | ||
} | ||
|
||
public class PledgeCreditCardView: UIView { | ||
// MARK: - Properties | ||
|
||
private let viewModel: CreditCardCellViewModelType = CreditCardCellViewModel() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be a good time to rename this to PledgeCreditCardViewModel
.
var newlyAddedCardSelected: Signal<Bool, Never> { get } | ||
|
||
/// Emits that select button should be in the selected state. | ||
var notifyButtonTapped: Signal<Void, Never> { get } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be notifyDelegateButtonTapped
just for clarity. Maybe even notifyDelegateSelectButtonTapped
? 🤔
} | ||
|
||
fileprivate let cardProperty = MutableProperty<GraphUserCreditCard.CreditCard?>(nil) | ||
public func configureWith(creditCard: GraphUserCreditCard.CreditCard) { | ||
fileprivate let isNewCardProperty = MutableProperty(false) | ||
public func configureWith(creditCard: GraphUserCreditCard.CreditCard, isNew: Bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for something like this we typically have one propery like MutableProperty<(GraphUserCreditCard.CreditCard, Bool)>
and maybe call it configDataPropery
and then in the init we split it out. Have we used this pattern of two properties elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to change that but yes, we have used the two property pattern before:
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/SettingsNewslettersCellViewModel.swift#L104
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/SettingsNotificationCellViewModel.swift#L176
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/TwoFactorViewModel.swift#L166
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/DashboardProjectsDrawerCellViewModel.swift#L61
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/DashboardTitleViewViewModel.swift#L89
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/DiscoveryPostcardViewModel.swift#L268
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/FindFriendsFriendFollowCellViewModel.swift#L178
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/FindFriendsStatsCellViewModel.swift#L65
https://github.com/kickstarter/ios-oss/blob/master/Library/ViewModels/LoginViewModel.swift#L210
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh ok, well I wouldn't worry about it too much then.
@@ -11,6 +11,10 @@ internal protocol AddNewCardViewControllerDelegate: AnyObject { | |||
didSucceedWithMessage message: String | |||
) | |||
func addNewCardViewControllerDismissed(_ viewController: AddNewCardViewController) | |||
func addNewCardViewController( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be:
func addNewCardViewController(
_ viewController: AddNewCardViewController,
didAdd newCard: GraphUserCreditCard.CreditCard
)
@@ -39,6 +39,7 @@ public protocol AddNewCardViewModelOutputs { | |||
var saveButtonIsEnabled: Signal<Bool, Never> { get } | |||
var setStripePublishableKey: Signal<String, Never> { get } | |||
var zipcodeTextFieldBecomeFirstResponder: Signal<Void, Never> { get } | |||
var newCardAdded: Signal<GraphUserCreditCard.CreditCard, Never> { get } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
notifyDelegateNewCardAdded
|
||
public typealias PledgePaymentMethodsValue = (user: User, project: Project, applePayCapable: Bool) | ||
|
||
public protocol PledgePaymentMethodsViewModelInputs { | ||
func addNewCardSucceeded() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we don't use this input.
func applePayButtonTapped() | ||
func configureWith(_ value: PledgePaymentMethodsValue) | ||
func didCreateCards(_ cards: [UIView]) | ||
func successfullyAddedCard(newCard: GraphUserCreditCard.CreditCard) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addNewCardViewControllerDidAdd(newCard card: GraphUserCreditCard.CreditCard)
@@ -178,6 +193,16 @@ final class PledgePaymentMethodsViewController: UIViewController { | |||
} | |||
} | |||
|
|||
extension PledgePaymentMethodsViewController: PledgeCreditCardViewDelegate { | |||
func didSelectCard(_ cardView: PledgeCreditCardView) { | |||
if let cards: [PledgeCreditCardView] = self.viewModel.outputs.savedCards() as? [PledgeCreditCardView] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of passing these views through the VM, let's keep references to them in an array in the VC. We can add them to that array when we create the views. That way we can just iterate over the array when selection changes and inform the view of the selected card. If that view is configured with that card then it can update its selected state.
Also just noticed that there's a fair bit of overlap with #847 in terms of the card selection stuff, we might want to just sync up to align on that. |
# Conflicts: # Kickstarter-iOS/Views/Cells/PledgeCreditCardView.swift # Kickstarter-iOS/Views/Controllers/PledgePaymentMethodsViewController.swift # Library/ViewModels/CreditCardCellViewModel.swift # Library/ViewModels/CreditCardCellViewModelTests.swift # Library/ViewModels/PledgePaymentMethodsViewModel.swift
… tests, button selection
…card-list-refresh
Hey @cdolm92 just tested the dismiss on save - it looks like this dismisses in settings too, so I think what we'd need to do is instead dismiss the modal from the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool lgtm!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had couple suggestions and comments...I think the important thing to fix is making sure that we don't increment the amount of subviews in stack view's hierarchy.
@@ -70,16 +70,12 @@ final class PledgeCreditCardView: UIView { | |||
|
|||
// MARK: - Styles | |||
|
|||
override func bindStyles() { | |||
public override func bindStyles() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is public
needed here? Are we exposing this to some other object?
} | ||
|
||
override func bindViewModel() { | ||
public override func bindViewModel() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above (re: public
)
self.viewModel.outputs.newCardAdded | ||
.observeForUI() | ||
.observeValues { [weak self] newCard in | ||
guard let _self = self else { return } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swift 4.2 allows us to simply do guard let self = self else { .. }
. I know if you search the project for guard let _self = self
there's 52
results as oppose to 27
when you do guard let self = self
but I think that's because we've been moving towards self = self
and haven't yet refactored the rest of the code base (good idea for an investment day or follow up PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also this same PR uses both variations _self = self
and self = self
so one of the reasons we should probably go with the newer option (self = self
).
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's do a sweep as part of an investment day to establish a convention for this.
self?.addCardsToStackView(cards) | ||
guard let self = self else { return } | ||
self.scrollView.setContentOffset(.zero, animated: false) | ||
self.addCardsToStackView(cards) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this operation will happen every time the payment methods refresh I'd suggest different naming ... using add
implies that something is being appended ... how about updateStackViewWith(cards)
or similar?
@@ -167,16 +176,24 @@ final class PledgePaymentMethodsViewController: UIViewController { | |||
private func addCardsToStackView(_ cards: [GraphUserCreditCard.CreditCard]) { | |||
self.cardsStackView.arrangedSubviews.forEach(self.cardsStackView.removeArrangedSubview) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was recently an article on NSHipster - about stack views with an interesting tidbit - calling removeArrangedSubview
does not really remove the subview from the superview so it still in the view hierarchy.
It should be safer iterating over subviews and calling .removeFromSuperview
See these 2 playgrounds and how the amount of subviews differ in the resolution panel.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, thanks for the reminder about this!
|
||
return cardView | ||
} | ||
|
||
let addNewCardView: UIView = PledgeAddNewCardView(frame: .zero) | ||
self.cardViews = cardViews |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the only reason for us to hold a reference to this list is for being able to iterate over an un-select all...which we could potentially even do like so:
self?.cardsStackView.arrangedSubviews.forEach { arrangedSubview in
guard let cardView = arrangedSubview as? PledgeCreditCardView else { return }
cardView.setSelectedCard(card)
}
This would remove the need for the cardViews
property altogether.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea!
let cardViews: [UIView] = cards | ||
let selectedCard = cards.first | ||
|
||
let cardViews = cards |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method seems to be doing quite a bit..
- removing previous subviews
- creating new cells
- appending add new card view
- adding to view hierarchy
Any chance we can split these events into more digestible pieces or at least arrange them better?
So for example:
A) Better ordering
- Create subview array (card views + add new card)
- Remove previous views from hierarchy
- Add new views to hierarchy
B) Extracted
private func creditCardViews(for: cards) -> [PledgeCreditCardView]
- this is where the construction happens- `private func cardViews
private func creditCardViews(for: cards) -> [PledgeCreditCardView] {
return cards
.map { card -> PledgeCreditCardView in
let cardView = PledgeCreditCardView(frame: .zero)
cardView.delegate = self
cardView.configureWith(value: card)
if let selectedCard = selectedCard {
cardView.setSelectedCard(selectedCard)
}
return cardView
}
}
private func cardViews(for: cards) -> [UIView] {
let creditCardViews = self.creditCardViews(for: cards)
let addCreditCardView = PledgeAddNewCardView = PledgeAddNewCardView(frame: .zero)
|> \.delegate .~ self
return creditCardViews.append(addCreditCardView) // or creditCardViews + [addCreditCardView]
}
private func updateStackView(with cards: [GraphUserCreditCard.CreditCard]) {
// Create views
let cardViews = self.cardViews(for: cards)
// Remove previous views from hierarchy
self.cardsStackView.arrangedStackView.forEach { subview in
subview.removeFromSuperview
}
// Add new views
cardViews.forEach { cardView in
self.cardsStackView.addArrangedSubview(cardView)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, broke this up a bit, not quite as much but that'll do 😄. Good suggestion.
@@ -35,6 +35,7 @@ public protocol AddNewCardViewModelOutputs { | |||
var creditCardValidationErrorContainerHidden: Signal<Bool, Never> { get } | |||
var cardholderNameBecomeFirstResponder: Signal<Void, Never> { get } | |||
var dismissKeyboard: Signal<Void, Never> { get } | |||
var newCardAdded: Signal<GraphUserCreditCard.CreditCard, Never> { get } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order for us to make this more readable, maybe we can define a typealias..
Currently there are 2 CreditCard
structs
- GraphUserCreditCard.CreditCard
- Query.User.CreditCard
How about we declare typealias public typealias GraphCreditCard = GraphUserCreditCard.CreditCard
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, let's not do that as part of this PR though 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚢 🚢 🚢
🚢 🚢 🚢
🚢 🚢 🚢
…card-list-refresh
📲 What
User can now add new credit card from the pledge screen.
🤔 Why
Users can now add a preferred credit card without leaving the pledge flow.
🛠 How
We have a method called
insertNewCard(_ newCard: GraphUserCreditCard.CreditCard)
that inserts the card in the first position of thecardStackView
. We also made changes with how the card is configured. In the cardsconfigureWith()
we now have the parameterisNew: Bool
so that it is in the selected state when added.👀 See
✅ Acceptance criteria
Note: Test this on staging. Make sure the recommended projects setting is disabled and use a test card from here: https://stripe.com/docs/testing
add new card
Done
when done.Further Testing:
Select
on more than one card. Only last card tapped should remain selected.