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

[ πŸ’³ Native Checkout ] Refresh Card List #835

Merged
merged 37 commits into from
Sep 26, 2019
Merged

Conversation

cdolm92
Copy link
Contributor

@cdolm92 cdolm92 commented Sep 11, 2019

πŸ“² 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 the cardStackView. We also made changes with how the card is configured. In the cards configureWith() we now have the parameter isNew: Bool so that it is in the selected state when added.

πŸ‘€ See

add-new-card

βœ… 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

  • Navigate to pledge screen and tap on add new card
  • Enter test card details and tap Done when done.
  • You should now see the newly added card in a selected state

Further Testing:

  • You will need to add at least two test cards. Tap Select on more than one card. Only last card tapped should remain selected.
  • Deselect/Select a card

@cdolm92 cdolm92 changed the title [ πŸ’³Native Checkout] Refresh Card List [ πŸ’³ Native Checkout] Refresh Card List Sep 11, 2019
@cdolm92 cdolm92 changed the title [ πŸ’³ Native Checkout] Refresh Card List [ πŸ’³ Native Checkout ] Refresh Card List Sep 11, 2019
@cdolm92 cdolm92 changed the title [ πŸ’³ Native Checkout ] Refresh Card List [ πŸ’³ Native Checkout ] Refresh Card List Sep 11, 2019
@ksr-ci-bot
Copy link

ksr-ci-bot commented Sep 17, 2019

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

@cdolm92 cdolm92 marked this pull request as ready for review September 20, 2019 17:58
Copy link
Contributor

@justinswart justinswart left a 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
Copy link
Contributor

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 backgroundColors.

This selected color looks like it's disabled though:

image

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) {
Copy link
Contributor

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()
Copy link
Contributor

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 }
Copy link
Contributor

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) {
Copy link
Contributor

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?

Copy link
Contributor

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(
Copy link
Contributor

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 }
Copy link
Contributor

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()
Copy link
Contributor

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)
Copy link
Contributor

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] {
Copy link
Contributor

@justinswart justinswart Sep 20, 2019

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.

@justinswart justinswart mentioned this pull request Sep 20, 2019
4 tasks
@justinswart
Copy link
Contributor

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
@justinswart
Copy link
Contributor

ok, @cdolm92 take a look at the last set of changes i pushed here. I've added tests but take a look to see if you think I missed some.

We still need to confirm the correct values for greyButtonWhiteTextStyle.

@justinswart
Copy link
Contributor

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 PledgeViewController based on the delegate saying that the call succeeded. Got held up with a few other things today so only managed to test this now, will catch up with you tomorrow about it.

@cdolm92 cdolm92 requested a review from dusi September 25, 2019 16:05
Copy link
Contributor

@justinswart justinswart left a comment

Choose a reason for hiding this comment

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

Cool lgtm!

Copy link
Contributor

@dusi dusi left a 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() {
Copy link
Contributor

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() {
Copy link
Contributor

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 }
Copy link
Contributor

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).

Copy link
Contributor

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?

Copy link
Contributor

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)
Copy link
Contributor

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)
Copy link
Contributor

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.

Do not do it this way
Screen Shot 2019-09-25 at 2 19 41 PM

But rather this way
Screen Shot 2019-09-25 at 2 19 54 PM

Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor

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
Copy link
Contributor

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..

  1. removing previous subviews
  2. creating new cells
  3. appending add new card view
  4. 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

  1. Create subview array (card views + add new card)
  2. Remove previous views from hierarchy
  3. Add new views to hierarchy

B) Extracted

  1. private func creditCardViews(for: cards) -> [PledgeCreditCardView] - this is where the construction happens
  2. `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)
  }
}

Copy link
Contributor

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 }
Copy link
Contributor

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?

Copy link
Contributor

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 πŸ‘

@justinswart justinswart assigned dusi and unassigned justinswart Sep 25, 2019
Copy link
Contributor

@dusi dusi left a comment

Choose a reason for hiding this comment

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

🚒 🚒 🚒
🚒 🚒 🚒
🚒 🚒 🚒

@cdolm92 cdolm92 merged commit 4ef012b into master Sep 26, 2019
@cdolm92 cdolm92 deleted the card-list-refresh branch September 26, 2019 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants