Skip to content

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation

public enum CollectOrderPaymentUseCaseError: LocalizedError {
case flowCanceledByUser
case paymentGatewayNotFound
case orderTotalChanged
case couldNotRefreshOrder(Error)
case orderAlreadyPaid

public var errorDescription: String? {
switch self {
case .flowCanceledByUser:
return Localization.paymentCancelledLocalizedDescription
case .paymentGatewayNotFound:
return Localization.paymentGatewayNotFoundLocalizedDescription
case .orderTotalChanged:
return Localization.orderTotalChangedLocalizedDescription
case .couldNotRefreshOrder(let error as LocalizedError):
return error.errorDescription
case .couldNotRefreshOrder(let error):
return String.localizedStringWithFormat(Localization.couldNotRefreshOrderLocalizedDescription, error.localizedDescription)
case .orderAlreadyPaid:
return Localization.orderAlreadyPaidLocalizedDescription
}
}

private enum Localization {
static let couldNotRefreshOrderLocalizedDescription = NSLocalizedString(
"Unable to process payment. We could not fetch the latest order details. Please check your network " +
"connection and try again. Underlying error: %1$@",
comment: "Error message when collecting an In-Person Payment and unable to update the order. %!$@ will " +
"be replaced with further error details.")

static let orderTotalChangedLocalizedDescription = NSLocalizedString(
"collectOrderPaymentUseCase.error.message.orderTotalChanged",
value: "Order total has changed since the beginning of payment. Please go back and check the order is " +
"correct, then try the payment again.",
comment: "Error message when collecting an In-Person Payment and the order total has changed remotely.")

static let orderAlreadyPaidLocalizedDescription = NSLocalizedString(
"Unable to process payment. This order is already paid, taking a further payment would result in the " +
"customer being charged twice for their order.",
comment: "Error message shown during In-Person Payments when the order is found to be paid after it's refreshed.")

static let paymentGatewayNotFoundLocalizedDescription = NSLocalizedString(
"Unable to process payment. We could not connect to the payment system. Please contact support if this " +
"error continues.",
comment: "Error message shown during In-Person Payments when the payment gateway is not available.")

static let paymentCancelledLocalizedDescription = NSLocalizedString(
"The payment was cancelled.", comment: "Message shown if a payment cancellation is shown as an error.")
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import Foundation
import Yosemite

/// Async/await version of `PaginationTracker`, consider renaming `PaginationTracker` as deprecated and this class to `PaginationTracker`.
/// Keeps track of the pagination for API syncing to support infinite scroll and pull-to-refresh.
final class AsyncPaginationTracker {
typealias SyncFunction = (_ pageNumber: Int) async throws -> Bool
public final class AsyncPaginationTracker {
public typealias SyncFunction = (_ pageNumber: Int) async throws -> Bool

/// State of loading the next page in `ensureNextPageIsSynced`.
enum NextPageSyncState {
public enum NextPageSyncState {
case syncing
case synced
case noNextPage
}

/// Default pagination settings.
enum Defaults {
static let pageFirstIndex = Store.Default.firstPageNumber
public enum Defaults {
public static let pageFirstIndex = Store.Default.firstPageNumber
}

/// The index of the first page in the API. So far, both Woo and WP.com API have the first page index at 1.
Expand All @@ -28,7 +27,7 @@ final class AsyncPaginationTracker {
private var pagesBeingSynced = IndexSet()

/// Whether there might be more pages to fetch from the API, set by the sync function.
private(set) var hasNextPage: Bool = true
private(set) public var hasNextPage: Bool = true

/// Returns the highest page number that has been successfully synced, if any.
private var highestPageSynced: Int? {
Expand All @@ -41,7 +40,7 @@ final class AsyncPaginationTracker {
}

/// Designated Initializer
init(pageFirstIndex: Int = Defaults.pageFirstIndex) {
public init(pageFirstIndex: Int = Defaults.pageFirstIndex) {
self.pageFirstIndex = pageFirstIndex
}

Expand All @@ -50,7 +49,7 @@ final class AsyncPaginationTracker {
/// 1. Proceed only if there is next page to sync.
/// 2. Verify if the next page isn't currently being synced.
/// 3. Proceed syncing the next page.
func ensureNextPageIsSynced(syncFunction: @escaping SyncFunction) async throws -> NextPageSyncState {
public func ensureNextPageIsSynced(syncFunction: @escaping SyncFunction) async throws -> NextPageSyncState {
guard hasNextPage else {
return .noNextPage
}
Expand All @@ -69,14 +68,14 @@ final class AsyncPaginationTracker {

/// Resets internal states and resyncs the first page of results.
///
func resync(syncFunction: @escaping SyncFunction) async throws {
public func resync(syncFunction: @escaping SyncFunction) async throws {
resetInternalState()
try await syncFirstPage(syncFunction: syncFunction)
}

/// Syncs the first page of results.
///
func syncFirstPage(syncFunction: @escaping SyncFunction) async throws {
public func syncFirstPage(syncFunction: @escaping SyncFunction) async throws {
try await sync(pageNumber: pageFirstIndex, syncFunction: syncFunction)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ import AudioToolbox
import UIKit

/// Allows mocking payment capture celebration UX so that the cha-ching sounds aren't played in unit testing.
protocol PaymentCaptureCelebrationProtocol {
public protocol PaymentCaptureCelebrationProtocol {
/// Called when a payment is captured successfully.
func celebrate()
}

/// Plays a sound and provides haptic feedback when a payment capture has been completed successfully
final class PaymentCaptureCelebration: NSObject, PaymentCaptureCelebrationProtocol {
public final class PaymentCaptureCelebration: NSObject, PaymentCaptureCelebrationProtocol {
private var soundID: SystemSoundID = 0

func celebrate() {
public override init() {
super.init()
}

public func celebrate() {
playSound()
shakeDevice()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import Combine
import enum Yosemite.ServerSidePaymentCaptureError
import enum Yosemite.CardReaderServiceError
import enum Yosemite.CollectOrderPaymentUseCaseError

final class CardPresentPaymentsAlertPresenterAdaptor: CardPresentPaymentAlertsPresenting {
typealias AlertDetails = CardPresentPaymentEventDetails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,8 @@ private struct POSExternalViewAdaptor: POSExternalViewProviding {
onSelection: onSelection
))
}

func createWCWebView(adminUrl: URL, completion: @escaping () -> Void) -> AnyView {
AnyView(WCSettingsWebView(adminUrl: adminUrl, completion: completion))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ extension WooAnalyticsEvent {
Key.cardReaderModel: readerModel(for: cardReaderModel),
Key.countryCode: countryCode.rawValue,
Key.gatewayID: safeGatewayID(for: forGatewayID),
Key.paymentMethodType: paymentMethod.analyticsValue,
Key.paymentMethodType: analyticsValue(for: paymentMethod),
Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStarted)",
Key.millisecondsSinceOrderSyncSuccess: "\(millisecondsSinceOrderSyncSuccess)",
Key.millisecondsSinceReaderReadyToCollect: "\(millisecondsSinceReaderReadyToCollect)",
Expand All @@ -134,6 +134,17 @@ extension WooAnalyticsEvent {
])
}

static func analyticsValue(for paymentMethod: PaymentMethod) -> String {
switch paymentMethod {
case .card, .cardPresent:
return "card"
case .interacPresent:
return "card_interac"
case .unknown:
return "unknown"
}
}

static func cashCollectPaymentSuccess(millisecondsSinceCustomerIteractionStarted: Double) -> WooAnalyticsEvent {
WooAnalyticsEvent(statName: .pointOfSaleCashCollectPaymentSuccess, properties: [
Key.millisecondsSinceCustomerInteractionStarted: "\(millisecondsSinceCustomerIteractionStarted)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import struct Yosemite.POSOrder
import struct Yosemite.POSOrderItem
import struct Yosemite.POSOrderRefund
import class Yosemite.Store
import class Yosemite.AsyncPaginationTracker

protocol POSOrderListControllerProtocol {
var ordersViewState: POSOrderListState { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import protocol Yosemite.PointOfSaleItemServiceProtocol
import protocol Yosemite.PointOfSaleCouponServiceProtocol
import struct Yosemite.PointOfSaleCouponFetchStrategyFactory
import protocol Yosemite.PointOfSaleCouponFetchStrategy
import class Yosemite.AsyncPaginationTracker

protocol PointOfSaleCouponsControllerProtocol: PointOfSaleSearchingItemsControllerProtocol {
/// Enables coupons in store settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import enum Yosemite.PointOfSaleItemServiceError
import struct Yosemite.POSVariableParentProduct
import class Yosemite.Store
import enum Yosemite.POSItemType
import class Yosemite.AsyncPaginationTracker

protocol PointOfSaleItemsControllerProtocol {
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import class WooFoundation.VersionHelpers
import protocol Yosemite.POSOrderServiceProtocol
import protocol Yosemite.POSReceiptServiceProtocol
import protocol Yosemite.PluginsServiceProtocol
import protocol Yosemite.PaymentCaptureCelebrationProtocol
import class Yosemite.PaymentCaptureCelebration
import struct Yosemite.Order
import struct Yosemite.POSCart
import struct Yosemite.POSCartItem
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import enum Yosemite.CollectOrderPaymentUseCaseError

enum PointOfSaleCardPresentPaymentEventPresentationStyle {
case message(PointOfSaleCardPresentPaymentMessageType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import SwiftUI
struct PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView: View {
@StateObject var viewModel: PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressAlertViewModel
let animation: POSCardPresentPaymentAlertAnimation
@Environment(\.posExternalViews) private var externalViews

var body: some View {
VStack(spacing: PointOfSaleReaderConnectionModalLayout.contentButtonSpacing) {
Expand All @@ -29,8 +30,8 @@ struct PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView: View {
.multilineTextAlignment(.center)
.accessibilityElement(children: .contain)
.posSheet(isPresented: $viewModel.shouldShowSettingsWebView) {
WCSettingsWebView(adminUrl: viewModel.settingsAdminUrl,
completion: viewModel.settingsWebViewWasDismissed)
externalViews.createWCWebView(adminUrl: viewModel.settingsAdminUrl,
completion: viewModel.settingsWebViewWasDismissed)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ struct PointOfSaleSettingsHardwareDetailView: View {
}
.navigationBarBackButtonHidden(true)
.posFullScreenCover(isPresented: $showCardReaderDocumentationModal) {
SafariView(url: WooConstants.URLs.inPersonPaymentsLearnMoreWCPay.asURL())
if let url = URL(string: Constants.inPersonPaymentsLearnMoreWCPay.rawValue) {
SafariView(url: url)
}
}
}

Expand Down Expand Up @@ -383,6 +385,13 @@ private extension PointOfSaleSettingsHardwareDetailView {
}
}

private extension PointOfSaleSettingsHardwareDetailView {
enum Constants: String {
case inPersonPaymentsLearnMoreWCPay =
"https://woocommerce.com/document/woocommerce-payments/in-person-payments/getting-started-with-in-person-payments/"
}
}

#if DEBUG
#Preview {
PointOfSaleSettingsHardwareDetailView(settingsController: PointOfSaleSettingsPreviewController())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ protocol POSExternalViewProviding {
title: String,
cancelButtonTitle: String,
onSelection: @escaping (Coupon.DiscountType) -> Void) -> AnyView
func createWCWebView(adminUrl: URL, completion: @escaping () -> Void) -> AnyView
}

/// Main protocol that combines all POS dependency providers
Expand Down
1 change: 1 addition & 0 deletions WooCommerce/Classes/POS/TabBar/POSIneligibleView.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import SwiftUI
import Yosemite

/// A view that displays when the Point of Sale (POS) feature is not available for the current store.
/// Shows the specific reason why POS is ineligible and provides a button to re-check eligibility.
Expand Down
3 changes: 3 additions & 0 deletions WooCommerce/Classes/POS/Utils/POSEnvironmentKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,8 @@ struct EmptyPOSExternalView: POSExternalViewProviding {
onSelection: @escaping (Coupon.DiscountType) -> Void) -> AnyView {
AnyView(EmptyView())
}
func createWCWebView(adminUrl: URL, completion: @escaping () -> Void) -> AnyView {
AnyView(EmptyView())
}
init() {}
}
Loading