Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

11.3
-----

- [*] Show spinner while preparing reader for payment, instead of saying it's ready before it is. [https://github.com/woocommerce/woocommerce-ios/pull/8115]

11.2
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import UIKit

/// Modal presented when an error occurs while connecting to a reader due to problems with the address
///
final class CardPresentModalPreparingReader: CardPresentPaymentsModalViewModel {
let cancelAction: (() -> Void)

let textMode: PaymentsModalTextMode = .reducedTopInfo
let actionsMode: PaymentsModalActionsMode = .secondaryOnlyAction

let topTitle: String = Localization.title

var topSubtitle: String? = nil

let image: UIImage = .paymentErrorImage

let showLoadingIndicator = true

var primaryButtonTitle: String? = nil

let secondaryButtonTitle: String? = Localization.cancel

let auxiliaryButtonTitle: String? = nil

var bottomTitle: String? = Localization.bottomTitle

let bottomSubtitle: String? = Localization.bottomSubitle

var accessibilityLabel: String? {
return topTitle
}

init(cancelAction: @escaping () -> Void) {
self.cancelAction = cancelAction
}

func didTapPrimaryButton(in viewController: UIViewController?) {

}

func didTapSecondaryButton(in viewController: UIViewController?) {
cancelAction()
}

func didTapAuxiliaryButton(in viewController: UIViewController?) { }
}

