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

[NT-364]Unsupported cards carousel #897

Merged
merged 36 commits into from
Oct 24, 2019
Merged

Conversation

cdolm92
Copy link
Contributor

@cdolm92 cdolm92 commented Oct 16, 2019

📲 What

We have disabled the select button, changed the button title to No longer available and provided copy below the unavailable credit card so that users understand that they cannot pledge with an unavailable card type.

🤔 Why

Users want to clearly and easily see which of their saved payment methods may not be used to back a particular project in the Pledge screen so that they know to select or add a different payment method to complete their pledge.

🛠 How

We determine unavailable card types by checking if the user card is included in project.availableCardTypes.

👀 See

Trello, screenshots, external resources?

Before 🐛 After 🦋
Simulator Screen Shot - iPhone XS - 2019-10-16 at 16 57 59 Simulator Screen Shot - iPhone XS - 2019-10-16 at 16 45 58

✅ Acceptance criteria

  • Navigate to a non-USD project. Scroll through the available payment options, all unacceptable payment options should have a disabled button with the title text that reads No longer available
  • Copy below the Cardview should read You can’t use this credit card to back a project from [project location]. Note: this will change to project country in an upcoming PR

✍🏾 TO DO

  • Snapshot testing for cards

@cdolm92 cdolm92 changed the title Unsupported cards carousel [NT-364]Unsupported cards carousel Oct 16, 2019
Copy link
Contributor

@ifbarrera ifbarrera left a comment

Choose a reason for hiding this comment

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

There's a lot of logic happening in the PledgePaymentMethodsViewController which should probably be in a view model so it can be tested. I'm wondering if there's a reason we didn't just add the "card unavailable" label to the PledgeCreditCardView and configured the PledgeCreditCardViewModel with a boolean that lets it determine whether or not it should be an unavailable view. This would allow us to better test the logic around disabling unavailable card types, and might mitigate some of the complexity that doing this in the view controller is causing.

I also noticed a couple of bugs that might have to do with the way we're adding the card views:

After adding a new card, it looks like there's a duplicate set of unavailable card views in the list.

toBws7mRL0

Also, the unavailable card label doesn't seem to be responding to dynamic type:

Simulator Screen Shot - iPhone 8 - 2019-10-17 at 17 53 58

@@ -169,3 +174,16 @@ private let labelsStackViewStyle: StackViewStyle = { stackView in
|> \.axis .~ .vertical
|> \.spacing .~ Styles.gridHalf(1)
}

