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-303] Pledge Button #847

Merged
merged 8 commits into from
Sep 23, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 28 additions & 2 deletions Kickstarter-iOS/Views/Cells/PledgeCreditCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@ import Library
import Prelude
import UIKit

protocol PledgeCreditCardViewDelegate: AnyObject {
func pledgeCreditCardViewSelected(
Copy link
Contributor

Choose a reason for hiding this comment

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

This might overlap with some of the work happening in #835.

_ pledgeCreditCardView: PledgeCreditCardView,
paymentSourceId: String
)
}

final class PledgeCreditCardView: UIView {
// MARK: - Properties

private let viewModel: CreditCardCellViewModelType = CreditCardCellViewModel()

private let adaptableStackView: UIStackView = { UIStackView(frame: .zero) }()
weak var delegate: PledgeCreditCardViewDelegate?
private let expirationDateLabel: UILabel = { UILabel(frame: .zero) }()
private let imageView: UIImageView = { UIImageView(frame: .zero) }()
private let labelsStackView: UIStackView = { UIStackView(frame: .zero) }()
private let lastFourLabel: UILabel = { UILabel(frame: .zero) }()
private let rootStackView: UIStackView = { UIStackView(frame: .zero) }()
private let selectButton: UIButton = { UIButton(type: .custom) }()
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.

I mentioned in #835 that we should probably rename this 😬

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I agree - I really don't think we should have used the same view model as the one in the Settings screen at all though :\

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 had no idea it was being used in both places.


// MARK: - Lifecycle

Expand Down Expand Up @@ -46,6 +53,11 @@ final class PledgeCreditCardView: UIView {
_ = (self.rootStackView, self)
|> ksr_addSubviewToParent()
|> ksr_constrainViewToMarginsInParent()

self.selectButton.addTarget(
self, action: #selector(PledgeCreditCardView.selectButtonTapped),
for: .touchUpInside
)
}

private func setupConstraints() {
Expand Down Expand Up @@ -101,11 +113,25 @@ final class PledgeCreditCardView: UIView {
_ = self?.imageView
?|> \.image .~ image
}

self.viewModel.outputs.notifyDelegateOfCardSelected
.observeForUI()
.observeValues { [weak self] paymentSourceId in
guard let self = self else { return }

self.delegate?.pledgeCreditCardViewSelected(self, paymentSourceId: paymentSourceId)
}
}

func configureWith(value: GraphUserCreditCard.CreditCard) {
self.viewModel.inputs.configureWith(creditCard: value)
}

// MARK: - Accessors

@objc func selectButtonTapped() {
self.viewModel.inputs.selectButtonTapped()
}
}

// MARK: - Styles
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ protocol PledgePaymentMethodsViewControllerDelegate: AnyObject {
func pledgePaymentMethodsViewControllerDidTapApplePayButton(
_ viewController: PledgePaymentMethodsViewController
)
func pledgePaymentMethodsViewController(
_ viewController: PledgePaymentMethodsViewController,
didSelectCreditCard paymentSourceId: String
)
}

