Skip to content

Commit b4c1714

Browse files
authored
[Woo POS][Barcodes] Implement barcode scan heuristics (#15768)
2 parents ca1cb04 + ef5ec51 commit b4c1714

File tree

6 files changed

+354
-49
lines changed

6 files changed

+354
-49
lines changed

WooCommerce/Classes/POS/Presentation/Barcode Scanning/BarcodeScannerContainer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ class BarcodeScannerHostingController: UIHostingController<EmptyView> {
103103
/// or change between the `began` call and the `ended` call.
104104
/// It's better practice for barcode scanning to only consider the presses when they end.
105105
for press in presses {
106-
guard let key = press.key?.charactersIgnoringModifiers else { continue }
106+
guard let key = press.key else { continue }
107107
scanner.processKeyPress(key)
108108
}
109109
}
Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import UIKit
23

34
/// Parses HID (Human Interface Device) keyboard input into barcode scans.
45
/// This class handles the core logic for interpreting keyboard input as barcode data,
@@ -9,27 +10,137 @@ final class HIDBarcodeParser {
910
/// Callback that is triggered when a barcode is successfully scanned
1011
let onScan: (String) -> Void
1112

13+
private let timeProvider: TimeProvider
14+
1215
private var buffer = ""
16+
private var lastKeyPressTime: Date?
1317

14-
init(configuration: HIDBarcodeParserConfiguration, onScan: @escaping (String) -> Void) {
18+
init(configuration: HIDBarcodeParserConfiguration,
19+
onScan: @escaping (String) -> Void,
20+
timeProvider: TimeProvider = DefaultTimeProvider()) {
1521
self.configuration = configuration
1622
self.onScan = onScan
23+
self.timeProvider = timeProvider
1724
}
1825

1926
/// Process a key press event
2027
/// - Parameter key: The key that was pressed
21-
func processKeyPress(_ key: String) {
22-
if configuration.terminatingStrings.contains(key) {
23-
onScan(buffer)
24-
buffer = ""
28+
func processKeyPress(_ key: UIKey) {
29+
let currentTime = timeProvider.now()
30+
31+
// If characters are entered too slowly, it's probably typing and we should ignore it
32+
if let lastTime = lastKeyPressTime,
33+
currentTime.timeIntervalSince(lastTime) > configuration.maximumInterCharacterTime {
34+
resetScan()
35+
}
36+
37+
lastKeyPressTime = currentTime
38+
39+
let character = key.characters
40+
if configuration.terminatingStrings.contains(character) {
41+
processScan()
2542
} else {
26-
buffer.append(key)
43+
guard !excludedKeys.contains(key.keyCode) else { return }
44+
buffer.append(character)
2745
}
2846
}
2947

48+
private let excludedKeys: [UIKeyboardHIDUsage] = [
49+
.keyboardCapsLock,
50+
.keyboardF1,
51+
.keyboardF2,
52+
.keyboardF3,
53+
.keyboardF4,
54+
.keyboardF5,
55+
.keyboardF6,
56+
.keyboardF7,
57+
.keyboardF8,
58+
.keyboardF9,
59+
.keyboardF10,
60+
.keyboardF11,
61+
.keyboardF12,
62+
.keyboardPrintScreen,
63+
.keyboardScrollLock,
64+
.keyboardPause,
65+
.keyboardInsert,
66+
.keyboardHome,
67+
.keyboardPageUp,
68+
.keyboardDeleteForward,
69+
.keyboardEnd,
70+
.keyboardPageDown,
71+
.keyboardRightArrow,
72+
.keyboardLeftArrow,
73+
.keyboardDownArrow,
74+
.keyboardUpArrow,
75+
.keypadNumLock,
76+
.keyboardApplication,
77+
.keyboardPower,
78+
.keyboardF13,
79+
.keyboardF14,
80+
.keyboardF15,
81+
.keyboardF16,
82+
.keyboardF17,
83+
.keyboardF18,
84+
.keyboardF19,
85+
.keyboardF20,
86+
.keyboardF21,
87+
.keyboardF22,
88+
.keyboardF23,
89+
.keyboardF24,
90+
.keyboardExecute,
91+
.keyboardHelp,
92+
.keyboardMenu,
93+
.keyboardSelect,
94+
.keyboardStop,
95+
.keyboardAgain,
96+
.keyboardUndo,
97+
.keyboardCut,
98+
.keyboardCopy,
99+
.keyboardPaste,
100+
.keyboardFind,
101+
.keyboardMute,
102+
.keyboardVolumeUp,
103+
.keyboardVolumeDown,
104+
.keyboardLockingCapsLock,
105+
.keyboardLockingNumLock,
106+
.keyboardLockingScrollLock,
107+
.keyboardAlternateErase,
108+
.keyboardSysReqOrAttention,
109+
.keyboardCancel,
110+
.keyboardClear,
111+
.keyboardPrior,
112+
.keyboardSeparator,
113+
.keyboardOut,
114+
.keyboardOper,
115+
.keyboardClearOrAgain,
116+
.keyboardCrSelOrProps,
117+
.keyboardExSel,
118+
.keyboardLeftControl,
119+
.keyboardLeftShift,
120+
.keyboardLeftAlt,
121+
.keyboardLeftGUI,
122+
.keyboardRightControl,
123+
.keyboardRightShift,
124+
.keyboardRightAlt,
125+
.keyboardRightGUI,
126+
.keyboard_Reserved
127+
]
128+
30129
/// Cancel the current scan and clear the buffer
31130
func cancel() {
131+
resetScan()
132+
}
133+
134+
private func resetScan() {
32135
buffer = ""
136+
lastKeyPressTime = nil
137+
}
138+
139+
private func processScan() {
140+
if buffer.count >= configuration.minimumBarcodeLength {
141+
onScan(buffer)
142+
}
143+
resetScan()
33144
}
34145
}
35146

@@ -38,8 +149,17 @@ struct HIDBarcodeParserConfiguration {
38149
/// Strings that indicate the end of a barcode scan
39150
let terminatingStrings: Set<String>
40151

152+
/// Minimum length to consider scanned input complete
153+
let minimumBarcodeLength: Int
154+
155+
/// Maximum time between scanned keystrokes
156+
/// After this time elapses, any further keystrokes result in the scan being rejected
157+
let maximumInterCharacterTime: TimeInterval
158+
41159
/// Default configuration suitable for most barcode scanners
42160
static let `default` = HIDBarcodeParserConfiguration(
43-
terminatingStrings: ["\r", "\n"]
161+
terminatingStrings: ["\r", "\n"],
162+
minimumBarcodeLength: 4,
163+
maximumInterCharacterTime: 0.2
44164
)
45165
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
protocol TimeProvider {
4+
func now() -> Date
5+
}
6+
7+
struct DefaultTimeProvider: TimeProvider {
8+
func now() -> Date {
9+
Date()
10+
}
11+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,8 @@
911911
20CC1EDB2AFA8381006BD429 /* InPersonPaymentsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */; };
912912
20CC1EDD2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CC1EDC2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift */; };
913913
20CCBF212B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CCBF202B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift */; };
914+
20CEBF232E02C760001F3300 /* TimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CEBF222E02C760001F3300 /* TimeProvider.swift */; };
915+
20CEBF252E02C7E6001F3300 /* MockTimeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CEBF242E02C7E6001F3300 /* MockTimeProvider.swift */; };
914916
20CF75BA2CF4E6A200ACCF4A /* PointOfSaleOrderController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CF75B92CF4E69000ACCF4A /* PointOfSaleOrderController.swift */; };
915917
20D210BE2B14C9B90099E517 /* WooPaymentsPayoutStatusDisplayDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D210BD2B14C9B90099E517 /* WooPaymentsPayoutStatusDisplayDetails.swift */; };
916918
20D2CCA32C7E175700051705 /* WavesProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D2CCA22C7E175700051705 /* WavesProgressViewStyle.swift */; };
@@ -4162,6 +4164,8 @@
41624164
20CC1EDA2AFA8381006BD429 /* InPersonPaymentsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenu.swift; sourceTree = "<group>"; };
41634165
20CC1EDC2AFA99DF006BD429 /* InPersonPaymentsMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenuViewModel.swift; sourceTree = "<group>"; };
41644166
20CCBF202B0E15C0003102E6 /* WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutsCurrencyOverviewViewModelTests.swift; sourceTree = "<group>"; };
4167+
20CEBF222E02C760001F3300 /* TimeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeProvider.swift; sourceTree = "<group>"; };
4168+
20CEBF242E02C7E6001F3300 /* MockTimeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTimeProvider.swift; sourceTree = "<group>"; };
41654169
20CF75B92CF4E69000ACCF4A /* PointOfSaleOrderController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderController.swift; sourceTree = "<group>"; };
41664170
20D210BD2B14C9B90099E517 /* WooPaymentsPayoutStatusDisplayDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooPaymentsPayoutStatusDisplayDetails.swift; sourceTree = "<group>"; };
41674171
20D2CCA22C7E175700051705 /* WavesProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WavesProgressViewStyle.swift; sourceTree = "<group>"; };
@@ -8443,6 +8447,7 @@
84438447
isa = PBXGroup;
84448448
children = (
84458449
202235F02DFAEAE500E13DE9 /* HIDBarcodeParser.swift */,
8450+
20CEBF222E02C760001F3300 /* TimeProvider.swift */,
84468451
20D5575C2DFADF5400D9EC8B /* BarcodeScannerContainer.swift */,
84478452
202240FB2DFAF41D00E13DE9 /* BarcodeScanningModifier.swift */,
84488453
);
@@ -8453,6 +8458,7 @@
84538458
isa = PBXGroup;
84548459
children = (
84558460
202235F22DFAEC2700E13DE9 /* HIDBarcodeParserTests.swift */,
8461+
20CEBF242E02C7E6001F3300 /* MockTimeProvider.swift */,
84568462
);
84578463
path = "Barcode Scanning";
84588464
sourceTree = "<group>";
@@ -16210,6 +16216,7 @@
1621016216
B9CA4F352AB2F33200285AB9 /* SelectedStoredTaxRateFetcher.swift in Sources */,
1621116217
DE4D23AA29B1B0CA003A4B5D /* WPCom2FALoginView.swift in Sources */,
1621216218
027CCBCD2C23495E002CE572 /* PointOfSaleCardPresentPaymentReaderUpdateFailedLowBatteryView.swift in Sources */,
16219+
20CEBF232E02C760001F3300 /* TimeProvider.swift in Sources */,
1621316220
023D1DD124AB2D05002B03A3 /* ProductListSelectorViewController.swift in Sources */,
1621416221
2004E2D02C077D2800D62521 /* CardPresentPaymentTransaction.swift in Sources */,
1621516222
0206483A23FA4160008441BB /* OrdersRootViewController.swift in Sources */,
@@ -17645,6 +17652,7 @@
1764517652
CC2E72F727B6BFB800A62872 /* ProductVariationFormatterTests.swift in Sources */,
1764617653
02B21C5529C84E4A00C5623B /* WPAdminWebViewModelTests.swift in Sources */,
1764717654
D802547826551DB8001B2CC1 /* CardPresentModalDisplayMessageTests.swift in Sources */,
17655+
20CEBF252E02C7E6001F3300 /* MockTimeProvider.swift in Sources */,
1764817656
02503C632538301400FD235D /* ProductVariationFormActionsFactory+ReadonlyVariationTests.swift in Sources */,
1764917657
B9A5317F2D2FCC5600208304 /* WooShippingCustomsItemViewModelTests.swift in Sources */,
1765017658
DE67D46926BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift in Sources */,

0 commit comments

Comments
 (0)