Skip to content

Commit

Permalink
[PAY-1816] Payment Sheet Loading States (#1713)
Browse files Browse the repository at this point in the history
  • Loading branch information
msadoon committed Aug 25, 2022
1 parent 8ccf0b4 commit 423abaa
Show file tree
Hide file tree
Showing 149 changed files with 387 additions and 36 deletions.
21 changes: 19 additions & 2 deletions Kickstarter-iOS/DataSources/PledgePaymentMethodsDataSource.swift
Expand Up @@ -46,12 +46,29 @@ internal final class PledgePaymentMethodsDataSource: ValueCellDataSource {
}

self.set(
values: [()],
values: [false],
cellClass: PledgePaymentMethodAddCell.self,
inSection: PaymentMethodsTableViewSection.addNewCard.rawValue
)
}

func updateAddNewPaymentCardLoad(state: Bool) {
self.set(
values: [state],
cellClass: PledgePaymentMethodAddCell.self,
inSection: PaymentMethodsTableViewSection.addNewCard.rawValue
)
}

func isLoadingStateCell(indexPath: IndexPath) -> Bool {
guard let value = self[indexPath] as? Bool,
value else {
return false
}

return true
}

override func configureCell(tableCell cell: UITableViewCell, withValue value: Any) {
switch (cell, value) {
case let (
Expand All @@ -64,7 +81,7 @@ internal final class PledgePaymentMethodsDataSource: ValueCellDataSource {
value as PaymentSheetPaymentMethodCellData
):
cell.configureWith(value: value)
case let (cell as PledgePaymentMethodAddCell, value as Void):
case let (cell as PledgePaymentMethodAddCell, value as Bool):
cell.configureWith(value: value)
case let (cell as PledgePaymentMethodLoadingCell, value as Void):
cell.configureWith(value: value)
Expand Down
Expand Up @@ -135,4 +135,17 @@ final class PledgePaymentMethodsDataSourceTests: XCTestCase {
self.dataSource.numberOfItems(in: PaymentMethodsTableViewSection.addNewCard.rawValue)
)
}

func testLoadingState_AddNewCardButton() {
self.dataSource.load([], paymentSheetCards: [], isLoading: true)
self.dataSource.updateAddNewPaymentCardLoad(state: true)

let addNewCardIndexPath = IndexPath(row: 0, section: 1)

XCTAssertTrue(self.dataSource.isLoadingStateCell(indexPath: addNewCardIndexPath))

self.dataSource.updateAddNewPaymentCardLoad(state: false)

XCTAssertFalse(self.dataSource.isLoadingStateCell(indexPath: addNewCardIndexPath))
}
}
61 changes: 54 additions & 7 deletions Kickstarter-iOS/Views/Cells/PledgePaymentMethodAddCell.swift
Expand Up @@ -9,6 +9,20 @@ final class PledgePaymentMethodAddCell: UITableViewCell, ValueCell {
private lazy var selectionView: UIView = { UIView(frame: .zero) }()
private lazy var addButton: UIButton = { UIButton(type: .custom) }()

private lazy var containerView: UIStackView = {
UIStackView(frame: .zero)
|> \.translatesAutoresizingMaskIntoConstraints .~ false
}()

private lazy var activityIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(frame: .zero)
|> \.translatesAutoresizingMaskIntoConstraints .~ false
indicator.startAnimating()
return indicator
}()

private let viewModel: PledgePaymentMethodAddCellViewModelType = PledgePaymentMethodAddCellViewModel()

// MARK: - Lifecycle

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
Expand All @@ -23,21 +37,32 @@ final class PledgePaymentMethodAddCell: UITableViewCell, ValueCell {
fatalError("init(coder:) has not been implemented")
}

// MARK: - View model

override func bindViewModel() {
self.activityIndicator.rac.animating = self.viewModel.outputs.showLoading
self.addButton.rac.hidden = self.viewModel.outputs.showLoading
}

// MARK: - Configuration

private func configureSubviews() {
_ = (self.addButton, self.contentView)
_ = (self.containerView, self.contentView)
|> ksr_addSubviewToParent()
}

private func setupConstraints() {
_ = (self.addButton, self.contentView)
_ = ([self.activityIndicator, self.addButton], self.containerView)
|> ksr_addArrangedSubviewsToStackView()

_ = (self.containerView, self.contentView)
|> ksr_constrainViewToEdgesInParent()
|> ksr_constrainViewToCenterInParent()

_ = self.addButton.heightAnchor.constraint(equalToConstant: Styles.grid(9))
|> \.priority .~ .defaultHigh
|> \.isActive .~ true
NSLayoutConstraint.activate([
self.activityIndicator.widthAnchor.constraint(equalToConstant: Styles.grid(9)),
self.containerView.heightAnchor.constraint(equalToConstant: Styles.grid(9))
])
}

// MARK: - Styles
Expand All @@ -53,18 +78,26 @@ final class PledgePaymentMethodAddCell: UITableViewCell, ValueCell {

_ = self.addButton
|> addButtonStyle

_ = self.activityIndicator
|> activityIndicatorStyle

_ = self.containerView
|> stackViewStyle
}

func configureWith(value _: Void) {}
func configureWith(value flag: Bool) {
self.viewModel.inputs.configureWith(value: flag)
}
}