private extension CardPresentModalPreparingReader {
enum Localization {
static let title = NSLocalizedString(
"Getting ready to collect payment",
comment: "Title of the alert presented with a spinner while the reader is being prepared"
)

static let bottomTitle = NSLocalizedString(
"Connecting to reader",
comment: "Bottom title of the alert presented with a spinner while the reader is being prepared"
)

static let bottomSubitle = NSLocalizedString(
"Please wait...",
comment: "Bottom subtitle of the alert presented with a spinner while the reader is being prepared"
)

static let cancel = NSLocalizedString(
"Cancel",
comment: "Button to dismiss the alert presented while the reader is being prepared."
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ protocol CardPresentPaymentsModalViewModel {
/// An illustration accompanying the modal
var image: UIImage { get }

var showLoadingIndicator: Bool { get }

/// Provides a title for a primary action button
var primaryButtonTitle: String? { get }

Expand Down Expand Up @@ -110,4 +112,8 @@ extension CardPresentPaymentsModalViewModel {
var auxiliaryButtonimage: UIImage? {
get { return nil }
}

var showLoadingIndicator: Bool {
get { return false }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
if let controller = _modalController {
return controller
} else {
let controller = CardPresentPaymentsModalViewController(viewModel: readerIsReady(onCancel: {}))
let controller = CardPresentPaymentsModalViewController(
viewModel: CardPresentModalPreparingReader(cancelAction: { [weak self] in
self?.presentingController?.dismiss(animated: true)
}))
_modalController = controller
return controller
}
Expand All @@ -43,6 +46,10 @@ final class OrderDetailsPaymentAlerts: OrderDetailsPaymentAlertsProtocol {
}
}

func preparingReader(onCancel: @escaping () -> Void) {
presentViewModel(viewModel: CardPresentModalPreparingReader(cancelAction: onCancel))
}

func readerIsReady(title: String, amount: String, onCancel: @escaping () -> Void) {
self.name = title
self.amount = amount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import UIKit
protocol OrderDetailsPaymentAlertsProtocol {
func presentViewModel(viewModel: CardPresentPaymentsModalViewModel)

func preparingReader(onCancel: @escaping () -> Void)

func readerIsReady(title: String, amount: String, onCancel: @escaping () -> Void)

func tapOrInsertCard(onCancel: @escaping () -> Void)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import UIKit
import SwiftUI
import WordPressAuthenticator
import SafariServices

Expand Down Expand Up @@ -32,7 +33,7 @@ final class CardPresentPaymentsModalViewController: UIViewController, CardReader
@IBOutlet weak var heightConstraint: NSLayoutConstraint!
@IBOutlet weak var widthConstraint: NSLayoutConstraint!


private var loadingView: UIView?

init(viewModel: CardPresentPaymentsModalViewModel) {
self.viewModel = viewModel
Expand All @@ -46,6 +47,7 @@ final class CardPresentPaymentsModalViewController: UIViewController, CardReader
override func viewDidLoad() {
super.viewDidLoad()

createViews()
initializeContent()
setBackgroundColor()
setButtonsActions()
Expand Down Expand Up @@ -95,6 +97,24 @@ private extension CardPresentPaymentsModalViewController {
containerView.backgroundColor = .tertiarySystemBackground
}

func createViews() {
createLoadingIndicator()
}

func createLoadingIndicator() {
let loadingIndicator = ProgressView()
.progressViewStyle(IndefiniteCircularProgressViewStyle(size: 96.0))
.background(Color(.tertiarySystemBackground))
let host = ConstraintsUpdatingHostingController(rootView: loadingIndicator)
add(host)

guard let index = mainStackView.arrangedSubviews.firstIndex(of: imageView) else {
return
}
mainStackView.insertArrangedSubview(host.view, at: index)
loadingView = host.view
}

func styleContent() {
styleTopTitle()
if shouldShowTopSubtitle() {
Expand Down Expand Up @@ -182,6 +202,8 @@ private extension CardPresentPaymentsModalViewController {

configureImageView()

configureLoadingIndicator()

if shouldShowActionButtons() {
configureActionButtonsView()
styleActionButtons()
Expand Down Expand Up @@ -226,6 +248,11 @@ private extension CardPresentPaymentsModalViewController {

func configureImageView() {
imageView.image = viewModel.image
imageView.isHidden = viewModel.showLoadingIndicator
}

func configureLoadingIndicator() {
loadingView?.isHidden = !viewModel.showLoadingIndicator
}

func setButtonsActions() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ private extension CardReaderSettingsAlerts {
CardPresentModalFoundReader(name: name, connect: connect, continueSearch: continueSearch, cancel: cancel)
}

func preparingReader(from: UIViewController, cancel: @escaping () -> Void) -> CardPresentPaymentsModalViewModel {
CardPresentModalPreparingReader(cancelAction: cancel)
}

func setViewModelAndPresent(from: UIViewController, viewModel: CardPresentPaymentsModalViewModel) {
guard modalController == nil else {
modalController?.setViewModel(viewModel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ final class CollectOrderPaymentUseCase: NSObject, CollectOrderPaymentProtocol {
// Inform about the collect payment state
switch result {
case .failure(CollectOrderPaymentUseCaseError.flowCanceledByUser):
self.rootViewController.presentedViewController?.dismiss(animated: true)
return onCancel()
default:
onCollect(result.map { _ in () }) // Transforms Result<CardPresentCapturedPaymentData, Error> to Result<Void, Error>
Expand Down Expand Up @@ -271,13 +272,11 @@ private extension CollectOrderPaymentUseCase {
return
}

// Show reader ready alert
alerts.readerIsReady(title: Localization.collectPaymentTitle(username: order.billingAddress?.firstName),
amount: formattedAmount,
onCancel: { [weak self] in
self?.cancelPayment {
// Show preparing reader alert
alerts.preparingReader(onCancel: { [weak self] in
self?.cancelPayment(onCompleted: {
onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser))
}
})
})

// Start collect payment process
Expand All @@ -288,12 +287,14 @@ private extension CollectOrderPaymentUseCase {
paymentMethodTypes: configuration.paymentMethods.map(\.rawValue),
stripeSmallestCurrencyUnitMultiplier: configuration.stripeSmallestCurrencyUnitMultiplier,
onWaitingForInput: { [weak self] in
// Request card input
self?.alerts.tapOrInsertCard(onCancel: { [weak self] in
self?.cancelPayment {
onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser))
}
})
guard let self = self else { return }
self.alerts.readerIsReady(title: Localization.collectPaymentTitle(username: self.order.billingAddress?.firstName),
amount: self.formattedAmount,
onCancel: { [weak self] in
self?.cancelPayment {
onCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser))
}
})

}, onProcessingMessage: { [weak self] in
// Waiting message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,7 @@ private extension RefundSubmissionUseCase {
paymentGatewayAccount: PaymentGatewayAccount,
onCompletion: @escaping (Result<Void, Error>) -> ()) {
// Shows reader ready alert.
alerts.readerIsReady(title: Localization.refundPaymentTitle(username: order.billingAddress?.firstName),
amount: formattedAmount,
onCancel: { [weak self] in
alerts.preparingReader(onCancel: { [weak self] in
self?.cancelRefund(charge: charge, paymentGatewayAccount: paymentGatewayAccount, onCompletion: onCompletion)
})

Expand All @@ -307,7 +305,10 @@ private extension RefundSubmissionUseCase {
paymentGatewayAccount: paymentGatewayAccount,
onWaitingForInput: { [weak self] in
// Requests card input.
self?.alerts.tapOrInsertCard(onCancel: { [weak self] in
guard let self = self else { return }
self.alerts.readerIsReady(title: Localization.refundPaymentTitle(username: self.order.billingAddress?.firstName),
amount: self.formattedAmount,
onCancel: { [weak self] in
self?.cancelRefund(charge: charge, paymentGatewayAccount: paymentGatewayAccount, onCompletion: onCompletion)
})
}, onProcessingMessage: { [weak self] in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import SwiftUI

public struct IndefiniteCircularProgressViewStyle: ProgressViewStyle {
var size: CGFloat
private let arcStart: Double = Constants.initialArcStart
private let animationDuration: Double = 1.6

@State private var arcEnd: Double = Constants.initialArcEnd
@State private var rotation: Angle = Constants.threeQuarterRotation
@State private var viewRotation: Angle = .radians(0)
@State private var arcTimer: Timer?

public func makeBody(configuration: ProgressViewStyleConfiguration) -> some View {
VStack {
ZStack {
progressCircleView()
.rotationEffect(viewRotation)
}.padding()
configuration.label
}
.onAppear() {
animateArc()
arcTimer = Timer.scheduledTimer(withTimeInterval: animationDuration, repeats: true) { _ in
animateArc()
}
// Gradual rotation of the view to avoid the arc stopping and starting in the same place each spin.
withAnimation(.linear(duration: animationDuration*8)
.repeatForever(autoreverses: false)) {
viewRotation += Constants.fullRotation
}
}
.onDisappear() {
arcTimer?.invalidate()
}
}

private func progressCircleView() -> some View {
Circle()
.stroke(
Color(.primary),
lineWidth: Constants.lineWidth)
.opacity(Constants.backgroundOpacity)
.overlay(progressFill())
.frame(width: size, height: size)
}

private func progressFill() -> some View {
Circle()
.trim(
from: CGFloat(Constants.initialArcStart),
to: CGFloat(arcEnd))
.stroke(
Color(.primary),
style: StrokeStyle(lineWidth: Constants.lineWidth, lineCap: .round))
.frame(width: size)
.rotationEffect(rotation)
}

private func animateArc() {
// Animate the end of the arc going to 100%
withAnimation(
.easeInOut(duration: animationDuration/2)) {
arcEnd = Constants.fullCircle
}
// Halfway through the above, but slower, rotate the arc 1 turn, and move the end back to the start
// This is a bit of a trick, and results in an apparently growing/shrinking arc around the circle.
withAnimation(
.easeOut(duration: animationDuration)
.delay(animationDuration/4)) {
arcEnd = Constants.initialArcEnd
rotation += Constants.fullRotation
}
}
}

private extension IndefiniteCircularProgressViewStyle {
enum Constants {
static let lineWidth: CGFloat = 10.0
static let backgroundOpacity: CGFloat = 0.2

static let initialArcStart: Double = 0
static let initialArcEnd: Double = 0.05
static let fullCircle: Double = 1

static let threeQuarterRotation: Angle = .radians((9 * Double.pi)/6)
static let fullRotation: Angle = .radians(Double.pi * 2)
}
}
Loading