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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ struct PointOfSaleCardPresentPaymentReaderUpdateCompletionPreviewView: View {
showsSheet = true
}
}
.sheet(isPresented: $showsSheet) {
.posSheet(isPresented: $showsSheet) {
PointOfSaleCardPresentPaymentReaderUpdateCompletionView(
viewModel: .init(),
animation: .init(namespace: namespace)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -89,7 +89,7 @@ private extension View {
isPresented: Binding<Bool>,
onSelection: @escaping (POSCouponDiscountType) -> Void
) -> some View {
sheet(isPresented: isPresented) {
posSheet(isPresented: isPresented) {
let command = DiscountTypeBottomSheetListSelectorCommand(selected: nil) { type in
onSelection(.init(discountType: type))
}
Expand Down
7 changes: 6 additions & 1 deletion WooCommerce/Classes/POS/Presentation/ItemListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -42,7 +43,7 @@ struct ItemListView: View {

private var isBarcodeScanningEnabled: Binding<Bool> {
Binding(
get: { !isSearching && !modalManager.isPresented },
get: { !isSearching && !modalManager.isPresented && !sheetManager.isPresented },
set: { _ in }
)
}
Expand Down Expand Up @@ -457,13 +458,17 @@ private extension ItemListView {
return ItemListView(selectedItemListType: .constant(.products(search: false)),
searchTerm: .constant(""))
.environment(posModel)
.environmentObject(POSModalManager())
.environmentObject(POSSheetManager())
}

@available(iOS 17.0, *)
#Preview("Loading") {
ItemListView(selectedItemListType: .constant(.products(search: false)),
searchTerm: .constant(""))
.environment(POSPreviewHelpers.makePreviewAggregateModel())
.environmentObject(POSModalManager())
.environmentObject(POSSheetManager())
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ struct PointOfSaleDashboardView: View {
.frame(maxWidth: Constants.exitPOSSheetMaxWidth)
}
.posRootModal()
.sheet(isPresented: $showSupport) {
.posSheet(isPresented: $showSupport) {
supportForm
.interactiveDismissDisabled(true)
Copy link
Contributor

Choose a reason for hiding this comment

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

unrelated to this PR, as it happens in trunk as well: I noticed that after confirming the email alert on top of the support form, there was no obvious way to exit the form. The "Done" CTA in the navigation bar is actually the CTA to dismiss the form after checking the code, but I thought it's to submit the form as a user. Since interactive dismissal is disabled, maybe we can rename this "Done" to some dismissing action like "Cancel" and place it in the leading edge.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great observation. It's an easy change, I will do it and merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simulator.Screen.Recording.-.iPad.Pro.11-inch.M4.-.2025-08-05.at.12.11.07.mov

}
.sheet(isPresented: $showDocumentation) {
.posSheet(isPresented: $showDocumentation) {
documentationView
}
.onChange(of: posModel.entryPointController.eligibilityState) { oldValue, newValue in
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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."
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -75,6 +76,7 @@ struct PointOfSaleEntryPointView: View {
barcodeScanService: barcodeScanService)
}
.environmentObject(posModalManager)
.environmentObject(posSheetManager)
.injectKeyboardObserver()
.onAppear {
onPointOfSaleModeActiveStateChange(true)
Expand Down
130 changes: 130 additions & 0 deletions WooCommerce/Classes/POS/Presentation/Reusable Views/POSSheet.swift
Original file line number Diff line number Diff line change
@@ -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<String> = []

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<SheetContent: View>: 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<Item: Identifiable & Equatable, SheetContent: View>: 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<SheetContent: View>(
isPresented: Binding<Bool>,
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: Identifiable & Equatable, SheetContent: View>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping (Item) -> SheetContent
) -> some View {
self.modifier(
POSSheetViewModifierForItem(
item: item,
onDismiss: onDismiss,
sheetContent: content
)
)
}
}
4 changes: 4 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -3227,6 +3228,7 @@
01695EB72E22600300B731DA /* PointOfSaleBarcodeScannerSetupFlowManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleBarcodeScannerSetupFlowManagerTests.swift; sourceTree = "<group>"; };
016A77682D9D24A70004FCD6 /* POSCouponCreationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSCouponCreationSheet.swift; sourceTree = "<group>"; };
016C6B962C74AB17000D86FD /* POSConnectivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSConnectivityView.swift; sourceTree = "<group>"; };
016DE5322E40B03200F53DF7 /* POSSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSSheet.swift; sourceTree = "<group>"; };
0174DDBA2CE5FD5D005D20CA /* ReceiptEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModel.swift; sourceTree = "<group>"; };
0174DDBE2CE600C0005D20CA /* ReceiptEmailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptEmailViewModelTests.swift; sourceTree = "<group>"; };
0177250B2E1CFF7F00016148 /* GameControllerBarcodeParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerBarcodeParser.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6427,6 +6429,7 @@
01620C4C2C5394A400D3EA2F /* Reusable Views */ = {
isa = PBXGroup;
children = (
016DE5322E40B03200F53DF7 /* POSSheet.swift */,
0210A2452D55EC140054C78D /* Buttons */,
01620C4D2C5394B200D3EA2F /* POSProgressViewStyle.swift */,
204D1D612C5A50840064A6BE /* POSModalViewModifier.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down