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
19 changes: 19 additions & 0 deletions WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct POSFloatingControlView: View {
@Binding private var showSupport: Bool
@Binding private var showDocumentation: Bool
@State private var showProductRestrictionsModal: Bool = false
@State private var showBarcodeScanningInformation: Bool = false

init(showExitPOSModal: Binding<Bool>,
showSupport: Binding<Bool>,
Expand Down Expand Up @@ -56,6 +57,15 @@ struct POSFloatingControlView: View {
title: { Text(Localization.productRestrictionsInfo) },
icon: { Image(systemName: "magnifyingglass") })
}
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleBarcodeScanningi1) {
Button {
showBarcodeScanningInformation = true
} label: {
Label(
title: { Text(Localization.barcodeScanning) },
icon: { Image(systemName: "barcode.viewfinder") })
}
}
} label: {
VStack {
Spacer()
Expand All @@ -81,6 +91,9 @@ struct POSFloatingControlView: View {
.posModal(isPresented: $showProductRestrictionsModal) {
SimpleProductsOnlyInformation(isPresented: $showProductRestrictionsModal)
}
.posModal(isPresented: $showBarcodeScanningInformation) {
PointOfSaleBarcodeScannerInformationModal(isPresented: $showBarcodeScanningInformation)
}
.frame(height: Constants.size)
.background(Color.clear)
.animation(.default, value: backgroundAppearance)
Expand Down Expand Up @@ -149,6 +162,12 @@ private extension POSFloatingControlView {
comment: "The title of the menu button to view product restrictions info, shown in a popover menu. " +
"We only show simple and variable products in POS, this shows a modal to help explain that limitation."
)

static let barcodeScanning = NSLocalizedString(
"pointOfSale.floatingButtons.barcodeScanning.button.title",
value: "Barcode scanning",
comment: "The title of the menu button to view barcode scanner documentation, shown in a popover menu."
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Foundation
import SwiftUI

struct PointOfSaleBarcodeScannerInformationModal: View {
@Binding var isPresented: Bool

init(isPresented: Binding<Bool>) {
self._isPresented = isPresented
}

var body: some View {
PointOfSaleInformationModal(isPresented: $isPresented, title: AttributedString(Localization.barcodeInfoHeading)) {
PointOfSaleInformationModalParagraphView {
Text(AttributedString(Localization.barcodeInfoIntroMessage))
}

PointOfSaleInformationModalParagraphView {
Text(bulletPointWithLink)
Text(AttributedString(Localization.barcodeInfoSecondaryMessage))
Text(AttributedString(Localization.barcodeInfoTertiaryMessage))
Text(AttributedString(Localization.barcodeInfoQuaternaryMessage))
}
.padding(.leading, POSSpacing.medium)

PointOfSaleInformationModalParagraphView(style: .outlined) {
Text(AttributedString(Localization.barcodeInfoQuinaryMessage))
}
}
}

private var bulletPointWithLink: AttributedString {
var secondary = AttributedString(Localization.barcodeInfoPrimaryMessage + " ")
var moreDetails = AttributedString(Localization.barcodeInfoMoreDetailsLink)
Copy link
Contributor

Choose a reason for hiding this comment

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

This link relates primarily to setting up barcodes. I mentioned to Andrey that it should be on the first item in the list, not the second, and I believe he changed it there, the screenshot from Android is probably out of date.

Could you move this to the first bullet point please?

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, moved.

moreDetails.link = Constants.detailsLink
moreDetails.foregroundColor = .posPrimary
moreDetails.underlineStyle = .single
secondary.append(moreDetails)
return secondary
}
}

private extension PointOfSaleBarcodeScannerInformationModal {
enum Constants {
static let detailsLink = URL(string: "https://woocommerce.com/document/barcode-and-qr-code-scanner/")
}

enum Localization {
static let barcodeInfoHeading = NSLocalizedString(
"pos.barcodeInfoModal.heading",
value: "Barcode scanning",
comment: "Heading for the barcode info modal in POS, introducing barcode scanning feature"
)
static let barcodeInfoIntroMessage = NSLocalizedString(
"pos.barcodeInfoModal.introMessage",
value: "You can scan barcodes using an external scanner to quickly build a cart.",
comment: "Introductory message in the barcode info modal in POS, explaining the use of external barcode scanners"
)
static let barcodeInfoPrimaryMessage = NSLocalizedString(
"pos.barcodeInfoModal.primaryMessage",
value: "• Set up barcodes in the \"GTIN, UPC, EAN, ISBN\" field in Products > Product Details > Inventory. ",
comment: "Primary bullet point in the barcode info modal in POS, instructing where to set up barcodes in product details"
)
static let barcodeInfoMoreDetailsLink = NSLocalizedString(
"pos.barcodeInfoModal.moreDetailsLink",
value: "More details.",
comment: "Link text in the barcode info modal in POS, leading to more details about barcode setup"
)
static let barcodeInfoSecondaryMessage = NSLocalizedString(
"pos.barcodeInfoModal.secondaryMessage",
value: "• Refer to your Bluetooth barcode scanner's instructions to set HID mode.",
comment: "Secondary bullet point in the barcode info modal in POS, instructing to set scanner to HID mode"
)
static let barcodeInfoTertiaryMessage = NSLocalizedString(
"pos.barcodeInfoModal.tertiaryMessage",
value: "• Connect your barcode scanner in iOS Bluetooth settings.",
comment: "Tertiary bullet point in the barcode info modal in POS, instructing to connect scanner via Bluetooth settings"
)
static let barcodeInfoQuaternaryMessage = NSLocalizedString(
"pos.barcodeInfoModal.quaternaryMessage",
value: "• Scan barcodes while on the item list to add products to the cart.",
comment: "Quaternary bullet point in the barcode info modal in POS, instructing to scan barcodes on item list to add to cart"
)
static let barcodeInfoQuinaryMessage = NSLocalizedString(
"pos.barcodeInfoModal.quinaryMessage",
value: "The scanner emulates a keyboard, so sometimes it will prevent the software keyboard from showing, e.g. in search. " +
"Tap on the keyboard icon to show it again.",
comment: "Quinary message in the barcode info modal in POS, explaining scanner keyboard emulation and how to show software keyboard again"
)
}
}

@available(iOS 17.0, *)
#Preview {
PointOfSaleBarcodeScannerInformationModal(isPresented: .constant(true))
}
128 changes: 128 additions & 0 deletions WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import SwiftUI

// Container view for displaying information modals in the POS.
//
struct PointOfSaleInformationModal<Content: View>: View {
@Binding var isPresented: Bool
let title: AttributedString
let content: Content

// Used to make ScrollView height increase together with the content height.
@State private var contentHeight: CGFloat = 0
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 that this (or something else about this struct) makes the modals grow as they transition for presentation/dismissal as well. That didn't happen in the past, they just faded in. It would be good if they could already have the correct size when we try to present them...

I'm not sure the best way to do it though...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's only an iOS 17 animation issue, not for the first time.

Yes, it's likely related to contentHeight, I haven't found a quick way to properly size content within the ScrollView.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found a quick workaround, applying a 0-second ease-in animation to contentHeigh transition does the job:

Simulator.Screen.Recording.-.iPad.Air.11-inch.17.5.-.2025-06-18.at.17.45.30.mp4

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice one, and thanks for commenting about it too. I checked on device and it looks good.


init(
isPresented: Binding<Bool>,
title: AttributedString,
@ViewBuilder content: () -> Content
) {
self._isPresented = isPresented
self.title = title
self.content = content()
}

var body: some View {
VStack(spacing: POSSpacing.xxLarge) {
HStack {
Text(title)
.font(.posHeadingBold)
Spacer()
Button {
isPresented = false
} label: {
Text(Image(systemName: "xmark"))
.font(.posButtonSymbolLarge)
}
}
.foregroundColor(Color.posOnSurface)

ScrollView {
VStack {
content
}
.measureHeight { height in
// Workaround for ScrollView not updating its height immediately on iOS 17
withAnimation(.easeIn(duration: 0)) {
contentHeight = height
}
}
}
.frame(maxHeight: contentHeight)

Button(action: {
isPresented = false
}) {
Text(Localization.okButtonTitle)
}
.buttonStyle(POSOutlinedButtonStyle(size: .normal))
}
.padding(POSPadding.xxLarge)
.background(Color.posSurfaceBright)
.frame(width: Constants.modalFrameWidth)
}
}

struct PointOfSaleInformationModalParagraphView<Content: View>: View {
enum Style {
case `default`
case outlined
}

let content: Content
let style: Style

init(style: Style = .default, @ViewBuilder content: () -> Content) {
self.content = content()
self.style = style
}

var body: some View {
VStack(alignment: style == .default ? .leading : .center) {
content
}
.if(style == .default, transform: { view in
view.modifier(PointOfSaleInformationModalDefaultParagraphStyle())
})
.if(style == .outlined, transform: { view in
view.modifier(PointOfSaleInformationModalOutlinedParagraphStyle())
})
}
}

private struct PointOfSaleInformationModalDefaultParagraphStyle: ViewModifier {
func body(content: Content) -> some View {
content
.frame(maxWidth: .infinity, alignment: .leading)
.font(.posBodyLargeRegular())
.foregroundStyle(Color.posOnSurface)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
}
}

private struct PointOfSaleInformationModalOutlinedParagraphStyle: ViewModifier {
func body(content: Content) -> some View {
content
.frame(maxWidth: .infinity, alignment: .center)
.font(.posBodySmallRegular())
.foregroundStyle(Color.posOnSurface)
.padding(POSPadding.medium)
.background(Color.posSurfaceDim)
.clipShape(RoundedRectangle(cornerRadius: POSCornerRadiusStyle.medium.value))
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
}
}

private extension PointOfSaleInformationModal {
enum Constants {
static var modalFrameWidth: CGFloat { 896 }
}
}

private enum Localization {
static let okButtonTitle = NSLocalizedString(
"pos.posInformationModal.ok.button.title",
value: "OK",
comment: "Title for the OK button on the pos information modal"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,60 +12,26 @@ struct SimpleProductsOnlyInformation: View {
}

var body: some View {
VStack(spacing: Constants.contentBlockSpacing) {
HStack {
Spacer()
Button {
isPresented = false
} label: {
Text(Image(systemName: "xmark"))
.font(.posButtonSymbolLarge)
}
.padding(Constants.dismissIconPadding)
.foregroundColor(Color.posOnSurfaceVariantLowest)
}

VStack(spacing: Constants.textSpacing) {
Text(Localization.modalTitle)
.font(.posHeadingBold)

Group {
Text(issueMessage)
Text(futureMessage)
}
.font(.posBodyLargeRegular())
PointOfSaleInformationModal(isPresented: $isPresented, title: AttributedString(Localization.modalTitle)) {
PointOfSaleInformationModalParagraphView {
Text(issueMessage)
Comment on lines +15 to +17
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice one for sharing this code 👍 Looks better all round

Text(futureMessage)
}
.foregroundStyle(Color.posOnSurface)
.multilineTextAlignment(.center)

VStack(spacing: Constants.textSpacing) {
PointOfSaleInformationModalParagraphView(style: .outlined) {
Text(hintMessage)
.font(.posBodySmallRegular())
.foregroundStyle(Color.posOnSurface)

Spacer().frame(height: POSSpacing.small)

Button {
deepLinkNavigator?.navigate(to: OrdersDestination.createOrder)
} label: {
Label(Localization.modalAction, systemImage: "plus")
.font(.posBodySmallRegular())
}
.foregroundStyle(Color.posPrimary)
}
.frame(maxWidth: .infinity)
.padding(.vertical, Constants.hintVerticalPadding)
.padding(.horizontal, Constants.hintHorizontalPadding)
.background(Color(.posSurfaceDim))
.clipShape(RoundedRectangle(cornerRadius: Constants.hintBackgroundCornerRadius))
.multilineTextAlignment(.center)

Button(action: {
isPresented = false
}) {
Text(Localization.okButtonTitle)
}
.buttonStyle(POSOutlinedButtonStyle(size: .normal))
}
.padding(Constants.modalContentPadding)
.frame(width: Constants.modalFrameWidth)
}

private var issueMessage: String {
Expand All @@ -84,17 +50,6 @@ struct SimpleProductsOnlyInformation: View {
// Constants and Localization enums
@available(iOS 17.0, *)
private extension SimpleProductsOnlyInformation {
enum Constants {
static let modalFrameWidth: CGFloat = 896
static let modalContentPadding: CGFloat = POSSpacing.medium
static let hintVerticalPadding: CGFloat = POSSpacing.medium
static let hintHorizontalPadding: CGFloat = POSSpacing.medium
static let hintBackgroundCornerRadius: CGFloat = POSCornerRadiusStyle.medium.value
static let contentBlockSpacing: CGFloat = POSSpacing.xxLarge
static let textSpacing: CGFloat = POSSpacing.small
static let dismissIconPadding: EdgeInsets = .init(top: 8, leading: 8, bottom: 8, trailing: 8)
}

enum Localization {
static let modalTitle = NSLocalizedString(
"pos.simpleProductsModal.title",
Expand Down
Loading