From 939e2e08e9e9fc625ff83293843dfa749eb74329 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:01:28 +0300 Subject: [PATCH 01/11] Create PointOfSaleInformationModal to generalize presentation of informational modals --- .../PointOfSaleInformationModal.swift | 89 +++++++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 12 ++- 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift new file mode 100644 index 00000000000..067f7e1e4a1 --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift @@ -0,0 +1,89 @@ +import SwiftUI + +struct PointOfSaleInformationModalViewModel { + struct Paragraph: Hashable, Identifiable { + let id = UUID() + let lines: [AttributedString] + + init(_ lines: [AttributedString]) { + self.lines = lines + } + + init(_ text: AttributedString) { + self.lines = [text] + } + } + let title: AttributedString + let paragraphs: [Paragraph] +} + +// SwiftUI modal for displaying information in the Point of Sale context +@available(iOS 17.0, *) +struct PointOfSaleInformationModal: View { + @Binding var isPresented: Bool + let viewModel: PointOfSaleInformationModalViewModel + + init( + isPresented: Binding, + viewModel: PointOfSaleInformationModalViewModel + ) { + self._isPresented = isPresented + self.viewModel = viewModel + } + + var body: some View { + VStack(spacing: POSSpacing.xxLarge) { + // Modal header with title and close button + HStack { + Text(viewModel.title) + .font(.posHeadingBold) + Spacer() + Button { + isPresented = false + } label: { + Text(Image(systemName: "xmark")) + .font(.posButtonSymbolLarge) + } + } + .foregroundColor(Color.posOnSurface) + + // Display each paragraph (single or multiple lines) + ForEach(viewModel.paragraphs, id: \.self) { paragraph in + VStack { + ForEach(paragraph.lines, id: \.self) { text in + Text(text) + .font(.posBodyLargeRegular()) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + + Button(action: { + isPresented = false + }) { + Text(Localization.okButtonTitle) + } + .buttonStyle(POSOutlinedButtonStyle(size: .normal)) + } + .padding(POSPadding.xxLarge) + .background(Color.posSurfaceBright) + .frame(width: Constants.modalFrameWidth) + } +} + +@available(iOS 17.0, *) +private extension PointOfSaleInformationModal { + enum Constants { + static let 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/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index b5d89fb4d29..efe0b1a9122 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 /* PointOfSaleBarcodeInformationModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014371262DFC8E2100C0279B /* PointOfSaleBarcodeInformationModalViewModel.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 */; }; @@ -1210,8 +1212,8 @@ 26FE09E124DB8FA000B9BDF5 /* SurveyCoordinatorControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26FE09E024DB8FA000B9BDF5 /* SurveyCoordinatorControllerTests.swift */; }; 26FFC50C2BED7C5A0067B3A4 /* WatchDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B249702BEC801400730730 /* WatchDependencies.swift */; }; 26FFC50D2BED7C5B0067B3A4 /* WatchDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26B249702BEC801400730730 /* WatchDependencies.swift */; }; - 2D88C1112DF883C300A6FB2C /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88C1102DF883BD00A6FB2C /* AttributedString+Helpers.swift */; }; 2D880B492DFB2F3F00A6FB2C /* OptionalBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D880B482DFB2F3D00A6FB2C /* OptionalBinding.swift */; }; + 2D88C1112DF883C300A6FB2C /* AttributedString+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D88C1102DF883BD00A6FB2C /* AttributedString+Helpers.swift */; }; 310D1B482734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310D1B472734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift */; }; 311237EE2714DA240033C44E /* CardPresentModalDisplayMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311237ED2714DA240033C44E /* CardPresentModalDisplayMessage.swift */; }; 311D21E8264AEDB900102316 /* CardPresentModalScanningForReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311D21E7264AEDB900102316 /* CardPresentModalScanningForReader.swift */; }; @@ -3291,6 +3293,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 /* PointOfSaleBarcodeInformationModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeInformationModalViewModel.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 = ""; }; @@ -4441,8 +4445,8 @@ 26FE09E024DB8FA000B9BDF5 /* SurveyCoordinatorControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurveyCoordinatorControllerTests.swift; sourceTree = ""; }; 26FFD32628C6A0A4002E5E5E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 26FFD32928C6A0F4002E5E5E /* UIImage+Widgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Widgets.swift"; sourceTree = ""; }; - 2D88C1102DF883BD00A6FB2C /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = ""; }; 2D880B482DFB2F3D00A6FB2C /* OptionalBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalBinding.swift; sourceTree = ""; }; + 2D88C1102DF883BD00A6FB2C /* AttributedString+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Helpers.swift"; sourceTree = ""; }; 310D1B472734919E001D55B4 /* InPersonPaymentsLiveSiteInTestModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsLiveSiteInTestModeView.swift; sourceTree = ""; }; 311237ED2714DA240033C44E /* CardPresentModalDisplayMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalDisplayMessage.swift; sourceTree = ""; }; 311D21E7264AEDB900102316 /* CardPresentModalScanningForReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalScanningForReader.swift; sourceTree = ""; }; @@ -7292,6 +7296,8 @@ DA013F502C65125100D9A391 /* PointOfSaleExitPosAlertView.swift */, DA0DBE2E2C4FC61D00DF14C0 /* POSFloatingControlView.swift */, 20D3D42A2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift */, + 01435CF72DFC2CE800C0279B /* PointOfSaleInformationModal.swift */, + 014371262DFC8E2100C0279B /* PointOfSaleBarcodeInformationModalViewModel.swift */, 68C53CBD2C1FE59B00C6D80B /* ItemListView.swift */, 026826A32BF59DF60036F959 /* CartView.swift */, 026826A22BF59DF60036F959 /* ItemRowView.swift */, @@ -15933,6 +15939,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 */, @@ -16729,6 +16736,7 @@ 68E952CC287536010095A23D /* SafariView.swift in Sources */, D449C51C26DE6B5000D75B02 /* IconListItem.swift in Sources */, EE9D03182B89E2B10077CED1 /* OrderStatusEnum+Analytics.swift in Sources */, + 014371272DFC8E2800C0279B /* PointOfSaleBarcodeInformationModalViewModel.swift in Sources */, CE16177A21B7192A00B82A47 /* AuthenticationConstants.swift in Sources */, 26A630ED253F3B5C00CBC3B1 /* RefundCreationUseCase.swift in Sources */, B95700AE2A72C39C001BADF2 /* CustomerSelectorView.swift in Sources */, From 15a674aab64c0d26fe55705f2840f255e36a7537 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:02:48 +0300 Subject: [PATCH 02/11] Define barcode scanner information modal --- ...ntOfSaleBarcodeScannerModalViewModel.swift | 78 +++++++++++++++++++ .../WooCommerce.xcodeproj/project.pbxproj | 8 +- 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift new file mode 100644 index 00000000000..e39a3665562 --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift @@ -0,0 +1,78 @@ +import Foundation + +extension PointOfSaleInformationModalViewModel { + static var barcodeScannerModel: PointOfSaleInformationModalViewModel { + let title = AttributedString(Localization.barcodeInfoHeading) + let intro = Paragraph(AttributedString(Localization.barcodeInfoIntroMessage)) + + let primary = AttributedString(Localization.barcodeInfoPrimaryMessage) + + var secondary = AttributedString(Localization.barcodeInfoSecondaryMessage + " ") + var moreDetails = AttributedString(Localization.barcodeInfoMoreDetailsLink) + moreDetails.link = Constants.detailsLink + moreDetails.foregroundColor = .posPrimary + secondary.append(moreDetails) + let secondaryBullet = secondary + + let tertiary = AttributedString(Localization.barcodeInfoTertiaryMessage) + let quaternary = AttributedString(Localization.barcodeInfoQuaternaryMessage) + + let bullets = Paragraph([primary, secondaryBullet, tertiary, quaternary]) + + let quinary = Paragraph(AttributedString(Localization.barcodeInfoQuinaryMessage)) + + return PointOfSaleInformationModalViewModel( + title: title, + paragraphs: [intro, bullets, quinary] + ) + } + + private enum Constants { + static let detailsLink = URL(string: "https://woocommerce.com/document/barcode-and-qr-code-scanner/") + } + + private 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 System 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 the software keyboard back.", + comment: "Quinary message in the barcode info modal in POS, explaining scanner keyboard emulation and how to show software keyboard again" + ) + } +} + diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index efe0b1a9122..6caf497b4ce 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -34,7 +34,7 @@ 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 /* PointOfSaleBarcodeInformationModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014371262DFC8E2100C0279B /* PointOfSaleBarcodeInformationModalViewModel.swift */; }; + 014371272DFC8E2800C0279B /* PointOfSaleBarcodeScannerModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerModalViewModel.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 */; }; @@ -3294,7 +3294,7 @@ 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 /* PointOfSaleBarcodeInformationModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeInformationModalViewModel.swift; sourceTree = ""; }; + 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeScannerModalViewModel.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 = ""; }; @@ -7297,7 +7297,7 @@ DA0DBE2E2C4FC61D00DF14C0 /* POSFloatingControlView.swift */, 20D3D42A2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift */, 01435CF72DFC2CE800C0279B /* PointOfSaleInformationModal.swift */, - 014371262DFC8E2100C0279B /* PointOfSaleBarcodeInformationModalViewModel.swift */, + 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerModalViewModel.swift */, 68C53CBD2C1FE59B00C6D80B /* ItemListView.swift */, 026826A32BF59DF60036F959 /* CartView.swift */, 026826A22BF59DF60036F959 /* ItemRowView.swift */, @@ -16736,7 +16736,7 @@ 68E952CC287536010095A23D /* SafariView.swift in Sources */, D449C51C26DE6B5000D75B02 /* IconListItem.swift in Sources */, EE9D03182B89E2B10077CED1 /* OrderStatusEnum+Analytics.swift in Sources */, - 014371272DFC8E2800C0279B /* PointOfSaleBarcodeInformationModalViewModel.swift in Sources */, + 014371272DFC8E2800C0279B /* PointOfSaleBarcodeScannerModalViewModel.swift in Sources */, CE16177A21B7192A00B82A47 /* AuthenticationConstants.swift in Sources */, 26A630ED253F3B5C00CBC3B1 /* RefundCreationUseCase.swift in Sources */, B95700AE2A72C39C001BADF2 /* CustomerSelectorView.swift in Sources */, From 9702df1aeaec6ef38a8b5b748553bd8bf599ac03 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:42:49 +0300 Subject: [PATCH 03/11] Apply additional styling to barcode scanner information modal --- ...ntOfSaleBarcodeScannerModalViewModel.swift | 5 ++- .../PointOfSaleInformationModal.swift | 35 ++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift index e39a3665562..b97dee0b1c5 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift @@ -17,9 +17,9 @@ extension PointOfSaleInformationModalViewModel { let tertiary = AttributedString(Localization.barcodeInfoTertiaryMessage) let quaternary = AttributedString(Localization.barcodeInfoQuaternaryMessage) - let bullets = Paragraph([primary, secondaryBullet, tertiary, quaternary]) + let bullets = Paragraph([primary, secondaryBullet, tertiary, quaternary], identation: POSSpacing.small) - let quinary = Paragraph(AttributedString(Localization.barcodeInfoQuinaryMessage)) + let quinary = Paragraph(AttributedString(Localization.barcodeInfoQuinaryMessage), style: .outlined) return PointOfSaleInformationModalViewModel( title: title, @@ -75,4 +75,3 @@ extension PointOfSaleInformationModalViewModel { ) } } - diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift index 067f7e1e4a1..c11d548db5e 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift @@ -2,15 +2,30 @@ import SwiftUI struct PointOfSaleInformationModalViewModel { struct Paragraph: Hashable, Identifiable { + enum Style { + case `default` + case outlined + } + let id = UUID() let lines: [AttributedString] + let style: Style + let identation: CGFloat - init(_ lines: [AttributedString]) { + init(_ lines: [AttributedString], + style: Style = .default, + identation: CGFloat = 0) { self.lines = lines + self.style = style + self.identation = identation } - init(_ text: AttributedString) { + init(_ text: AttributedString, + style: Style = .default, + identation: CGFloat = 0) { self.lines = [text] + self.style = style + self.identation = identation } } let title: AttributedString @@ -52,12 +67,24 @@ struct PointOfSaleInformationModal: View { VStack { ForEach(paragraph.lines, id: \.self) { text in Text(text) - .font(.posBodyLargeRegular()) - .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .frame(maxWidth: .infinity, alignment: .leading) } } + .padding(.leading, paragraph.identation) + .if(paragraph.style == .outlined) { view in + view + .frame(maxWidth: .infinity) + .padding(POSPadding.medium) + .background(Color(.posSurfaceDim)) + .clipShape(RoundedRectangle(cornerRadius: POSCornerRadiusStyle.medium.value)) + .multilineTextAlignment(.center) + } + .if(paragraph.style == .default) { view in + view + .font(.posBodyLargeRegular()) + .multilineTextAlignment(.leading) + } } Button(action: { From f5ba1536949d9464369f663559ec38ddf32a108f Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 13 Jun 2025 20:43:15 +0300 Subject: [PATCH 04/11] Display barcode scanning popover menu item on POS --- .../Presentation/POSFloatingControlView.swift | 20 +++++++++++++++++++ ...ntOfSaleBarcodeScannerModalViewModel.swift | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index abf1aa93531..1ccfe1cfa28 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 showBarcodeScanning: Bool = false init(showExitPOSModal: Binding, showSupport: Binding, @@ -56,6 +57,13 @@ struct POSFloatingControlView: View { title: { Text(Localization.productRestrictionsInfo) }, icon: { Image(systemName: "magnifyingglass") }) } + Button { + showBarcodeScanning = true + } label: { + Label( + title: { Text(Localization.barcodeScanning) }, + icon: { Image(systemName: "barcode.viewfinder") }) + } } label: { VStack { Spacer() @@ -81,6 +89,12 @@ struct POSFloatingControlView: View { .posModal(isPresented: $showProductRestrictionsModal) { SimpleProductsOnlyInformation(isPresented: $showProductRestrictionsModal) } + .posModal(isPresented: $showBarcodeScanning) { + PointOfSaleInformationModal( + isPresented: $showBarcodeScanning, + viewModel: .barcodeScannerModel + ) + } .frame(height: Constants.size) .background(Color.clear) .animation(.default, value: backgroundAppearance) @@ -149,6 +163,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/PointOfSaleBarcodeScannerModalViewModel.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift index b97dee0b1c5..1fc0250a768 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift @@ -17,7 +17,7 @@ extension PointOfSaleInformationModalViewModel { let tertiary = AttributedString(Localization.barcodeInfoTertiaryMessage) let quaternary = AttributedString(Localization.barcodeInfoQuaternaryMessage) - let bullets = Paragraph([primary, secondaryBullet, tertiary, quaternary], identation: POSSpacing.small) + let bullets = Paragraph([primary, secondaryBullet, tertiary, quaternary], identation: POSSpacing.medium) let quinary = Paragraph(AttributedString(Localization.barcodeInfoQuinaryMessage), style: .outlined) From d24c03bb301840377653decd5bcfdd062b4ec766 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 13 Jun 2025 22:44:34 +0300 Subject: [PATCH 05/11] Structure PointOfSaleInformationModal as a container view for more flexibility PointOfSaleBarcodeScannerInformationModal can use PointOfSaleInformationModal and built it more flexibly using PointOfSaleInformationParagraphView with its styles. --- .../Presentation/POSFloatingControlView.swift | 5 +- ...fSaleBarcodeScannerInformationModal.swift} | 53 ++++--- .../PointOfSaleInformationModal.swift | 129 +++++++++--------- .../WooCommerce.xcodeproj/project.pbxproj | 8 +- 4 files changed, 102 insertions(+), 93 deletions(-) rename WooCommerce/Classes/POS/Presentation/{PointOfSaleBarcodeScannerModalViewModel.swift => PointOfSaleBarcodeScannerInformationModal.swift} (71%) diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index 1ccfe1cfa28..d4e1146d654 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -90,10 +90,7 @@ struct POSFloatingControlView: View { SimpleProductsOnlyInformation(isPresented: $showProductRestrictionsModal) } .posModal(isPresented: $showBarcodeScanning) { - PointOfSaleInformationModal( - isPresented: $showBarcodeScanning, - viewModel: .barcodeScannerModel - ) + PointOfSaleBarcodeScannerInformationModal(isPresented: $showBarcodeScanning) } .frame(height: Constants.size) .background(Color.clear) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift similarity index 71% rename from WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift rename to WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift index 1fc0250a768..84334e3dc8c 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerModalViewModel.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift @@ -1,37 +1,50 @@ import Foundation +import SwiftUI -extension PointOfSaleInformationModalViewModel { - static var barcodeScannerModel: PointOfSaleInformationModalViewModel { - let title = AttributedString(Localization.barcodeInfoHeading) - let intro = Paragraph(AttributedString(Localization.barcodeInfoIntroMessage)) +struct PointOfSaleBarcodeScannerInformationModal: View { + @Binding var isPresented: Bool - let primary = AttributedString(Localization.barcodeInfoPrimaryMessage) + init(isPresented: Binding) { + self._isPresented = isPresented + } + + var body: some View { + PointOfSaleInformationModal(isPresented: $isPresented, title: AttributedString(Localization.barcodeInfoHeading)) { + PointOfSaleInformationParagraphView { + Text(AttributedString(Localization.barcodeInfoIntroMessage)) + } + + PointOfSaleInformationParagraphView { + Text(AttributedString(Localization.barcodeInfoPrimaryMessage)) + Text(bulletPointWithLink) + Text(AttributedString(Localization.barcodeInfoTertiaryMessage)) + Text(AttributedString(Localization.barcodeInfoQuaternaryMessage)) + } + .padding(.leading, POSSpacing.medium) + PointOfSaleInformationParagraphView(style: .outlined) { + Text(AttributedString(Localization.barcodeInfoQuinaryMessage)) + } + } + } + + private var bulletPointWithLink: AttributedString { var secondary = AttributedString(Localization.barcodeInfoSecondaryMessage + " ") var moreDetails = AttributedString(Localization.barcodeInfoMoreDetailsLink) moreDetails.link = Constants.detailsLink moreDetails.foregroundColor = .posPrimary + moreDetails.underlineStyle = .single secondary.append(moreDetails) - let secondaryBullet = secondary - - let tertiary = AttributedString(Localization.barcodeInfoTertiaryMessage) - let quaternary = AttributedString(Localization.barcodeInfoQuaternaryMessage) - - let bullets = Paragraph([primary, secondaryBullet, tertiary, quaternary], identation: POSSpacing.medium) - - let quinary = Paragraph(AttributedString(Localization.barcodeInfoQuinaryMessage), style: .outlined) - - return PointOfSaleInformationModalViewModel( - title: title, - paragraphs: [intro, bullets, quinary] - ) + return secondary } +} - private enum Constants { +private extension PointOfSaleBarcodeScannerInformationModal { + enum Constants { static let detailsLink = URL(string: "https://woocommerce.com/document/barcode-and-qr-code-scanner/") } - private enum Localization { + enum Localization { static let barcodeInfoHeading = NSLocalizedString( "pos.barcodeInfoModal.heading", value: "Barcode scanning", diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift index c11d548db5e..d639d0d5fa3 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift @@ -1,56 +1,26 @@ import SwiftUI -struct PointOfSaleInformationModalViewModel { - struct Paragraph: Hashable, Identifiable { - enum Style { - case `default` - case outlined - } - - let id = UUID() - let lines: [AttributedString] - let style: Style - let identation: CGFloat - - init(_ lines: [AttributedString], - style: Style = .default, - identation: CGFloat = 0) { - self.lines = lines - self.style = style - self.identation = identation - } - - init(_ text: AttributedString, - style: Style = .default, - identation: CGFloat = 0) { - self.lines = [text] - self.style = style - self.identation = identation - } - } - let title: AttributedString - let paragraphs: [Paragraph] -} - -// SwiftUI modal for displaying information in the Point of Sale context -@available(iOS 17.0, *) -struct PointOfSaleInformationModal: View { +// Container view for displaying information modals in the POS. +// +struct PointOfSaleInformationModal: View { @Binding var isPresented: Bool - let viewModel: PointOfSaleInformationModalViewModel + let title: AttributedString + let content: Content init( isPresented: Binding, - viewModel: PointOfSaleInformationModalViewModel + title: AttributedString, + @ViewBuilder content: () -> Content ) { self._isPresented = isPresented - self.viewModel = viewModel + self.title = title + self.content = content() } var body: some View { VStack(spacing: POSSpacing.xxLarge) { - // Modal header with title and close button HStack { - Text(viewModel.title) + Text(title) .font(.posHeadingBold) Spacer() Button { @@ -62,29 +32,8 @@ struct PointOfSaleInformationModal: View { } .foregroundColor(Color.posOnSurface) - // Display each paragraph (single or multiple lines) - ForEach(viewModel.paragraphs, id: \.self) { paragraph in - VStack { - ForEach(paragraph.lines, id: \.self) { text in - Text(text) - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - .padding(.leading, paragraph.identation) - .if(paragraph.style == .outlined) { view in - view - .frame(maxWidth: .infinity) - .padding(POSPadding.medium) - .background(Color(.posSurfaceDim)) - .clipShape(RoundedRectangle(cornerRadius: POSCornerRadiusStyle.medium.value)) - .multilineTextAlignment(.center) - } - .if(paragraph.style == .default) { view in - view - .font(.posBodyLargeRegular()) - .multilineTextAlignment(.leading) - } + VStack(spacing: POSSpacing.xxLarge) { + content } Button(action: { @@ -100,10 +49,60 @@ struct PointOfSaleInformationModal: View { } } -@available(iOS 17.0, *) +struct PointOfSaleInformationParagraphView: 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: .leading) { + content + .fixedSize(horizontal: false, vertical: true) + .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 + .font(.posBodyLargeRegular()) + .foregroundStyle(Color.posOnSurface) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +private struct PointOfSaleInformationModalOutlinedParagraphStyle: ViewModifier { + func body(content: Content) -> some View { + content + .font(.posBodySmallRegular()) + .foregroundStyle(Color.posOnSurface) + .padding(POSPadding.medium) + .background(Color.posSurfaceDim) + .clipShape(RoundedRectangle(cornerRadius: POSCornerRadiusStyle.medium.value)) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + } +} + private extension PointOfSaleInformationModal { enum Constants { - static let modalFrameWidth: CGFloat = 896 + static var modalFrameWidth: CGFloat { 896 } } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 6caf497b4ce..617c73ab1a4 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -34,7 +34,7 @@ 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 /* PointOfSaleBarcodeScannerModalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerModalViewModel.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 */; }; @@ -3294,7 +3294,7 @@ 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 /* PointOfSaleBarcodeScannerModalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeScannerModalViewModel.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 = ""; }; @@ -7297,7 +7297,7 @@ DA0DBE2E2C4FC61D00DF14C0 /* POSFloatingControlView.swift */, 20D3D42A2C64D7CC004CE6E3 /* SimpleProductsOnlyInformation.swift */, 01435CF72DFC2CE800C0279B /* PointOfSaleInformationModal.swift */, - 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerModalViewModel.swift */, + 014371262DFC8E2100C0279B /* PointOfSaleBarcodeScannerInformationModal.swift */, 68C53CBD2C1FE59B00C6D80B /* ItemListView.swift */, 026826A32BF59DF60036F959 /* CartView.swift */, 026826A22BF59DF60036F959 /* ItemRowView.swift */, @@ -16736,7 +16736,7 @@ 68E952CC287536010095A23D /* SafariView.swift in Sources */, D449C51C26DE6B5000D75B02 /* IconListItem.swift in Sources */, EE9D03182B89E2B10077CED1 /* OrderStatusEnum+Analytics.swift in Sources */, - 014371272DFC8E2800C0279B /* PointOfSaleBarcodeScannerModalViewModel.swift in Sources */, + 014371272DFC8E2800C0279B /* PointOfSaleBarcodeScannerInformationModal.swift in Sources */, CE16177A21B7192A00B82A47 /* AuthenticationConstants.swift in Sources */, 26A630ED253F3B5C00CBC3B1 /* RefundCreationUseCase.swift in Sources */, B95700AE2A72C39C001BADF2 /* CustomerSelectorView.swift in Sources */, From 40d91748f36e8e457236da3274595e903994674e Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Fri, 13 Jun 2025 23:42:18 +0300 Subject: [PATCH 06/11] Use PointOfSaleInformationModal for SimpleProductsOnlyInformation --- ...OfSaleBarcodeScannerInformationModal.swift | 11 +++- .../PointOfSaleInformationModal.swift | 23 +++---- .../SimpleProductsOnlyInformation.swift | 61 +++---------------- 3 files changed, 28 insertions(+), 67 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift index 84334e3dc8c..3c5cc32a6cb 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift @@ -10,11 +10,11 @@ struct PointOfSaleBarcodeScannerInformationModal: View { var body: some View { PointOfSaleInformationModal(isPresented: $isPresented, title: AttributedString(Localization.barcodeInfoHeading)) { - PointOfSaleInformationParagraphView { + PointOfSaleInformationModalParagraphView { Text(AttributedString(Localization.barcodeInfoIntroMessage)) } - PointOfSaleInformationParagraphView { + PointOfSaleInformationModalParagraphView { Text(AttributedString(Localization.barcodeInfoPrimaryMessage)) Text(bulletPointWithLink) Text(AttributedString(Localization.barcodeInfoTertiaryMessage)) @@ -22,7 +22,7 @@ struct PointOfSaleBarcodeScannerInformationModal: View { } .padding(.leading, POSSpacing.medium) - PointOfSaleInformationParagraphView(style: .outlined) { + PointOfSaleInformationModalParagraphView(style: .outlined) { Text(AttributedString(Localization.barcodeInfoQuinaryMessage)) } } @@ -88,3 +88,8 @@ private extension PointOfSaleBarcodeScannerInformationModal { ) } } + +@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 index d639d0d5fa3..854a163190c 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift @@ -49,7 +49,7 @@ struct PointOfSaleInformationModal: View { } } -struct PointOfSaleInformationParagraphView: View { +struct PointOfSaleInformationModalParagraphView: View { enum Style { case `default` case outlined @@ -64,39 +64,40 @@ struct PointOfSaleInformationParagraphView: View { } var body: some View { - VStack(alignment: .leading) { + VStack(alignment: style == .default ? .leading : .center) { content - .fixedSize(horizontal: false, vertical: true) - .if(style == .default, transform: { view in - view.modifier(PointOfSaleInformationModalDefaultParagraphStyle()) - }) - .if(style == .outlined, transform: { view in - view.modifier(PointOfSaleInformationModalOutlinedParagraphStyle()) - }) } + .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) - .frame(maxWidth: .infinity, alignment: .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) - .frame(maxWidth: .infinity, alignment: .center) + .fixedSize(horizontal: false, vertical: true) } } 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", From 9260aa9b13b1ca481ab98dfd2d6a20a9f194ccbd Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:40:10 +0300 Subject: [PATCH 07/11] Make modal scrollable to support large dynamic type sizes --- .../Presentation/PointOfSaleInformationModal.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift index 854a163190c..865d528782c 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift @@ -7,6 +7,9 @@ struct PointOfSaleInformationModal: View { 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, @@ -32,9 +35,15 @@ struct PointOfSaleInformationModal: View { } .foregroundColor(Color.posOnSurface) - VStack(spacing: POSSpacing.xxLarge) { - content + ScrollView { + VStack { + content + } + .measureHeight { height in + contentHeight = height + } } + .frame(maxHeight: contentHeight) Button(action: { isPresented = false From a2af2677954cd87c4f9be791353570cbcd821451 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:47:27 +0300 Subject: [PATCH 08/11] Hide barcode documentation behind the feature flag --- .../POS/Presentation/POSFloatingControlView.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index d4e1146d654..4f8d91df061 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -57,12 +57,14 @@ struct POSFloatingControlView: View { title: { Text(Localization.productRestrictionsInfo) }, icon: { Image(systemName: "magnifyingglass") }) } - Button { - showBarcodeScanning = true - } label: { - Label( - title: { Text(Localization.barcodeScanning) }, - icon: { Image(systemName: "barcode.viewfinder") }) + if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleBarcodeScanningi1) { + Button { + showBarcodeScanning = true + } label: { + Label( + title: { Text(Localization.barcodeScanning) }, + icon: { Image(systemName: "barcode.viewfinder") }) + } } } label: { VStack { From cd1e7ef2ac9c1c4363ab1da24ba724898a234c55 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:26:20 +0300 Subject: [PATCH 09/11] Update the content in barcode scanner information modal --- .../PointOfSaleBarcodeScannerInformationModal.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift index 3c5cc32a6cb..4dc58c2ef42 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleBarcodeScannerInformationModal.swift @@ -15,8 +15,8 @@ struct PointOfSaleBarcodeScannerInformationModal: View { } PointOfSaleInformationModalParagraphView { - Text(AttributedString(Localization.barcodeInfoPrimaryMessage)) Text(bulletPointWithLink) + Text(AttributedString(Localization.barcodeInfoSecondaryMessage)) Text(AttributedString(Localization.barcodeInfoTertiaryMessage)) Text(AttributedString(Localization.barcodeInfoQuaternaryMessage)) } @@ -29,7 +29,7 @@ struct PointOfSaleBarcodeScannerInformationModal: View { } private var bulletPointWithLink: AttributedString { - var secondary = AttributedString(Localization.barcodeInfoSecondaryMessage + " ") + var secondary = AttributedString(Localization.barcodeInfoPrimaryMessage + " ") var moreDetails = AttributedString(Localization.barcodeInfoMoreDetailsLink) moreDetails.link = Constants.detailsLink moreDetails.foregroundColor = .posPrimary @@ -72,7 +72,7 @@ private extension PointOfSaleBarcodeScannerInformationModal { ) static let barcodeInfoTertiaryMessage = NSLocalizedString( "pos.barcodeInfoModal.tertiaryMessage", - value: "• Connect your barcode scanner in System Bluetooth settings.", + 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( @@ -83,7 +83,7 @@ private extension PointOfSaleBarcodeScannerInformationModal { 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 the software keyboard back.", + "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" ) } From 0f523d547bae5f6a981df4639f41d60223907c6e Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:26:35 +0300 Subject: [PATCH 10/11] Rename variable to showBarcodeScanningInformation --- .../Classes/POS/Presentation/POSFloatingControlView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift index 4f8d91df061..b57a9201708 100644 --- a/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift +++ b/WooCommerce/Classes/POS/Presentation/POSFloatingControlView.swift @@ -9,7 +9,7 @@ struct POSFloatingControlView: View { @Binding private var showSupport: Bool @Binding private var showDocumentation: Bool @State private var showProductRestrictionsModal: Bool = false - @State private var showBarcodeScanning: Bool = false + @State private var showBarcodeScanningInformation: Bool = false init(showExitPOSModal: Binding, showSupport: Binding, @@ -59,7 +59,7 @@ struct POSFloatingControlView: View { } if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.pointOfSaleBarcodeScanningi1) { Button { - showBarcodeScanning = true + showBarcodeScanningInformation = true } label: { Label( title: { Text(Localization.barcodeScanning) }, @@ -91,8 +91,8 @@ struct POSFloatingControlView: View { .posModal(isPresented: $showProductRestrictionsModal) { SimpleProductsOnlyInformation(isPresented: $showProductRestrictionsModal) } - .posModal(isPresented: $showBarcodeScanning) { - PointOfSaleBarcodeScannerInformationModal(isPresented: $showBarcodeScanning) + .posModal(isPresented: $showBarcodeScanningInformation) { + PointOfSaleBarcodeScannerInformationModal(isPresented: $showBarcodeScanningInformation) } .frame(height: Constants.size) .background(Color.clear) From ccac2ee5bcbaedef912187b8b023628bf6cf3bb9 Mon Sep 17 00:00:00 2001 From: Povilas Staskus <4062343+staskus@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:55:48 +0300 Subject: [PATCH 11/11] Apply a workaround for modal on iOS17 to appear without expansion transition --- .../POS/Presentation/PointOfSaleInformationModal.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift index 865d528782c..a1e98f68265 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleInformationModal.swift @@ -40,7 +40,10 @@ struct PointOfSaleInformationModal: View { content } .measureHeight { height in - contentHeight = height + // Workaround for ScrollView not updating its height immediately on iOS 17 + withAnimation(.easeIn(duration: 0)) { + contentHeight = height + } } } .frame(maxHeight: contentHeight)