Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
@@ -1,12 +1,17 @@
import SwiftUI
import enum Yosemite.AppSettingsAction

struct PointOfSaleSettingsHardwareDetailView: View {
@State private var navigationPath: [PointOfSaleSettingsView.HardwareDestination] = []
@State private var navigationPath: [NavigationDestination] = []
Copy link
Contributor

Choose a reason for hiding this comment

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

In a broader conversation, I think the fact that "< Back" button is displayed within the details view, the "X" (Close) should be presented within the the list view.

And we likely need to use POSPageHeaderView and not the native navigation bar, and of course, not both. As I understand, we leave this for later 👍

Simulator Screenshot - iPad Pro 11-inch (M4) - 2025-08-26 at 14 11 48

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In a broader conversation, I think the fact that "< Back" button is displayed within the details view, the "X" (Close) should be presented within the the list view. And we likely need to use POSPageHeaderView and not the native navigation bar, and of course, not both. As I understand, we leave this for later 👍

Yes, that's something I'm precisely discussing today, as Android currently does not show a header across the full view (separates list and detail view), nor X to close the view that I can see. Neither this has been provided in the initial design drafts p1756257438170999?thread_ts=1755784099.956039&cid=C070SJRA8DP-slack-C070SJRA8DP

I think could even make sense to get the "Help" card up with the rest, and take that bottom-left position to go back to the dashboard, let's see what design thinks 👍

@State private var lastKnownLoadedCardReader: String?
@State private var showBarcodeScanningSetupModal: Bool = false
@State private var showBarcodeScanningDocumentationModal: Bool = false
@State private var showCardReaderDocumentationModal: Bool = false

var body: some View {
NavigationStack(path: $navigationPath) {
List(PointOfSaleSettingsView.HardwareDestination.allCases) { destination in
NavigationLink(value: destination) {
NavigationLink(value: NavigationDestination.hardware(destination)) {
HStack(alignment: .firstTextBaseline) {
Image(systemName: destination.icon)
.font(.posBodyLargeRegular())
Expand All @@ -20,19 +25,242 @@ struct PointOfSaleSettingsHardwareDetailView: View {
}
}
}
.navigationDestination(for: PointOfSaleSettingsView.HardwareDestination.self) { destination in
VStack(spacing: POSSpacing.medium) {
Image(systemName: destination.icon).font(.largeTitle)
.navigationDestination(for: NavigationDestination.self) { destination in
switch destination {
case .hardware(.cardReaders):
cardReadersView
case .hardware(.scanners):
scannersView
case .scanner:
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we remove scanner from the NavigationDestination then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I thought would be needed but it isn't since only buttons are there now. Updated: 9fdcf36

// This case in the navigation stack is not reached,
// as we present the destination modally instead of further navigation through the stack.
EmptyView()
}
}
.posModal(isPresented: $showBarcodeScanningSetupModal) {
PointOfSaleBarcodeScannerSetup(isPresented: $showBarcodeScanningSetupModal)
}
.posFullScreenCover(isPresented: $showBarcodeScanningDocumentationModal) {
SafariView(url: WooConstants.URLs.pointOfSaleDocumentation.asURL())
}
}
}

private func handleScannerDestination(_ destination: ScannerDestination) {
switch destination {
case .setup:
showBarcodeScanningSetupModal = true
case .documentation:
showBarcodeScanningDocumentationModal = true
}
}

private var cardReadersView: some View {
VStack(spacing: POSSpacing.large) {
VStack(spacing: POSSpacing.medium) {
VStack(spacing: POSPadding.small) {
Text(Localization.cardReadersDescription)
.font(.posBodyLargeRegular())
Text("\(destination.title) settings")
.multilineTextAlignment(.center)
Text(Localization.cardReadersSubtitle1)
.font(.posBodyMediumRegular())
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Text(Localization.cardReadersSubtitle2)
.font(.posBodyMediumRegular())
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Text(Localization.cardReadersSubtitle3)
.font(.posBodyMediumRegular())
Text("Some placeholder")
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
}
.padding()

if let lastKnownLoadedCardReader {
HStack {
Text("Model: \(lastKnownLoadedCardReader)")
.font(.posBodyMediumRegular())
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.padding()
.navigationTitle(destination.title)
}

List {
Button {
showCardReaderDocumentationModal = true
} label: {
HStack(alignment: .firstTextBaseline) {
Image(systemName: "doc.text")
.font(.posBodyLargeRegular())
VStack(alignment: .leading, spacing: POSPadding.xSmall) {
Text(Localization.cardReaderDocumentationTitle)
.font(.posBodyLargeRegular())
Text(Localization.cardReaderDocumentationSubtitle)
.font(.posBodyMediumRegular())
.foregroundStyle(.secondary)
}
}
}
.buttonStyle(.plain)
}
}
.navigationTitle(Localization.cardReadersTitle)
.posFullScreenCover(isPresented: $showCardReaderDocumentationModal) {
SafariView(url: WooConstants.URLs.inPersonPaymentsLearnMoreWCPay.asURL())
}
.task { @MainActor in
// TODO: WOOMOB-1172
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is temporary, most likely we'll fetch card reader information from passing in either a reference to the aggregate model or expose its card reader details.

Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Yes, the aggregate model works.

If we think that this state sharing in an aggregate model is not needed, and we only need information within this view, we could rely on a separate view model for this view that fetches information.

I had the same question for orders, and at least for order it felt like it made sense to have something separate and there was no value putting information within the aggregate model.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Jinx! I just had the same question when reviewing your PR :D

I also think that in this case, for settings, it makes more sense to go through the aggregate, as we'll deal with settings across different features that are part of such aggregate, but we'll see what fits better 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I agree. 👍

let action = AppSettingsAction.loadCardReader { reader in
switch reader {
case let .success(foundReader):
lastKnownLoadedCardReader = foundReader
case let .failure(error):
debugPrint(error)
}
}
ServiceLocator.stores.dispatch(action)
}
}

private var scannersView: some View {
List(ScannerDestination.allCases) { destination in
Button {
handleScannerDestination(destination)
} label: {
HStack(alignment: .firstTextBaseline) {
Image(systemName: destination.icon)
.font(.posBodyLargeRegular())
VStack(alignment: .leading, spacing: POSPadding.xSmall) {
Text(destination.title)
.font(.posBodyLargeRegular())
Text(destination.subtitle)
.font(.posBodyMediumRegular())
.foregroundStyle(.secondary)
}
}
}
.buttonStyle(.plain)
}
.navigationTitle(Localization.scannersTitle)
}

}

extension PointOfSaleSettingsHardwareDetailView {
enum NavigationDestination: Hashable {
case hardware(PointOfSaleSettingsView.HardwareDestination)
case scanner(ScannerDestination)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be removed

Suggested change
enum NavigationDestination: Hashable {
case hardware(PointOfSaleSettingsView.HardwareDestination)
case scanner(ScannerDestination)
}
enum NavigationDestination: Hashable {
case hardware(PointOfSaleSettingsView.HardwareDestination)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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


enum ScannerDestination: Identifiable, CaseIterable {
case setup
case documentation

var id: Self { self }

var title: String {
switch self {
case .setup:
return Localization.scannerSetupTitle
case .documentation:
return Localization.scannerDocumentationTitle
}
}

var subtitle: String {
switch self {
case .setup:
return Localization.scannerSetupSubtitle
case .documentation:
return Localization.scannerDocumentationSubtitle
}
}

var icon: String {
switch self {
case .setup:
return "gearshape"
case .documentation:
return "doc.text"
}
}
}
}

private extension PointOfSaleSettingsHardwareDetailView {
enum Localization {
static let cardReadersTitle = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.cardReadersTitle",
value: "Card readers",
comment: "Navigation title for card readers settings in Point of Sale."
)

static let scannersTitle = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.scannersTitle",
value: "Barcode scanners",
comment: "Navigation title for barcode scanners settings in Point of Sale."
)

static let scannerSetupTitle = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.scannerSetupTitle",
value: "Scanner Setup",
comment: "Title for scanner setup option in barcode scanners settings in Point of Sale."
)

static let scannerSetupSubtitle = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.scannerSetupSubtitle",
value: "Configure and test your barcode scanner",
comment: "Subtitle describing scanner setup in Point of Sale settings."
)

static let scannerDocumentationTitle = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.scannerDocumentationTitle",
value: "Documentation",
comment: "Title for barcode scanner documentation option in Point of Sale settings."
)

static let scannerDocumentationSubtitle = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.scannerDocumentationSubtitle",
value: "Learn more about barcode scanning in POS",
comment: "Subtitle describing barcode scanner documentation in Point of Sale settings."
)

static let cardReadersDescription = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.cardReadersDescription",
value: "Accept secure and fast payments in person",
comment: "Main description for card readers functionality in Point of Sale settings."
)

static let cardReadersSubtitle1 = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.cardReadersSubtitle.1",
value: "Make sure the card reader is charged",
comment: "Subtitle describing card reader connection in Point of Sale settings."
)

static let cardReadersSubtitle2 = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.cardReadersSubtitle.2",
value: "Turn the card reader on, and place it next to the mobile device",
comment: "Subtitle describing card reader connection in Point of Sale settings."
)

static let cardReadersSubtitle3 = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.cardReadersSubtitle.3",
value: "Turn the mobile device bluetooth on",
comment: "Subtitle describing card reader connection in Point of Sale settings."
)

