diff --git a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift index 05c46e02f0d..82b1651188a 100644 --- a/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift +++ b/WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift @@ -82,6 +82,8 @@ protocol PointOfSaleAggregateModelProtocol { private var startPaymentOnCardReaderConnection: AnyCancellable? private var cardReaderDisconnection: AnyCancellable? + private let soundPlayer: PointOfSaleSoundPlayerProtocol + private var cancellables: Set = [] // Private storage of the concrete coordinator @@ -108,6 +110,7 @@ protocol PointOfSaleAggregateModelProtocol { searchHistoryService: POSSearchHistoryProviding, popularPurchasableItemsController: PointOfSaleItemsControllerProtocol, barcodeScanService: PointOfSaleBarcodeScanServiceProtocol, + soundPlayer: PointOfSaleSoundPlayerProtocol = PointOfSaleSoundPlayer(), paymentState: PointOfSalePaymentState = .card(.idle)) { self.purchasableItemsController = itemsController self.purchasableItemsSearchController = purchasableItemsSearchController @@ -121,6 +124,7 @@ protocol PointOfSaleAggregateModelProtocol { self.paymentState = paymentState self.popularPurchasableItemsController = popularPurchasableItemsController self.barcodeScanService = barcodeScanService + self.soundPlayer = soundPlayer publishCardReaderConnectionStatus() publishPaymentMessages() @@ -190,6 +194,7 @@ extension PointOfSaleAggregateModel { } catch { DDLogInfo("Failed to find item by barcode: \(error)") cart.updateLoadingItem(id: placeholderItemID, with: error) + await soundPlayer.playSound(.barcodeScanFailure) } } } diff --git a/WooCommerce/Classes/POS/Utils/Audio/PointOfSaleSoundPlayer.swift b/WooCommerce/Classes/POS/Utils/Audio/PointOfSaleSoundPlayer.swift new file mode 100644 index 00000000000..86e099c70d5 --- /dev/null +++ b/WooCommerce/Classes/POS/Utils/Audio/PointOfSaleSoundPlayer.swift @@ -0,0 +1,45 @@ +import Foundation +import AVFoundation + +struct PointOfSaleSound: Equatable, Hashable { + let name: String + let type: String + + static var barcodeScanFailure: PointOfSaleSound { + PointOfSaleSound(name: "pos_scan_failure", type: "mp3") + } +} + +protocol PointOfSaleSoundPlayerProtocol { + func playSound(_ sound: PointOfSaleSound) async +} + +final class PointOfSaleSoundPlayer: PointOfSaleSoundPlayerProtocol { + private var audioPlayer: AVAudioPlayer? + private var playerCache: [PointOfSaleSound: AVAudioPlayer] = [:] + + @MainActor + func playSound(_ sound: PointOfSaleSound) async { + guard let url = Bundle.main.url(forResource: sound.name, withExtension: sound.type) else { + DDLogError("Sound file not found: \(sound.name).\(sound.type)") + return + } + + if let cachedPlayer = playerCache[sound] { + if !cachedPlayer.isPlaying { + cachedPlayer.currentTime = 0 + cachedPlayer.play() + } + return + } + + do { + audioPlayer = try AVAudioPlayer(contentsOf: url) + audioPlayer?.prepareToPlay() + audioPlayer?.play() + playerCache[sound] = audioPlayer + } catch { + DDLogError("Failed to play sound: \(error)") + } + } +} diff --git a/WooCommerce/Classes/POS/Utils/Audio/Resources/pos_scan_failure.mp3 b/WooCommerce/Classes/POS/Utils/Audio/Resources/pos_scan_failure.mp3 new file mode 100644 index 00000000000..1ed3bee2f12 Binary files /dev/null and b/WooCommerce/Classes/POS/Utils/Audio/Resources/pos_scan_failure.mp3 differ diff --git a/WooCommerce/Resources/HTML/licenses.html b/WooCommerce/Resources/HTML/licenses.html index 42ea9c1f819..8772346081c 100644 --- a/WooCommerce/Resources/HTML/licenses.html +++ b/WooCommerce/Resources/HTML/licenses.html @@ -9,7 +9,7 @@ background-color: #f7f7f7; } a { - color: #96588a; + color: #720EEC; } @media (prefers-color-scheme: dark) { @@ -18,7 +18,7 @@ background-color: transparent; } a { - color: #B07DD1; + color: #873EFF; } } @@ -62,6 +62,7 @@

The following libraries are licensed under Cash register sound ("cha-ching") by CapsLok remixed for extra depth by Zott820 +
  • Barcode scan error sound by VoiceBosch from Pixabay
  • diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index b5d89fb4d29..f024763eaa6 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -87,6 +87,9 @@ 01F42C162CE34AB8003D0A5A /* CardPresentModalTapToPaySuccessEmailSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F42C152CE34AB3003D0A5A /* CardPresentModalTapToPaySuccessEmailSent.swift */; }; 01F42C182CE34AD2003D0A5A /* CardPresentModalSuccessEmailSent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F42C172CE34AD1003D0A5A /* CardPresentModalSuccessEmailSent.swift */; }; 01F579952C7DE709008BCA28 /* PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F579942C7DE709008BCA28 /* PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift */; }; + 01F935532DFC0B9900B50B03 /* PointOfSaleSoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F935522DFC0B9700B50B03 /* PointOfSaleSoundPlayer.swift */; }; + 01F935572DFC0C6400B50B03 /* pos_scan_failure.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 01F935562DFC0C6400B50B03 /* pos_scan_failure.mp3 */; }; + 01F935592DFC0D4C00B50B03 /* MockPointOfSaleSoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */; }; 01FB19582C6E901800A44FF0 /* DynamicHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FB19572C6E901800A44FF0 /* DynamicHStack.swift */; }; 0202B68D23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0202B68C23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift */; }; 0202B6922387AB0C00F3EBE0 /* WooTab+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0202B6912387AB0C00F3EBE0 /* WooTab+Tag.swift */; }; @@ -3345,6 +3348,9 @@ 01F42C152CE34AB3003D0A5A /* CardPresentModalTapToPaySuccessEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalTapToPaySuccessEmailSent.swift; sourceTree = ""; }; 01F42C172CE34AD1003D0A5A /* CardPresentModalSuccessEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalSuccessEmailSent.swift; sourceTree = ""; }; 01F579942C7DE709008BCA28 /* PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift; sourceTree = ""; }; + 01F935522DFC0B9700B50B03 /* PointOfSaleSoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleSoundPlayer.swift; sourceTree = ""; }; + 01F935562DFC0C6400B50B03 /* pos_scan_failure.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = pos_scan_failure.mp3; sourceTree = ""; }; + 01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleSoundPlayer.swift; sourceTree = ""; }; 01FB19572C6E901800A44FF0 /* DynamicHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicHStack.swift; sourceTree = ""; }; 0202B68C23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductsTabProductViewModel+ProductVariation.swift"; sourceTree = ""; }; 0202B6912387AB0C00F3EBE0 /* WooTab+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WooTab+Tag.swift"; sourceTree = ""; }; @@ -6628,6 +6634,23 @@ path = Location; sourceTree = ""; }; + 01F935542DFC0C3B00B50B03 /* Audio */ = { + isa = PBXGroup; + children = ( + 01F935552DFC0C5A00B50B03 /* Resources */, + 01F935522DFC0B9700B50B03 /* PointOfSaleSoundPlayer.swift */, + ); + path = Audio; + sourceTree = ""; + }; + 01F935552DFC0C5A00B50B03 /* Resources */ = { + isa = PBXGroup; + children = ( + 01F935562DFC0C6400B50B03 /* pos_scan_failure.mp3 */, + ); + path = Resources; + sourceTree = ""; + }; 0202B6932387ACE000F3EBE0 /* TabBar */ = { isa = PBXGroup; children = ( @@ -7268,6 +7291,7 @@ 02055B132D5DAB6400E51059 /* POSCornerRadiusStyle.swift */, 020564972D5DC96600E51059 /* POSShadowStyle.swift */, 02F3884B2D6C38BB00619396 /* POSErrorAndAlertIconSize.swift */, + 01F935542DFC0C3B00B50B03 /* Audio */, ); path = Utils; sourceTree = ""; @@ -7832,6 +7856,7 @@ 02CD3BFC2C35D01600E575C4 /* Mocks */ = { isa = PBXGroup; children = ( + 01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */, 01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */, 686A71B72DC9EB6D0006E835 /* MockPOSSearchHistoryService.swift */, 2050D2652DF07BF700C25211 /* MockPointOfSaleBarcodeScanService.swift */, @@ -14885,6 +14910,7 @@ 456AB0E8283E610500019CFF /* WCShipInstallTableViewCell.xib in Resources */, 02162727237963AF000208D2 /* ProductFormViewController.xib in Resources */, D8EE9693264D328A0033B2F9 /* LegacyReceiptViewController.xib in Resources */, + 01F935572DFC0C6400B50B03 /* pos_scan_failure.mp3 in Resources */, DE1B030E268DD01A00804330 /* ReviewOrderViewController.xib in Resources */, 45FDDD66267784AD00ADACE8 /* ShippingLabelSummaryTableViewCell.xib in Resources */, E1D4E84526776AD900256B83 /* HeadlineTableViewCell.xib in Resources */, @@ -16637,6 +16663,7 @@ 20A3AFE32B10EF860033AF2D /* CardReaderSettingsFlowPresentingView.swift in Sources */, CE14452E2188C11700A991D8 /* ZendeskManager.swift in Sources */, 02577A7F2BFC4BB300B63FE6 /* PaymentMethodsWrapperHostingController.swift in Sources */, + 01F935532DFC0B9900B50B03 /* PointOfSaleSoundPlayer.swift in Sources */, 02BA53432A380D7D0069224D /* ProductDescriptionAICoordinator.swift in Sources */, FE28F7122684CA29004465C7 /* RoleErrorViewController.swift in Sources */, B5BE75DB213F1D1E00909A14 /* OverlayMessageView.swift in Sources */, @@ -17477,6 +17504,7 @@ 02645D8227BA20A30065DC68 /* InboxViewModelTests.swift in Sources */, 57ABE36824EB048A00A64F49 /* MockSwitchStoreUseCase.swift in Sources */, 311F827626CD8AB100DF5BAD /* MockCardReaderSettingsAlerts.swift in Sources */, + 01F935592DFC0D4C00B50B03 /* MockPointOfSaleSoundPlayer.swift in Sources */, 26A280D62B45F00F00ACEE87 /* OrderNotificationViewModelTests.swift in Sources */, EE45E2C22A42C9D80085F227 /* ProductDescriptionAITooltipUseCaseTests.swift in Sources */, CEEF74282B99F57A00B03948 /* RevenueReportCardViewModelTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleBarcodeScanService.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleBarcodeScanService.swift index 4898e5e4ec8..0c8b0ff5e49 100644 --- a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleBarcodeScanService.swift +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleBarcodeScanService.swift @@ -1,8 +1,14 @@ import Foundation import Yosemite -struct MockPointOfSaleBarcodeScanService: PointOfSaleBarcodeScanServiceProtocol { +class MockPointOfSaleBarcodeScanService: PointOfSaleBarcodeScanServiceProtocol { + var errorToThrow: PointOfSaleBarcodeScanError? + func getItem(barcode: String) async throws(PointOfSaleBarcodeScanError) -> POSItem { + if let error = errorToThrow { + throw error + } + return .simpleProduct(POSSimpleProduct( id: UUID(), name: "Scanned Item", diff --git a/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleSoundPlayer.swift b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleSoundPlayer.swift new file mode 100644 index 00000000000..97ee2e39be9 --- /dev/null +++ b/WooCommerce/WooCommerceTests/POS/Mocks/MockPointOfSaleSoundPlayer.swift @@ -0,0 +1,10 @@ +import Foundation +@testable import WooCommerce + +final class MockPointOfSaleSoundPlayer: PointOfSaleSoundPlayerProtocol { + var onPlaySound: ((PointOfSaleSound) -> Void)? + + func playSound(_ sound: PointOfSaleSound) { + onPlaySound?(sound) + } +} diff --git a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift index 9f20cdb8ca0..6f329e5b6fd 100644 --- a/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift +++ b/WooCommerce/WooCommerceTests/POS/Models/PointOfSaleAggregateModelTests.swift @@ -24,7 +24,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // Then #expect(sut.orderStage == .building) } @@ -42,7 +43,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) await sut.checkOut() try #require(sut.orderStage == .finalizing) @@ -69,7 +71,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) // When @@ -92,7 +95,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) await sut.checkOut() try #require(sut.orderStage == .finalizing) @@ -164,7 +168,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) try #require(sut.cart.isEmpty) let item = makePurchasableItem() @@ -188,7 +193,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) let items = [makePurchasableItem(), makePurchasableItem(), makePurchasableItem()] // When @@ -217,7 +223,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) let item = makePurchasableItem(name: "Item 1") let anotherItem = makePurchasableItem(name: "Item 2") @@ -247,7 +254,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) let item = makePurchasableItem(name: "Item 1") let anotherItem = makePurchasableItem(name: "Item 2") @@ -275,7 +283,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) let item = makePurchasableItem(name: "Item 1") let anotherItem = makePurchasableItem(name: "Item 2") let couponItem = makeCouponItem(code: "VALID") @@ -321,7 +330,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) @@ -347,7 +357,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) sut.addToCart(makePurchasableItem()) @@ -376,7 +387,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) sut.removeAllItemsFromCart() @@ -404,7 +416,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) @@ -432,7 +445,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // When try await sut.sendReceipt(to: "") @@ -458,7 +472,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) do { // When @@ -486,7 +501,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) @@ -519,7 +535,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // Then #expect(sut.paymentState == .card(.idle)) @@ -540,6 +557,7 @@ struct PointOfSaleAggregateModelTests { searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer(), paymentState: .card(.cardPaymentSuccessful)) // When @@ -564,7 +582,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) cardPresentPaymentService.paymentEvent = .show(eventDetails: .paymentSuccess(done: {})) try #require(sut.cardPresentPaymentInlineMessage != nil) @@ -591,6 +610,7 @@ struct PointOfSaleAggregateModelTests { searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer(), paymentState: .card(.cardPaymentSuccessful)) // When @@ -615,7 +635,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) cardPresentPaymentService.paymentEvent = .show( eventDetails: .tapSwipeOrInsertCard( @@ -646,7 +667,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // When await sut.startCashPayment() @@ -671,7 +693,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // When await sut.startCashPayment() @@ -695,7 +718,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) await sut.startCashPayment() #expect(sut.paymentState == .cash(.collectingCash)) @@ -721,7 +745,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) #expect(sut.orderStage == .building) await sut.checkOut() @@ -757,7 +782,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // When cardPresentPaymentService.paymentEvent = .show(eventDetails: .paymentSuccess(done: {})) @@ -785,7 +811,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) struct TestError: Error {} // When paymentIntentCreationError event is received @@ -819,7 +846,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) struct TestError: Error {} await sut.checkOut() @@ -856,7 +884,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) cardPresentPaymentService.connectedReader = nil orderController.orderStateToReturn = makeLoadedOrderState(orderTotal: "$1.00", orderTotalDecimal: 1) @@ -892,7 +921,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) orderController.orderStateToReturn = makeLoadedOrderState(orderTotal: "$0.01", orderTotalDecimal: 0.01) @@ -918,7 +948,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) cardPresentPaymentService.connectedReader = .init(name: "Test reader", batteryLevel: 0.7) orderController.orderStateToReturn = makeLoadedOrderState(orderTotal: "$0.00", orderTotalDecimal: 0.0) @@ -944,7 +975,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) cardPresentPaymentService.connectedReader = CardPresentPaymentCardReader(name: "Test", batteryLevel: 0.5) orderController.orderStateToReturn = makeLoadedOrderState(orderTotal: "$1.00", orderTotalDecimal: 1) @@ -981,7 +1013,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) orderController.orderStateToReturn = makeLoadedOrderState(cartTotal: "$1.00") await orderController.syncOrder(for: .init(), retryHandler: {}) @@ -1013,7 +1046,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) let onboardingViewModel = CardPresentPaymentsOnboardingViewModel( fixedState: .pluginNotActivated(plugin: .stripe), useCase: MockCardPresentPaymentsOnboardingUseCase(initial: .pluginNotActivated(plugin: .stripe)) @@ -1055,7 +1089,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) @@ -1090,7 +1125,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) sut.addToCart(makePurchasableItem()) @@ -1116,7 +1152,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) //When sut.connectCardReader() @@ -1140,7 +1177,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) //When sut.disconnectCardReader() @@ -1162,7 +1200,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: analyticsTracker, searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // When await sut.checkOut() @@ -1185,7 +1224,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: analyticsTracker, searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // When await sut.cancelCashPayment() @@ -1208,7 +1248,8 @@ struct PointOfSaleAggregateModelTests { collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), searchHistoryService: MockPOSSearchHistoryService(), popularPurchasableItemsController: MockPointOfSaleItemsController(), - barcodeScanService: MockPointOfSaleBarcodeScanService()) + barcodeScanService: MockPointOfSaleBarcodeScanService(), + soundPlayer: MockPointOfSaleSoundPlayer()) // When await sut.startCashPayment() @@ -1217,6 +1258,37 @@ struct PointOfSaleAggregateModelTests { #expect(mockAnalyticsProvider.receivedEvents.first(where: { $0 == "cash_payment_tapped" }) != nil) } } + + struct BarcodeTests { + @available(iOS 17.0, *) + @Test func barcodeScanned_when_fails_then_plays_sound() async { + // Given + let soundPlayer = MockPointOfSaleSoundPlayer() + let barcodeScanService = MockPointOfSaleBarcodeScanService() + let sut = PointOfSaleAggregateModel(itemsController: MockPointOfSaleItemsController(), + purchasableItemsSearchController: MockPointOfSalePurchasableItemsSearchController(), + couponsController: MockPointOfSaleCouponsController(), + couponsSearchController: MockPointOfSaleCouponsController(), + cardPresentPaymentService: MockCardPresentPaymentService(), + orderController: MockPointOfSaleOrderController(), + analytics: WooAnalytics(analyticsProvider: MockAnalyticsProvider()), + collectOrderPaymentAnalyticsTracker: MockPOSCollectOrderPaymentAnalyticsTracker(), + searchHistoryService: MockPOSSearchHistoryService(), + popularPurchasableItemsController: MockPointOfSaleItemsController(), + barcodeScanService: barcodeScanService, + soundPlayer: soundPlayer) + barcodeScanService.errorToThrow = .notFound(scannedCode: "123") + + // When & Then + await withCheckedContinuation { continuation in + soundPlayer.onPlaySound = { sound in + #expect(sound == .barcodeScanFailure) + continuation.resume() + } + sut.barcodeScanned("123") + } + } + } } private func makePurchasableItem(name: String = "") -> POSItem {