Skip to content

Commit 27cb6f3

Browse files
committed
Extract POSErrorView for reuse in modals
1 parent 023770e commit 27cb6f3

File tree

3 files changed

+150
-84
lines changed

3 files changed

+150
-84
lines changed

Modules/Sources/PointOfSale/Presentation/PointOfSaleDashboardView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ struct PointOfSaleDashboardView: View {
7373
.frame(maxWidth: .infinity)
7474
case .error(let error):
7575
PointOfSaleItemListFullscreenErrorView(error: error, onAction: {
76+
if error.errorType == .initialCatalogSyncError {
77+
analytics.track(event: WooAnalyticsEvent.LocalCatalog.splashScreenRetryTapped())
78+
}
79+
7680
Task {
7781
switch viewStateCoordinator.selectedItemListType {
7882
case .products(search: false):
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import SwiftUI
2+
import WooFoundation
3+
4+
struct POSErrorView: View {
5+
@Environment(\.keyboardObserver) private var keyboard
6+
7+
let viewModel: POSErrorViewModel
8+
@Binding var buttonWidth: CGFloat?
9+
10+
init(viewModel: POSErrorViewModel, buttonWidth: Binding<CGFloat?>? = nil) {
11+
self.viewModel = viewModel
12+
if let buttonWidth {
13+
self._buttonWidth = buttonWidth
14+
} else {
15+
self._buttonWidth = Binding<CGFloat?>(
16+
get: { nil },
17+
set: { _ in })
18+
}
19+
}
20+
21+
var body: some View {
22+
VStack(alignment: .center, spacing: POSSpacing.none) {
23+
if !keyboard.isFullSizeKeyboardVisible {
24+
if let image = viewModel.imageAsset {
25+
image
26+
.resizable()
27+
.aspectRatio(contentMode: .fit)
28+
.frame(width: 88, height: 88)
29+
.foregroundColor(.posOnSurfaceVariantHighest)
30+
} else {
31+
POSErrorXMark(size: .large)
32+
}
33+
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.imageAndTextSpacing)
34+
}
35+
36+
Text(viewModel.title)
37+
.accessibilityAddTraits(.isHeader)
38+
.foregroundStyle(Color.posOnSurface)
39+
.font(.posHeadingBold)
40+
41+
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textSpacing)
42+
43+
Text(viewModel.subtitle)
44+
.foregroundStyle(Color.posOnSurface)
45+
.font(.posBodyLargeRegular())
46+
.padding([.leading, .trailing])
47+
48+
if viewModel.primaryButton != nil || viewModel.secondaryButton != nil {
49+
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textAndButtonSpacing)
50+
VStack(spacing: POSSpacing.medium) {
51+
if let primaryButtonViewModel = viewModel.primaryButton {
52+
POSErrorButton(viewModel: primaryButtonViewModel)
53+
}
54+
55+
if let secondaryButtonViewModel = viewModel.secondaryButton {
56+
POSErrorButton(viewModel: secondaryButtonViewModel)
57+
}
58+
}
59+
.frame(width: buttonWidth)
60+
}
61+
}
62+
.multilineTextAlignment(.center)
63+
.dynamicTypeSize(..<DynamicTypeSize.accessibility2)
64+
}
65+
}
66+
67+
struct POSErrorButton: View {
68+
let viewModel: POSErrorButtonViewModel
69+
70+
var body: some View {
71+
Button(action: {
72+
viewModel.action()
73+
}, label: {
74+
Text(viewModel.title)
75+
.dynamicTypeSize(..<DynamicTypeSize.accessibility2)
76+
})
77+
.buttonStyle(viewModel.buttonStyle)
78+
}
79+
}
80+
81+
struct POSErrorViewModel {
82+
let title: String
83+
let subtitle: String
84+
let imageAsset: Image?
85+
let primaryButton: POSErrorButtonViewModel?
86+
let secondaryButton: POSErrorButtonViewModel?
87+
88+
init(error: PointOfSaleErrorState,
89+
primaryButton: POSErrorButtonViewModel? = nil,
90+
secondaryButton: POSErrorButtonViewModel? = nil) {
91+
self.title = error.title
92+
self.subtitle = error.subtitle
93+
self.primaryButton = primaryButton
94+
self.secondaryButton = secondaryButton
95+
switch error.errorType {
96+
case .couponsDisabled:
97+
self.imageAsset = SharedImageAsset.coupons.decorativeImage
98+
default:
99+
self.imageAsset = nil
100+
}
101+
}
102+
}
103+
104+
struct POSErrorButtonViewModel {
105+
let title: String
106+
let buttonStyle: AnyButtonStyle
107+
let action: () -> Void
108+
109+
init(title: String, buttonStyle: any ButtonStyle, action: @escaping () -> Void) {
110+
self.title = title
111+
self.buttonStyle = AnyButtonStyle(buttonStyle)
112+
self.action = action
113+
}
114+
}
115+
116+
struct AnyButtonStyle: ButtonStyle {
117+
private let _makeBody: (Configuration) -> AnyView
118+
119+
init<S: ButtonStyle>(_ style: S) {
120+
_makeBody = { configuration in
121+
AnyView(style.makeBody(configuration: configuration))
122+
}
123+
}
124+
125+
func makeBody(configuration: Configuration) -> some View {
126+
_makeBody(configuration)
127+
}
128+
}

Modules/Sources/PointOfSale/Presentation/Reusable Views/POSListErrorView.swift

Lines changed: 18 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -8,85 +8,38 @@ struct POSListErrorView: View {
88
@Environment(\.posAnalytics) private var analytics
99

1010
private let error: PointOfSaleErrorState
11-
private let viewModel: POSListErrorViewModel
12-
private let onAction: (() -> Void)?
13-
private let onExit: (() -> Void)?
14-
15-
@State private var viewWidth: CGFloat = 0
11+
@State private var buttonWidth: CGFloat? = nil
12+
private let viewModel: POSErrorViewModel
1613

1714
@Environment(\.keyboardObserver) private var keyboard
1815

1916
init(error: PointOfSaleErrorState, onAction: (() -> Void)? = nil, onExit: (() -> Void)? = nil) {
2017
self.error = error
21-
self.viewModel = POSListErrorViewModel(error: error)
22-
self.onAction = onAction
23-
self.onExit = onExit
18+
let actionButton: POSErrorButtonViewModel? = {
19+
guard let onAction else { return nil }
20+
return POSErrorButtonViewModel(title: error.buttonText,
21+
buttonStyle: (POSFilledButtonStyle(size: .normal)),
22+
action: onAction)
23+
}()
24+
let exitButton: POSErrorButtonViewModel? = {
25+
guard let onExit else { return nil }
26+
return POSErrorButtonViewModel(title: Localization.exitButtonText,
27+
buttonStyle: POSOutlinedButtonStyle(size: .normal),
28+
action: onExit)
29+
}()
30+
31+
self.viewModel = POSErrorViewModel(error: error, primaryButton: actionButton, secondaryButton: exitButton)
2432
}
2533

2634
var body: some View {
2735
ScrollableVStack {
2836
Spacer()
29-
VStack(alignment: .center, spacing: POSSpacing.none) {
30-
if !keyboard.isFullSizeKeyboardVisible {
31-
if let image = viewModel.imageAsset {
32-
image
33-
.resizable()
34-
.aspectRatio(contentMode: .fit)
35-
.frame(width: 88, height: 88)
36-
.foregroundColor(.posOnSurfaceVariantHighest)
37-
} else {
38-
POSErrorXMark(size: .large)
39-
}
40-
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.imageAndTextSpacing)
41-
}
42-
43-
Text(viewModel.title)
44-
.accessibilityAddTraits(.isHeader)
45-
.foregroundStyle(Color.posOnSurface)
46-
.multilineTextAlignment(.center)
47-
.font(.posHeadingBold)
48-
49-
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textSpacing)
50-
51-
Text(viewModel.subtitle)
52-
.foregroundStyle(Color.posOnSurface)
53-
.font(.posBodyLargeRegular())
54-
.multilineTextAlignment(.center)
55-
.padding([.leading, .trailing])
56-
57-
if let onAction {
58-
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textAndButtonSpacing)
59-
Button(action: {
60-
// Track retry tapped for splash screen errors (initial catalog sync)
61-
if error.errorType == .initialCatalogSyncError {
62-
analytics.track(event: WooAnalyticsEvent.LocalCatalog.splashScreenRetryTapped())
63-
}
64-
onAction()
65-
}, label: {
66-
Text(viewModel.buttonText)
67-
})
68-
.buttonStyle(POSFilledButtonStyle(size: .normal))
69-
.frame(width: viewWidth / 2)
70-
.padding([.leading, .trailing])
71-
}
72-
73-
if let onExit {
74-
Spacer().frame(height: POSSpacing.medium)
75-
Button(action: {
76-
onExit()
77-
}, label: {
78-
Text(Localization.exitButtonText)
79-
})
80-
.buttonStyle(POSOutlinedButtonStyle(size: .normal))
81-
.frame(width: viewWidth / 2)
82-
.padding([.leading, .trailing])
83-
}
84-
}
37+
POSErrorView(viewModel: viewModel, buttonWidth: $buttonWidth)
8538
Spacer()
8639
}
8740
.padding(.bottom, !keyboard.isFullSizeKeyboardVisible ? floatingControlAreaSize.height : 0)
8841
.measureWidth { width in
89-
viewWidth = width
42+
buttonWidth = width / 2
9043
}
9144
.onAppear {
9245
// Track error shown for splash screen errors (initial catalog sync)
@@ -97,25 +50,6 @@ struct POSListErrorView: View {
9750
}
9851
}
9952

100-
struct POSListErrorViewModel {
101-
let title: String
102-
let subtitle: String
103-
let buttonText: String
104-
let imageAsset: Image?
105-
106-
init(error: PointOfSaleErrorState) {
107-
self.title = error.title
108-
self.subtitle = error.subtitle
109-
self.buttonText = error.buttonText
110-
switch error.errorType {
111-
case .couponsDisabled:
112-
self.imageAsset = SharedImageAsset.coupons.decorativeImage
113-
default:
114-
self.imageAsset = nil
115-
}
116-
}
117-
}
118-
11953
private enum Localization {
12054
static let exitButtonText = NSLocalizedString(
12155
"pos.listError.exitButton",

0 commit comments

Comments
 (0)