// MARK: - Styles

private let addButtonStyle: ButtonStyle = { button in
button
|> UIButton.lens.title(for: .normal) %~ { _ in Strings.New_payment_method() }
|> UIButton.lens.titleLabel.font .~ UIFont.boldSystemFont(ofSize: 15)
|> UIButton.lens.image(for: .normal) .~ Library.image(named: "icon-add-round-green")
|> UIButton.lens.title(for: .normal) %~ { _ in Strings.New_payment_method() }
|> UIButton.lens.isUserInteractionEnabled .~ false
|> UIButton.lens.titleColor(for: .normal) .~ .ksr_create_700
|> UIButton.lens.tintColor .~ .ksr_create_700
Expand All @@ -75,3 +108,17 @@ private let selectionViewStyle: ViewStyle = { view in
view
|> \.backgroundColor .~ .ksr_support_100
}

private let activityIndicatorStyle: ActivityIndicatorStyle = { activityIndicator in
activityIndicator
|> \.color .~ UIColor.ksr_support_400
|> \.hidesWhenStopped .~ true
}

private let stackViewStyle: StackViewStyle = { stackView in
stackView
|> \.axis .~ .horizontal
|> \.alignment .~ .center
|> \.spacing .~ Styles.grid(0)
|> \.isLayoutMarginsRelativeArrangement .~ true
}
Expand Up @@ -10,11 +10,6 @@ protocol PledgePaymentMethodsViewControllerDelegate: AnyObject {
_ viewController: PledgePaymentMethodsViewController,
didSelectCreditCard paymentSource: PaymentSourceSelected
)

func pledgePaymentMethodsViewController(
_ viewController: PledgePaymentMethodsViewController,
loading flag: Bool
)
}

final class PledgePaymentMethodsViewController: UIViewController {
Expand Down Expand Up @@ -137,12 +132,12 @@ final class PledgePaymentMethodsViewController: UIViewController {
strongSelf.goToPaymentSheet(data: data)
}

self.viewModel.outputs.showLoadingIndicatorView
self.viewModel.outputs.updateAddNewCardLoading
.observeForUI()
.observeValues { [weak self] showLoadingIndicator in
guard let strongSelf = self else { return }

strongSelf.delegate?.pledgePaymentMethodsViewController(strongSelf, loading: showLoadingIndicator)
strongSelf.updateAddNewPaymentMethodButtonLoading(state: showLoadingIndicator)
}
}

Expand Down Expand Up @@ -170,10 +165,10 @@ final class PledgePaymentMethodsViewController: UIViewController {
configuration: data.configuration
) { [weak self] result in
guard let strongSelf = self else { return }
strongSelf.delegate?.pledgePaymentMethodsViewController(strongSelf, loading: false)

switch result {
case let .failure(error):
strongSelf.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)
strongSelf.messageDisplayingDelegate?
.pledgeViewController(strongSelf, didErrorWith: error.localizedDescription)
case let .success(paymentSheetFlowController):
Expand All @@ -188,9 +183,19 @@ final class PledgePaymentMethodsViewController: UIViewController {
}

private func confirmPaymentResult(with clientSecret: String) {
guard self.paymentSheetFlowController?.paymentOption != nil else {
self.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)

return
}

self.paymentSheetFlowController?.confirm(from: self) { [weak self] paymentResult in
guard let strongSelf = self,
let existingPaymentOption = strongSelf.paymentSheetFlowController?.paymentOption else { return }

guard let strongSelf = self else { return }

strongSelf.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)

guard let existingPaymentOption = strongSelf.paymentSheetFlowController?.paymentOption else { return }

switch paymentResult {
case .completed:
Expand All @@ -205,6 +210,14 @@ final class PledgePaymentMethodsViewController: UIViewController {
}
}
}

private func updateAddNewPaymentMethodButtonLoading(state: Bool) {
self.dataSource.updateAddNewPaymentCardLoad(state: state)

let addNewCardButtonSection = self.tableView.numberOfSections - 1

self.tableView.reloadSections([addNewCardButtonSection], with: .none)
}
}

