diff --git a/WooCommerce/Classes/Analytics/TracksProvider.swift b/WooCommerce/Classes/Analytics/TracksProvider.swift index 3f5e96e014a..75f69d07b29 100644 --- a/WooCommerce/Classes/Analytics/TracksProvider.swift +++ b/WooCommerce/Classes/Analytics/TracksProvider.swift @@ -153,6 +153,16 @@ private extension TracksProvider { WooAnalyticsStat.pointOfSaleBarcodeScannerSetupFlowShown, WooAnalyticsStat.pointOfSaleBarcodeScanningSuccess, WooAnalyticsStat.pointOfSaleBarcodeScanningFailed, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupScannerSelected, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupNextTapped, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupBackTapped, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupOpenSystemSettingsTapped, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupTestScanSuccess, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupTestScanFailed, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupTestScanTimedOut, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupDismissed, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupRetryTapped, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupScannerConnected, // Order WooAnalyticsStat.orderCreationSuccess, diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index c31e56da63f..8be1792ae0a 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -1308,6 +1308,16 @@ enum WooAnalyticsStat: String { case pointOfSaleBarcodeScannerSetupFlowShown = "barcode_scanner_setup_flow_shown" case pointOfSaleBarcodeScanningSuccess = "barcode_scanned" case pointOfSaleBarcodeScanningFailed = "barcode_scanning_failed" + case pointOfSaleBarcodeScannerSetupScannerSelected = "barcode_scanner_setup_scanner_selected" + case pointOfSaleBarcodeScannerSetupNextTapped = "barcode_scanner_setup_next_tapped" + case pointOfSaleBarcodeScannerSetupBackTapped = "barcode_scanner_setup_back_tapped" + case pointOfSaleBarcodeScannerSetupOpenSystemSettingsTapped = "barcode_scanner_setup_open_system_settings_tapped" + case pointOfSaleBarcodeScannerSetupTestScanSuccess = "barcode_scanner_setup_test_scan_success" + case pointOfSaleBarcodeScannerSetupTestScanFailed = "barcode_scanner_setup_test_scan_failed" + case pointOfSaleBarcodeScannerSetupTestScanTimedOut = "barcode_scanner_setup_test_scan_timed_out" + case pointOfSaleBarcodeScannerSetupDismissed = "barcode_scanner_setup_dismissed" + case pointOfSaleBarcodeScannerSetupRetryTapped = "barcode_scanner_setup_retry_tapped" + case pointOfSaleBarcodeScannerSetupScannerConnected = "barcode_scanner_setup_scanner_connected" // MARK: Custom Fields events case productDetailCustomFieldsTapped = "product_detail_custom_fields_tapped" diff --git a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift index 6d92180c297..ee93978e17b 100644 --- a/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift +++ b/WooCommerce/Classes/POS/Analytics/WooAnalyticsEvent+PointOfSale.swift @@ -34,6 +34,9 @@ extension WooAnalyticsEvent { static let scanDurationMs = "scan_duration_ms" static let barcodeLength = "barcode_length" static let failReason = "fail_reason" + static let scanner = "scanner" + static let step = "step" + static let scanValue = "scan_value" } static func paymentsOnboardingShown() -> WooAnalyticsEvent { @@ -214,6 +217,72 @@ extension WooAnalyticsEvent { Key.failReason: failReason ]) } + + static func barcodeScannerSetupScannerSelected(scanner: PointOfSaleBarcodeScannerType) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupScannerSelected, + properties: [Key.scanner: scanner.analyticsName]) + } + + static func barcodeScannerSetupNextTapped(scanner: PointOfSaleBarcodeScannerType, step: String) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupNextTapped, + properties: [ + Key.scanner: scanner.analyticsName, + Key.step: step + ]) + } + + static func barcodeScannerSetupBackTapped(scanner: PointOfSaleBarcodeScannerType, step: String) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupBackTapped, + properties: [ + Key.scanner: scanner.analyticsName, + Key.step: step + ]) + } + + static func barcodeScannerSetupOpenSystemSettingsTapped(scanner: PointOfSaleBarcodeScannerType) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupOpenSystemSettingsTapped, + properties: [Key.scanner: scanner.analyticsName]) + } + + static func barcodeScannerSetupTestScanSuccess(scanner: PointOfSaleBarcodeScannerType) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupTestScanSuccess, + properties: [Key.scanner: scanner.analyticsName]) + } + + static func barcodeScannerSetupTestScanFailed(scanner: PointOfSaleBarcodeScannerType, scanValue: String) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupTestScanFailed, + properties: [ + Key.scanner: scanner.analyticsName, + Key.scanValue: scanValue + ]) + } + + static func barcodeScannerSetupTestScanTimedOut(scanner: PointOfSaleBarcodeScannerType) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupTestScanTimedOut, + properties: [Key.scanner: scanner.analyticsName]) + } + + static func barcodeScannerSetupDismissed(scanner: PointOfSaleBarcodeScannerType? = nil, step: String? = nil) -> WooAnalyticsEvent { + var properties: [String: String] = [:] + if let scanner { + properties[Key.scanner] = scanner.analyticsName + } + if let step { + properties[Key.step] = step + } + return WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupDismissed, + properties: properties) + } + + static func barcodeScannerSetupRetryTapped(scanner: PointOfSaleBarcodeScannerType) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupRetryTapped, + properties: [Key.scanner: scanner.analyticsName]) + } + + static func barcodeScannerSetupScannerConnected(scanner: PointOfSaleBarcodeScannerType, step: String) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupScannerConnected, + properties: [Key.scanner: scanner.analyticsName]) + } } } diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index f0a91355521..e62c808f2fc 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -34,7 +34,7 @@ protocol PointOfSaleAggregateModelProtocol { var couponsSearchController: PointOfSaleSearchingItemsControllerProtocol { get } var cart: Cart { get } - func barcodeScanned(_ result: Result) + func barcodeScanned(_ result: Result) func addToCart(_ item: POSItem) func remove(cartItem: CartItem) func removeAllItemsFromCart(types: [CartItemType]) @@ -185,7 +185,7 @@ extension PointOfSaleAggregateModel { // MARK: - Barcode Scanning @available(iOS 17.0, *) extension PointOfSaleAggregateModel { - func barcodeScanned(_ result: Result) { + func barcodeScanned(_ result: Result) { Task { @MainActor [weak self] in guard let self else { return } switch result { diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetup.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetup.swift index e2f196a95c9..87a2e3848a6 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetup.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetup.swift @@ -35,6 +35,9 @@ struct PointOfSaleBarcodeScannerSetup: View { .onAppear { ServiceLocator.analytics.track(.pointOfSaleBarcodeScannerSetupFlowShown) } + .onDisappear { + flowManager.onDisappear() + } } // MARK: - Computed Properties diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlow.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlow.swift index f30322b1c9b..5038844d292 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlow.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlow.swift @@ -1,36 +1,30 @@ import SwiftUI +import WooFoundation // MARK: - Point of Sale Barcode Scanner Setup Flow @available(iOS 17.0, *) @Observable class PointOfSaleBarcodeScannerSetupFlow { private let scannerType: PointOfSaleBarcodeScannerType - private let onComplete: () -> Void private let onBackToSelection: () -> Void private var flowSteps: [PointOfSaleBarcodeScannerStepID: PointOfSaleBarcodeScannerSetupStep] = [:] - private var currentStepKey: PointOfSaleBarcodeScannerStepID = .start + private var currentStepKey: PointOfSaleBarcodeScannerStepID = .setupBarcodeHID + private let analytics: Analytics init(scannerType: PointOfSaleBarcodeScannerType, - onComplete: @escaping () -> Void, + analytics: Analytics = ServiceLocator.analytics, onBackToSelection: @escaping () -> Void) { self.scannerType = scannerType - self.onComplete = onComplete + self.analytics = analytics self.onBackToSelection = onBackToSelection self.flowSteps = createFlowSteps(for: scannerType) + self.currentStepKey = initialStep(for: scannerType) } var currentStep: PointOfSaleBarcodeScannerSetupStep? { flowSteps[currentStepKey] } - var isComplete: Bool { - currentStepKey == .complete - } - - var nextButtonTitle: String { - isComplete ? Localization.doneButtonTitle : Localization.nextButtonTitle - } - func nextStep() { transition(to: .next) } @@ -38,12 +32,13 @@ class PointOfSaleBarcodeScannerSetupFlow { func previousStep() { transition(to: .back) { [weak self] in // If no back transition is defined, go back to selection + self?.trackSetupBack() self?.onBackToSelection() } } func restartFlow() { - currentStepKey = .start + currentStepKey = .setupBarcodeHID } // MARK: - Generic Transition Methods @@ -52,6 +47,10 @@ class PointOfSaleBarcodeScannerSetupFlow { self.transition(to: transitionType, fallback: nil) } + func transition(to stepKey: PointOfSaleBarcodeScannerStepID) { + self.currentStepKey = stepKey + } + func getButtonConfiguration() -> PointOfSaleFlowButtonConfiguration { guard let step = currentStep else { return .noButtons() @@ -65,13 +64,9 @@ class PointOfSaleBarcodeScannerSetupFlow { // Default button configuration return PointOfSaleFlowButtonConfiguration( primaryButton: PointOfSaleFlowButtonConfiguration.ButtonConfig( - title: nextButtonTitle, + title: Localization.nextButtonTitle, action: { [weak self] in - if self?.isComplete == true { - self?.onComplete() - } else { - self?.nextStep() - } + self?.nextStep() } ), secondaryButton: PointOfSaleFlowButtonConfiguration.ButtonConfig( @@ -92,6 +87,15 @@ class PointOfSaleBarcodeScannerSetupFlow { return } + switch transitionType { + case .next: + trackSetupNext() + case .back: + trackSetupBack() + case .retry: + trackRetry() + } + currentStepKey = targetStep } @@ -99,12 +103,12 @@ class PointOfSaleBarcodeScannerSetupFlow { switch scannerType { case .socketS720: return [ - .start: createWelcomeStep(title: "Socket S720 Setup") + .setupBarcodeHID: createWelcomeStep(title: "Socket S720 Setup") // TODO: Add more steps for Socket S720 WOOMOB-698 ] case .starBSH20B: return [ - .start: PointOfSaleBarcodeScannerSetupStep( + .setupBarcodeHID: PointOfSaleBarcodeScannerSetupStep( content: { PointOfSaleBarcodeScannerBarcodeView( title: String(format: Localization.starSetUpBarcodeStepTitleFormat, scannerType.name), @@ -121,72 +125,67 @@ class PointOfSaleBarcodeScannerSetupFlow { }, transitions: [ .next: .test, - .back: .start - ] - ), - .test: PointOfSaleBarcodeScannerSetupStep( - content: { - PointOfSaleBarcodeScannerTestBarcodeView( - scanTester: PointOfSaleBarcodeScannerSetupScanTester( - onTestPass: { [weak self] in - self?.nextStep() - }, - onTestFailure: { [weak self] in - self?.transition(to: .error) - }, - barcodeDefinition: .ean13) - ) - }, - buttonCustomization: PointOfSaleBarcodeScannerBackOnlyButtonCustomization(), - transitions: [ - .next: .complete, - .error: .testFailed, - .back: .pairing + .back: .setupBarcodeHID ] ), + .test: testBarcodeStep(barcode: .ean13, timerCompleted: false), + .testScanTimedOut: testBarcodeStep(barcode: .ean13, timerCompleted: true), .complete: PointOfSaleBarcodeScannerSetupStep( content: { PointOfSaleBarcodeScannerSetupCompleteView() }, buttonCustomization: PointOfSaleBarcodeScannerOptionalScannerInformationButtonCustomization(), transitions: [ - .next: .information, + .next: .setupInformation, ]), - .testFailed: PointOfSaleBarcodeScannerSetupStep( + .testScanFailed: PointOfSaleBarcodeScannerSetupStep( content: { PointOfSaleBarcodeScannerErrorView() }, buttonCustomization: PointOfSaleBarcodeScannerErrorButtonCustomization(), transitions: [ - .retry: .start, + .retry: .setupBarcodeHID, .back: .test ] ), - .information: PointOfSaleBarcodeScannerSetupStep( + .setupInformation: PointOfSaleBarcodeScannerSetupStep( content: { ProductBarcodeSetupInformation() }, buttonCustomization: PointOfSaleBarcodeScannerNoButtonsButtonCustomization() ) ] case .tbcScanner: return [ - .start: createWelcomeStep(title: "TBC Scanner Setup") + .setupBarcodeHID: createWelcomeStep(title: "TBC Scanner Setup") // TODO: Add more steps for TBC Scanner WOOMOB-699 ] case .other: return [ - .start: PointOfSaleBarcodeScannerSetupStep( + .setupInformation: PointOfSaleBarcodeScannerSetupStep( content: { BarcodeScannerInformation() }, - transitions: [.next: .information] + transitions: [.next: .setupProducts] ), - .information: PointOfSaleBarcodeScannerSetupStep( + .setupProducts: PointOfSaleBarcodeScannerSetupStep( content: { ProductBarcodeSetupInformation() }, buttonCustomization: PointOfSaleBarcodeScannerBackOnlyButtonCustomization(), - transitions: [.back: .start] + transitions: [.back: .setupInformation] ) ] } } + private func initialStep(for scannerType: PointOfSaleBarcodeScannerType) -> PointOfSaleBarcodeScannerStepID { + switch scannerType { + case .socketS720, .starBSH20B, .tbcScanner: + return .setupBarcodeHID + case .other: + return .setupInformation + } + } + + func getCurrentAnalyticsStepValue() -> String? { + return currentStepKey.analyticsValue + } + private func createWelcomeStep(title: String) -> PointOfSaleBarcodeScannerSetupStep { PointOfSaleBarcodeScannerSetupStep( title: title, @@ -194,6 +193,34 @@ class PointOfSaleBarcodeScannerSetupFlow { buttonCustomization: PointOfSaleBarcodeScannerWelcomeButtonCustomization() ) } + + private func testBarcodeStep(barcode: PointOfSaleBarcodeScannerTestBarcode, timerCompleted: Bool) -> PointOfSaleBarcodeScannerSetupStep { + PointOfSaleBarcodeScannerSetupStep( + content: { + PointOfSaleBarcodeScannerTestBarcodeView( + scanTester: PointOfSaleBarcodeScannerSetupScanTester( + onTestPass: { [weak self] in + self?.trackTestScanSuccess() + self?.transition(to: .complete) + }, + onTestFailure: { [weak self] barcode in + self?.trackTestScanFailed(scanValue: barcode) + self?.transition(to: .testScanFailed) + }, + onTestTimeout: { [weak self] in + self?.trackTestScanTimedOut() + self?.transition(to: .testScanTimedOut) + }, + barcodeDefinition: barcode), + timerCompleted: timerCompleted + ) + }, + buttonCustomization: PointOfSaleBarcodeScannerBackOnlyButtonCustomization(), + transitions: [ + .back: .pairing + ] + ) + } } @available(iOS 17.0, *) @@ -270,16 +297,45 @@ struct PointOfSaleBarcodeScannerNoButtonsButtonCustomization: PointOfSaleBarcode } } +// MARK: - Analytics + +@available(iOS 17.0, *) +private extension PointOfSaleBarcodeScannerSetupFlow { + private func trackTestScanSuccess() { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupTestScanSuccess(scanner: scannerType)) + } + + private func trackTestScanFailed(scanValue: String) { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupTestScanFailed(scanner: scannerType, scanValue: scanValue)) + } + + private func trackTestScanTimedOut() { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupTestScanTimedOut(scanner: scannerType)) + } + + private func trackSetupNext() { + if let step = getCurrentAnalyticsStepValue() { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupNextTapped(scanner: scannerType, step: step)) + } + } + + private func trackSetupBack() { + if let step = getCurrentAnalyticsStepValue() { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupBackTapped(scanner: scannerType, step: step)) + } + } + + private func trackRetry() { + if let step = getCurrentAnalyticsStepValue() { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupRetryTapped(scanner: scannerType)) + } + } +} // MARK: - Private Localization Extension @available(iOS 17.0, *) private extension PointOfSaleBarcodeScannerSetupFlow { enum Localization { - static let doneButtonTitle = NSLocalizedString( - "pos.barcodeScannerSetup.done.button.title", - value: "Done", - comment: "Title for the done button in barcode scanner setup navigation" - ) static let nextButtonTitle = NSLocalizedString( "pos.barcodeScannerSetup.next.button.title", value: "Next", diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManager.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManager.swift index f4bff61f6ab..d6aafcaf174 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManager.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManager.swift @@ -1,4 +1,6 @@ import SwiftUI +import GameController +import WooFoundation // MARK: - Point of Sale Barcode Scanner Setup Flow Manager @available(iOS 17.0, *) @@ -7,17 +9,29 @@ class PointOfSaleBarcodeScannerSetupFlowManager { var currentState: PointOfSaleBarcodeScannerSetupFlowState = .scannerSelection @ObservationIgnored @Binding var isPresented: Bool private var currentFlow: PointOfSaleBarcodeScannerSetupFlow? + private let analytics: Analytics + private var keyboardObserver: NSObjectProtocol? - init(isPresented: Binding) { + init(isPresented: Binding, analytics: Analytics = ServiceLocator.analytics) { self._isPresented = isPresented + self.analytics = analytics + setupKeyboardObserver() + } + + deinit { + removeKeyboardObserver() } func selectScanner(_ scannerType: PointOfSaleBarcodeScannerType) { - currentFlow = PointOfSaleBarcodeScannerSetupFlow(scannerType: scannerType, onComplete: { [weak self] in - self?.isPresented = false - }, onBackToSelection: { [weak self] in - self?.goBackToSelection() - }) + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupScannerSelected(scanner: scannerType)) + + currentFlow = PointOfSaleBarcodeScannerSetupFlow( + scannerType: scannerType, + analytics: analytics, + onBackToSelection: { [weak self] in + self?.goBackToSelection() + } + ) currentState = .setupFlow(scannerType) } @@ -38,10 +52,6 @@ class PointOfSaleBarcodeScannerSetupFlowManager { currentFlow?.currentStep } - func isComplete() -> Bool { - currentFlow?.isComplete ?? false - } - var buttonConfiguration: PointOfSaleFlowButtonConfiguration { switch currentState { case .scannerSelection: @@ -54,4 +64,38 @@ class PointOfSaleBarcodeScannerSetupFlowManager { return flow.getButtonConfiguration() } } + + func onDisappear() { + if case .setupFlow(let scannerType) = currentState, let step = getCurrentSetupStepValue() { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupDismissed(scanner: scannerType, step: step)) + } else { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupDismissed()) + } + } + + private func getCurrentSetupStepValue() -> String? { + return currentFlow?.getCurrentAnalyticsStepValue() + } + + private func setupKeyboardObserver() { + keyboardObserver = NotificationCenter.default.addObserver( + forName: .GCKeyboardDidConnect, + object: nil, + queue: .main + ) { [weak self] _ in + self?.handleKeyboardConnected() + } + } + + private func removeKeyboardObserver() { + if let keyboardObserver = keyboardObserver { + NotificationCenter.default.removeObserver(keyboardObserver) + self.keyboardObserver = nil + } + } + + private func handleKeyboardConnected() { + guard case .setupFlow(let scannerType) = currentState, let step = getCurrentSetupStepValue() else { return } + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupScannerConnected(scanner: scannerType, step: step)) + } } diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupModels.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupModels.swift index 3720e6a0547..a185ddb4862 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupModels.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupModels.swift @@ -25,6 +25,19 @@ enum PointOfSaleBarcodeScannerType { return Localization.otherName } } + + var analyticsName: String { + switch self { + case .socketS720: + return "Socket_S720" + case .starBSH20B: + return "Star_BSH_20B" + case .tbcScanner: + return "TBC" + case .other: + return "other" + } + } } private extension PointOfSaleBarcodeScannerType { @@ -45,14 +58,38 @@ enum PointOfSaleBarcodeScannerSetupFlowState { // MARK: - Step Identifiers enum PointOfSaleBarcodeScannerStepID: String, CaseIterable { - case start - case setupBarcode1 - case setupBarcode2 + case setupBarcodeHID + case setupBarcodePair + case setupInformation + case setupProducts case pairing case test + case testScanFailed + case testScanTimedOut case complete - case testFailed - case information + + var analyticsValue: String? { + switch self { + case .setupBarcodeHID: + return "setup_barcode_hid" + case .setupBarcodePair: + return "setup_barcode_pair" + case .setupInformation: + return "setup_information" + case .setupProducts: + return "setup_products" + case .pairing: + return "pairing" + case .test: + return "test_barcode" + case .testScanFailed: + return "test_scan_failed" + case .testScanTimedOut: + return "test_scan_timed_out" + case .complete: + return "setup_complete" + } + } } // MARK: - Button Customization Protocol @@ -64,7 +101,6 @@ protocol PointOfSaleBarcodeScannerButtonCustomization { // MARK: - Transition Types enum PointOfSaleBarcodeScannerTransitionType: Hashable { case next - case error case retry case back } diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift index bc530c92ecf..5cace78af7b 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift @@ -1,13 +1,21 @@ import Foundation -struct PointOfSaleBarcodeScannerSetupScanTester { +@available(iOS 17.0, *) +@Observable +class PointOfSaleBarcodeScannerSetupScanTester { private let onTestPass: () -> Void - private let onTestFailure: () -> Void + private let onTestFailure: (String) -> Void + private let onTestTimeout: () -> Void private let barcodeDefinition: PointOfSaleBarcodeScannerTestBarcode + private var timer: Timer? - init(onTestPass: @escaping () -> Void, onTestFailure: @escaping () -> Void, barcodeDefinition: PointOfSaleBarcodeScannerTestBarcode) { + init(onTestPass: @escaping () -> Void, + onTestFailure: @escaping (String) -> Void, + onTestTimeout: @escaping () -> Void, + barcodeDefinition: PointOfSaleBarcodeScannerTestBarcode) { self.onTestPass = onTestPass self.onTestFailure = onTestFailure + self.onTestTimeout = onTestTimeout self.barcodeDefinition = barcodeDefinition } @@ -15,12 +23,25 @@ struct PointOfSaleBarcodeScannerSetupScanTester { barcodeDefinition.barcodeAsset } - func handleScan(_ scanResult: Result) { + func handleScan(_ scanResult: Result) { switch scanResult { case .success(barcodeDefinition.expectedValue): onTestPass() - case .success, .failure: - onTestFailure() + case .success(let scannedValue): + onTestFailure(scannedValue) + case .failure(let error): + onTestFailure(error.barcode) } } + + func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { [weak self] _ in + self?.onTestTimeout() + } + } + + func stopTimer() { + timer?.invalidate() + timer = nil + } } diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupStepViews.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupStepViews.swift index 93bf4610ab6..57692e7e125 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupStepViews.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupStepViews.swift @@ -74,6 +74,8 @@ struct PointOfSaleBarcodeScannerPairingView: View { } Button { + ServiceLocator.analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupOpenSystemSettingsTapped(scanner: scanner)) + guard let targetURL = URL(string: UIApplication.openSettingsURLString) else { return } @@ -106,8 +108,7 @@ private extension PointOfSaleBarcodeScannerPairingView { @available(iOS 17.0, *) struct PointOfSaleBarcodeScannerTestBarcodeView: View { let scanTester: PointOfSaleBarcodeScannerSetupScanTester - @State private var timerCompleted = false - @State private var timer: Timer? + let timerCompleted: Bool var body: some View { PointOfSaleBarcodeScannerBarcodeView(title: timerCompleted ? Localization.timeoutTitle : Localization.title, @@ -117,19 +118,15 @@ struct PointOfSaleBarcodeScannerTestBarcodeView: View { scanTester.handleScan(result) } .onAppear { - startTimer() + if !timerCompleted { + scanTester.startTimer() + } } .onDisappear { - timer?.invalidate() - timer = nil + scanTester.stopTimer() } } - private func startTimer() { - timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { _ in - timerCompleted = true - } - } } @available(iOS 17.0, *) diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScannerContainer.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScannerContainer.swift index 11b5e3dc685..3836a34664c 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScannerContainer.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScannerContainer.swift @@ -11,11 +11,11 @@ struct BarcodeScannerContainer: View { /// Configuration for the barcode scanner let configuration: HIDBarcodeParserConfiguration /// Callback that is triggered when a barcode scan completes (success or failure) - let onScan: (Result) -> Void + let onScan: (Result) -> Void init( configuration: HIDBarcodeParserConfiguration = .default, - onScan: @escaping (Result) -> Void + onScan: @escaping (Result) -> Void ) { self.configuration = configuration self.onScan = onScan @@ -37,7 +37,7 @@ struct BarcodeScannerContainer: View { /// keyboard input for barcode scanning. struct BarcodeScannerContainerRepresentable: UIViewControllerRepresentable { let configuration: HIDBarcodeParserConfiguration - let onScan: (Result) -> Void + let onScan: (Result) -> Void func makeUIViewController(context: Context) -> UIViewController { return GameControllerBarcodeScannerHostingController( @@ -56,7 +56,7 @@ final class GameControllerBarcodeScannerHostingController: UIHostingController) -> Void + onScan: @escaping (Result) -> Void ) { super.init(rootView: EmptyView()) diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScanningModifier.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScanningModifier.swift index dfc259dd7cd..d1d09f555e3 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScanningModifier.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScanningModifier.swift @@ -7,7 +7,7 @@ struct BarcodeScanningModifier: ViewModifier { /// Whether barcode scanning is enabled @Binding var enabled: Bool /// Callback that is triggered when a barcode is successfully scanned - let onScan: (Result) -> Void + let onScan: (Result) -> Void private var isBarcodeScani1FeatureEnabled: Bool { ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleBarcodeScanningi1) @@ -32,7 +32,7 @@ extension View { /// - onScan: Callback that is triggered when a barcode is successfully scanned. /// - Returns: A view with barcode scanning capability. func barcodeScanning(enabled: Binding = .constant(true), - onScan: @escaping (Result) -> Void) -> some View { + onScan: @escaping (Result) -> Void) -> some View { modifier(BarcodeScanningModifier(enabled: enabled, onScan: onScan)) } } diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanning/GameControllerBarcodeObserver.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanning/GameControllerBarcodeObserver.swift index 7b4808687af..6a902238d64 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanning/GameControllerBarcodeObserver.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanning/GameControllerBarcodeObserver.swift @@ -9,8 +9,8 @@ import GameController /// final class GameControllerBarcodeObserver { /// A closure that is called when a barcode scan is completed. - /// The result will be a `success` with the barcode string or a `failure` with an error. - let onScan: (Result) -> Void + /// The result will be a `success` with the barcode string or a `failure` with an HIDBarcodeParserError. + let onScan: (Result) -> Void /// Track the coalesced keyboard and its parser /// According to Apple's documentation, all connected keyboards are coalesced into one keyboard object @@ -27,7 +27,7 @@ final class GameControllerBarcodeObserver { /// - Parameters: /// - configuration: The configuration to use for the barcode parser. Defaults to the standard configuration. /// - onScan: The closure to be called when a scan is completed. - init(configuration: HIDBarcodeParserConfiguration = .default, onScan: @escaping (Result) -> Void) { + init(configuration: HIDBarcodeParserConfiguration = .default, onScan: @escaping (Result) -> Void) { self.onScan = onScan self.configuration = configuration addObservers() diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanning/GameControllerBarcodeParser.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanning/GameControllerBarcodeParser.swift index 97d09a73a1b..e2f4c25409b 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanning/GameControllerBarcodeParser.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanning/GameControllerBarcodeParser.swift @@ -253,7 +253,7 @@ enum HIDBarcodeParserResult { case success(barcode: String, scanDurationMs: Int) case failure(error: HIDBarcodeParserError, scanDurationMs: Int) - var asResult: Result { + var asResult: Result { switch self { case .success(let barcode, _): return .success(barcode) diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index e4d505615e8..096dcea8fe9 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -7,8 +7,8 @@ struct ItemListView: View { @Environment(\.dynamicTypeSize) private var dynamicTypeSize @Environment(PointOfSaleAggregateModel.self) private var posModel - @Environment(\.keyboardObserver) private var keyboardObserver + @EnvironmentObject var modalManager: POSModalManager @Binding var selectedItemListType: ItemListType @Binding var searchTerm: String @@ -40,9 +40,9 @@ struct ItemListView: View { _isSearching.wrappedValue } - private var isNotSearching: Binding { + private var isBarcodeScanningEnabled: Binding { Binding( - get: { !isSearching }, + get: { !isSearching && !modalManager.isPresented }, set: { _ in } ) } @@ -113,7 +113,7 @@ struct ItemListView: View { await posModel.couponsController.refreshItems(base: .root) } }) - .barcodeScanning(enabled: isNotSearching) { scannedCode in + .barcodeScanning(enabled: isBarcodeScanningEnabled) { scannedCode in posModel.barcodeScanned(scannedCode) } } @@ -211,7 +211,7 @@ struct ItemListView: View { sourceViewType: .init(isSearching: selectedItemListType.isSearching, searchTerm: searchTerm) ) ) - .barcodeScanning(enabled: isNotSearching) { scannedCode in + .barcodeScanning(enabled: isBarcodeScanningEnabled) { scannedCode in posModel.barcodeScanned(scannedCode) } default: diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 6abfbe13681..bfab2af03f0 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 01620C4E2C5394B200D3EA2F /* POSProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01620C4D2C5394B200D3EA2F /* POSProgressViewStyle.swift */; }; 01664F9E2C50E685007CB5DD /* POSFontStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01664F9D2C50E685007CB5DD /* POSFontStyle.swift */; }; 016910982E1D019500B731DA /* GameControllerBarcodeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016910972E1D019500B731DA /* GameControllerBarcodeObserver.swift */; }; + 01695EB82E22600800B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01695EB72E22600300B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift */; }; 016A77692D9D24B00004FCD6 /* POSCouponCreationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016A77682D9D24A70004FCD6 /* POSCouponCreationSheet.swift */; }; 016C6B972C74AB17000D86FD /* POSConnectivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */; }; 0174DDBB2CE5FD60005D20CA /* ReceiptEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */; }; @@ -3210,6 +3211,7 @@ 01620C4D2C5394B200D3EA2F /* POSProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSProgressViewStyle.swift; sourceTree = ""; }; 01664F9D2C50E685007CB5DD /* POSFontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSFontStyle.swift; sourceTree = ""; }; 016910972E1D019500B731DA /* GameControllerBarcodeObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerBarcodeObserver.swift; sourceTree = ""; }; + 01695EB72E22600300B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeScannerSetupFlowManagerTests.swift; sourceTree = ""; }; 016A77682D9D24A70004FCD6 /* POSCouponCreationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCouponCreationSheet.swift; sourceTree = ""; }; 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSConnectivityView.swift; sourceTree = ""; }; 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModel.swift; sourceTree = ""; }; @@ -8244,6 +8246,7 @@ 207CEA862E1FD6D80023EC35 /* Barcode Scanner Setup */ = { isa = PBXGroup; children = ( + 01695EB72E22600300B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift */, 207CEA872E1FD6F80023EC35 /* PointOfSaleBarcodeScannerSetupScanTesterTests.swift */, ); path = "Barcode Scanner Setup"; @@ -17467,6 +17470,7 @@ D802547D26551EF2001B2CC1 /* CardPresentModalSuccessTests.swift in Sources */, DE8311C02C6C8D3800A88709 /* BlazeCampaignListItemCustomizationsTests.swift in Sources */, 02ECD1E124FF496200735BE5 /* PaginationTrackerTests.swift in Sources */, + 01695EB82E22600800B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift in Sources */, 45E9A6EB24DAFC3E00A600E8 /* ProductReviewsViewModelTests.swift in Sources */, 268EC46426D3F9C100716F5C /* EditCustomerNoteViewModelTests.swift in Sources */, 20B0D65E2AD45BDE0059735A /* TapToPayEducationContactlessLimitViewModelTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift index 6b0cb57d937..be326bbffd0 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleAggregateModel.swift @@ -62,7 +62,7 @@ final class MockPointOfSaleAggregateModel: PointOfSaleAggregateModelProtocol { var cart: Cart = .init() - func barcodeScanned(_ result: Result) { } + func barcodeScanned(_ result: Result) { } func addToCart(_ item: POSItem) { } diff --git a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift new file mode 100644 index 00000000000..ac7a56dec8e --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift @@ -0,0 +1,157 @@ +import Testing +import Foundation +import GameController +@testable import WooCommerce + +struct PointOfSaleBarcodeScannerSetupFlowManagerTests { + + @available(iOS 17.0, *) + @Test func test_flowManager_tracks_scanner_selected_when_selectScanner_called() { + // Given a flow manager + let mockAnalytics = MockAnalytics() + let sut = PointOfSaleBarcodeScannerSetupFlowManager( + isPresented: .constant(true), + analytics: mockAnalytics + ) + + // When a scanner is selected + sut.selectScanner(.starBSH20B) + + // Then it tracks the scanner selected event + let event = mockAnalytics.events.first + #expect(event?.eventName == WooAnalyticsStat.pointOfSaleBarcodeScannerSetupScannerSelected.rawValue) + #expect(event?.properties["scanner"] as? String == "Star_BSH_20B") + } + + @available(iOS 17.0, *) + @Test func test_flowManager_tracks_dismissal_when_onDisappear_called_on_non_completion_step() { + // Given a flow manager with a setup flow in progress + let mockAnalytics = MockAnalytics() + let sut = PointOfSaleBarcodeScannerSetupFlowManager( + isPresented: .constant(true), + analytics: mockAnalytics + ) + + // Setup a scanner flow (not on completion step) + sut.selectScanner(.starBSH20B) + mockAnalytics.events.removeAll() // Clear the selection event + + // When onDisappear is called (not on completion step) + sut.onDisappear() + + // Then it tracks the dismissal event + let event = mockAnalytics.events.first + #expect(event?.eventName == WooAnalyticsStat.pointOfSaleBarcodeScannerSetupDismissed.rawValue) + #expect(event?.properties["scanner"] as? String == "Star_BSH_20B") + #expect(event?.properties["step"] as? String == "setup_barcode_hid") + } + + @available(iOS 17.0, *) + @Test func test_flowManager_tracks_dismissal_when_onDisappear_called_on_scanner_selection() { + // Given a flow manager on scanner selection screen + let mockAnalytics = MockAnalytics() + let sut = PointOfSaleBarcodeScannerSetupFlowManager( + isPresented: .constant(true), + analytics: mockAnalytics + ) + + // When onDisappear is called without selecting a scanner + sut.onDisappear() + + // Then it tracks the dismissal event without scanner info + let event = mockAnalytics.events.first + #expect(event?.eventName == WooAnalyticsStat.pointOfSaleBarcodeScannerSetupDismissed.rawValue) + #expect(event?.properties["scanner"] == nil) + #expect(event?.properties["step"] == nil) + } + + @available(iOS 17.0, *) + @Test func test_flowManager_tracks_keyboard_connected_when_in_setup_flow() { + // Given a flow manager with a setup flow + let mockAnalytics = MockAnalytics() + let sut = PointOfSaleBarcodeScannerSetupFlowManager( + isPresented: .constant(true), + analytics: mockAnalytics + ) + + // Setup a scanner flow + sut.selectScanner(.socketS720) + mockAnalytics.events.removeAll() // Clear the selection event + + // When keyboard connected notification is posted + NotificationCenter.default.post(name: .GCKeyboardDidConnect, object: nil) + + // Then it tracks the scanner connected event + let event = mockAnalytics.events.first + #expect(event?.eventName == WooAnalyticsStat.pointOfSaleBarcodeScannerSetupScannerConnected.rawValue) + #expect(event?.properties["scanner"] as? String == "Socket_S720") + } + + @available(iOS 17.0, *) + @Test func test_flowManager_does_not_track_keyboard_connected_when_on_scanner_selection() { + // Given a flow manager on scanner selection + let mockAnalytics = MockAnalytics() + let sut = PointOfSaleBarcodeScannerSetupFlowManager( + isPresented: .constant(true), + analytics: mockAnalytics + ) + + // When keyboard connected notification is posted + NotificationCenter.default.post(name: .GCKeyboardDidConnect, object: nil) + + // Then it does not track any scanner connected event + #expect(mockAnalytics.events.isEmpty) + } + + @available(iOS 17.0, *) + @Test func test_flowManager_returns_correct_state_after_scanner_selection() { + // Given a flow manager + let mockAnalytics = MockAnalytics() + let sut = PointOfSaleBarcodeScannerSetupFlowManager( + isPresented: .constant(true), + analytics: mockAnalytics + ) + + // Initially on scanner selection + if case .scannerSelection = sut.currentState { + } else { + #expect(Bool(false), "Expected scannerSelection state") + } + + // When a scanner is selected + sut.selectScanner(.tbcScanner) + + // Then state changes to setup flow + if case .setupFlow(let scannerType) = sut.currentState { + #expect(scannerType == .tbcScanner) + } else { + #expect(Bool(false), "Expected setupFlow state") + } + } + + @available(iOS 17.0, *) + @Test func test_flowManager_returns_to_scanner_selection_when_goBackToSelection_called() { + // Given a flow manager in setup flow + let mockAnalytics = MockAnalytics() + let sut = PointOfSaleBarcodeScannerSetupFlowManager( + isPresented: .constant(true), + analytics: mockAnalytics + ) + + sut.selectScanner(.other) + if case .setupFlow = sut.currentState { + } else { + #expect(Bool(false), "Expected setupFlow state after selection") + } + + // When going back to selection + sut.goBackToSelection() + + // Then state returns to scanner selection + if case .scannerSelection = sut.currentState { + } else { + #expect(Bool(false), "Expected scannerSelection state after going back") + } + #expect(sut.getCurrentStep() == nil) + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift index 9e42ed01628..38bd1f6ad3b 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift @@ -3,15 +3,18 @@ import Testing struct PointOfSaleBarcodeScannerSetupScanTesterTests { + @available(iOS 17.0, *) @Test func test_scanTester_calls_onTestPass_when_scan_received_for_expected_barcode() { // Given a test EAN13 barcode let expectedBarcode = PointOfSaleBarcodeScannerTestBarcode.ean13 var onTestPassCalled = false var onTestFailureCalled = false + var onTestTimeoutCalled = false let sut = PointOfSaleBarcodeScannerSetupScanTester( onTestPass: { onTestPassCalled = true }, - onTestFailure: { onTestFailureCalled = true }, + onTestFailure: { _ in onTestFailureCalled = true }, + onTestTimeout: { onTestTimeoutCalled = true }, barcodeDefinition: expectedBarcode) // When the barcode is scanned @@ -20,57 +23,75 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { // Then it calls the pass closure #expect(onTestPassCalled == true) #expect(onTestFailureCalled == false) + #expect(onTestTimeoutCalled == false) } + @available(iOS 17.0, *) @Test func test_scanTester_calls_onTestFailure_when_scan_received_for_unexpected_barcode() { // Given a test EAN13 barcode let expectedBarcode = PointOfSaleBarcodeScannerTestBarcode.ean13 var onTestPassCalled = false var onTestFailureCalled = false + var onTestTimeoutCalled = false + var receivedScanValue = "" let sut = PointOfSaleBarcodeScannerSetupScanTester( onTestPass: { onTestPassCalled = true }, - onTestFailure: { onTestFailureCalled = true }, + onTestFailure: { scanValue in + onTestFailureCalled = true + receivedScanValue = scanValue + }, + onTestTimeout: { onTestTimeoutCalled = true }, barcodeDefinition: expectedBarcode) // When an unexpected barcode is scanned - sut.handleScan(.success("9999999999999")) + let unexpectedBarcode = "9999999999999" + sut.handleScan(.success(unexpectedBarcode)) - // Then it calls the failure closure + // Then it calls the failure closure with the scanned value #expect(onTestPassCalled == false) #expect(onTestFailureCalled == true) + #expect(onTestTimeoutCalled == false) + #expect(receivedScanValue == unexpectedBarcode) } - @Test func test_scanTester_calls_onTestFailure_when_scan_fails() { + @available(iOS 17.0, *) + @Test func test_scanTester_calls_onTestFailure_when_scan_fails() { // Given a test EAN13 barcode let expectedBarcode = PointOfSaleBarcodeScannerTestBarcode.ean13 var onTestPassCalled = false var onTestFailureCalled = false + var onTestTimeoutCalled = false + var receivedScanValue = "" let sut = PointOfSaleBarcodeScannerSetupScanTester( onTestPass: { onTestPassCalled = true }, - onTestFailure: { onTestFailureCalled = true }, + onTestFailure: { scanValue in + onTestFailureCalled = true + receivedScanValue = scanValue + }, + onTestTimeout: { onTestTimeoutCalled = true }, barcodeDefinition: expectedBarcode) - // When the scan fails - sut.handleScan(.failure(TestError.scanFailed)) + // When the scan fails with scanTooShort error + sut.handleScan(.failure(HIDBarcodeParserError.scanTooShort(barcode: "short"))) // Then it calls the failure closure #expect(onTestPassCalled == false) #expect(onTestFailureCalled == true) + #expect(onTestTimeoutCalled == false) + #expect(receivedScanValue == "short") } - private enum TestError: Error { - case scanFailed - } - + @available(iOS 17.0, *) @Test func test_scanTester_provides_correct_barcode_asset() { // Given a test EAN13 barcode let expectedBarcode = PointOfSaleBarcodeScannerTestBarcode.ean13 let sut = PointOfSaleBarcodeScannerSetupScanTester( onTestPass: {}, - onTestFailure: {}, + onTestFailure: { _ in }, + onTestTimeout: {}, barcodeDefinition: expectedBarcode) // Then it provides the correct barcode asset