From 124d8a03c005497c12d1e443f998ae6979c7db29 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:40:46 +0300 Subject: [PATCH 1/9] Add barcode scanner setup analytics events - Add 11 new barcode scanner setup events to WooAnalyticsStat - Add events to POS event list in TracksProvider - Add factory methods in WooAnalyticsEvent+PointOfSale --- .../Classes/Analytics/TracksProvider.swift | 11 +++ .../Classes/Analytics/WooAnalyticsStat.swift | 11 +++ .../WooAnalyticsEvent+PointOfSale.swift | 74 +++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/WooCommerce/Classes/Analytics/TracksProvider.swift b/WooCommerce/Classes/Analytics/TracksProvider.swift index 3f5e96e014a..e9ad1c59831 100644 --- a/WooCommerce/Classes/Analytics/TracksProvider.swift +++ b/WooCommerce/Classes/Analytics/TracksProvider.swift @@ -153,6 +153,17 @@ 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.pointOfSaleBarcodeScannerSetupComplete, + WooAnalyticsStat.pointOfSaleBarcodeScannerSetupScannerConnected, // Order WooAnalyticsStat.orderCreationSuccess, diff --git a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift index c31e56da63f..fb3507fd2b3 100644 --- a/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift +++ b/WooCommerce/Classes/Analytics/WooAnalyticsStat.swift @@ -1308,6 +1308,17 @@ 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 pointOfSaleBarcodeScannerSetupComplete = "barcode_scanner_setup_complete" + 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..c909009d895 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,77 @@ 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 barcodeScannerSetupComplete(scanner: PointOfSaleBarcodeScannerType) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupComplete, + properties: [Key.scanner: scanner.analyticsName]) + } + + static func barcodeScannerSetupScannerConnected(scanner: PointOfSaleBarcodeScannerType, step: String) -> WooAnalyticsEvent { + WooAnalyticsEvent(statName: .pointOfSaleBarcodeScannerSetupScannerConnected, + properties: [Key.scanner: scanner.analyticsName]) + } } } From 6ae110eaa0837ddbd6ce99084937863c084c27c9 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:40:59 +0300 Subject: [PATCH 2/9] Update barcode scanner setup models and scan tester - Add analyticsName to PointOfSaleBarcodeScannerType - Add PointOfSaleBarcodeScannerSetupStepType enum with analyticsValue - Update scan tester to use callbacks with scan value parameter --- ...PointOfSaleBarcodeScannerSetupModels.swift | 37 +++++++++++++++++++ ...tOfSaleBarcodeScannerSetupScanTester.swift | 23 ++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupModels.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupModels.swift index c0732ce9e9a..471a14aba55 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupModels.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupModels.swift @@ -26,6 +26,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 { @@ -38,6 +51,27 @@ private extension PointOfSaleBarcodeScannerType { } } +// MARK: - Step Type +enum PointOfSaleBarcodeScannerSetupStepType { + case setupBarcode + case pairing + case testBarcode + case complete + + var analyticsValue: String { + switch self { + case .setupBarcode: + return "setup_barcode" + case .pairing: + return "pairing" + case .testBarcode: + return "test_barcode" + case .complete: + return "setup_barcode" + } + } +} + // MARK: - Flow State enum PointOfSaleBarcodeScannerSetupFlowState { case scannerSelection @@ -56,13 +90,16 @@ struct PointOfSaleBarcodeScannerSetupStep { let title: String let content: any View let buttonCustomization: PointOfSaleBarcodeScannerButtonCustomization? + let stepType: PointOfSaleBarcodeScannerSetupStepType init( title: String = "", + stepType: PointOfSaleBarcodeScannerSetupStepType, @ViewBuilder content: () -> any View, buttonCustomization: PointOfSaleBarcodeScannerButtonCustomization? = nil ) { self.title = title + self.stepType = stepType self.content = content() self.buttonCustomization = buttonCustomization } diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift index bc530c92ecf..f0fa4bc3991 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift @@ -2,12 +2,17 @@ import Foundation struct PointOfSaleBarcodeScannerSetupScanTester { private let onTestPass: () -> Void - private let onTestFailure: () -> Void + private let onTestFailure: (String) -> Void + private let onTestTimeout: () -> Void private let barcodeDefinition: PointOfSaleBarcodeScannerTestBarcode - 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 } @@ -19,8 +24,18 @@ struct PointOfSaleBarcodeScannerSetupScanTester { switch scanResult { case .success(barcodeDefinition.expectedValue): onTestPass() - case .success, .failure: - onTestFailure() + case .success(let scannedValue): + onTestFailure(scannedValue) + case .failure(HIDBarcodeParserError.scanTooShort(barcode: let scannedValue)): + onTestFailure(scannedValue) + case .failure(HIDBarcodeParserError.timedOut(barcode: let scannedValue)): + onTestFailure(scannedValue) + case .failure: + onTestFailure("") } } + + func handleScanTimeout() { + onTestTimeout() + } } From 1bb17df6d84eee65fc91d01257d24e86da15e291 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:41:11 +0300 Subject: [PATCH 3/9] Add analytics tracking to barcode scanner setup flow - Track scanner selection, navigation, and completion events - Track dismissals except on completion step - Track keyboard connection during setup - Track system settings button taps --- .../PointOfSaleBarcodeScannerSetupFlow.swift | 84 +++++++++++++++---- ...OfSaleBarcodeScannerSetupFlowManager.swift | 65 ++++++++++++-- ...ntOfSaleBarcodeScannerSetupStepViews.swift | 3 + 3 files changed, 132 insertions(+), 20 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlow.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlow.swift index 9258559eac0..eb410c5dca8 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlow.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlow.swift @@ -1,4 +1,5 @@ import SwiftUI +import WooFoundation // MARK: - Point of Sale Barcode Scanner Setup Flow @available(iOS 17.0, *) @@ -8,13 +9,16 @@ class PointOfSaleBarcodeScannerSetupFlow { private let onComplete: () -> Void private let onBackToSelection: () -> Void private var currentStepIndex: Int = 0 + private let analytics: Analytics init(scannerType: PointOfSaleBarcodeScannerType, onComplete: @escaping () -> Void, - onBackToSelection: @escaping () -> Void) { + onBackToSelection: @escaping () -> Void, + analytics: Analytics = ServiceLocator.analytics) { self.scannerType = scannerType self.onComplete = onComplete self.onBackToSelection = onBackToSelection + self.analytics = analytics } var currentStep: PointOfSaleBarcodeScannerSetupStep? { @@ -33,6 +37,7 @@ class PointOfSaleBarcodeScannerSetupFlow { if currentStepIndex < steps.count - 1 { currentStepIndex += 1 } else { + trackSetupComplete() onComplete() } } @@ -49,6 +54,38 @@ class PointOfSaleBarcodeScannerSetupFlow { currentStepIndex = 0 } + func getCurrentAnalyticsStepValue() -> String? { + return currentStep?.stepType.analyticsValue ?? "setup_barcode" + } + + 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 trackSetupComplete() { + analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupComplete(scanner: scannerType)) + } + func getButtonConfiguration() -> PointOfSaleFlowButtonConfiguration { guard let step = currentStep else { return .noButtons() @@ -64,12 +101,14 @@ class PointOfSaleBarcodeScannerSetupFlow { primaryButton: PointOfSaleFlowButtonConfiguration.ButtonConfig( title: nextButtonTitle, action: { [weak self] in + self?.trackSetupNext() self?.nextStep() } ), secondaryButton: PointOfSaleFlowButtonConfiguration.ButtonConfig( title: Localization.backButtonTitle, action: { [weak self] in + self?.trackSetupBack() self?.previousStep() } ) @@ -80,61 +119,78 @@ class PointOfSaleBarcodeScannerSetupFlow { switch scannerType { case .socketS720: return [ - createWelcomeStep(title: "Socket S720 Setup") + createWelcomeStep(title: "Socket S720 Setup", stepType: .setupBarcode) // TODO: Add more steps for Socket S720 WOOMOB-698 ] case .starBSH20B: return [ - PointOfSaleBarcodeScannerSetupStep(content: { + PointOfSaleBarcodeScannerSetupStep(stepType: .setupBarcode, content: { PointOfSaleBarcodeScannerBarcodeView( title: String(format: Localization.starSetUpBarcodeStepTitleFormat, scannerType.name), instruction: Localization.setUpBarcodeStepInstruction, barcode: .starBsh20SetupBarcode) }), - PointOfSaleBarcodeScannerSetupStep(content: { + PointOfSaleBarcodeScannerSetupStep(stepType: .pairing, content: { PointOfSaleBarcodeScannerPairingView(scanner: scannerType) }), PointOfSaleBarcodeScannerSetupStep( - content: { - PointOfSaleBarcodeScannerTestBarcodeView( - scanTester: PointOfSaleBarcodeScannerSetupScanTester( - onTestPass: { [weak self] in - self?.nextStep() - }, - onTestFailure: {}, - barcodeDefinition: .ean13) + stepType: .testBarcode, + content: { [weak self] in + guard let self else { return EmptyView() } + return PointOfSaleBarcodeScannerTestBarcodeView( + scanTester: self.createScanTester(barcodeDefinition: .ean13) ) }, buttonCustomization: PointOfSaleBarcodeScannerBackOnlyButtonCustomization() ), PointOfSaleBarcodeScannerSetupStep( + stepType: .complete, content: { PointOfSaleBarcodeScannerSetupCompleteView() }) // TODO: Add optional error step and documentation step for Star BSH-20B WOOMOB-696 + // TODO: Track barcodeScannerSetupRetryTapped ] case .tbcScanner: return [ - createWelcomeStep(title: "TBC Scanner Setup") + createWelcomeStep(title: "TBC Scanner Setup", stepType: .setupBarcode) // TODO: Add more steps for TBC Scanner WOOMOB-699 ] case .other: return [ PointOfSaleBarcodeScannerSetupStep( title: "General Scanner Setup", + stepType: .setupBarcode, content: { BarcodeScannerInformationContent() } ) ] } } - private func createWelcomeStep(title: String) -> PointOfSaleBarcodeScannerSetupStep { + private func createWelcomeStep(title: String, stepType: PointOfSaleBarcodeScannerSetupStepType) -> PointOfSaleBarcodeScannerSetupStep { PointOfSaleBarcodeScannerSetupStep( title: title, + stepType: stepType, content: { PointOfSaleBarcodeScannerWelcomeView(title: title) }, buttonCustomization: PointOfSaleBarcodeScannerWelcomeButtonCustomization() ) } + + private func createScanTester(barcodeDefinition: PointOfSaleBarcodeScannerTestBarcode) -> PointOfSaleBarcodeScannerSetupScanTester { + PointOfSaleBarcodeScannerSetupScanTester( + onTestPass: { [weak self] in + self?.trackTestScanSuccess() + self?.nextStep() + }, + onTestFailure: { [weak self] scanValue in + self?.trackTestScanFailed(scanValue: scanValue) + }, + onTestTimeout: { [weak self] in + self?.trackTestScanTimedOut() + }, + barcodeDefinition: barcodeDefinition + ) + } } @available(iOS 17.0, *) diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManager.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManager.swift index f4bff61f6ab..020b8e0c947 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,32 @@ 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, + onComplete: { [weak self] in + self?.isPresented = false + }, + onBackToSelection: { [weak self] in + self?.goBackToSelection() + }, + analytics: analytics + ) currentState = .setupFlow(scannerType) } @@ -54,4 +71,40 @@ class PointOfSaleBarcodeScannerSetupFlowManager { return flow.getButtonConfiguration() } } + + func onDisappear() { + guard !isComplete() else { return } + + 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/PointOfSaleBarcodeScannerSetupStepViews.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupStepViews.swift index 458eb01dcea..5f949f04add 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupStepViews.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupStepViews.swift @@ -65,6 +65,8 @@ struct PointOfSaleBarcodeScannerPairingView: View { } Button { + ServiceLocator.analytics.track(event: WooAnalyticsEvent.PointOfSale.barcodeScannerSetupOpenSystemSettingsTapped(scanner: scanner)) + guard let targetURL = URL(string: UIApplication.openSettingsURLString) else { return } @@ -115,6 +117,7 @@ struct PointOfSaleBarcodeScannerTestBarcodeView: View { private func startTimer() { timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { _ in timerCompleted = true + scanTester.handleScanTimeout() } } } From 3b80b0479ceb10dc352265e6c08ce7dcab0346de Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:41:23 +0300 Subject: [PATCH 4/9] Update barcode scanner setup entry point and project - Add dismissal tracking via onDisappear in main setup view - Update Xcode project file for new test files --- .../PointOfSaleBarcodeScannerSetup.swift | 3 +++ WooCommerce/WooCommerce.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetup.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetup.swift index 5f6af304c92..9c58222f871 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetup.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetup.swift @@ -34,6 +34,9 @@ struct PointOfSaleBarcodeScannerSetup: View { .onAppear { ServiceLocator.analytics.track(.pointOfSaleBarcodeScannerSetupFlowShown) } + .onDisappear { + flowManager.onDisappear() + } } // MARK: - Computed Properties 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 */, From eae2f574001942c5983e6c88aca1668342deb1b0 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:41:35 +0300 Subject: [PATCH 5/9] Add and update tests for barcode scanner setup - Update ScanTester tests for new callback structure - Add FlowManager tests for analytics tracking and state management - Test dismissal tracking behavior and completion step logic --- ...eBarcodeScannerSetupFlowManagerTests.swift | 160 ++++++++++++++++++ ...leBarcodeScannerSetupScanTesterTests.swift | 35 +++- 2 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift 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..5085f44f17d --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift @@ -0,0 +1,160 @@ +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") + } + + @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 { + // Expected state + } 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 { + // Expected state after selection + } 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 { + // Expected state after going back + } 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..63dd28ad8b4 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift @@ -8,10 +8,12 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { 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,6 +22,7 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { // Then it calls the pass closure #expect(onTestPassCalled == true) #expect(onTestFailureCalled == false) + #expect(onTestTimeoutCalled == false) } @Test func test_scanTester_calls_onTestFailure_when_scan_received_for_unexpected_barcode() { @@ -27,29 +30,44 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { 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() { + @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 @@ -58,6 +76,8 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { // Then it calls the failure closure #expect(onTestPassCalled == false) #expect(onTestFailureCalled == true) + #expect(onTestTimeoutCalled == false) + #expect(receivedScanValue == "") } private enum TestError: Error { @@ -70,7 +90,8 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { let sut = PointOfSaleBarcodeScannerSetupScanTester( onTestPass: {}, - onTestFailure: {}, + onTestFailure: { _ in }, + onTestTimeout: {}, barcodeDefinition: expectedBarcode) // Then it provides the correct barcode asset From 61abcde4f427b00c92fae2ffc5aa534d4fbf3ba0 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:52:02 +0300 Subject: [PATCH 6/9] Update barcode scanning to use specific HIDBarcodeParserError type - Change Result to Result throughout barcode scanning chain - Update GameControllerBarcodeParser.asResult to return specific error type - Update scan tester to handle specific error type with error.barcode property - Update tests to use HIDBarcodeParserError instead of generic TestError --- .../Classes/POS/Models/PointOfSaleAggregateModel.swift | 4 ++-- .../PointOfSaleBarcodeScannerSetupScanTester.swift | 10 +++------- .../Barcode Scanning/BarcodeScannerContainer.swift | 8 ++++---- .../Barcode Scanning/BarcodeScanningModifier.swift | 4 ++-- .../GameControllerBarcodeObserver.swift | 6 +++--- .../Barcode Scanning/GameControllerBarcodeParser.swift | 2 +- .../POS/Mocks/MockPointOfSaleAggregateModel.swift | 2 +- ...PointOfSaleBarcodeScannerSetupScanTesterTests.swift | 10 +++------- 8 files changed, 19 insertions(+), 27 deletions(-) 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/PointOfSaleBarcodeScannerSetupScanTester.swift b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift index f0fa4bc3991..d3f405400d7 100644 --- a/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift +++ b/WooCommerce/Classes/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTester.swift @@ -20,18 +20,14 @@ struct PointOfSaleBarcodeScannerSetupScanTester { barcodeDefinition.barcodeAsset } - func handleScan(_ scanResult: Result) { + func handleScan(_ scanResult: Result) { switch scanResult { case .success(barcodeDefinition.expectedValue): onTestPass() case .success(let scannedValue): onTestFailure(scannedValue) - case .failure(HIDBarcodeParserError.scanTooShort(barcode: let scannedValue)): - onTestFailure(scannedValue) - case .failure(HIDBarcodeParserError.timedOut(barcode: let scannedValue)): - onTestFailure(scannedValue) - case .failure: - onTestFailure("") + case .failure(let error): + onTestFailure(error.barcode) } } 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/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/PointOfSaleBarcodeScannerSetupScanTesterTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift index 63dd28ad8b4..916c19ccfe1 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift @@ -70,18 +70,14 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { 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 == "") - } - - private enum TestError: Error { - case scanFailed + #expect(receivedScanValue == "short") } @Test func test_scanTester_provides_correct_barcode_asset() { From f6bc6128fa97b9c77cb6cde9354aec8132b28ffa Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:23:44 +0300 Subject: [PATCH 7/9] Updated tests after the merge --- .../PointOfSaleBarcodeScannerSetupFlowManagerTests.swift | 2 +- .../PointOfSaleBarcodeScannerSetupScanTesterTests.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift index 5085f44f17d..e7f44f50d7b 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift @@ -43,7 +43,7 @@ struct PointOfSaleBarcodeScannerSetupFlowManagerTests { 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") + #expect(event?.properties["step"] as? String == "setup_barcode_hid") } @available(iOS 17.0, *) diff --git a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift index 916c19ccfe1..38bd1f6ad3b 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupScanTesterTests.swift @@ -3,6 +3,7 @@ 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 @@ -25,6 +26,7 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { #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 @@ -53,6 +55,7 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { #expect(receivedScanValue == unexpectedBarcode) } + @available(iOS 17.0, *) @Test func test_scanTester_calls_onTestFailure_when_scan_fails() { // Given a test EAN13 barcode let expectedBarcode = PointOfSaleBarcodeScannerTestBarcode.ean13 @@ -80,6 +83,7 @@ struct PointOfSaleBarcodeScannerSetupScanTesterTests { #expect(receivedScanValue == "short") } + @available(iOS 17.0, *) @Test func test_scanTester_provides_correct_barcode_asset() { // Given a test EAN13 barcode let expectedBarcode = PointOfSaleBarcodeScannerTestBarcode.ean13 From 2c85102e81e19b933fe168b9a96ea71794cc29a3 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:43:51 +0300 Subject: [PATCH 8/9] Disable barcode scanning if a modal is presented --- .../Classes/POS/Presentation/ItemListView.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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: From 20bee219334d2a8e70f87696f5b0321ea0aae838 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Tue, 15 Jul 2025 13:31:05 +0300 Subject: [PATCH 9/9] Remove unnecessary comments fro FlowManagerTests Co-authored-by: Gabriel Maldonado --- .../PointOfSaleBarcodeScannerSetupFlowManagerTests.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift index e7f44f50d7b..ac7a56dec8e 100644 --- a/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Presentation/Barcode Scanner Setup/PointOfSaleBarcodeScannerSetupFlowManagerTests.swift @@ -114,7 +114,6 @@ struct PointOfSaleBarcodeScannerSetupFlowManagerTests { // Initially on scanner selection if case .scannerSelection = sut.currentState { - // Expected state } else { #expect(Bool(false), "Expected scannerSelection state") } @@ -141,7 +140,6 @@ struct PointOfSaleBarcodeScannerSetupFlowManagerTests { sut.selectScanner(.other) if case .setupFlow = sut.currentState { - // Expected state after selection } else { #expect(Bool(false), "Expected setupFlow state after selection") } @@ -151,7 +149,6 @@ struct PointOfSaleBarcodeScannerSetupFlowManagerTests { // Then state returns to scanner selection if case .scannerSelection = sut.currentState { - // Expected state after going back } else { #expect(Bool(false), "Expected scannerSelection state after going back") }