// MARK: - AddNewCardViewControllerDelegate
Expand All @@ -229,6 +242,9 @@ extension PledgePaymentMethodsViewController: AddNewCardViewControllerDelegate {

extension PledgePaymentMethodsViewController: UITableViewDelegate {
func tableView(_: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard !self.dataSource.isLoadingStateCell(indexPath: indexPath) else {
return nil
}
return self.viewModel.inputs.willSelectRowAtIndexPath(indexPath)
}

Expand All @@ -238,3 +254,11 @@ extension PledgePaymentMethodsViewController: UITableViewDelegate {
self.viewModel.inputs.didSelectRowAtIndexPath(indexPath)
}
}

// MARK: - PaymentSheetAppearanceDelegate

extension PledgePaymentMethodsViewController: PaymentSheetAppearanceDelegate {
func pledgeViewControllerPaymentSheet(_: PledgeViewController, hidden: Bool) {
self.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: hidden)
}
}
@@ -0,0 +1,115 @@
@testable import Kickstarter_Framework
@testable import KsApi
@testable import Library
import Prelude
import UIKit

final class PledgePaymentMethodsViewControllerTests: TestCase {
private let userWithCards = GraphUser.template |> \.storedCards .~ UserCreditCards(
storedCards: [
UserCreditCards.visa,
UserCreditCards.masterCard
]
)

override func setUp() {
super.setUp()
AppEnvironment.pushEnvironment(mainBundle: Bundle.framework)
UIView.setAnimationsEnabled(false)
}

override func tearDown() {
AppEnvironment.popEnvironment()
UIView.setAnimationsEnabled(true)

super.tearDown()
}

func testView_PledgeContext_AddNewCardNonLoadingState_Success() {
let response = UserEnvelope<GraphUser>(me: self.userWithCards)
let envelope = ClientSecretEnvelope(clientSecret: "test")
let mockOptimizelyClient = MockOptimizelyClient()
|> \.features .~ [OptimizelyFeature.paymentSheetEnabled.rawValue: true]
let mockService = MockService(
createStripeSetupIntentResult: .success(envelope),
fetchGraphUserResult: .success(response)
)
let project = Project.template
|> \.availableCardTypes .~ [CreditCardType.visa.rawValue, CreditCardType.mastercard.rawValue]

combos(Language.allLanguages, [Device.pad, Device.phone4_7inch]).forEach { language, device in
withEnvironment(
apiService: mockService,
currentUser: User.template,
language: language,
optimizelyClient: mockOptimizelyClient
) {
let controller = PledgePaymentMethodsViewController.instantiate()

let reward = Reward.template
let data = PledgePaymentMethodsValue(
user: .template,
project: project,
reward: reward,
context: .pledge,
refTag: nil
)

controller.configure(with: data)

let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)
parent.view.frame.size.height = 400

self.scheduler.advance(by: .seconds(1))

FBSnapshotVerifyView(parent.view, identifier: "lang_\(language)_device_\(device)")
}
}
}

func testView_PledgeContext_AddNewCardLoadingState_Success() {
let response = UserEnvelope<GraphUser>(me: self.userWithCards)
let mockOptimizelyClient = MockOptimizelyClient()
|> \.features .~ [OptimizelyFeature.paymentSheetEnabled.rawValue: true]
/// Using .failure case to prevent real Stripe sheet from being shown.
let mockService = MockService(
createStripeSetupIntentResult: .failure(.couldNotParseJSON),
fetchGraphUserResult: .success(response)
)
let project = Project.template
|> \.availableCardTypes .~ [CreditCardType.visa.rawValue, CreditCardType.mastercard.rawValue]

combos(Language.allLanguages, [Device.pad, Device.phone4_7inch]).forEach { language, device in
withEnvironment(
apiService: mockService,
currentUser: User.template,
language: language,
optimizelyClient: mockOptimizelyClient
) {
let controller = PledgePaymentMethodsViewController.instantiate()

let reward = Reward.template
let data = PledgePaymentMethodsValue(
user: .template,
project: project,
reward: reward,
context: .pledge,
refTag: nil
)

controller.configure(with: data)

let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)
parent.view.frame.size.height = 400

self.scheduler.advance(by: .seconds(1))

controller.pledgeViewControllerPaymentSheet(PledgeViewController(), hidden: false)

self.scheduler.advance(by: .seconds(1))

FBSnapshotVerifyView(parent.view, identifier: "lang_\(language)_device_\(device)")
}
}
}
}

0 comments on commit 423abaa

Please sign in to comment.