Skip to content

Commit 9d56402

Browse files
authored
[Woo POS] Coupons: Show an error and CTA to proceed without Coupons - Dummy UI (#15420)
2 parents c5c3fce + 6376ff7 commit 9d56402

File tree

14 files changed

+319
-67
lines changed

14 files changed

+319
-67
lines changed

WooCommerce/Classes/POS/Controllers/PointOfSaleOrderController.swift

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import struct Yosemite.PaymentGateway
88
import struct Yosemite.POSCart
99
import struct Yosemite.POSCartItem
1010
import struct Yosemite.POSCoupon
11+
import struct Yosemite.CouponsError
1112
import enum Yosemite.OrderAction
1213
import enum Yosemite.OrderUpdateField
1314
import class WooFoundation.CurrencyFormatter
@@ -102,13 +103,19 @@ protocol PointOfSaleOrderControllerProtocol {
102103

103104
private func setOrderStateToError(_ error: Error,
104105
retryHandler: @escaping () async -> Void) {
105-
// Consider removing error or handle specific errors with our own formatting and localization
106-
orderState = .error(.init(message: error.localizedDescription,
107-
handler: {
108-
Task {
109-
await retryHandler()
110-
}
111-
}))
106+
if let couponsError = CouponsError(underlyingError: error) {
107+
orderState = .error(.invalidCoupon(couponsError.message), {
108+
Task {
109+
await retryHandler()
110+
}
111+
})
112+
} else {
113+
orderState = .error(.other(error.localizedDescription), {
114+
Task {
115+
await retryHandler()
116+
}
117+
})
118+
}
112119
}
113120

114121
func sendReceipt(recipientEmail: String) async throws {
@@ -192,7 +199,7 @@ enum PointOfSaleInternalOrderState {
192199
case idle
193200
case syncing
194201
case loaded(PointOfSaleOrderTotals, Order)
195-
case error(PointOfSaleOrderSyncErrorMessageViewModel)
202+
case error(PointOfSaleOrderState.OrderStateError, PointOfSaleOrderState.OrderStateRetryHandler)
196203

197204
var isSyncing: Bool {
198205
switch self {
@@ -207,8 +214,8 @@ enum PointOfSaleInternalOrderState {
207214
switch self {
208215
case .idle:
209216
return .idle
210-
case .error(let error):
211-
return .error(error)
217+
case .error(let error, let handler):
218+
return .error(error, handler)
212219
case .loaded(let totals, _):
213220
return .loaded(totals)
214221
case .syncing:
@@ -222,9 +229,8 @@ extension PointOfSaleInternalOrderState: Equatable {
222229
switch (lhs, rhs) {
223230
case (.idle, .idle):
224231
return true
225-
case (.error(let lhsError), .error(let rhsError)):
226-
return lhsError.title == rhsError.title &&
227-
lhsError.message == rhsError.message
232+
case (.error(let lhsError, _), .error(let rhsError, _)):
233+
return lhsError == rhsError
228234
case (.syncing, .syncing):
229235
return true
230236
case (.loaded(let lhsTotals, let lhsOrder), .loaded(let rhsTotals, let rhsOrder)):

WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ protocol PointOfSaleAggregateModelProtocol {
3333
func remove(cartItem: CartItem)
3434
func remove(cartCouponItem: CartCouponItem)
3535
func removeAllItemsFromCart()
36+
func removeAllCouponsFromCart()
3637
func addMoreToCart()
3738
func startNewCart()
3839

@@ -139,6 +140,10 @@ extension PointOfSaleAggregateModel {
139140
cart.removeAll()
140141
}
141142

143+
func removeAllCouponsFromCart() {
144+
cart.coupons.removeAll()
145+
}
146+
142147
func addMoreToCart() {
143148
setStateForEditing()
144149
}

WooCommerce/Classes/POS/Models/PointOfSaleOrderState.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,25 @@ enum PointOfSaleOrderState: Equatable {
44
case idle
55
case syncing
66
case loaded(PointOfSaleOrderTotals)
7-
case error(PointOfSaleOrderSyncErrorMessageViewModel)
7+
case error(OrderStateError, OrderStateRetryHandler)
8+
9+
typealias OrderStateRetryHandler = () -> Void
10+
11+
enum OrderStateError: Equatable {
12+
case other(String)
13+
case invalidCoupon(String)
14+
15+
static func == (lhs: OrderStateError, rhs: OrderStateError) -> Bool {
16+
switch (lhs, rhs) {
17+
case (.other(let lhsError), .other(let rhsError)):
18+
return lhsError == rhsError
19+
case (.invalidCoupon(let lhsCoupon), .invalidCoupon(let rhsCoupon)):
20+
return lhsCoupon == rhsCoupon
21+
default:
22+
return false
23+
}
24+
}
25+
}
826

927
static func == (lhs: PointOfSaleOrderState, rhs: PointOfSaleOrderState) -> Bool {
1028
switch (lhs, rhs) {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import SwiftUI
2+
3+
@available(iOS 17.0, *)
4+
struct PointOfSaleOrderSyncCouponsErrorMessageView: View {
5+
let message: String
6+
let retryHandler: () -> Void
7+
8+
@Environment(PointOfSaleAggregateModel.self) private var posModel
9+
@Environment(\.dynamicTypeSize) var dynamicTypeSize
10+
11+
private var attributedMessage: AttributedString {
12+
if let data = message.data(using: .utf8),
13+
let nsAttributedString = try? NSAttributedString(
14+
data: data,
15+
options: [.documentType: NSAttributedString.DocumentType.html],
16+
documentAttributes: nil) {
17+
var attributedString = AttributedString(nsAttributedString)
18+
attributedString.font = POSFontStyle.posBodyLargeRegular().font()
19+
attributedString.foregroundColor = UIColor(Color.posOnSurface)
20+
return attributedString
21+
}
22+
return AttributedString(message)
23+
}
24+
25+
var body: some View {
26+
HStack(alignment: .center) {
27+
Spacer()
28+
VStack(alignment: .center, spacing: POSSpacing.none) {
29+
Spacer()
30+
POSErrorExclamationMark(size: .large)
31+
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.imageAndTextSpacing)
32+
VStack(alignment: .center, spacing: PointOfSaleCardPresentPaymentLayout.textSpacing) {
33+
Text("Invalid coupons")
34+
.foregroundStyle(Color.posOnSurface)
35+
.font(.posHeadingBold)
36+
37+
Text(attributedMessage)
38+
.padding([.leading, .trailing])
39+
}
40+
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.textAndButtonSpacing)
41+
Button("Continue without coupons", action: {
42+
posModel.removeAllCouponsFromCart()
43+
retryHandler()
44+
})
45+
.buttonStyle(POSFilledButtonStyle(size: .normal))
46+
.padding([.leading, .trailing], Constants.buttonSidePadding)
47+
.padding([.bottom], Constants.buttonBottomPadding)
48+
49+
Spacer()
50+
}
51+
.multilineTextAlignment(.center)
52+
Spacer()
53+
}
54+
}
55+
}
56+
57+
@available(iOS 17.0, *)
58+
private extension PointOfSaleOrderSyncCouponsErrorMessageView {
59+
enum Constants {
60+
static let headerSpacing: CGFloat = POSSpacing.large
61+
static let textSpacing: CGFloat = POSSpacing.medium
62+
static let buttonSidePadding: CGFloat = POSPadding.xxLarge
63+
static let buttonBottomPadding: CGFloat = POSPadding.medium
64+
}
65+
}
66+
67+
// MARK: - TODO https://github.com/woocommerce/woocommerce-ios/issues/15424
68+
//
69+
//private extension PointOfSaleOrderSyncErrorMessageView {
70+
// enum Localization {
71+
// static let title = NSLocalizedString(
72+
// "pointOfSale.orderSync.couponsError.title",
73+
// value: "Invalid coupons",
74+
// comment: "Title of the error when failing to validate coupons and calculate order totals"
75+
// )
76+
//
77+
// static let actionTitle = NSLocalizedString(
78+
// "pointOfSale.orderSync.couponsError.proceed",
79+
// value: "Continue without coupons",
80+
// comment: "Button title to remove coupons and retry synchronizing order and calculating order totals"
81+
// )
82+
// }
83+
//}
84+
85+
#Preview {
86+
if #available(iOS 17.0, *) {
87+
PointOfSaleOrderSyncCouponsErrorMessageView(message: "An error happened!") {}
88+
}
89+
}

WooCommerce/Classes/POS/Presentation/Order Messages/PointOfSaleOrderSyncErrorMessageView.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import SwiftUI
22

33
struct PointOfSaleOrderSyncErrorMessageView: View {
4-
let viewModel: PointOfSaleOrderSyncErrorMessageViewModel
4+
let message: String
5+
let retryHandler: () -> Void
56

67
var body: some View {
78
HStack(alignment: .center) {
@@ -11,17 +12,17 @@ struct PointOfSaleOrderSyncErrorMessageView: View {
1112
POSErrorExclamationMark(size: .large)
1213
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.imageAndTextSpacing)
1314
VStack(alignment: .center, spacing: PointOfSaleCardPresentPaymentLayout.textSpacing) {
14-
Text(viewModel.title)
15+
Text(Localization.title)
1516
.foregroundStyle(Color.posOnSurface)
1617
.font(.posHeadingBold)
1718

18-
Text(viewModel.message)
19+
Text(message)
1920
.foregroundStyle(Color.posOnSurface)
2021
.font(.posBodyLargeRegular())
2122
.padding([.leading, .trailing])
2223
}
2324
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.textAndButtonSpacing)
24-
Button(viewModel.actionModel.title, action: viewModel.actionModel.handler)
25+
Button(Localization.actionTitle, action: retryHandler)
2526
.buttonStyle(POSFilledButtonStyle(size: .normal))
2627
.padding([.leading, .trailing], Constants.buttonSidePadding)
2728
.padding([.bottom], Constants.buttonBottomPadding)
@@ -42,7 +43,22 @@ private extension PointOfSaleOrderSyncErrorMessageView {
4243
}
4344
}
4445

46+
private extension PointOfSaleOrderSyncErrorMessageView {
47+
enum Localization {
48+
static let title = NSLocalizedString(
49+
"pointOfSale.orderSync.error.title",
50+
value: "Couldn't load totals",
51+
comment: "Title of the error when failing to synchronize order and calculate order totals"
52+
)
53+
54+
static let actionTitle = NSLocalizedString(
55+
"pointOfSale.orderSync.error.tryAgain",
56+
value: "Try again",
57+
comment: "Button title to retry synchronizing order and calculating order totals"
58+
)
59+
}
60+
}
61+
4562
#Preview {
46-
PointOfSaleOrderSyncErrorMessageView(viewModel: PointOfSaleOrderSyncErrorMessageViewModel(message: "An error happened!",
47-
handler: {}))
63+
PointOfSaleOrderSyncErrorMessageView(message: "An error happened!") {}
4864
}

WooCommerce/Classes/POS/Presentation/Order Messages/PointOfSaleOrderSyncErrorMessageViewModel.swift

Lines changed: 0 additions & 37 deletions
This file was deleted.

WooCommerce/Classes/POS/Presentation/TotalsView.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,11 @@ struct TotalsView: View {
7575
cardReaderConnectionStatus: posModel.cardReaderConnectionStatus))
7676
}
7777
.animation(.default, value: isShowingPaymentView)
78-
case .error(let viewModel):
79-
PointOfSaleOrderSyncErrorMessageView(viewModel: viewModel)
78+
case .error(.other(let message), let handler):
79+
PointOfSaleOrderSyncErrorMessageView(message: message, retryHandler: handler)
80+
.transition(.opacity)
81+
case .error(.invalidCoupon(let message), let handler):
82+
PointOfSaleOrderSyncCouponsErrorMessageView(message: message, retryHandler: handler)
8083
.transition(.opacity)
8184
}
8285
}

WooCommerce/Classes/POS/Utils/POSFontStyle.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ enum POSFontStyle {
1818
case posButtonSymbolMedium
1919
case posButtonSymbolLarge
2020

21-
fileprivate func font(maximumContentSizeCategory: UIContentSizeCategory? = nil) -> Font {
21+
func font(maximumContentSizeCategory: UIContentSizeCategory? = nil) -> Font {
2222
switch self {
2323
case .posHeadingBold:
2424
Font.system(size: scaledValue(FontSize.heading, maximumContentSizeCategory: maximumContentSizeCategory ?? .accessibilityLarge), weight: .bold)

0 commit comments

Comments
 (0)