static let cardReaderDocumentationTitle = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.cardReaderDocumentationTitle",
value: "Documentation",
comment: "Title for card reader documentation option in Point of Sale settings."
)

static let cardReaderDocumentationSubtitle = NSLocalizedString(
"pointOfSaleSettingsHardwareDetailView.cardReaderDocumentationSubtitle",
value: "Learn more about accepting mobile payments",
comment: "Subtitle describing card reader documentation in Point of Sale settings."
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,73 +14,79 @@ struct PointOfSaleSettingsView: View {
}
.foregroundColor(.posOnSurface)
})
HStack(spacing: POSSpacing.none) {
VStack(alignment: .leading, spacing: POSSpacing.none) {
List(selection: $selection) {
Section {
ForEach([SidebarNavigation.store, SidebarNavigation.hardware], id: \.self) { item in
HStack {
Image(systemName: item.icon)
.font(.posBodyLargeRegular())
VStack(alignment: .leading) {
Text(item.title)
GeometryReader { geometry in
HStack(spacing: POSSpacing.none) {
VStack(alignment: .leading, spacing: POSSpacing.none) {
List(selection: $selection) {
Section {
ForEach([SidebarNavigation.store, SidebarNavigation.hardware], id: \.self) { item in
HStack {
Image(systemName: item.icon)
.font(.posBodyLargeRegular())
Text(item.subtitle)
.font(.posBodyMediumRegular())
.foregroundStyle(.secondary)
VStack(alignment: .leading) {
Text(item.title)
.font(.posBodyLargeRegular())
Text(item.subtitle)
.font(.posBodyMediumRegular())
.foregroundStyle(.secondary)
}
}
.tag(item)
}
.tag(item)
}
}
}
.safeAreaInset(edge: .bottom) {
Button {
selection = .help
} label: {
HStack {
Image(systemName: SidebarNavigation.help.icon)
.font(.posBodyLargeRegular())
.foregroundStyle(selection == .help ? .white : .primary)
VStack(alignment: .leading) {
Text(SidebarNavigation.help.title)
.safeAreaInset(edge: .bottom) {
Button {
selection = .help
} label: {
HStack {
Image(systemName: SidebarNavigation.help.icon)
.font(.posBodyLargeRegular())
.foregroundStyle(selection == .help ? .white : .primary)
Text(SidebarNavigation.help.subtitle)
.font(.posBodyMediumRegular())
.foregroundStyle(selection == .help ? .secondary : .secondary)
VStack(alignment: .leading) {
Text(SidebarNavigation.help.title)
.font(.posBodyLargeRegular())
.foregroundStyle(selection == .help ? .white : .primary)
Text(SidebarNavigation.help.subtitle)
.font(.posBodyMediumRegular())
.foregroundStyle(selection == .help ? .secondary : .secondary)
}
Spacer()
}
Spacer()
.padding(.vertical, POSPadding.small)
.padding(.horizontal, POSPadding.medium)
.contentShape(Rectangle())
}
.padding(.vertical, POSPadding.small)
.padding(.horizontal, POSPadding.medium)
.contentShape(Rectangle())
.buttonStyle(.plain)
.background(
RoundedRectangle(cornerRadius: POSCornerRadiusStyle.large.value, style: .continuous)
.fill(selection == .help ? Color.accentColor : Color.clear)
)
}
.buttonStyle(.plain)
.background(
RoundedRectangle(cornerRadius: POSCornerRadiusStyle.large.value, style: .continuous)
.fill(selection == .help ? Color.accentColor : Color.clear)
)
}
}
Group {
switch selection {
case .store:
PointOfSaleSettingsStoreDetailView()
case .hardware:
PointOfSaleSettingsHardwareDetailView()
case .help:
PointOfSaleSettingsHelpDetailView()
default:
EmptyView()
.frame(width: geometry.size.width * Constants.sidebarWidthFraction)
Group {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wwe could avoid using Group if you would move the switch into a separate @ViewBuilder:

    @ViewBuilder
    private var detailView: some View {
        switch selection {
        case .store:
            PointOfSaleSettingsStoreDetailView()
        case .hardware:
            PointOfSaleSettingsHardwareDetailView()
        case .help:
            PointOfSaleSettingsHelpDetailView()
        default:
            EmptyView()
        }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! 2098428

switch selection {
case .store:
PointOfSaleSettingsStoreDetailView()
case .hardware:
PointOfSaleSettingsHardwareDetailView()
case .help:
PointOfSaleSettingsHelpDetailView()
default:
EmptyView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}

extension PointOfSaleSettingsView {
enum Constants {
static let sidebarWidthFraction: CGFloat = 0.35
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is one of those we can share between Dashboard/Orders/Settings when we decide if we need a unified look and functionality.

}
enum HardwareDestination: Identifiable, CaseIterable {
case cardReaders
case scanners
Expand Down
Loading