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 @@ -7,6 +7,7 @@ struct PointOfSaleEntryPointView: View {
@State private var posModel: PointOfSaleAggregateModel?
@StateObject private var posModalManager = POSModalManager()
@StateObject private var posSheetManager = POSSheetManager()
@StateObject private var posCoverManager = POSFullScreenCoverManager()
@State private var posEntryPointController: POSEntryPointController
@Environment(\.horizontalSizeClass) private var horizontalSizeClass

Expand Down Expand Up @@ -77,6 +78,7 @@ struct PointOfSaleEntryPointView: View {
}
.environmentObject(posModalManager)
.environmentObject(posSheetManager)
.environmentObject(posCoverManager)
.injectKeyboardObserver()
.onAppear {
onPointOfSaleModeActiveStateChange(true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import SwiftUI

extension View {
/// Shows a full screen cover that automatically manages modal hierarchy.
/// This is a wrapper around SwiftUI's fullScreenCover that integrates with POS modal system.
///
/// Full screen covers automatically establish a new presentation layer, so:
/// - Modals/sheets from views behind the cover will not show
/// - Modals/sheet within the cover content will show normally
/// - Multiple covers can be stacked with proper hierarchy management
///
/// - Parameters:
/// - isPresented: Binding to control when the full screen cover is shown
/// - onDismiss: Optional closure called when the cover is dismissed
/// - content: Content to show in full screen
/// - Returns: a modified view that shows full screen content with automatic modal hierarchy
// periphery:ignore
func posFullScreenCover<Content: View>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping () -> Content
) -> some View {
self.modifier(
POSFullScreenCoverModifier(
isPresented: isPresented,
onDismiss: onDismiss,
coverContent: content
)
)
}

/// - Parameters:
/// - item: Binding to control when the full screen cover is shown
/// - onDismiss: Optional closure called when the cover is dismissed
/// - content: Content to show in full screen
/// - Returns: a modified view that shows full screen content with automatic modal hierarchy
// periphery:ignore
func posFullScreenCover<Item: Identifiable & Equatable, Content: View>(
item: Binding<Item?>,
onDismiss: (() -> Void)? = nil,
@ViewBuilder content: @escaping (Item) -> Content
) -> some View {
self.modifier(
POSFullScreenCoverModifierForItem(
item: item,
onDismiss: onDismiss,
coverContent: content
)
)
}
}

final class POSFullScreenCoverManager: ObservableObject {
@Published fileprivate(set) var isPresented: Bool = false
}

// MARK: - Modifiers
// periphery:ignore
struct POSFullScreenCoverModifier<CoverContent: View>: ViewModifier {
@Binding var isPresented: Bool
let onDismiss: (() -> Void)?
let coverContent: () -> CoverContent

@EnvironmentObject var parentCoverManager: POSFullScreenCoverManager
@StateObject private var modalManager = POSModalManager()
@StateObject private var sheetManager = POSSheetManager()
@StateObject private var coverManager = POSFullScreenCoverManager()

@State private var sheetId = UUID().uuidString

func body(content: Content) -> some View {
content
.fullScreenCover(isPresented: $isPresented, onDismiss: onDismiss, content: {
coverContent()
.posRootModal()
.environmentObject(modalManager)
.environmentObject(sheetManager)
.environmentObject(coverManager)
})
.onChange(of: isPresented) { newValue in
parentCoverManager.isPresented = newValue
}
}
}
// periphery:ignore
struct POSFullScreenCoverModifierForItem<Item: Identifiable & Equatable, CoverContent: View>: ViewModifier {
@Binding var item: Item?
let onDismiss: (() -> Void)?
let coverContent: (Item) -> CoverContent

@EnvironmentObject var parentCoverManager: POSFullScreenCoverManager
@StateObject private var modalManager = POSModalManager()
@StateObject private var sheetManager = POSSheetManager()
@StateObject private var coverManager = POSFullScreenCoverManager()

@State private var sheetId = UUID().uuidString

func body(content: Content) -> some View {
content
.fullScreenCover(item: $item, onDismiss: onDismiss, content: {
coverContent($0)
.posRootModal()
.environmentObject(modalManager)
.environmentObject(sheetManager)
.environmentObject(coverManager)
})
.onChange(of: item) { newValue in
parentCoverManager.isPresented = newValue != nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,17 @@ extension View {

struct POSModalViewModifier<Item: Identifiable & Equatable, ModalContent: View>: ViewModifier {
@EnvironmentObject var modalManager: POSModalManager
@EnvironmentObject var coverManager: POSFullScreenCoverManager
@Binding var item: Item?
let onDismiss: (() -> Void)?
let modalContent: (Item) -> ModalContent

func body(content: Content) -> some View {
content
.onChange(of: item) { newItem in
// Don't show a modal if a full screen overlay is presented on top
guard !coverManager.isPresented else { return }

if let newItem = newItem {
modalManager.present(onDismiss: {
// Internal dismissal, i.e. from tapping the background
Expand All @@ -100,13 +104,17 @@ struct POSModalViewModifier<Item: Identifiable & Equatable, ModalContent: View>:

struct POSModalViewModifierForBool<ModalContent: View>: ViewModifier {
@EnvironmentObject var modalManager: POSModalManager
@EnvironmentObject var coverManager: POSFullScreenCoverManager
@Binding var isPresented: Bool
let onDismiss: (() -> Void)?
let modalContent: () -> ModalContent

func body(content: Content) -> some View {
content
.onChange(of: isPresented) { newValue in
// Don't show a modal if a full screen overlay is presented on top
guard !coverManager.isPresented else { return }

if newValue {
modalManager.present(onDismiss: {
// Internal dismissal, i.e. from tapping the background
Expand Down
27 changes: 24 additions & 3 deletions WooCommerce/Classes/POS/Presentation/Reusable Views/POSSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,25 @@ final class POSSheetManager: ObservableObject {

struct POSSheetViewModifier<SheetContent: View>: ViewModifier {
@EnvironmentObject var sheetManager: POSSheetManager
@EnvironmentObject var coverManager: POSFullScreenCoverManager
@Binding var isPresented: Bool
let onDismiss: (() -> Void)?
let sheetContent: () -> SheetContent

@State private var sheetId = UUID().uuidString

private var sheetIsPresented: Binding<Bool> {
Binding<Bool>(get: {
// Don't show a sheet if a full screen overlay is presented on top
return self.$isPresented.wrappedValue && !coverManager.isPresented
}, set: {
self.$isPresented.wrappedValue = $0
})
}

func body(content: Content) -> some View {
content
.sheet(isPresented: $isPresented, onDismiss: onDismiss, content: sheetContent)
.sheet(isPresented: sheetIsPresented, onDismiss: onDismiss, content: sheetContent)
.onChange(of: isPresented) { newValue in
if newValue {
sheetManager.registerSheetPresented(id: sheetId)
Expand All @@ -53,16 +63,27 @@ struct POSSheetViewModifier<SheetContent: View>: ViewModifier {

struct POSSheetViewModifierForItem<Item: Identifiable & Equatable, SheetContent: View>: ViewModifier {
@EnvironmentObject var sheetManager: POSSheetManager
@EnvironmentObject var coverManager: POSFullScreenCoverManager
@Binding var item: Item?
let onDismiss: (() -> Void)?
let sheetContent: (Item) -> SheetContent

@State private var sheetId = UUID().uuidString

private var sheetItem: Binding<Item?> {
Binding<Item?>(get: {
// Don't show a sheet if a full screen overlay is presented on top
guard !coverManager.isPresented else { return nil }
return self.$item.wrappedValue
}, set: {
self.$item.wrappedValue = $0
})
}

func body(content: Content) -> some View {
content
.sheet(item: $item, onDismiss: onDismiss, content: sheetContent)
.onChange(of: item) { newItem in
.sheet(item: sheetItem, onDismiss: onDismiss, content: sheetContent)
.onChange(of: sheetItem.wrappedValue) { newItem in
let newValue = newItem != nil
if newValue {
sheetManager.registerSheetPresented(id: sheetId)
Expand Down
6 changes: 5 additions & 1 deletion WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
0196FF942DA8067A0063CEF1 /* CouponCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0196FF932DA806720063CEF1 /* CouponCardView.swift */; };
019A86842D89C13800ABBB71 /* TapToPayCardReaderPaymentAlertsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019A86832D89C13800ABBB71 /* TapToPayCardReaderPaymentAlertsProvider.swift */; };
01A3093C2DAE768600B672F6 /* MockPointOfSaleCouponService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A3093B2DAE768000B672F6 /* MockPointOfSaleCouponService.swift */; };
01AA4FA12E4CB22900FA9B4C /* POSFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AA4FA02E4CB22700FA9B4C /* POSFullScreenCover.swift */; };
01AAD8142D92E37A0081D60B /* PointOfSaleOrderSyncCouponsErrorMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AAD8132D92E37A0081D60B /* PointOfSaleOrderSyncCouponsErrorMessageView.swift */; };
01AB2D122DDC7AD300AA67FD /* PointOfSaleItemListAnalyticsTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AB2D112DDC7AD100AA67FD /* PointOfSaleItemListAnalyticsTrackerTests.swift */; };
01AB2D142DDC7CD200AA67FD /* POSItemActionHandlerFactoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01AB2D132DDC7CD000AA67FD /* POSItemActionHandlerFactoryTests.swift */; };
Expand Down Expand Up @@ -3267,6 +3268,7 @@
0196FF932DA806720063CEF1 /* CouponCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponCardView.swift; sourceTree = "<group>"; };
019A86832D89C13800ABBB71 /* TapToPayCardReaderPaymentAlertsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapToPayCardReaderPaymentAlertsProvider.swift; sourceTree = "<group>"; };
01A3093B2DAE768000B672F6 /* MockPointOfSaleCouponService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPointOfSaleCouponService.swift; sourceTree = "<group>"; };
01AA4FA02E4CB22700FA9B4C /* POSFullScreenCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSFullScreenCover.swift; sourceTree = "<group>"; };
01AAD8132D92E37A0081D60B /* PointOfSaleOrderSyncCouponsErrorMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleOrderSyncCouponsErrorMessageView.swift; sourceTree = "<group>"; };
01AB2D112DDC7AD100AA67FD /* PointOfSaleItemListAnalyticsTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointOfSaleItemListAnalyticsTrackerTests.swift; sourceTree = "<group>"; };
01AB2D132DDC7CD000AA67FD /* POSItemActionHandlerFactoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = POSItemActionHandlerFactoryTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6447,8 +6449,9 @@
01620C4C2C5394A400D3EA2F /* Reusable Views */ = {
isa = PBXGroup;
children = (
016DE5322E40B03200F53DF7 /* POSSheet.swift */,
0210A2452D55EC140054C78D /* Buttons */,
016DE5322E40B03200F53DF7 /* POSSheet.swift */,
01AA4FA02E4CB22700FA9B4C /* POSFullScreenCover.swift */,
01620C4D2C5394B200D3EA2F /* POSProgressViewStyle.swift */,
204D1D612C5A50840064A6BE /* POSModalViewModifier.swift */,
20D2CCA42C7E328300051705 /* POSModalCloseButton.swift */,
Expand Down Expand Up @@ -16201,6 +16204,7 @@
0212276124498A270042161F /* ProductFormBottomSheetListSelectorCommand.swift in Sources */,
D831E2E0230E0BA7000037D0 /* Logs.swift in Sources */,
02CEBB8224C98861002EDF35 /* ProductFormDataModel.swift in Sources */,
01AA4FA12E4CB22900FA9B4C /* POSFullScreenCover.swift in Sources */,
3120491B26DD80E000A4EC4F /* ActivitySpinnerAndLabelTableViewCell.swift in Sources */,
DEC51AFD276AEAE3009F3DF4 /* SystemStatusReportView.swift in Sources */,
CECC759C23D61C1400486676 /* AggregateDataHelper.swift in Sources */,
Expand Down