private let selectButtonStyle: ButtonStyle = { button in
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is it necessary to create a custom style for this button? The blackButtonStyle should have a .disabled style, in which case we should be able to just set the button to disabled to get the correct treatment 🤔

@@ -21,6 +21,8 @@ final class PledgePaymentMethodsViewController: UIViewController {
private lazy var cardsStackView: UIStackView = { UIStackView(frame: .zero) }()
internal weak var delegate: PledgePaymentMethodsViewControllerDelegate?
internal weak var messageDisplayingDelegate: PledgeViewControllerMessageDisplaying?
private lazy var pledgeButton: UIButton = { UIButton.init(type: .custom) }()
Copy link
Contributor

Choose a reason for hiding this comment

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

Did this get accidentally re-added as part of a merge? I believe @justinswart refactored this button into the main PledgeViewController

@@ -98,10 +104,10 @@ final class PledgePaymentMethodsViewController: UIViewController {

self.viewModel.outputs.reloadPaymentMethods
.observeForUI()
.observeValues { [weak self] cards in
.observeValues { [weak self] cardValues in
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason for this rename?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see below the naming has changed.

cardView.configureWith(value: card)

guard let cardBrand = card.type?.rawValue else { return self.pledgeCardStackView }
let isAvailableCardType = availableCardTypes.contains(cardBrand)
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm I feel like this logic should probably live in the view model.

|> \.axis .~ .vertical
|> \.spacing .~ Styles.grid(2)

let label = UILabel(frame: .zero)
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we put this label within the PledgeCreditCardView itself, it might be easier to manage it's hide/shown state from PledgeCreditCardViewModel.

@@ -25,6 +28,9 @@ public protocol PledgeCreditCardViewModelOutputs {
/// Emits a formatted string containing the card's last four digits with the format: Ending in 8844.
var cardNumberTextShortStyle: Signal<String, Never> { get }

/// Emits whether or not the button is enabled.
var disableButton: Signal<Bool, 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.

Usually we try to match the output name with the property being modified: so here it would be var selectButtonEnabled: Signal<Bool, Never> so that in the rac binding it would be:

self.selectButton.rac.enabled = self.viewModel.outputs.selectButtonEnabled

@@ -95,6 +103,11 @@ public final class PledgeCreditCardViewModel: PledgeCreditCardViewModelInputs,
self.selectedCardProperty.value = creditCard
}

private let disabledCardProperty = MutableProperty<Bool?>(nil)
public func setDisabledCard(_ disabled: 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 would if we could have just wrapped up the card's disabled state as part of the configure function:

  typealias PledgeCreditCardValue = (card: GraphUserCreditCard.CreditCard, isEnabled: Bool)

  fileprivate let creditCardProperty = MutableProperty<GraphUserCreditCard.CreditCard?>(nil)
  public func configureWith(value: PledgeCreditCardValue) {
    self.creditCardProperty.value = value
  }

Then every card would be configured with whether or not it's enabled and we wouldn't need to do all that logic in the PledgePaymentMethodsViewController.

@@ -4,9 +4,14 @@ import ReactiveExtensions
import ReactiveSwift
import UIKit

public typealias PledgeCreditCardValue = (
card: GraphUserCreditCard.CreditCard,
isEnabled: Bool, projectCountry: String?
Copy link
Contributor

Choose a reason for hiding this comment

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

projectCountry probably doesn't have to be optional.

Copy link
Contributor

Choose a reason for hiding this comment

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

@cdolm92 does this have to be optional?

}

fileprivate let creditCardProperty = MutableProperty<GraphUserCreditCard.CreditCard?>(nil)
public func configureWith(value: GraphUserCreditCard.CreditCard) {
fileprivate let creditCardProperty = MutableProperty<PledgeCreditCardValue?>(nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we update the name of this property to match the fact that we now configure with PledgeCreditCardValue? Maybe pledgeCreditCardValueProperty?

project.map { $0.location.displayableName }
).map { $0 as CardViewValues }

self.reloadPaymentMethods = cardValues
Copy link
Contributor

Choose a reason for hiding this comment

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

Here is where we could move even more logic out of the view controller – something we could do is instead of returning CardViewValues, return an array of PledgeCreditCardValue:

    self.reloadPaymentMethods = Signal.combineLatest(
      cards,
      availableCardTypes,
      project.map { $0.location.displayableName }
    ).map { cards, availableCardTypes, projectLocation in
      return cards.map { card in
        let isEnabled = availableCardTypes.contains(card.type.rawValue)

        return (card, isEnabled, projectLocation)
      }
    }

Then we can test that given a list of stored cards, and a list of availableCardTypes in a project, that the output is exactly what we're feeding into each PledgeCreditCardViewModel to configure the card view.

@ksr-ci-bot
Copy link

ksr-ci-bot commented Oct 22, 2019

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

@@ -5,6 +5,10 @@ import ReactiveSwift
import UIKit

public typealias PledgePaymentMethodsValue = (user: User, project: Project, applePayCapable: Bool)
public typealias CardViewValues = (
cardAndIsAvailableCardType: [(cards: GraphUserCreditCard.CreditCard, cardTypeIsAvailable: Bool)],

Choose a reason for hiding this comment

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

🚫 Line should be 110 characters or less: currently 122 characters
line_length PledgePaymentMethodsViewModel.swift:9


if let selectedCard = selectedCard {
cardView.setSelectedCard(selectedCard)
if isAvailableCardType {
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 might be able to simplify this logic by doing:

      let card = cardAndAvailableType.cards
      let isAvailableCardType = cardAndAvailableType.cardTypeIsAvailable
      let projectCountry = cardValues.projectCountry

      cardView.configureWith(value: (card: card, isEnabled: isAvailableCardType, projectCountry: projectCountry))

      if let selectedCard = selectedCard {
        cardView.setSelectedCard(selectedCard)
      }

@@ -229,6 +236,14 @@ final class PledgePaymentMethodsViewController: UIViewController {
|> \.font .~ UIFont.ksr_caption1()
|> \.textAlignment .~ .center
}

private let cardRestrictionLabelStyle: LabelStyle = { label in
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this still being used in this file?

@@ -4,9 +4,14 @@ import ReactiveExtensions
import ReactiveSwift
import UIKit

public typealias PledgeCreditCardValue = (
card: GraphUserCreditCard.CreditCard,
isEnabled: Bool, projectCountry: String?
Copy link
Contributor

Choose a reason for hiding this comment

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

@cdolm92 does this have to be optional?

@@ -13,8 +13,12 @@ internal final class PledgeCreditCardViewModelTests: TestCase {
private let cardNumberTextShortStyle = TestObserver<String, Never>()
private let expirationDateText = TestObserver<String, Never>()
private let notifyDelegateOfCardSelected = TestObserver<String, Never>()
private let selectButtonEnable = TestObserver<Bool, Never>()
Copy link
Contributor

Choose a reason for hiding this comment

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

Small typo here, should be selectButtonEnabled.

@@ -24,12 +28,16 @@ internal final class PledgeCreditCardViewModelTests: TestCase {
self.vm.outputs.cardNumberTextShortStyle.observe(self.cardNumberTextShortStyle.observer)
self.vm.outputs.expirationDateText.observe(self.expirationDateText.observer)
self.vm.outputs.notifyDelegateOfCardSelected.observe(self.notifyDelegateOfCardSelected.observer)
self.vm.outputs.selectButtonEnabled.observe(self.selectButtonEnable.observer) //
Copy link
Contributor

Choose a reason for hiding this comment

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

There's some trailing slashes here 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh right, I still need to test those

@@ -5,6 +5,10 @@ import ReactiveSwift
import UIKit

public typealias PledgePaymentMethodsValue = (user: User, project: Project, applePayCapable: Bool)
public typealias CardViewValues = (
cardAndIsAvailableCardType: [(cards: GraphUserCreditCard.CreditCard, cardTypeIsAvailable: 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 wonder if this should be cardAndIsAvailableCardType: (card: GraphUserCreditCard.CreditCard, cardTypeIsAvailable: Bool) (singular card instead of cards).

Library/ViewModels/PledgePaymentMethodsViewModel.swift Outdated Show resolved Hide resolved
Library/ViewModels/PledgePaymentMethodsViewModel.swift Outdated Show resolved Hide resolved

self.vm.inputs.configureWith((User.template, Project.template, false))
self.vm.inputs.viewDidLoad()

self.scheduler.run()

self.reloadPaymentMethods.assertValue(response.me.storedCards.nodes)
self.reloadPaymentMethodsCards.assertValue(response.me.storedCards.nodes)
self.reloadPaymentMethodsAvailableCardTypes.assertValues([
Copy link
Contributor

Choose a reason for hiding this comment

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

It's a little difficult to follow these assertions when they're split up. Is it possible to do the assertions on the tuples? Something like this:

self.reloadPaymentMethodsCardsAndAvailable.assertValues([(GraphUserCreditCard.visa, true), (GraphUser CreditCard.masterCard, true)...etc])

@cdolm92 cdolm92 merged commit 4c65b33 into master Oct 24, 2019
@cdolm92 cdolm92 deleted the unsupported-cards-carousel branch October 24, 2019 16:56
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

3 participants