Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ protocol PointOfSaleAggregateModelProtocol {
private var startPaymentOnCardReaderConnection: AnyCancellable?
private var cardReaderDisconnection: AnyCancellable?

private let soundPlayer: PointOfSaleSoundPlayerProtocol

private var cancellables: Set<AnyCancellable> = []

// Private storage of the concrete coordinator
Expand All @@ -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
Expand All @@ -121,6 +124,7 @@ protocol PointOfSaleAggregateModelProtocol {
self.paymentState = paymentState
self.popularPurchasableItemsController = popularPurchasableItemsController
self.barcodeScanService = barcodeScanService
self.soundPlayer = soundPlayer

publishCardReaderConnectionStatus()
publishPaymentMessages()
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
45 changes: 45 additions & 0 deletions WooCommerce/Classes/POS/Utils/Audio/PointOfSaleSoundPlayer.swift
Original file line number Diff line number Diff line change
@@ -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 {
Copy link

Copilot AI Jun 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Consider stopping or reusing the existing audioPlayer before creating a new instance to avoid overlapping or resource leakage when sounds are played in rapid succession.

Suggested change
do {
do {
if let currentPlayer = audioPlayer {
currentPlayer.stop()
audioPlayer = nil
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried with this suggestion... but it doesn't really seem to make any difference to how it sounds with lots of rapid failures.

The leak comment is worth thinking about, but I don't really see how it would leak.

It probably doesn't hurt to add this, but I'll leave it up to you @staskus

Copy link
Contributor Author

@staskus staskus Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a little bit of optimization makes sense here, especially given that if an item is scanned once, it's likely to be scanned multiple times.

Plus, in an extreme case when we scan multiple wrong items in a row fast, it doesn't make sense to stop the previous sound and start another.

audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer?.prepareToPlay()
audioPlayer?.play()
playerCache[sound] = audioPlayer
} catch {
DDLogError("Failed to play sound: \(error)")
}
}
}
Binary file not shown.
5 changes: 3 additions & 2 deletions WooCommerce/Resources/HTML/licenses.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
background-color: #f7f7f7;
}
a {
color: #96588a;
color: #720EEC;
}

@media (prefers-color-scheme: dark) {
Expand All @@ -18,7 +18,7 @@
background-color: transparent;
}
a {
color: #B07DD1;
color: #873EFF;
}
}
</style>
Expand Down Expand Up @@ -62,6 +62,7 @@ <h4>The following libraries are licensed under <a href="https://opensource.org/l
<h4>Additional credits:</h4>
<ul>
<li><a href="https://freesound.org/people/Zott820/sounds/209578/">Cash register sound</a> ("cha-ching") by CapsLok remixed for extra depth by Zott820</li>
<li><a href="https://pixabay.com/sound-effects/error-message-182475/">Barcode scan error sound</a> by <a href="https://pixabay.com/users/voicebosch-30143949/">VoiceBosch</a> from <a href="https://pixabay.com">Pixabay</a></li>
</ul>
</body>
</html>
28 changes: 28 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -3345,6 +3348,9 @@
01F42C152CE34AB3003D0A5A /* CardPresentModalTapToPaySuccessEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalTapToPaySuccessEmailSent.swift; sourceTree = "<group>"; };
01F42C172CE34AD1003D0A5A /* CardPresentModalSuccessEmailSent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalSuccessEmailSent.swift; sourceTree = "<group>"; };
01F579942C7DE709008BCA28 /* PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleCardPresentPaymentCaptureErrorMessageViewModelTests.swift; sourceTree = "<group>"; };
01F935522DFC0B9700B50B03 /* PointOfSaleSoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleSoundPlayer.swift; sourceTree = "<group>"; };
01F935562DFC0C6400B50B03 /* pos_scan_failure.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = pos_scan_failure.mp3; sourceTree = "<group>"; };
01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleSoundPlayer.swift; sourceTree = "<group>"; };
01FB19572C6E901800A44FF0 /* DynamicHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicHStack.swift; sourceTree = "<group>"; };
0202B68C23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductsTabProductViewModel+ProductVariation.swift"; sourceTree = "<group>"; };
0202B6912387AB0C00F3EBE0 /* WooTab+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WooTab+Tag.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6628,6 +6634,23 @@
path = Location;
sourceTree = "<group>";
};
01F935542DFC0C3B00B50B03 /* Audio */ = {
isa = PBXGroup;
children = (
01F935552DFC0C5A00B50B03 /* Resources */,
01F935522DFC0B9700B50B03 /* PointOfSaleSoundPlayer.swift */,
);
path = Audio;
sourceTree = "<group>";
};
01F935552DFC0C5A00B50B03 /* Resources */ = {
isa = PBXGroup;
children = (
01F935562DFC0C6400B50B03 /* pos_scan_failure.mp3 */,
);
path = Resources;
sourceTree = "<group>";
};
0202B6932387ACE000F3EBE0 /* TabBar */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -7268,6 +7291,7 @@
02055B132D5DAB6400E51059 /* POSCornerRadiusStyle.swift */,
020564972D5DC96600E51059 /* POSShadowStyle.swift */,
02F3884B2D6C38BB00619396 /* POSErrorAndAlertIconSize.swift */,
01F935542DFC0C3B00B50B03 /* Audio */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -7832,6 +7856,7 @@
02CD3BFC2C35D01600E575C4 /* Mocks */ = {
isa = PBXGroup;
children = (
01F935582DFC0D4800B50B03 /* MockPointOfSaleSoundPlayer.swift */,
01AB2D152DDC8CD600AA67FD /* MockAnalytics.swift */,
686A71B72DC9EB6D0006E835 /* MockPOSSearchHistoryService.swift */,
2050D2652DF07BF700C25211 /* MockPointOfSaleBarcodeScanService.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation
@testable import WooCommerce

final class MockPointOfSaleSoundPlayer: PointOfSaleSoundPlayerProtocol {
var onPlaySound: ((PointOfSaleSound) -> Void)?

func playSound(_ sound: PointOfSaleSound) {
onPlaySound?(sound)
}
}
Loading