Skip to content

Commit

Permalink
[PAY-1954] Loading User Settings And Cancelling Payment Sheet (#1738)
Browse files Browse the repository at this point in the history
* payment methods view cancellation of payment sheet presentation.

* wrapped up on-device loading and cancellation states.

* ensure no loading wheel is shown when feature flag is off.

* loading states tested on payments view model

* updated screenshots after payment methods footer view xib was updated.

* pr comments
  • Loading branch information
msadoon committed Sep 26, 2022
1 parent ce821ad commit d100e91
Show file tree
Hide file tree
Showing 50 changed files with 109 additions and 22 deletions.
Expand Up @@ -4,11 +4,16 @@ import Prelude
import Stripe
import UIKit

protocol PaymentMethodsViewControllerDelegate: AnyObject {
func cancelLoadingPaymentMethodsViewController(
_ viewController: PaymentMethodsViewController)
}

internal final class PaymentMethodsViewController: UIViewController, MessageBannerViewControllerPresenting {
private let dataSource = PaymentMethodsDataSource()
private let viewModel: PaymentMethodsViewModelType = PaymentMethodsViewModel()
private var paymentSheetFlowController: PaymentSheet.FlowController?

private weak var cancellationDelegate: PaymentMethodsViewControllerDelegate?
@IBOutlet private var tableView: UITableView!

fileprivate lazy var editButton: UIBarButtonItem = {
Expand Down Expand Up @@ -45,6 +50,7 @@ internal final class PaymentMethodsViewController: UIViewController, MessageBann
]

self.dataSource.deletionHandler = { [weak self] creditCard in
self?.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)
self?.viewModel.inputs.didDelete(creditCard, visibleCellCount: self?.tableView.visibleCells.count ?? 0)
}

Expand Down Expand Up @@ -76,6 +82,13 @@ internal final class PaymentMethodsViewController: UIViewController, MessageBann

self.editButton.rac.enabled = self.viewModel.outputs.editButtonIsEnabled

self.viewModel.outputs.cancelAddNewCardLoadingState
.observeForUI()
.observeValues { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.cancellationDelegate?.cancelLoadingPaymentMethodsViewController(strongSelf)
}

self.viewModel.outputs.paymentMethods
.observeForUI()
.observeValues { [weak self] result in
Expand Down Expand Up @@ -145,6 +158,7 @@ internal final class PaymentMethodsViewController: UIViewController, MessageBann

@objc private func edit() {
self.viewModel.inputs.editButtonTapped()
self.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)
}

private func goToAddCardScreen(with intent: AddNewCardIntent) {
Expand All @@ -169,10 +183,7 @@ internal final class PaymentMethodsViewController: UIViewController, MessageBann

switch result {
case let .failure(error):
/** TODO: https://kickstarter.atlassian.net/browse/PAY-1954
* strongSelf.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)
*/

strongSelf.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)
strongSelf.messageBannerViewController?
.showBanner(with: .error, message: error.localizedDescription)
case let .success(paymentSheetFlowController):
Expand All @@ -188,9 +199,7 @@ internal final class PaymentMethodsViewController: UIViewController, MessageBann

private func confirmPaymentResult(with clientSecret: String) {
guard self.paymentSheetFlowController?.paymentOption != nil else {
/** TODO: https://kickstarter.atlassian.net/browse/PAY-1954
* strongSelf.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)
*/
self.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)

return
}
Expand All @@ -199,9 +208,7 @@ internal final class PaymentMethodsViewController: UIViewController, MessageBann

guard let strongSelf = self else { return }

/** TODO: https://kickstarter.atlassian.net/browse/PAY-1954
* strongSelf.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)
*/
strongSelf.viewModel.inputs.shouldCancelPaymentSheetAppearance(state: true)

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

Expand Down Expand Up @@ -236,6 +243,7 @@ internal final class PaymentMethodsViewController: UIViewController, MessageBann

if let footer = PaymentMethodsFooterView.fromNib(nib: Nib.PaymentMethodsFooterView) {
footer.delegate = self
self.cancellationDelegate = footer

let footerContainer = UIView(frame: .zero)
_ = (footer, footerContainer) |> ksr_addSubviewToParent()
Expand Down
@@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
Expand All @@ -16,7 +14,13 @@
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZVF-Hv-L8e">
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="NYB-xa-rq9">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<constraints>
<constraint firstAttribute="height" priority="750" constant="44" id="r2r-QJ-TBt"/>
</constraints>
</activityIndicatorView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZVF-Hv-L8e">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<constraints>
<constraint firstAttribute="height" priority="750" constant="44" id="UK4-hM-zKF"/>
Expand All @@ -34,22 +38,28 @@
</constraints>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="SB4-Lk-Ajb" firstAttribute="width" secondItem="iN0-l3-epB" secondAttribute="width" id="3ZU-mb-b3a"/>
<constraint firstItem="SB4-Lk-Ajb" firstAttribute="trailing" secondItem="iN0-l3-epB" secondAttribute="trailing" id="AFm-OP-jdz"/>
<constraint firstItem="ZVF-Hv-L8e" firstAttribute="bottom" secondItem="iN0-l3-epB" secondAttribute="bottom" id="Jgf-jz-MGs"/>
<constraint firstItem="SB4-Lk-Ajb" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="Tcv-X7-qhe"/>
<constraint firstItem="SB4-Lk-Ajb" firstAttribute="top" secondItem="NYB-xa-rq9" secondAttribute="bottom" id="ZTG-NB-oQ9"/>
<constraint firstItem="ZVF-Hv-L8e" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="ZmG-1O-aKF"/>
<constraint firstItem="NYB-xa-rq9" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="dg4-Jh-MZ3"/>
<constraint firstAttribute="trailing" secondItem="NYB-xa-rq9" secondAttribute="trailing" id="kTK-t3-wKg"/>
<constraint firstItem="SB4-Lk-Ajb" firstAttribute="bottom" secondItem="iN0-l3-epB" secondAttribute="bottom" id="r8U-Cm-ivH"/>
<constraint firstItem="NYB-xa-rq9" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="smE-1j-5ZE"/>
<constraint firstItem="NYB-xa-rq9" firstAttribute="width" secondItem="iN0-l3-epB" secondAttribute="width" id="vVF-M0-WFt"/>
<constraint firstItem="ZVF-Hv-L8e" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="xwQ-EY-95A"/>
<constraint firstItem="ZVF-Hv-L8e" firstAttribute="trailing" secondItem="iN0-l3-epB" secondAttribute="trailing" id="yCu-B8-SVL"/>
<constraint firstItem="ZVF-Hv-L8e" firstAttribute="width" secondItem="iN0-l3-epB" secondAttribute="width" id="ynk-qM-yO6"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<connections>
<outlet property="addCardButton" destination="ZVF-Hv-L8e" id="DTW-cN-pEC"/>
<outlet property="loadingIndicator" destination="NYB-xa-rq9" id="B9G-2D-IDa"/>
<outlet property="separatorView" destination="SB4-Lk-Ajb" id="khu-HH-QZj"/>
</connections>
<point key="canvasLocation" x="138.40000000000001" y="-129.53523238380811"/>
Expand Down
Expand Up @@ -11,6 +11,7 @@ public final class PaymentMethodsFooterView: UIView, NibLoading {

@IBOutlet private var addCardButton: UIButton!
@IBOutlet private var separatorView: UIView!
@IBOutlet private var loadingIndicator: UIActivityIndicatorView!

public override func bindStyles() {
super.bindViewModel()
Expand All @@ -27,13 +28,28 @@ public final class PaymentMethodsFooterView: UIView, NibLoading {
?|> \.tintColor .~ .ksr_create_700

_ = self
|> \.backgroundColor .~ .ksr_support_100
|> \.backgroundColor .~ .ksr_white

_ = self.separatorView
|> separatorStyle
}

@IBAction func addNewCardButtonTapped(_: Any) {
if featureSettingsPaymentSheetEnabled() {
self.loadingIndicator.startAnimating()
self.addCardButton.isHidden = true
}

self.addCardButton.isUserInteractionEnabled = false
self.delegate?.paymentMethodsFooterViewDidTapAddNewCardButton(self)
}
}

extension PaymentMethodsFooterView: PaymentMethodsViewControllerDelegate {
func cancelLoadingPaymentMethodsViewController(
_: PaymentMethodsViewController) {
self.addCardButton.isHidden = false
self.addCardButton.isUserInteractionEnabled = true
self.loadingIndicator.stopAnimating()
}
}
32 changes: 29 additions & 3 deletions Library/ViewModels/PaymentMethodsViewModel.swift
Expand Up @@ -13,10 +13,12 @@ public protocol PaymentMethodsViewModelInputs {
func paymentMethodsFooterViewDidTapAddNewCardButton()
func paymentSheetDidAdd(newCard card: PaymentSheet.FlowController.PaymentOptionDisplayData,
setupIntent: String)
func shouldCancelPaymentSheetAppearance(state: Bool)
func viewDidLoad()
}

public protocol PaymentMethodsViewModelOutputs {
var cancelAddNewCardLoadingState: Signal<Void, Never> { get }
var editButtonIsEnabled: Signal<Bool, Never> { get }
var editButtonTitle: Signal<String, Never> { get }
var errorLoadingPaymentMethodsOrSetupIntent: Signal<String, Never> { get }
Expand Down Expand Up @@ -183,10 +185,15 @@ public final class PaymentMethodsViewModel: PaymentMethodsViewModelType,
.materialize()
}

/** TODO: https://kickstarter.atlassian.net/browse/PAY-1954
* Add cancellation signal similiar to `shouldCancelPaymentSheetAppearance` in `PledgePaymentMethodsViewModel`
*/
self.cancelAddNewCardLoadingState = self.shouldCancelPaymentSheetAppearance.signal.filter(isTrue)
.ignoreValues()

self.goToPaymentSheet = createSetupIntentEvent.values()
.withLatestFrom(self.shouldCancelPaymentSheetAppearance.signal)
.map { (data, shouldCancel) -> PaymentSheetSetupData? in
shouldCancel ? nil : data
}
.skipNil()

self.errorLoadingPaymentMethodsOrSetupIntent = Signal.merge(
paymentMethodsEvent.errors(),
Expand All @@ -195,6 +202,19 @@ public final class PaymentMethodsViewModel: PaymentMethodsViewModelType,
)
.map { $0.localizedDescription }

self.shouldCancelPaymentSheetAppearance <~ Signal
.merge(
self.didTapAddCardButtonProperty.signal
.ignoreValues()
.mapConst(false),
self.addNewCardSucceededProperty.signal
.ignoreValues()
.mapConst(true),
self.errorLoadingPaymentMethodsOrSetupIntent.signal
.ignoreValues()
.mapConst(true)
)

self.setStripePublishableKey = self.viewDidLoadProperty.signal
.map { _ in AppEnvironment.current.environmentType.stripePublishableKey }
}
Expand Down Expand Up @@ -250,6 +270,12 @@ public final class PaymentMethodsViewModel: PaymentMethodsViewModelType,
self.newSetupIntentCreditCardProperty.value = (card, setupIntent)
}

private let shouldCancelPaymentSheetAppearance = MutableProperty<Bool>(false)
public func shouldCancelPaymentSheetAppearance(state: Bool) {
self.shouldCancelPaymentSheetAppearance.value = state
}

public let cancelAddNewCardLoadingState: Signal<Void, Never>
public let editButtonIsEnabled: Signal<Bool, Never>
public let editButtonTitle: Signal<String, Never>
public let errorLoadingPaymentMethodsOrSetupIntent: Signal<String, Never>
Expand Down
27 changes: 27 additions & 0 deletions Library/ViewModels/PaymentMethodsViewModelTests.swift
Expand Up @@ -10,6 +10,7 @@ import XCTest
internal final class PaymentMethodsViewModelTests: TestCase {
private let vm = PaymentMethodsViewModel()
private let userTemplate = GraphUser.template |> \.storedCards .~ UserCreditCards.template
private let cancelLoadingState = TestObserver<Void, Never>()
private let editButtonIsEnabled = TestObserver<Bool, Never>()
private let editButtonTitle = TestObserver<String, Never>()
private let errorLoadingPaymentMethodsOrSetupIntent = TestObserver<String, Never>()
Expand All @@ -25,6 +26,7 @@ internal final class PaymentMethodsViewModelTests: TestCase {
internal override func setUp() {
super.setUp()

self.vm.outputs.cancelAddNewCardLoadingState.observe(self.cancelLoadingState.observer)
self.vm.outputs.editButtonIsEnabled.observe(self.editButtonIsEnabled.observer)
self.vm.outputs.editButtonTitle.observe(self.editButtonTitle.observer)
self.vm.outputs.errorLoadingPaymentMethodsOrSetupIntent
Expand Down Expand Up @@ -174,6 +176,31 @@ internal final class PaymentMethodsViewModelTests: TestCase {
}
}

func testCancelLoadingState_Success() {
let response = UserEnvelope<GraphUser>(me: userTemplate)
let apiService = MockService(
fetchGraphUserResult: .success(response)
)

withEnvironment(apiService: apiService) {
self.cancelLoadingState.assertDidNotEmitValue()

self.vm.inputs
.shouldCancelPaymentSheetAppearance(state: false)

self.scheduler.advance()

self.cancelLoadingState.assertDidNotEmitValue()

self.vm.inputs
.shouldCancelPaymentSheetAppearance(state: true)

self.scheduler.advance()

self.cancelLoadingState.assertDidEmitValue()
}
}

func testPaymentSheetDidAdd_WhenSettingsPaymentSheetIsDisabled_OnAddNewCardFailed_ErrorShown() {
let response = UserEnvelope<GraphUser>(me: userTemplate)
let apiService = MockService(
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d100e91

Please sign in to comment.