diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index abf1aa93531..b57a9201708 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -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, showSupport: Binding, @@ -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() @@ -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) @@ -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." + ) } } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift new file mode 100644 index 00000000000..4dc58c2ef42 --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift @@ -0,0 +1,95 @@ +import Foundation +import SwiftUI + +struct PointOfSaleBarcodeScannerInformationModal: View { + @Binding var isPresented: Bool + + init(isPresented: Binding) { + 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) + 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)) +} diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift new file mode 100644 index 00000000000..a1e98f68265 --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift @@ -0,0 +1,128 @@ +import SwiftUI + +// Container view for displaying information modals in the POS. +// +struct PointOfSaleInformationModal: 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 + + init( + isPresented: Binding, + 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: 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" + ) +} diff --git a/WooCommerce/Classes/POS/Presentation/SimpleProductsOnlyInformation.swift b/WooCommerce/Classes/POS/Presentation/SimpleProductsOnlyInformation.swift index 0aaca41c938..4a1ab9e817d 100644 --- a/WooCommerce/Classes/POS/Presentation/SimpleProductsOnlyInformation.swift +++ b/WooCommerce/Classes/POS/Presentation/SimpleProductsOnlyInformation.swift @@ -12,36 +12,16 @@ 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) + 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) @@ -49,23 +29,9 @@ struct SimpleProductsOnlyInformation: View { 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 { @@ -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", diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index e7da31e6894..9f3db3b51f6 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -33,6 +33,8 @@ 0139BB522D91B45800C78FDE /* CouponRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0139BB512D91B45500C78FDE /* CouponRowView.swift */; }; 013D2FB42CFEFEC600845D75 /* TapToPayCardReaderMerchantEducationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013D2FB32CFEFEA800845D75 /* TapToPayCardReaderMerchantEducationPresenter.swift */; }; 013D2FB62CFF54BB00845D75 /* TapToPayEducationStepsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013D2FB52CFF54B600845D75 /* TapToPayEducationStepsFactory.swift */; }; + 01435CF82DFC2CE800C0279B /* PointOfSaleInformationModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01435CF72DFC2CE800C0279B /* PointOfSaleInformationModal.swift */; }; + 014371272DFC8E2800C0279B /* PointOfSaleBarcodeScannerInformationModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerInformationModal.swift */; }; 014BD4B82C64E2BA0011A66E /* PointOfSaleOrderSyncErrorMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014BD4B72C64E2BA0011A66E /* PointOfSaleOrderSyncErrorMessageView.swift */; }; 015456CE2DB0341D0071C3C4 /* POSPageHeaderActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015456CD2DB033FF0071C3C4 /* POSPageHeaderActionButton.swift */; }; 0157A9962C4FEA7200866FFD /* PointOfSaleLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0157A9952C4FEA7200866FFD /* PointOfSaleLoadingView.swift */; }; @@ -3262,6 +3264,8 @@ 0139BB512D91B45500C78FDE /* CouponRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponRowView.swift; sourceTree = ""; }; 013D2FB32CFEFEA800845D75 /* TapToPayCardReaderMerchantEducationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToPayCardReaderMerchantEducationPresenter.swift; sourceTree = ""; }; 013D2FB52CFF54B600845D75 /* TapToPayEducationStepsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToPayEducationStepsFactory.swift; sourceTree = ""; }; + 01435CF72DFC2CE800C0279B /* PointOfSaleInformationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleInformationModal.swift; sourceTree = ""; }; + 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerInformationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeScannerInformationModal.swift; sourceTree = ""; }; 014BD4B72C64E2BA0011A66E /* PointOfSaleOrderSyncErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderSyncErrorMessageView.swift; sourceTree = ""; }; 015456CD2DB033FF0071C3C4 /* POSPageHeaderActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSPageHeaderActionButton.swift; sourceTree = ""; }; 0157A9952C4FEA7200866FFD /* PointOfSaleLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleLoadingView.swift; sourceTree = ""; }; @@ -7235,6 +7239,8 @@ DA013F502C65125100D9A391 /* PointOfSaleExitPosAlertView.swift */, DA0DBE2E2C4FC61D00DF14C0 /* POSFloatingControlView.swift */, 20D3D42A2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift */, + 01435CF72DFC2CE800C0279B /* PointOfSaleInformationModal.swift */, + 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerInformationModal.swift */, 68C53CBD2C1FE59B00C6D80B /* ItemListView.swift */, 026826A32BF59DF60036F959 /* CartView.swift */, 026826A22BF59DF60036F959 /* ItemRowView.swift */, @@ -15856,6 +15862,7 @@ CE27257F21925AE8002B22EB /* ValueOneTableViewCell.swift in Sources */, DE4A33552A45A4DC00795DA9 /* WPComSitePlan+SimpleSite.swift in Sources */, 860476E82B6CA0FC00AF0AEB /* BottomSheetProductType.swift in Sources */, + 01435CF82DFC2CE800C0279B /* PointOfSaleInformationModal.swift in Sources */, 68C53CBE2C1FE59B00C6D80B /* ItemListView.swift in Sources */, 028296EC237D28B600E84012 /* TextViewViewController.swift in Sources */, 02B8650F24A9E2D800265779 /* Product+SwiftUIPreviewHelpers.swift in Sources */, @@ -16649,6 +16656,7 @@ 68E952CC287536010095A23D /* SafariView.swift in Sources */, D449C51C26DE6B5000D75B02 /* IconListItem.swift in Sources */, EE9D03182B89E2B10077CED1 /* OrderStatusEnum+Analytics.swift in Sources */, + 014371272DFC8E2800C0279B /* PointOfSaleBarcodeScannerInformationModal.swift in Sources */, CE16177A21B7192A00B82A47 /* AuthenticationConstants.swift in Sources */, 26A630ED253F3B5C00CBC3B1 /* RefundCreationUseCase.swift in Sources */, B95700AE2A72C39C001BADF2 /* CustomerSelectorView.swift in Sources */,