From 4158a7ccf4855b2c5fa4e9a0e3d24dbe967508dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Thu, 27 Nov 2025 12:26:34 +0100 Subject: [PATCH 1/3] Add new design for refund button and overflow actions --- .../Orders/POSOrderDetailsView.swift | 99 ++++++++++++++----- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift b/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift index 03edd60f98b..22d5bffaac2 100644 --- a/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift +++ b/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift @@ -32,8 +32,8 @@ struct POSOrderDetailsView: View { title: POSOrderListView.Localization.orderTitle(order.number), backButtonConfiguration: shouldShowBackButton ? .init(state: .enabled, action: onBack) : nil, trailingContent: { - if actions.isNotEmpty { - actionsSection(actions) + if primaryAction != nil || overflowActions.isNotEmpty { + actionsSection(overflowActions) } }, bottomContent: { @@ -377,7 +377,24 @@ private extension POSOrderDetailsView { // MARK: - Actions private extension POSOrderDetailsView { - enum POSOrderDetailsAction: Identifiable, CaseIterable { + // Primary action for the header + enum POSPrimaryAction { + case issueRefund + + var title: String { + switch self { + case .issueRefund: + Localization.issueRefundActionTitle + } + } + + func available(for order: POSOrder) -> Bool { + // Adjust the condition as needed + return order.status == .completed + } + } + + enum POSOverflowAction: Identifiable, CaseIterable { case emailReceipt var id: String { title } @@ -392,43 +409,53 @@ private extension POSOrderDetailsView { func available(for order: POSOrder) -> Bool { switch self { case .emailReceipt: - order.status == .completed + return order.status == .completed } } } - var actions: [POSOrderDetailsAction] { - POSOrderDetailsAction.allCases.filter { $0.available(for: order) } + // Primary + overflow actions (formerly `actions`) + var primaryAction: POSPrimaryAction? { + let candidate: POSPrimaryAction = .issueRefund + return candidate.available(for: order) ? candidate : nil + } + + var overflowActions: [POSOverflowAction] { + POSOverflowAction.allCases.filter { $0.available(for: order) } } @ViewBuilder - func actionsSection(_ actions: [POSOrderDetailsAction]) -> some View { - VStack { - HStack { - ForEach(actions) { action in - Button(action: { + func actionsSection(_ actions: [POSOverflowAction]) -> some View { + HStack(spacing: POSSpacing.large) { + if let primaryAction { + Button(primaryAction.title) {} + .buttonStyle(POSFilledButtonStyle(size: .extraSmall)) + .accessibilityHint(Localization.issueRefundAccessibilityHint) + .lineLimit(1) + .minimumScaleFactor(0.5) + } + + if overflowActions.isNotEmpty { + Menu { + ForEach(overflowActions) { action in switch action { case .emailReceipt: - analytics.track(event: WooAnalyticsEvent.PointOfSale.orderDetailsEmailReceiptTapped()) - isShowingEmailReceiptView = true + Button(action.title) { + analytics.track(event: WooAnalyticsEvent.PointOfSale.orderDetailsEmailReceiptTapped()) + isShowingEmailReceiptView = true + } + .accessibilityHint(Localization.emailReceiptAccessibilityHint) } - }) { - Text(Localization.emailReceiptActionTitle) - .lineLimit(1) - .minimumScaleFactor(0.5) } - .buttonStyle(POSFilledButtonStyle(size: .extraSmall)) - .accessibilityHint(accessibilityHint(for: action)) + } label: { + Image(systemName: "ellipsis") + .font(.posBodyLargeBold) + .dynamicTypeSize(...DynamicTypeSize.accessibility2) + .foregroundColor(.posOnSurface) + .padding(POSPadding.small) } + .menuIndicator(.hidden) } - Spacer() - } - } - - private func accessibilityHint(for action: POSOrderDetailsAction) -> String { - switch action { - case .emailReceipt: - return Localization.emailReceiptAccessibilityHint } } } @@ -529,6 +556,24 @@ private enum Localization { comment: "Accessibility hint for email receipt button on order details view" ) + static let issueRefundActionTitle = NSLocalizedString( + "pos.orderDetailsView.issueRefundAction.title", + value: "Issue refund", + comment: "Primary action button to start issuing a refund on the order details view" + ) + + static let issueRefundAccessibilityHint = NSLocalizedString( + "pos.orderDetailsView.issueRefundAction.accessibilityHint", + value: "Start refund flow for this order", + comment: "Accessibility hint for issue refund button" + ) + + static let moreActionsA11yLabel = NSLocalizedString( + "pos.orderDetailsView.moreActions.label", + value: "More actions", + comment: "Accessibility label for the overflow actions menu button (three dots)" + ) + static func headerBottomContentAccessibilityLabel(date: String, email: String?, status: String) -> String { let baseFormat = NSLocalizedString( "pos.orderDetailsView.headerBottomContent.accessibilityLabel", From 62596ddb003abe0d7c19969b668bd909f25d3d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Thu, 27 Nov 2025 16:47:43 +0100 Subject: [PATCH 2/3] Apply feature flag logic --- .../Orders/POSOrderDetailsView.swift | 139 ++++++++++-------- 1 file changed, 75 insertions(+), 64 deletions(-) diff --git a/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift b/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift index 22d5bffaac2..684b40b7a61 100644 --- a/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift +++ b/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift @@ -14,6 +14,7 @@ struct POSOrderDetailsView: View { @Environment(\.siteTimezone) private var siteTimezone @Environment(POSOrderListModel.self) private var orderListModel @Environment(\.posAnalytics) private var analytics + @Environment(\.posFeatureFlags) private var featureFlags @State private var isShowingEmailReceiptView: Bool = false private var shouldShowBackButton: Bool { @@ -32,9 +33,7 @@ struct POSOrderDetailsView: View { title: POSOrderListView.Localization.orderTitle(order.number), backButtonConfiguration: shouldShowBackButton ? .init(state: .enabled, action: onBack) : nil, trailingContent: { - if primaryAction != nil || overflowActions.isNotEmpty { - actionsSection(overflowActions) - } + actionsSection(actions: availableActions) }, bottomContent: { headerBottomContent(for: order) @@ -377,87 +376,99 @@ private extension POSOrderDetailsView { // MARK: - Actions private extension POSOrderDetailsView { - // Primary action for the header - enum POSPrimaryAction { - case issueRefund + enum POSAction: Identifiable, CaseIterable { + case issueRefund + case emailReceipt - var title: String { - switch self { - case .issueRefund: - Localization.issueRefundActionTitle - } - } + var id: String { title } - func available(for order: POSOrder) -> Bool { - // Adjust the condition as needed - return order.status == .completed - } - } + var title: String { + switch self { + case .issueRefund: Localization.issueRefundActionTitle + case .emailReceipt: Localization.emailReceiptActionTitle + } + } - enum POSOverflowAction: Identifiable, CaseIterable { - case emailReceipt + var accessibilityHint: String { + switch self { + case .issueRefund: Localization.issueRefundAccessibilityHint + case .emailReceipt: Localization.emailReceiptAccessibilityHint + } + } - var id: String { title } + var priority: Int { + switch self { + case .issueRefund: 100 + case .emailReceipt: 50 + } + } - var title: String { - switch self { - case .emailReceipt: - Localization.emailReceiptActionTitle + func isAvailable(for order: POSOrder, flags: POSFeatureFlagProviding) -> Bool { + guard order.status == .completed else { return false } + switch self { + case .issueRefund: + return flags.isFeatureFlagEnabled(.pointOfSaleRefundsi1) + case .emailReceipt: + return true + } } } - func available(for order: POSOrder) -> Bool { - switch self { + func handler(for action: POSAction) -> @MainActor () -> Void { + switch action { case .emailReceipt: - return order.status == .completed + return { + analytics.track(event: WooAnalyticsEvent.PointOfSale.orderDetailsEmailReceiptTapped()) + isShowingEmailReceiptView = true + } + case .issueRefund: + return { } } } - } - - // Primary + overflow actions (formerly `actions`) - var primaryAction: POSPrimaryAction? { - let candidate: POSPrimaryAction = .issueRefund - return candidate.available(for: order) ? candidate : nil - } - var overflowActions: [POSOverflowAction] { - POSOverflowAction.allCases.filter { $0.available(for: order) } - } + var availableActions: [POSAction] { + POSAction.allCases + .filter { $0.isAvailable(for: order, flags: featureFlags) } + .sorted { $0.priority > $1.priority } + } @ViewBuilder - func actionsSection(_ actions: [POSOverflowAction]) -> some View { - HStack(spacing: POSSpacing.large) { - if let primaryAction { - Button(primaryAction.title) {} - .buttonStyle(POSFilledButtonStyle(size: .extraSmall)) - .accessibilityHint(Localization.issueRefundAccessibilityHint) - .lineLimit(1) - .minimumScaleFactor(0.5) - } - - if overflowActions.isNotEmpty { - Menu { - ForEach(overflowActions) { action in - switch action { - case .emailReceipt: - Button(action.title) { - analytics.track(event: WooAnalyticsEvent.PointOfSale.orderDetailsEmailReceiptTapped()) - isShowingEmailReceiptView = true - } - .accessibilityHint(Localization.emailReceiptAccessibilityHint) + func actionsSection(actions: [POSAction]) -> some View { + if actions.isEmpty { + EmptyView() + } else { + HStack(spacing: POSSpacing.large) { + let primary = actions[0] + Button(primary.title, action: handler(for: primary)) + .buttonStyle(POSFilledButtonStyle(size: .extraSmall)) + .accessibilityHint(primary.accessibilityHint) + .lineLimit(1) + .minimumScaleFactor(0.5) + + let overflow = actions.dropFirst() + if !overflow.isEmpty { + Menu { + ForEach(Array(overflow)) { action in + Button(action.title, action: handler(for: action)) + .accessibilityHint(action.accessibilityHint) } + } label: { + Image(systemName: "ellipsis") + .font(.posBodyLargeBold) + .dynamicTypeSize(...DynamicTypeSize.accessibility2) + .foregroundColor(.posOnSurface) + .padding(POSPadding.small) } - } label: { - Image(systemName: "ellipsis") - .font(.posBodyLargeBold) - .dynamicTypeSize(...DynamicTypeSize.accessibility2) - .foregroundColor(.posOnSurface) - .padding(POSPadding.small) + .menuIndicator(.hidden) } - .menuIndicator(.hidden) } } } + + func emailReceiptAction() { + analytics.track(event: WooAnalyticsEvent.PointOfSale.orderDetailsEmailReceiptTapped()) + isShowingEmailReceiptView = true + } } private extension POSOrderDetailsView { From 6ab93913c0b9ebaf4963f1113b5ca11305f99254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81sar=20Vargas=20Casaseca?= Date: Thu, 27 Nov 2025 16:50:37 +0100 Subject: [PATCH 3/3] Right indentation --- .../Orders/POSOrderDetailsView.swift | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift b/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift index 684b40b7a61..e5dda75b9c5 100644 --- a/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift +++ b/Modules/Sources/PointOfSale/Presentation/Orders/POSOrderDetailsView.swift @@ -377,60 +377,60 @@ private extension POSOrderDetailsView { // MARK: - Actions private extension POSOrderDetailsView { enum POSAction: Identifiable, CaseIterable { - case issueRefund - case emailReceipt + case issueRefund + case emailReceipt - var id: String { title } + var id: String { title } - var title: String { - switch self { - case .issueRefund: Localization.issueRefundActionTitle - case .emailReceipt: Localization.emailReceiptActionTitle - } - } - - var accessibilityHint: String { - switch self { - case .issueRefund: Localization.issueRefundAccessibilityHint - case .emailReceipt: Localization.emailReceiptAccessibilityHint - } + var title: String { + switch self { + case .issueRefund: Localization.issueRefundActionTitle + case .emailReceipt: Localization.emailReceiptActionTitle } + } - var priority: Int { - switch self { - case .issueRefund: 100 - case .emailReceipt: 50 - } + var accessibilityHint: String { + switch self { + case .issueRefund: Localization.issueRefundAccessibilityHint + case .emailReceipt: Localization.emailReceiptAccessibilityHint } + } - func isAvailable(for order: POSOrder, flags: POSFeatureFlagProviding) -> Bool { - guard order.status == .completed else { return false } - switch self { - case .issueRefund: - return flags.isFeatureFlagEnabled(.pointOfSaleRefundsi1) - case .emailReceipt: - return true - } + var priority: Int { + switch self { + case .issueRefund: 100 + case .emailReceipt: 50 } } - func handler(for action: POSAction) -> @MainActor () -> Void { - switch action { - case .emailReceipt: - return { - analytics.track(event: WooAnalyticsEvent.PointOfSale.orderDetailsEmailReceiptTapped()) - isShowingEmailReceiptView = true - } + func isAvailable(for order: POSOrder, flags: POSFeatureFlagProviding) -> Bool { + guard order.status == .completed else { return false } + switch self { case .issueRefund: - return { } + return flags.isFeatureFlagEnabled(.pointOfSaleRefundsi1) + case .emailReceipt: + return true } } + } - var availableActions: [POSAction] { - POSAction.allCases - .filter { $0.isAvailable(for: order, flags: featureFlags) } - .sorted { $0.priority > $1.priority } + func handler(for action: POSAction) -> @MainActor () -> Void { + switch action { + case .emailReceipt: + return { + analytics.track(event: WooAnalyticsEvent.PointOfSale.orderDetailsEmailReceiptTapped()) + isShowingEmailReceiptView = true + } + case .issueRefund: + return { } } + } + + var availableActions: [POSAction] { + POSAction.allCases + .filter { $0.isAvailable(for: order, flags: featureFlags) } + .sorted { $0.priority > $1.priority } + } @ViewBuilder func actionsSection(actions: [POSAction]) -> some View {