final class PledgePaymentMethodsViewController: UIViewController {
Expand All @@ -17,6 +21,7 @@ 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) }()
private lazy var rootStackView: UIStackView = { UIStackView(frame: .zero) }()
private lazy var scrollView: UIScrollView = { UIScrollView(frame: .zero) }()
private lazy var scrollViewContainer: UIView = { UIView(frame: .zero) }()
Expand Down Expand Up @@ -44,7 +49,10 @@ final class PledgePaymentMethodsViewController: UIViewController {
|> ksr_addSubviewToParent()
|> ksr_constrainViewToEdgesInParent()

_ = ([self.applePayButton, self.spacer, self.titleLabel, self.scrollViewContainer], self.rootStackView)
_ = (
[self.applePayButton, self.spacer, self.titleLabel, self.scrollViewContainer, self.pledgeButton],
self.rootStackView
)
|> ksr_addArrangedSubviewsToStackView()

_ = (self.rootStackView, self.view)
Expand All @@ -61,7 +69,8 @@ final class PledgePaymentMethodsViewController: UIViewController {
private func setupConstraints() {
NSLayoutConstraint.activate([
self.cardsStackView.heightAnchor.constraint(equalTo: self.scrollViewContainer.heightAnchor),
self.applePayButton.heightAnchor.constraint(greaterThanOrEqualToConstant: Styles.minTouchSize.height)
self.applePayButton.heightAnchor.constraint(greaterThanOrEqualToConstant: Styles.minTouchSize.height),
self.pledgeButton.heightAnchor.constraint(greaterThanOrEqualToConstant: Styles.minTouchSize.height)
])
}

Expand All @@ -78,6 +87,10 @@ final class PledgePaymentMethodsViewController: UIViewController {
_ = self.applePayButton
|> applePayButtonStyle

_ = self.pledgeButton
|> greenButtonStyle
|> UIButton.lens.title(for: .normal) %~ { _ in Strings.Pledge() }

_ = self.scrollView
|> checkoutBackgroundStyle

Expand Down Expand Up @@ -115,7 +128,16 @@ final class PledgePaymentMethodsViewController: UIViewController {
self.delegate?.pledgePaymentMethodsViewControllerDidTapApplePayButton(self)
}

self.viewModel.outputs.notifyDelegateCreditCardSelected
.observeForUI()
.observeValues { [weak self] paymentSourceId in
guard let self = self else { return }

self.delegate?.pledgePaymentMethodsViewController(self, didSelectCreditCard: paymentSourceId)
}

self.applePayButton.rac.hidden = self.viewModel.outputs.applePayButtonHidden
self.pledgeButton.rac.enabled = self.viewModel.outputs.pledgeButtonEnabled
}

// MARK: - Configuration
Expand All @@ -130,12 +152,16 @@ final class PledgePaymentMethodsViewController: UIViewController {
self.viewModel.inputs.configureWith(pledgePaymentMethodsValue)
}

// MARK: - Actions
// MARK: - Accessors

@objc private func applePayButtonTapped() {
self.viewModel.inputs.applePayButtonTapped()
}

func updatePledgeButton(_ enabled: Bool) {
self.viewModel.inputs.updatePledgeButtonEnabled(isEnabled: enabled)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The real "brains" of the pledge button enabled state is actually the PledgeViewModel. This is because we expect the pledge button's enabled state to be driven by a variety of factors: whether the reward minimum is met, whether a shipping location has been selected, etc. Since the PledgeViewModel is the only source of truth for this information, it needs to be the one to drive the pledge button's enabled state.

}

// MARK: - Functions

private func addCardsToStackView(_ cards: [GraphUserCreditCard.CreditCard]) {
Expand All @@ -145,6 +171,8 @@ final class PledgePaymentMethodsViewController: UIViewController {
.map { card -> PledgeCreditCardView in
let cardView = PledgeCreditCardView(frame: .zero)
cardView.configureWith(value: card)
cardView.delegate = self

return cardView
}

Expand Down Expand Up @@ -201,3 +229,9 @@ extension PledgePaymentMethodsViewController: AddNewCardViewControllerDelegate {
// TODO:
}
}

extension PledgePaymentMethodsViewController: PledgeCreditCardViewDelegate {
func pledgeCreditCardViewSelected(_: PledgeCreditCardView, paymentSourceId: String) {
self.viewModel.creditCardSelected(paymentSourceId: paymentSourceId)
}
}
16 changes: 14 additions & 2 deletions Kickstarter-iOS/Views/Controllers/PledgeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ final class PledgeViewController: UIViewController, MessageBannerViewControllerP
self?.viewModel.inputs.userSessionStarted()
}

self.viewModel.outputs.updatePledgeButtonEnabled
.observeForUI()
.observeValues { [weak self] isEnabled in
self?.paymentMethodsViewController.updatePledgeButton(isEnabled)
}

self.viewModel.outputs.goToApplePayPaymentAuthorization
.observeForControllerAction()
.observeValues { [weak self] paymentAuthorizationData in
Expand Down Expand Up @@ -384,11 +390,17 @@ extension PledgeViewController: PledgeViewControllerMessageDisplaying {

extension PledgeViewController: PledgePaymentMethodsViewControllerDelegate {
func pledgePaymentMethodsViewControllerDidTapApplePayButton(
_:
PledgePaymentMethodsViewController
_: PledgePaymentMethodsViewController
) {
self.viewModel.inputs.applePayButtonTapped()
}

func pledgePaymentMethodsViewController(
_: PledgePaymentMethodsViewController,
didSelectCreditCard paymentSourceId: String
) {
self.viewModel.inputs.creditCardSelected(with: paymentSourceId)
}
}

// MARK: - Styles
Expand Down
15 changes: 15 additions & 0 deletions Library/ViewModels/CreditCardCellViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import UIKit
public protocol CreditCardCellViewModelInputs {
/// Call to configure cell with card value.
func configureWith(creditCard: GraphUserCreditCard.CreditCard)
func selectButtonTapped()
}

public protocol CreditCardCellViewModelOutputs {
Expand All @@ -24,6 +25,9 @@ public protocol CreditCardCellViewModelOutputs {

/// Emits the formatted card's expirationdate.
var expirationDateText: Signal<String, Never> { get }

/// Emits the paymentSourceId of the current card
var notifyDelegateOfCardSelected: Signal<String, Never> { get }
}

public protocol CreditCardCellViewModelType {
Expand Down Expand Up @@ -52,18 +56,29 @@ public final class CreditCardCellViewModel: CreditCardCellViewModelInputs,

self.expirationDateText = self.cardProperty.signal.skipNil()
.map { Strings.Credit_card_expiration(expiration_date: $0.expirationDate()) }

self.notifyDelegateOfCardSelected = self.cardProperty.signal
.takeWhen(self.selectButtonTappedProperty.signal)
.skipNil()
.map { $0.id }
}

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

fileprivate let selectButtonTappedProperty = MutableProperty(())
public func selectButtonTapped() {
self.selectButtonTappedProperty.value = ()
}

public let cardImage: Signal<UIImage?, Never>
public let cardNumberAccessibilityLabel: Signal<String, Never>
public let cardNumberTextLongStyle: Signal<String, Never>
public let cardNumberTextShortStyle: Signal<String, Never>
public let expirationDateText: Signal<String, Never>
public let notifyDelegateOfCardSelected: Signal<String, Never>

public var inputs: CreditCardCellViewModelInputs { return self }
public var outputs: CreditCardCellViewModelOutputs { return self }
Expand Down
25 changes: 20 additions & 5 deletions Library/ViewModels/CreditCardCellViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import XCTest
internal final class CreditCardCellViewModelTests: TestCase {
internal let vm: CreditCardCellViewModelType = CreditCardCellViewModel()

let cardImage = TestObserver<UIImage?, Never>()
let cardNumberAccessibilityLabel = TestObserver<String, Never>()
let cardNumberTextLongStyle = TestObserver<String, Never>()
let cardNumberTextShortStyle = TestObserver<String, Never>()
let expirationDateText = TestObserver<String, Never>()
private let cardImage = TestObserver<UIImage?, Never>()
private let cardNumberAccessibilityLabel = TestObserver<String, Never>()
private let cardNumberTextLongStyle = TestObserver<String, Never>()
private let cardNumberTextShortStyle = TestObserver<String, Never>()
private let expirationDateText = TestObserver<String, Never>()
private let notifyDelegateOfCardSelected = TestObserver<String, Never>()

internal override func setUp() {
super.setUp()
Expand All @@ -22,6 +23,7 @@ internal final class CreditCardCellViewModelTests: TestCase {
self.vm.outputs.cardNumberTextLongStyle.observe(self.cardNumberTextLongStyle.observer)
self.vm.outputs.cardNumberTextShortStyle.observe(self.cardNumberTextShortStyle.observer)
self.vm.outputs.expirationDateText.observe(self.expirationDateText.observer)
self.vm.outputs.notifyDelegateOfCardSelected.observe(self.notifyDelegateOfCardSelected.observer)
}

func testCardInfoForSupportedCards() {
Expand Down Expand Up @@ -94,4 +96,17 @@ internal final class CreditCardCellViewModelTests: TestCase {
self.cardNumberTextShortStyle.assertLastValue("Ending in 1882")
self.expirationDateText.assertValue("Expires 01/2024")
}

func testSelectButtonTapped() {
let card = GraphUserCreditCard.amex
|> \.id .~ "123"

self.vm.inputs.configureWith(creditCard: card)

self.notifyDelegateOfCardSelected.assertDidNotEmitValue()

self.vm.inputs.selectButtonTapped()

self.notifyDelegateOfCardSelected.assertValues(["123"])
}
}
26 changes: 25 additions & 1 deletion Library/ViewModels/PledgePaymentMethodsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ public typealias PledgePaymentMethodsValue = (user: User, project: Project, appl
public protocol PledgePaymentMethodsViewModelInputs {
func applePayButtonTapped()
func configureWith(_ value: PledgePaymentMethodsValue)
func creditCardSelected(paymentSourceId: String)
func updatePledgeButtonEnabled(isEnabled: Bool)
func viewDidLoad()
}

public protocol PledgePaymentMethodsViewModelOutputs {
var notifyDelegateApplePayButtonTapped: Signal<Void, Never> { get }
var applePayButtonHidden: Signal<Bool, Never> { get }
var notifyDelegateApplePayButtonTapped: Signal<Void, Never> { get }
var notifyDelegateCreditCardSelected: Signal<String, Never> { get }
var notifyDelegateLoadPaymentMethodsError: Signal<String, Never> { get }
var pledgeButtonEnabled: Signal<Bool, Never> { get }
var reloadPaymentMethods: Signal<[GraphUserCreditCard.CreditCard], Never> { get }
}

Expand Down Expand Up @@ -44,6 +48,11 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT
.map(showApplePayButton(for:applePayCapable:))
.negate()

self.pledgeButtonEnabled = Signal.merge(
configureWithValue.mapConst(false),
self.pledgeButtonEnabledSignal
).skipRepeats()

self.reloadPaymentMethods = storedCardsEvent
.values()
.map { $0.me.storedCards.nodes }
Expand All @@ -53,6 +62,9 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT
self.notifyDelegateLoadPaymentMethodsError = storedCardsEvent
.errors()
.map { $0.localizedDescription }

self.notifyDelegateCreditCardSelected = self.creditCardSelectedSignal
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now this is very simple - has a credit card been selected? An upcoming PR will add the rest of the requirements for the pledge button being enabled: reward minimum is met, shipping rule selected, etc.

.skipRepeats()
}

private let applePayButtonTappedProperty = MutableProperty(())
Expand All @@ -65,6 +77,16 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT
self.configureWithValueProperty.value = value
}

private let (creditCardSelectedSignal, creditCardSelectedObserver) = Signal<String, Never>.pipe()
public func creditCardSelected(paymentSourceId: String) {
self.creditCardSelectedObserver.send(value: paymentSourceId)
}

private let (pledgeButtonEnabledSignal, pledgeButtonEnabledObserver) = Signal<Bool, Never>.pipe()
public func updatePledgeButtonEnabled(isEnabled: Bool) {
self.pledgeButtonEnabledObserver.send(value: isEnabled)
}

private let viewDidLoadProperty = MutableProperty(())
public func viewDidLoad() {
self.viewDidLoadProperty.value = ()
Expand All @@ -75,7 +97,9 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT

public let notifyDelegateApplePayButtonTapped: Signal<Void, Never>
public let applePayButtonHidden: Signal<Bool, Never>
public let notifyDelegateCreditCardSelected: Signal<String, Never>
public let notifyDelegateLoadPaymentMethodsError: Signal<String, Never>
public let pledgeButtonEnabled: Signal<Bool, Never>
public let reloadPaymentMethods: Signal<[GraphUserCreditCard.CreditCard], Never>
}

Expand Down
Loading