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 @@ -8,6 +8,7 @@ import struct Yosemite.PaymentGateway
import struct Yosemite.POSCart
import struct Yosemite.POSCartItem
import struct Yosemite.POSCoupon
import struct Yosemite.CouponsError
import enum Yosemite.OrderAction
import enum Yosemite.OrderUpdateField
import class WooFoundation.CurrencyFormatter
Expand Down Expand Up @@ -102,13 +103,19 @@ protocol PointOfSaleOrderControllerProtocol {

private func setOrderStateToError(_ error: Error,
retryHandler: @escaping () async -> Void) {
// Consider removing error or handle specific errors with our own formatting and localization
orderState = .error(.init(message: error.localizedDescription,
handler: {
Task {
await retryHandler()
}
}))
if let couponsError = CouponsError(underlyingError: error) {
orderState = .error(.invalidCoupon(couponsError.message), {
Task {
await retryHandler()
}
})
} else {
orderState = .error(.other(error.localizedDescription), {
Task {
await retryHandler()
}
})
}
}

func sendReceipt(recipientEmail: String) async throws {
Expand Down Expand Up @@ -192,7 +199,7 @@ enum PointOfSaleInternalOrderState {
case idle
case syncing
case loaded(PointOfSaleOrderTotals, Order)
case error(PointOfSaleOrderSyncErrorMessageViewModel)
case error(PointOfSaleOrderState.OrderStateError, PointOfSaleOrderState.OrderStateRetryHandler)

var isSyncing: Bool {
switch self {
Expand All @@ -207,8 +214,8 @@ enum PointOfSaleInternalOrderState {
switch self {
case .idle:
return .idle
case .error(let error):
return .error(error)
case .error(let error, let handler):
return .error(error, handler)
case .loaded(let totals, _):
return .loaded(totals)
case .syncing:
Expand All @@ -222,9 +229,8 @@ extension PointOfSaleInternalOrderState: Equatable {
switch (lhs, rhs) {
case (.idle, .idle):
return true
case (.error(let lhsError), .error(let rhsError)):
return lhsError.title == rhsError.title &&
lhsError.message == rhsError.message
case (.error(let lhsError, _), .error(let rhsError, _)):
return lhsError == rhsError
case (.syncing, .syncing):
return true
case (.loaded(let lhsTotals, let lhsOrder), .loaded(let rhsTotals, let rhsOrder)):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ protocol PointOfSaleAggregateModelProtocol {
func remove(cartItem: CartItem)
func remove(cartCouponItem: CartCouponItem)
func removeAllItemsFromCart()
func removeAllCouponsFromCart()
func addMoreToCart()
func startNewCart()

Expand Down Expand Up @@ -128,6 +129,10 @@ extension PointOfSaleAggregateModel {
cart.removeAll()
}

func removeAllCouponsFromCart() {
cart.coupons.removeAll()
}

func addMoreToCart() {
setStateForEditing()
}
Expand Down
20 changes: 19 additions & 1 deletion WooCommerce/Classes/POS/Models/PointOfSaleOrderState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,25 @@ enum PointOfSaleOrderState: Equatable {
case idle
case syncing
case loaded(PointOfSaleOrderTotals)
case error(PointOfSaleOrderSyncErrorMessageViewModel)
case error(OrderStateError, OrderStateRetryHandler)

typealias OrderStateRetryHandler = () -> Void

enum OrderStateError: Equatable {
case other(String)
case invalidCoupon(String)

static func == (lhs: OrderStateError, rhs: OrderStateError) -> Bool {
switch (lhs, rhs) {
case (.other(let lhsError), .other(let rhsError)):
return lhsError == rhsError
case (.invalidCoupon(let lhsCoupon), .invalidCoupon(let rhsCoupon)):
return lhsCoupon == rhsCoupon
default:
return false
}
}
}

static func == (lhs: PointOfSaleOrderState, rhs: PointOfSaleOrderState) -> Bool {
switch (lhs, rhs) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import SwiftUI

@available(iOS 17.0, *)
struct PointOfSaleOrderSyncCouponsErrorMessageView: View {
let message: String
let retryHandler: () -> Void

@Environment(PointOfSaleAggregateModel.self) private var posModel
@Environment(\.dynamicTypeSize) var dynamicTypeSize

private var attributedMessage: AttributedString {
if let data = message.data(using: .utf8),
let nsAttributedString = try? NSAttributedString(
data: data,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil) {
var attributedString = AttributedString(nsAttributedString)
Copy link
Contributor

Choose a reason for hiding this comment

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

TIL we can also attempt to parse this to markdown via try AttributedString(markdown: message) (need to import Foundation). Maybe would be cleaner than using NSAttributedString, or we could keep both as a fallback if the initial decoding fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I'll try 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, it doesn't parse through all the HTML tags. I tried different options but it didn't work.

attributedString.font = POSFontStyle.posBodyLargeRegular().font()
attributedString.foregroundColor = UIColor(Color.posOnSurface)
return attributedString
}
return AttributedString(message)
}

var body: some View {
HStack(alignment: .center) {
Spacer()
VStack(alignment: .center, spacing: POSSpacing.none) {
Spacer()
POSErrorExclamationMark(size: .large)
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.imageAndTextSpacing)
VStack(alignment: .center, spacing: PointOfSaleCardPresentPaymentLayout.textSpacing) {
Text("Invalid coupons")
.foregroundStyle(Color.posOnSurface)
.font(.posHeadingBold)

Text(attributedMessage)
.padding([.leading, .trailing])
}
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.textAndButtonSpacing)
Button("Continue without coupons", action: {
posModel.removeAllCouponsFromCart()
retryHandler()
})
.buttonStyle(POSFilledButtonStyle(size: .normal))
.padding([.leading, .trailing], Constants.buttonSidePadding)
.padding([.bottom], Constants.buttonBottomPadding)

Spacer()
}
.multilineTextAlignment(.center)
Spacer()
}
}
}

@available(iOS 17.0, *)
private extension PointOfSaleOrderSyncCouponsErrorMessageView {
enum Constants {
static let headerSpacing: CGFloat = POSSpacing.large
static let textSpacing: CGFloat = POSSpacing.medium
static let buttonSidePadding: CGFloat = POSPadding.xxLarge
static let buttonBottomPadding: CGFloat = POSPadding.medium
}
}

// MARK: - TODO https://github.com/woocommerce/woocommerce-ios/issues/15424
//
//private extension PointOfSaleOrderSyncErrorMessageView {
// enum Localization {
// static let title = NSLocalizedString(
// "pointOfSale.orderSync.couponsError.title",
// value: "Invalid coupons",
// comment: "Title of the error when failing to validate coupons and calculate order totals"
// )
//
// static let actionTitle = NSLocalizedString(
// "pointOfSale.orderSync.couponsError.proceed",
// value: "Continue without coupons",
// comment: "Button title to remove coupons and retry synchronizing order and calculating order totals"
// )
// }
//}

#Preview {
if #available(iOS 17.0, *) {
PointOfSaleOrderSyncCouponsErrorMessageView(message: "An error happened!") {}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import SwiftUI

struct PointOfSaleOrderSyncErrorMessageView: View {
let viewModel: PointOfSaleOrderSyncErrorMessageViewModel
let message: String
let retryHandler: () -> Void

var body: some View {
HStack(alignment: .center) {
Expand All @@ -11,17 +12,17 @@ struct PointOfSaleOrderSyncErrorMessageView: View {
POSErrorExclamationMark(size: .large)
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.imageAndTextSpacing)
VStack(alignment: .center, spacing: PointOfSaleCardPresentPaymentLayout.textSpacing) {
Text(viewModel.title)
Text(Localization.title)
.foregroundStyle(Color.posOnSurface)
.font(.posHeadingBold)

Text(viewModel.message)
Text(message)
.foregroundStyle(Color.posOnSurface)
.font(.posBodyLargeRegular())
.padding([.leading, .trailing])
}
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.textAndButtonSpacing)
Button(viewModel.actionModel.title, action: viewModel.actionModel.handler)
Button(Localization.actionTitle, action: retryHandler)
.buttonStyle(POSFilledButtonStyle(size: .normal))
.padding([.leading, .trailing], Constants.buttonSidePadding)
.padding([.bottom], Constants.buttonBottomPadding)
Expand All @@ -42,7 +43,22 @@ private extension PointOfSaleOrderSyncErrorMessageView {
}
}

private extension PointOfSaleOrderSyncErrorMessageView {
enum Localization {
static let title = NSLocalizedString(
"pointOfSale.orderSync.error.title",
value: "Couldn't load totals",
comment: "Title of the error when failing to synchronize order and calculate order totals"
)

static let actionTitle = NSLocalizedString(
"pointOfSale.orderSync.error.tryAgain",
value: "Try again",
comment: "Button title to retry synchronizing order and calculating order totals"
)
}
}

#Preview {
PointOfSaleOrderSyncErrorMessageView(viewModel: PointOfSaleOrderSyncErrorMessageViewModel(message: "An error happened!",
handler: {}))
PointOfSaleOrderSyncErrorMessageView(message: "An error happened!") {}
}

This file was deleted.

7 changes: 5 additions & 2 deletions WooCommerce/Classes/POS/Presentation/TotalsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,11 @@ struct TotalsView: View {
cardReaderConnectionStatus: posModel.cardReaderConnectionStatus))
}
.animation(.default, value: isShowingPaymentView)
case .error(let viewModel):
PointOfSaleOrderSyncErrorMessageView(viewModel: viewModel)
case .error(.other(let message), let handler):
PointOfSaleOrderSyncErrorMessageView(message: message, retryHandler: handler)
.transition(.opacity)
case .error(.invalidCoupon(let message), let handler):
PointOfSaleOrderSyncCouponsErrorMessageView(message: message, retryHandler: handler)
.transition(.opacity)
}
}
Expand Down
2 changes: 1 addition & 1 deletion WooCommerce/Classes/POS/Utils/POSFontStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ enum POSFontStyle {
case posButtonSymbolMedium
case posButtonSymbolLarge

fileprivate func font(maximumContentSizeCategory: UIContentSizeCategory? = nil) -> Font {
func font(maximumContentSizeCategory: UIContentSizeCategory? = nil) -> Font {
switch self {
case .posHeadingBold:
Font.system(size: scaledValue(FontSize.heading, maximumContentSizeCategory: maximumContentSizeCategory ?? .accessibilityLarge), weight: .bold)
Expand Down
Loading