Skip to content

Commit f5af05b

Browse files
authored
[POS Settings] Display card reader details in hardware section (#16045)
2 parents f4a2d04 + e590dc1 commit f5af05b

File tree

6 files changed

+146
-71
lines changed

6 files changed

+146
-71
lines changed

WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ extension PointOfSaleAggregateModel {
323323
}
324324
}
325325

326+
326327
/// Starts a payment immediately if a reader is connected.
327328
/// Otherwise, schedules a payment to start the next time a reader connects.
328329
/// Note that any scheduled payments are cancelled by `cancelReaderPreparation`

WooCommerce/Classes/POS/Models/PointOfSaleSettingsController.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import Combine
23
import struct Yosemite.SiteSetting
34
import enum Yosemite.Plugin
45
import class Yosemite.PluginsService
@@ -19,6 +20,8 @@ protocol PointOfSaleSettingsControllerProtocol {
1920
var storeName: String { get }
2021
var storeAddress: String { get }
2122

23+
var connectedCardReader: CardPresentPaymentCardReader? { get }
24+
2225
func retrievePOSReceiptSettings() async
2326
}
2427

@@ -34,13 +37,33 @@ protocol PointOfSaleSettingsControllerProtocol {
3437
private let defaultSiteName: String?
3538
private let settingsService: PointOfSaleSettingsServiceProtocol
3639
private let siteSettings: [SiteSetting]
40+
private(set) var connectedCardReader: CardPresentPaymentCardReader?
41+
private var cancellables: AnyCancellable?
3742

3843
init(settingsService: PointOfSaleSettingsServiceProtocol,
44+
cardPresentPaymentService: CardPresentPaymentFacade,
3945
defaultSiteName: String? = ServiceLocator.stores.sessionManager.defaultSite?.name,
4046
siteSettings: [SiteSetting] = ServiceLocator.selectedSiteSettings.siteSettings) {
4147
self.settingsService = settingsService
4248
self.defaultSiteName = defaultSiteName
4349
self.siteSettings = siteSettings
50+
51+
observeCardReader(from: cardPresentPaymentService)
52+
}
53+
54+
private func observeCardReader(from service: CardPresentPaymentFacade) {
55+
cancellables = service.readerConnectionStatusPublisher
56+
.sink(receiveValue: { [weak self] connectionStatus in
57+
guard let self else { return }
58+
let cardReader: CardPresentPaymentCardReader?
59+
switch connectionStatus {
60+
case .connected(let reader):
61+
cardReader = reader
62+
default:
63+
cardReader = nil
64+
}
65+
connectedCardReader = cardReader
66+
})
4467
}
4568

4669
var storeName: String {
@@ -128,12 +151,18 @@ final class PointOfSaleSettingsPreviewController: PointOfSaleSettingsControllerP
128151
var shouldShowReceiptInformation: Bool = true
129152
var storeName: String = "Sample Store"
130153

154+
var connectedCardReader: CardPresentPaymentCardReader? = CardPresentPaymentCardReader(
155+
name: "WisePad 3",
156+
batteryLevel: 0.75
157+
)
158+
131159
var storeAddress: String {
132160
"123 Main Street\nAnytown, ST 12345"
133161
}
134162

135163
func retrievePOSReceiptSettings() async {
136164
// no-op
137165
}
166+
138167
}
139168
#endif

WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsHardwareDetailView.swift

Lines changed: 71 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,30 @@
11
import SwiftUI
2-
import enum Yosemite.AppSettingsAction
32

43
struct PointOfSaleSettingsHardwareDetailView: View {
4+
let settingsController: PointOfSaleSettingsControllerProtocol
5+
56
@State private var navigationPath: [NavigationDestination] = []
6-
@State private var lastKnownLoadedCardReader: String?
77
@State private var showBarcodeScanningSetupModal: Bool = false
88
@State private var showBarcodeScanningDocumentationModal: Bool = false
99
@State private var showCardReaderDocumentationModal: Bool = false
1010

11+
private var cardReaderName: String {
12+
if let cardReaderName = settingsController.connectedCardReader?.name {
13+
return cardReaderName
14+
} else {
15+
return Localization.cardReaderNotConnected
16+
}
17+
}
18+
19+
private var formattedBatteryLevel: String {
20+
if let batteryLevel = settingsController.connectedCardReader?.batteryLevel {
21+
let percentage = Int(batteryLevel * 100)
22+
return "\(percentage)%"
23+
} else {
24+
return Localization.batteryLevelUnknown
25+
}
26+
}
27+
1128
var body: some View {
1229
NavigationStack(path: $navigationPath) {
1330
List(HardwareDestination.allCases) { destination in
@@ -53,37 +70,29 @@ struct PointOfSaleSettingsHardwareDetailView: View {
5370

5471
private var cardReadersView: some View {
5572
VStack(spacing: POSSpacing.large) {
56-
VStack(spacing: POSSpacing.medium) {
57-
VStack(spacing: POSPadding.small) {
58-
Text(Localization.cardReadersDescription)
59-
.font(.posBodyLargeRegular())
60-
.multilineTextAlignment(.center)
61-
Text(Localization.cardReadersSubtitle1)
62-
.font(.posBodyMediumRegular())
63-
.foregroundStyle(.secondary)
64-
.multilineTextAlignment(.center)
65-
Text(Localization.cardReadersSubtitle2)
66-
.font(.posBodyMediumRegular())
67-
.foregroundStyle(.secondary)
68-
.multilineTextAlignment(.center)
69-
Text(Localization.cardReadersSubtitle3)
70-
.font(.posBodyMediumRegular())
71-
.foregroundStyle(.secondary)
72-
.multilineTextAlignment(.center)
73-
}
74-
}
75-
.padding()
76-
77-
if let lastKnownLoadedCardReader {
78-
HStack {
79-
Text("Model: \(lastKnownLoadedCardReader)")
80-
.font(.posBodyMediumRegular())
81-
.foregroundStyle(.secondary)
82-
.multilineTextAlignment(.center)
83-
}
84-
}
85-
8673
List {
74+
VStack(spacing: POSPadding.xSmall) {
75+
HStack {
76+
Text(Localization.readerModelTitle)
77+
.font(.posBodyMediumRegular())
78+
.foregroundStyle(.primary)
79+
Spacer()
80+
Text(cardReaderName)
81+
.font(.posBodyMediumRegular())
82+
.foregroundStyle(.secondary)
83+
}
84+
.padding()
85+
HStack {
86+
Text(Localization.readerBatteryTitle)
87+
.font(.posBodyMediumRegular())
88+
.foregroundStyle(.primary)
89+
Spacer()
90+
Text(formattedBatteryLevel)
91+
.font(.posBodyMediumRegular())
92+
.foregroundStyle(.secondary)
93+
}
94+
.padding()
95+
}
8796
Button {
8897
showCardReaderDocumentationModal = true
8998
} label: {
@@ -106,18 +115,6 @@ struct PointOfSaleSettingsHardwareDetailView: View {
106115
.posFullScreenCover(isPresented: $showCardReaderDocumentationModal) {
107116
SafariView(url: WooConstants.URLs.inPersonPaymentsLearnMoreWCPay.asURL())
108117
}
109-
.task { @MainActor in
110-
// TODO: WOOMOB-1172
111-
let action = AppSettingsAction.loadCardReader { reader in
112-
switch reader {
113-
case let .success(foundReader):
114-
lastKnownLoadedCardReader = foundReader
115-
case let .failure(error):
116-
debugPrint(error)
117-
}
118-
}
119-
ServiceLocator.stores.dispatch(action)
120-
}
121118
}
122119

123120
private var scannersView: some View {
@@ -220,6 +217,30 @@ extension PointOfSaleSettingsHardwareDetailView {
220217

221218
private extension PointOfSaleSettingsHardwareDetailView {
222219
enum Localization {
220+
static let readerModelTitle = NSLocalizedString(
221+
"pointOfSaleSettingsHardwareDetailView.readerModelTitle",
222+
value: "Model",
223+
comment: "Text displayed on Point of Sale settings pointing to the card reader model."
224+
)
225+
226+
static let readerBatteryTitle = NSLocalizedString(
227+
"pointOfSaleSettingsHardwareDetailView.readerBatteryTitle",
228+
value: "Battery",
229+
comment: "Text displayed on Point of Sale settings pointing to the card reader battery."
230+
)
231+
232+
static let cardReaderNotConnected = NSLocalizedString(
233+
"pointOfSaleSettingsHardwareDetailView.cardReaderNotConnected",
234+
value: "Reader not connected",
235+
comment: "Text displayed on Point of Sale settings when the card reader is not connected."
236+
)
237+
238+
static let batteryLevelUnknown = NSLocalizedString(
239+
"pointOfSaleSettingsHardwareDetailView.batteryLevelUnknown",
240+
value: "Unknown",
241+
comment: "Text displayed on Point of Sale settings when card reader battery is unknown."
242+
)
243+
223244
static let cardReadersTitle = NSLocalizedString(
224245
"pointOfSaleSettingsHardwareDetailView.cardReadersTitle",
225246
value: "Card readers",
@@ -256,30 +277,6 @@ private extension PointOfSaleSettingsHardwareDetailView {
256277
comment: "Subtitle describing barcode scanner documentation in Point of Sale settings."
257278
)
258279

259-
static let cardReadersDescription = NSLocalizedString(
260-
"pointOfSaleSettingsHardwareDetailView.cardReadersDescription",
261-
value: "Accept secure and fast payments in person",
262-
comment: "Main description for card readers functionality in Point of Sale settings."
263-
)
264-
265-
static let cardReadersSubtitle1 = NSLocalizedString(
266-
"pointOfSaleSettingsHardwareDetailView.cardReadersSubtitle.1",
267-
value: "Make sure the card reader is charged",
268-
comment: "Subtitle describing card reader connection in Point of Sale settings."
269-
)
270-
271-
static let cardReadersSubtitle2 = NSLocalizedString(
272-
"pointOfSaleSettingsHardwareDetailView.cardReadersSubtitle.2",
273-
value: "Turn the card reader on, and place it next to the mobile device",
274-
comment: "Subtitle describing card reader connection in Point of Sale settings."
275-
)
276-
277-
static let cardReadersSubtitle3 = NSLocalizedString(
278-
"pointOfSaleSettingsHardwareDetailView.cardReadersSubtitle.3",
279-
value: "Turn the mobile device bluetooth on",
280-
comment: "Subtitle describing card reader connection in Point of Sale settings."
281-
)
282-
283280
static let cardReaderDocumentationTitle = NSLocalizedString(
284281
"pointOfSaleSettingsHardwareDetailView.cardReaderDocumentationTitle",
285282
value: "Documentation",
@@ -317,3 +314,9 @@ private extension PointOfSaleSettingsHardwareDetailView {
317314
)
318315
}
319316
}
317+
318+
#if DEBUG
319+
#Preview {
320+
PointOfSaleSettingsHardwareDetailView(settingsController: PointOfSaleSettingsPreviewController())
321+
}
322+
#endif

WooCommerce/Classes/POS/Presentation/Settings/PointOfSaleSettingsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ extension PointOfSaleSettingsView {
6464
case .store:
6565
PointOfSaleSettingsStoreDetailView(settingsController: settingsController)
6666
case .hardware:
67-
PointOfSaleSettingsHardwareDetailView()
67+
PointOfSaleSettingsHardwareDetailView(settingsController: settingsController)
6868
case .help:
6969
PointOfSaleSettingsHelpDetailView()
7070
default:

WooCommerce/Classes/POS/TabBar/POSTabCoordinator.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ private extension POSTabCoordinator {
126126
cardPresentPaymentService: cardPresentPaymentService,
127127
orderController: PointOfSaleOrderController(orderService: orderService,
128128
receiptService: receiptService),
129-
settingsController: PointOfSaleSettingsController(settingsService: settingsService),
129+
settingsController: PointOfSaleSettingsController(settingsService: settingsService,
130+
cardPresentPaymentService: cardPresentPaymentService),
130131
collectOrderPaymentAnalyticsTracker: collectOrderPaymentAnalyticsTracker,
131132
searchHistoryService: POSSearchHistoryService(siteID: siteID),
132133
popularPurchasableItemsController: PointOfSaleItemsController(

WooCommerce/WooCommerceTests/POS/Controllers/PointOfSaleSettingsControllerTests.swift

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import Testing
32
import Foundation
43
@testable import WooCommerce
@@ -9,11 +8,13 @@ import Storage
98
struct PointOfSaleSettingsControllerTests {
109
private let mockSettingsService = MockPointOfSaleSettingsService()
1110
private let mockStorageManager = MockStorageManager()
11+
private let mockCardPresentPaymentService = MockCardPresentPaymentService()
1212

1313
@Test func storeName_when_defaultSiteName_provided_then_returns_defaultSiteName() async throws {
1414
// Given
1515
let expectedStoreName = "My Test Store"
1616
let sut = PointOfSaleSettingsController(settingsService: mockSettingsService,
17+
cardPresentPaymentService: mockCardPresentPaymentService,
1718
defaultSiteName: expectedStoreName,
1819
siteSettings: [])
1920

@@ -27,6 +28,7 @@ struct PointOfSaleSettingsControllerTests {
2728
@Test func storeName_when_defaultSiteName_nil_then_returns_notSet() async throws {
2829
// Given
2930
let sut = PointOfSaleSettingsController(settingsService: mockSettingsService,
31+
cardPresentPaymentService: mockCardPresentPaymentService,
3032
defaultSiteName: nil,
3133
siteSettings: [])
3234

@@ -41,6 +43,7 @@ struct PointOfSaleSettingsControllerTests {
4143
// Given
4244
let siteSettings = makeSampleSiteSettings()
4345
let sut = PointOfSaleSettingsController(settingsService: mockSettingsService,
46+
cardPresentPaymentService: mockCardPresentPaymentService,
4447
defaultSiteName: "Test Store",
4548
siteSettings: siteSettings)
4649

@@ -51,6 +54,41 @@ struct PointOfSaleSettingsControllerTests {
5154
#expect(!storeAddress.isEmpty)
5255
}
5356

57+
@Test func connectedCardReader_initially_nil() async throws {
58+
// Given
59+
let sut = PointOfSaleSettingsController(settingsService: mockSettingsService,
60+
cardPresentPaymentService: mockCardPresentPaymentService,
61+
defaultSiteName: "Test Store",
62+
siteSettings: [])
63+
64+
// When
65+
let cardReader = sut.connectedCardReader
66+
67+
// Then
68+
#expect(cardReader == nil)
69+
}
70+
71+
@Test func cardReader_observation_updates_connectedCardReader() async throws {
72+
// Given
73+
let mockService = MockCardPresentPaymentService()
74+
let sut = PointOfSaleSettingsController(settingsService: mockSettingsService,
75+
cardPresentPaymentService: mockService,
76+
defaultSiteName: "Test Store",
77+
siteSettings: [])
78+
79+
// Initially nil
80+
#expect(sut.connectedCardReader == nil)
81+
82+
// When
83+
let cardReader = CardPresentPaymentCardReader(name: "WisePad 3", batteryLevel: 0.75)
84+
mockService.connectedReader = cardReader
85+
86+
// Then
87+
#expect(sut.connectedCardReader?.name == "WisePad 3")
88+
#expect(sut.connectedCardReader?.batteryLevel == 0.75)
89+
}
90+
91+
5492
private func makeSampleSiteSettings() -> [Yosemite.SiteSetting] {
5593
return [
5694
SiteSetting(siteID: 123,
@@ -105,7 +143,10 @@ final class MockPointOfSaleSettingsController: PointOfSaleSettingsControllerProt
105143
"123 Main Street\nAnytown, ST 12345"
106144
}
107145

146+
var connectedCardReader: CardPresentPaymentCardReader? = nil
147+
108148
func retrievePOSReceiptSettings() async {
109149
// no-op
110150
}
151+
111152
}

0 commit comments

Comments
 (0)