diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView.swift index 95329a9baed..f599a51ccfd 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView.swift @@ -28,7 +28,7 @@ struct PointOfSaleCardPresentPaymentConnectingFailedUpdateAddressView: View { accessibilityLabel: viewModel.cancelButtonViewModel.title) .multilineTextAlignment(.center) .accessibilityElement(children: .contain) - .sheet(isPresented: $viewModel.shouldShowSettingsWebView) { + .posSheet(isPresented: $viewModel.shouldShowSettingsWebView) { WCSettingsWebView(adminUrl: viewModel.settingsAdminUrl, completion: viewModel.settingsWebViewWasDismissed) } diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressView.swift index 1a44c965f39..27a1ea64c2e 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressView.swift @@ -69,7 +69,7 @@ struct PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressPreviewView: V showsSheet = true } } - .sheet(isPresented: $showsSheet) { + .posSheet(isPresented: $showsSheet) { PointOfSaleCardPresentPaymentOptionalReaderUpdateInProgressView(viewModel: .init( progress: 0.6, cancel: nil ), animation: .init(namespace: namespace)) diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentReaderUpdateCompletionView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentReaderUpdateCompletionView.swift index 08ff986880d..680bed0c600 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentReaderUpdateCompletionView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentReaderUpdateCompletionView.swift @@ -49,7 +49,7 @@ struct PointOfSaleCardPresentPaymentReaderUpdateCompletionPreviewView: View { showsSheet = true } } - .sheet(isPresented: $showsSheet) { + .posSheet(isPresented: $showsSheet) { PointOfSaleCardPresentPaymentReaderUpdateCompletionView( viewModel: .init(), animation: .init(namespace: namespace) diff --git a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressView.swift b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressView.swift index 135bc25adcd..a3d501b9aa4 100644 --- a/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressView.swift +++ b/WooCommerce/Classes/POS/Presentation/CardReaderConnection/UI States/Connection Alerts/PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressView.swift @@ -70,7 +70,7 @@ struct CardPresentPaymentRequiredReaderUpdateInProgressPreviewView: View { showsSheet = true } } - .sheet(isPresented: $showsSheet) { + .posSheet(isPresented: $showsSheet) { PointOfSaleCardPresentPaymentRequiredReaderUpdateInProgressView(viewModel: .init( progress: 0.6, cancel: nil ), animation: .init(namespace: namespace)) diff --git a/WooCommerce/Classes/POS/Presentation/Coupons/POSCouponCreationSheet.swift b/WooCommerce/Classes/POS/Presentation/Coupons/POSCouponCreationSheet.swift index f7a76fe019d..190ae57a85d 100644 --- a/WooCommerce/Classes/POS/Presentation/Coupons/POSCouponCreationSheet.swift +++ b/WooCommerce/Classes/POS/Presentation/Coupons/POSCouponCreationSheet.swift @@ -22,7 +22,7 @@ private struct POSCouponCreationSheetModifier: ViewModifier { func body(content: Content) -> some View { content - .sheet(item: $selectedType) { (posDiscountType: POSCouponDiscountType) in + .posSheet(item: $selectedType) { (posDiscountType: POSCouponDiscountType) in POSCouponCreationView( discountType: posDiscountType.discountType, showTypeSelection: $showCouponSelectionSheet, @@ -89,7 +89,7 @@ private extension View { isPresented: Binding, onSelection: @escaping (POSCouponDiscountType) -> Void ) -> some View { - sheet(isPresented: isPresented) { + posSheet(isPresented: isPresented) { let command = DiscountTypeBottomSheetListSelectorCommand(selected: nil) { type in onSelection(.init(discountType: type)) } diff --git a/WooCommerce/Classes/POS/Presentation/ItemListView.swift b/WooCommerce/Classes/POS/Presentation/ItemListView.swift index 096dcea8fe9..0bc3b11da35 100644 --- a/WooCommerce/Classes/POS/Presentation/ItemListView.swift +++ b/WooCommerce/Classes/POS/Presentation/ItemListView.swift @@ -9,6 +9,7 @@ struct ItemListView: View { @Environment(PointOfSaleAggregateModel.self) private var posModel @Environment(\.keyboardObserver) private var keyboardObserver @EnvironmentObject var modalManager: POSModalManager + @EnvironmentObject var sheetManager: POSSheetManager @Binding var selectedItemListType: ItemListType @Binding var searchTerm: String @@ -42,7 +43,7 @@ struct ItemListView: View { private var isBarcodeScanningEnabled: Binding { Binding( - get: { !isSearching && !modalManager.isPresented }, + get: { !isSearching && !modalManager.isPresented && !sheetManager.isPresented }, set: { _ in } ) } @@ -457,6 +458,8 @@ private extension ItemListView { return ItemListView(selectedItemListType: .constant(.products(search: false)), searchTerm: .constant("")) .environment(posModel) + .environmentObject(POSModalManager()) + .environmentObject(POSSheetManager()) } @available(iOS 17.0, *) @@ -464,6 +467,8 @@ private extension ItemListView { ItemListView(selectedItemListType: .constant(.products(search: false)), searchTerm: .constant("")) .environment(POSPreviewHelpers.makePreviewAggregateModel()) + .environmentObject(POSModalManager()) + .environmentObject(POSSheetManager()) } #endif diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift index 9d5f92f0b8c..7d4db359c0c 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleDashboardView.swift @@ -123,11 +123,11 @@ struct PointOfSaleDashboardView: View { .frame(maxWidth: Constants.exitPOSSheetMaxWidth) } .posRootModal() - .sheet(isPresented: $showSupport) { + .posSheet(isPresented: $showSupport) { supportForm .interactiveDismissDisabled(true) } - .sheet(isPresented: $showDocumentation) { + .posSheet(isPresented: $showDocumentation) { documentationView } .onChange(of: posModel.entryPointController.eligibilityState) { oldValue, newValue in @@ -190,8 +190,8 @@ private extension PointOfSaleDashboardView { viewModel: SupportFormViewModel(sourceTag: Constants.supportTag, defaultSite: ServiceLocator.stores.sessionManager.defaultSite)) .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button(Localization.supportDone) { + ToolbarItem(placement: .cancellationAction) { + Button(Localization.supportCancel) { showSupport = false } } @@ -258,9 +258,9 @@ private extension PointOfSaleDashboardView { } enum Localization { - static let supportDone = NSLocalizedString( - "pointOfSaleDashboard.support.done", - value: "Done", + static let supportCancel = NSLocalizedString( + "pointOfSaleDashboard.support.cancel", + value: "Cancel", comment: "Button to dismiss the support form from the POS dashboard." ) } diff --git a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift index e4534694753..a4fa2d55c92 100644 --- a/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift +++ b/WooCommerce/Classes/POS/Presentation/PointOfSaleEntryPointView.swift @@ -6,6 +6,7 @@ import protocol Yosemite.PointOfSaleBarcodeScanServiceProtocol struct PointOfSaleEntryPointView: View { @State private var posModel: PointOfSaleAggregateModel? @StateObject private var posModalManager = POSModalManager() + @StateObject private var posSheetManager = POSSheetManager() @State private var posEntryPointController: POSEntryPointController @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -75,6 +76,7 @@ struct PointOfSaleEntryPointView: View { barcodeScanService: barcodeScanService) } .environmentObject(posModalManager) + .environmentObject(posSheetManager) .injectKeyboardObserver() .onAppear { onPointOfSaleModeActiveStateChange(true) diff --git a/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSheet.swift b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSheet.swift new file mode 100644 index 00000000000..bd1773daaef --- /dev/null +++ b/WooCommerce/Classes/POS/Presentation/Reusable Views/POSSheet.swift @@ -0,0 +1,130 @@ +import SwiftUI + +// +// POSSheet - Wraps default SwiftUI .sheet() modifiers to support sheet presentation detection within POS. +// +// Usage: Replace .sheet() with .posSheet() and inject POSSheetManager at the POS root. +// Sheet presentation can be detected via POSSheetManager.isPresented. +// + +// MARK: - Sheet Detection Infrastructure + +final class POSSheetManager: ObservableObject { + @Published private(set) var isPresented: Bool = false + private var presentedSheets: Set = [] + + func registerSheetPresented(id: String) { + presentedSheets.insert(id) + updateState() + } + + func registerSheetDismissed(id: String) { + presentedSheets.remove(id) + updateState() + } + + private func updateState() { + isPresented = !presentedSheets.isEmpty + } +} + +// MARK: - Individual Sheet Modifiers + +struct POSSheetViewModifier: ViewModifier { + @EnvironmentObject var sheetManager: POSSheetManager + @Binding var isPresented: Bool + let onDismiss: (() -> Void)? + let sheetContent: () -> SheetContent + + @State private var sheetId = UUID().uuidString + + func body(content: Content) -> some View { + content + .sheet(isPresented: $isPresented, onDismiss: onDismiss, content: sheetContent) + .onChange(of: isPresented) { newValue in + if newValue { + sheetManager.registerSheetPresented(id: sheetId) + } else { + sheetManager.registerSheetDismissed(id: sheetId) + } + } + } +} + +struct POSSheetViewModifierForItem: ViewModifier { + @EnvironmentObject var sheetManager: POSSheetManager + @Binding var item: Item? + let onDismiss: (() -> Void)? + let sheetContent: (Item) -> SheetContent + + @State private var sheetId = UUID().uuidString + + func body(content: Content) -> some View { + content + .sheet(item: $item, onDismiss: onDismiss, content: sheetContent) + .onChange(of: item) { newItem in + let newValue = newItem != nil + if newValue { + sheetManager.registerSheetPresented(id: sheetId) + } else { + sheetManager.registerSheetDismissed(id: sheetId) + } + } + } +} + +// MARK: - View Modifiers + +extension View { + /// Shows a sheet with automatic detection of presentation. + /// + /// This works exactly like the native .sheet() modifier but automatically tracks + /// presentation state. + /// + /// This will only work in a view hierarchy containing a `POSSheetManager` environment object. + /// + /// - Parameters: + /// - isPresented: Binding to control when the sheet is shown. + /// - onDismiss: Optional closure executed when the sheet is dismissed. + /// - content: Content to show in the sheet + /// - Returns: a modified view which can show the sheet content specified, when applicable. + func posSheet( + isPresented: Binding, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping () -> SheetContent + ) -> some View { + self.modifier( + POSSheetViewModifier( + isPresented: isPresented, + onDismiss: onDismiss, + sheetContent: content + ) + ) + } + + /// Shows a sheet with automatic detection of presentation. + /// + /// This works exactly like the native .sheet(item:) modifier but automatically tracks + /// presentation state. + /// + /// This will only work in a view hierarchy containing a `POSSheetManager` environment object. + /// + /// - Parameters: + /// - item: Binding to control when the sheet is shown. When non-nil, the item is used to build the content. + /// - onDismiss: Optional closure executed when the sheet is dismissed. + /// - content: Content to show in the sheet + /// - Returns: a modified view which can show the sheet content specified, when applicable. + func posSheet( + item: Binding, + onDismiss: (() -> Void)? = nil, + @ViewBuilder content: @escaping (Item) -> SheetContent + ) -> some View { + self.modifier( + POSSheetViewModifierForItem( + item: item, + onDismiss: onDismiss, + sheetContent: content + ) + ) + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index f1cae1c283e..33272984bfd 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 01695EB82E22600800B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01695EB72E22600300B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift */; }; 016A77692D9D24B00004FCD6 /* POSCouponCreationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016A77682D9D24A70004FCD6 /* POSCouponCreationSheet.swift */; }; 016C6B972C74AB17000D86FD /* POSConnectivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */; }; + 016DE5332E40B03200F53DF7 /* POSSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016DE5322E40B03200F53DF7 /* POSSheet.swift */; }; 0174DDBB2CE5FD60005D20CA /* ReceiptEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */; }; 0174DDBF2CE600C5005D20CA /* ReceiptEmailViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */; }; 0177250C2E1CFF7F00016148 /* GameControllerBarcodeParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177250B2E1CFF7F00016148 /* GameControllerBarcodeParser.swift */; }; @@ -3227,6 +3228,7 @@ 01695EB72E22600300B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeScannerSetupFlowManagerTests.swift; sourceTree = ""; }; 016A77682D9D24A70004FCD6 /* POSCouponCreationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCouponCreationSheet.swift; sourceTree = ""; }; 016C6B962C74AB17000D86FD /* POSConnectivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSConnectivityView.swift; sourceTree = ""; }; + 016DE5322E40B03200F53DF7 /* POSSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSSheet.swift; sourceTree = ""; }; 0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModel.swift; sourceTree = ""; }; 0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModelTests.swift; sourceTree = ""; }; 0177250B2E1CFF7F00016148 /* GameControllerBarcodeParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerBarcodeParser.swift; sourceTree = ""; }; @@ -6427,6 +6429,7 @@ 01620C4C2C5394A400D3EA2F /* Reusable Views */ = { isa = PBXGroup; children = ( + 016DE5322E40B03200F53DF7 /* POSSheet.swift */, 0210A2452D55EC140054C78D /* Buttons */, 01620C4D2C5394B200D3EA2F /* POSProgressViewStyle.swift */, 204D1D612C5A50840064A6BE /* POSModalViewModifier.swift */, @@ -15213,6 +15216,7 @@ 26D1E9E82949818B00A7DC62 /* AnalyticsHubTimeRageAdapter.swift in Sources */, CE32B11A20BF8E32006FBCF4 /* UIButton+Helpers.swift in Sources */, 02667A1C2AC159A000C77B56 /* GiftCardCodeScannerNavigationView.swift in Sources */, + 016DE5332E40B03200F53DF7 /* POSSheet.swift in Sources */, 262562352C52A6410075A8CC /* WooAnalyticsEvent+BackgroudUpdates.swift in Sources */, 45BBFBC5274FDCE900213001 /* HubMenu.swift in Sources */, 02A9BCD62737F73C00159C79 /* JetpackBenefitItem.swift in Sources */,