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
14 changes: 14 additions & 0 deletions Modules/Sources/PointOfSale/Models/PointOfSaleErrorState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import enum Alamofire.AFError
struct PointOfSaleErrorState: Equatable {
enum ErrorType: Equatable {
case initialCatalogSyncError
case refreshCatalogSyncError
case productsLoadError
case variationsLoadError
case productsNextPageError
Expand Down Expand Up @@ -117,6 +118,14 @@ struct PointOfSaleErrorState: Equatable {
buttonText: Constants.retryButtonTitle)
}

static func errorOnRefreshingCatalog(error: Error? = nil) -> Self {
PointOfSaleErrorState(
errorType: .refreshCatalogSyncError,
title: Constants.failedToRefreshCatalogTitle,
subtitle: subtitle(for: error),
buttonText: Constants.retryButtonTitle)
}

private static func subtitle(for error: Error?) -> String {
if let error, error.isConnectivityError {
return Constants.connectivityErrorSubtitle
Expand All @@ -141,6 +150,11 @@ struct PointOfSaleErrorState: Equatable {
value: "Unable to sync catalog",
comment: "Title appearing on the item list screen when there's an error syncing the catalog for the first time."
)
static let failedToRefreshCatalogTitle = NSLocalizedString(
"pos.catalog.refreshFailedTitle",
value: "Unable to refresh catalog",
comment: "Title appearing in a modal when there's an error refreshing the catalog."
)
static let loadingCouponsErrorTitle = NSLocalizedString(
"pos.itemList.loadingCouponsErrorTitle.2",
value: "Unable to load coupons",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

enum PointOfSaleEmptyErrorStateViewLayout {
static let imageAndTextSpacing: CGFloat = POSSpacing.medium
static let textAndButtonSpacing: CGFloat = POSSpacing.large
static let textAndButtonSpacing: CGFloat = POSSpacing.xxLarge
static let textSpacing: CGFloat = POSSpacing.small
static let buttonSpacing: CGFloat = POSSpacing.medium
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ struct PointOfSaleDashboardView: View {
.frame(maxWidth: .infinity)
case .error(let error):
PointOfSaleItemListFullscreenErrorView(error: error, onAction: {
if error.errorType == .initialCatalogSyncError {
analytics.track(event: WooAnalyticsEvent.LocalCatalog.splashScreenRetryTapped())
}

Task {
switch viewStateCoordinator.selectedItemListType {
case .products(search: false):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import SwiftUI
import WooFoundation

struct POSErrorView: View {
@Environment(\.keyboardObserver) private var keyboard

let viewModel: POSErrorViewModel
@Binding var buttonWidth: CGFloat?

init(viewModel: POSErrorViewModel, buttonWidth: Binding<CGFloat?>? = nil) {
self.viewModel = viewModel
if let buttonWidth {
self._buttonWidth = buttonWidth
} else {
self._buttonWidth = Binding<CGFloat?>(
get: { nil },
set: { _ in })
}
}

var body: some View {
VStack(alignment: .center, spacing: POSSpacing.none) {
if !keyboard.isFullSizeKeyboardVisible {
if let image = viewModel.imageAsset {
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 88, height: 88)
.foregroundColor(.posOnSurfaceVariantHighest)
} else {
POSErrorXMark(size: .large)
}
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.imageAndTextSpacing)
}

Text(viewModel.title)
.accessibilityAddTraits(.isHeader)
.foregroundStyle(Color.posOnSurface)
.font(.posHeadingBold)

Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textSpacing)

Text(viewModel.subtitle)
.foregroundStyle(Color.posOnSurface)
.font(.posBodyLargeRegular())
.padding([.leading, .trailing])

if viewModel.primaryButton != nil || viewModel.secondaryButton != nil {
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textAndButtonSpacing)
VStack(spacing: POSSpacing.medium) {
if let primaryButtonViewModel = viewModel.primaryButton {
POSErrorButton(viewModel: primaryButtonViewModel)
}

if let secondaryButtonViewModel = viewModel.secondaryButton {
POSErrorButton(viewModel: secondaryButtonViewModel)
}
}
.frame(width: buttonWidth)
}
}
.multilineTextAlignment(.center)
.dynamicTypeSize(..<DynamicTypeSize.accessibility2)
}
}

struct POSErrorButton: View {
let viewModel: POSErrorButtonViewModel

var body: some View {
Button(action: {
viewModel.action()
}, label: {
Text(viewModel.title)
.dynamicTypeSize(..<DynamicTypeSize.accessibility2)
})
.buttonStyle(viewModel.buttonStyle)
}
}

struct POSErrorViewModel {
let title: String
let subtitle: String
let imageAsset: Image?
let primaryButton: POSErrorButtonViewModel?
let secondaryButton: POSErrorButtonViewModel?

init(error: PointOfSaleErrorState,
primaryButton: POSErrorButtonViewModel? = nil,
secondaryButton: POSErrorButtonViewModel? = nil) {
self.title = error.title
self.subtitle = error.subtitle
self.primaryButton = primaryButton
self.secondaryButton = secondaryButton
switch error.errorType {
case .couponsDisabled:
self.imageAsset = SharedImageAsset.coupons.decorativeImage
default:
self.imageAsset = nil
}
}
}

struct POSErrorButtonViewModel {
let title: String
let buttonStyle: AnyButtonStyle
let action: () -> Void

init(title: String, buttonStyle: any ButtonStyle, action: @escaping () -> Void) {
self.title = title
self.buttonStyle = AnyButtonStyle(buttonStyle)
self.action = action
}
}

struct AnyButtonStyle: ButtonStyle {
private let _makeBody: (Configuration) -> AnyView

init<S: ButtonStyle>(_ style: S) {
_makeBody = { configuration in
AnyView(style.makeBody(configuration: configuration))
}
}

func makeBody(configuration: Configuration) -> some View {
_makeBody(configuration)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,85 +8,38 @@ struct POSListErrorView: View {
@Environment(\.posAnalytics) private var analytics

private let error: PointOfSaleErrorState
private let viewModel: POSListErrorViewModel
private let onAction: (() -> Void)?
private let onExit: (() -> Void)?

@State private var viewWidth: CGFloat = 0
@State private var buttonWidth: CGFloat? = nil
private let viewModel: POSErrorViewModel

@Environment(\.keyboardObserver) private var keyboard

init(error: PointOfSaleErrorState, onAction: (() -> Void)? = nil, onExit: (() -> Void)? = nil) {
self.error = error
self.viewModel = POSListErrorViewModel(error: error)
self.onAction = onAction
self.onExit = onExit
let actionButton: POSErrorButtonViewModel? = {
guard let onAction else { return nil }
return POSErrorButtonViewModel(title: error.buttonText,
buttonStyle: (POSFilledButtonStyle(size: .normal)),
action: onAction)
}()
let exitButton: POSErrorButtonViewModel? = {
guard let onExit else { return nil }
return POSErrorButtonViewModel(title: Localization.exitButtonText,
buttonStyle: POSOutlinedButtonStyle(size: .normal),
action: onExit)
}()

self.viewModel = POSErrorViewModel(error: error, primaryButton: actionButton, secondaryButton: exitButton)
}

var body: some View {
ScrollableVStack {
Spacer()
VStack(alignment: .center, spacing: POSSpacing.none) {
if !keyboard.isFullSizeKeyboardVisible {
if let image = viewModel.imageAsset {
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 88, height: 88)
.foregroundColor(.posOnSurfaceVariantHighest)
} else {
POSErrorXMark(size: .large)
}
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.imageAndTextSpacing)
}

Text(viewModel.title)
.accessibilityAddTraits(.isHeader)
.foregroundStyle(Color.posOnSurface)
.multilineTextAlignment(.center)
.font(.posHeadingBold)

Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textSpacing)

Text(viewModel.subtitle)
.foregroundStyle(Color.posOnSurface)
.font(.posBodyLargeRegular())
.multilineTextAlignment(.center)
.padding([.leading, .trailing])

if let onAction {
Spacer().frame(height: PointOfSaleEmptyErrorStateViewLayout.textAndButtonSpacing)
Button(action: {
// Track retry tapped for splash screen errors (initial catalog sync)
if error.errorType == .initialCatalogSyncError {
analytics.track(event: WooAnalyticsEvent.LocalCatalog.splashScreenRetryTapped())
}
onAction()
}, label: {
Text(viewModel.buttonText)
})
.buttonStyle(POSFilledButtonStyle(size: .normal))
.frame(width: viewWidth / 2)
.padding([.leading, .trailing])
}

if let onExit {
Spacer().frame(height: POSSpacing.medium)
Button(action: {
onExit()
}, label: {
Text(Localization.exitButtonText)
})
.buttonStyle(POSOutlinedButtonStyle(size: .normal))
.frame(width: viewWidth / 2)
.padding([.leading, .trailing])
}
}
POSErrorView(viewModel: viewModel, buttonWidth: $buttonWidth)
Spacer()
}
.padding(.bottom, !keyboard.isFullSizeKeyboardVisible ? floatingControlAreaSize.height : 0)
.measureWidth { width in
viewWidth = width
buttonWidth = width / 2
}
.onAppear {
// Track error shown for splash screen errors (initial catalog sync)
Expand All @@ -97,25 +50,6 @@ struct POSListErrorView: View {
}
}

struct POSListErrorViewModel {
let title: String
let subtitle: String
let buttonText: String
let imageAsset: Image?

init(error: PointOfSaleErrorState) {
self.title = error.title
self.subtitle = error.subtitle
self.buttonText = error.buttonText
switch error.errorType {
case .couponsDisabled:
self.imageAsset = SharedImageAsset.coupons.decorativeImage
default:
self.imageAsset = nil
}
}
}

private enum Localization {
static let exitButtonText = NSLocalizedString(
"pos.listError.exitButton",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,37 @@ extension View {
action: @escaping (() -> Void),
accessibilityLabel: String = POSModalCloseButton.Localization.defaultAccessibilityLabel) -> some View {
self.modifier(
POSModalCloseButton(
POSModalCloseButtonModifier(
closeAction: action,
accessibilityLabel: accessibilityLabel)
)
}
}

struct POSModalCloseButton: ViewModifier {
struct POSModalCloseButton: View {
let accessibilityLabel: String
let closeAction: () -> Void

var body: some View {
HStack {
Spacer()
Button(action: closeAction, label: {
Text(Image(systemName: "xmark"))
.font(.posButtonSymbolMedium)
})
.foregroundColor(Color.posOnSurface)
.accessibilityLabel(accessibilityLabel)
}
}
}

struct POSModalCloseButtonModifier: ViewModifier {
let closeAction: () -> Void
let accessibilityLabel: String

func body(content: Content) -> some View {
VStack(spacing: 0) {
HStack {
Spacer()
Button(action: closeAction, label: {
Text(Image(systemName: "xmark"))
.font(.posButtonSymbolMedium)
})
.foregroundColor(Color.posOnSurface)
.accessibilityLabel(accessibilityLabel)
}
POSModalCloseButton(accessibilityLabel: accessibilityLabel, closeAction: closeAction)

Spacer()

Expand Down
Loading