Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a57403f
Add error type to POS error state
iamgabrielma Apr 3, 2025
9265da8
adapt error view action to accept different callbacks
iamgabrielma Apr 3, 2025
451bc55
enable coupons when tap on cta
iamgabrielma Apr 3, 2025
c1cc174
refactor for simplicity
iamgabrielma Apr 3, 2025
1c84292
make test compile
iamgabrielma Apr 3, 2025
f2df5ac
Merge branch 'trunk' into task/15348-followup-enable-coupon-via-cta
iamgabrielma Apr 4, 2025
009c621
moves enable coupons call to coupons controller
iamgabrielma Apr 4, 2025
a2283f2
move enable coupons to service
iamgabrielma Apr 4, 2025
0ffcc10
add follow-up issue reference
iamgabrielma Apr 4, 2025
1044f68
remove unnecessary import
iamgabrielma Apr 4, 2025
fac5a07
localizations
iamgabrielma Apr 4, 2025
85aef17
make test compile
iamgabrielma Apr 4, 2025
8395fe7
Merge branch 'trunk' into task/15348-followup-enable-coupon-via-cta
iamgabrielma Apr 7, 2025
15b6568
make compile. ammend merge conflict
iamgabrielma Apr 7, 2025
562c1c1
Add a shared current item view state based on item type
iamgabrielma Apr 7, 2025
093fc72
remove unnecessary associated values
iamgabrielma Apr 7, 2025
fb126bc
update enableCoupons signature to be throwable
iamgabrielma Apr 7, 2025
b248310
update issue references
iamgabrielma Apr 8, 2025
e1979c9
revert itemsViewState rename
iamgabrielma Apr 8, 2025
f7db770
remove duplicated conformance
iamgabrielma Apr 8, 2025
0abe0bb
make PointOfSaleCouponsControllerProtocol
iamgabrielma Apr 8, 2025
342f887
update tests with new mock
iamgabrielma Apr 8, 2025
b85c7bc
lint
iamgabrielma Apr 8, 2025
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 @@ -5,7 +5,7 @@ import protocol Yosemite.PointOfSaleItemServiceProtocol
import protocol Yosemite.PointOfSaleCouponServiceProtocol

@available(iOS 17.0, *)
@Observable final class PointOfSaleCouponsController: PointOfSaleItemsControllerProtocol {
@Observable final class PointOfSaleCouponsController: PointOfSaleCouponsControllerProtocol {
var itemsViewState: ItemsViewState = ItemsViewState(containerState: .loading,
itemsStack: ItemsStackState(root: .loading([]),
itemStates: [:]))
Expand Down Expand Up @@ -39,6 +39,19 @@ import protocol Yosemite.PointOfSaleCouponServiceProtocol
// Pagination https://github.com/woocommerce/woocommerce-ios/issues/15343
await loadFirstPage()
}

@MainActor
func enableCoupons() async {
// TODO: WOOMOB-255
// Handle loading state while coupons are being enabled
do {
try await couponProvider.enableCoupons()
} catch {
// TODO: WOOMOB-267
// Handle error when failed to enable, and allow retry action
debugPrint(error)
}
}
}

@available(iOS 17.0, *)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ protocol PointOfSaleItemsControllerProtocol {
func loadNextItems(base: ItemListBaseItem) async
}

@available(iOS 17.0, *)
protocol PointOfSaleCouponsControllerProtocol: PointOfSaleItemsControllerProtocol {
/// Enables coupons in store settings, if needed
func enableCoupons() async
}

@available(iOS 17.0, *)
protocol PointOfSaleSearchingItemsControllerProtocol: PointOfSaleItemsControllerProtocol {
/// Searches for items
Expand Down
28 changes: 26 additions & 2 deletions WooCommerce/Classes/POS/Models/PointOfSaleAggregateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ protocol PointOfSaleAggregateModelProtocol {
func trackCardPaymentsOnboardingShown()

var itemsViewState: ItemsViewState { get }

func loadItems(base: ItemListBaseItem) async
func loadNextItems(base: ItemListBaseItem) async

Expand Down Expand Up @@ -56,14 +57,15 @@ protocol PointOfSaleAggregateModelProtocol {

var itemsViewState: ItemsViewState { itemsController.itemsViewState }
var couponsViewState: ItemsViewState { couponsController.itemsViewState }
var currentViewState: ItemsViewState

private(set) var cart: Cart = .init()

var orderState: PointOfSaleOrderState { orderController.orderState.externalState }
private var internalOrderState: PointOfSaleInternalOrderState { orderController.orderState }

private let itemsController: PointOfSaleItemsControllerProtocol
private let couponsController: PointOfSaleItemsControllerProtocol
private let couponsController: PointOfSaleCouponsControllerProtocol

private let cardPresentPaymentService: CardPresentPaymentFacade
private let orderController: PointOfSaleOrderControllerProtocol
Expand All @@ -76,7 +78,7 @@ protocol PointOfSaleAggregateModelProtocol {
private var cancellables: Set<AnyCancellable> = []

init(itemsController: PointOfSaleItemsControllerProtocol,
couponsController: PointOfSaleItemsControllerProtocol,
couponsController: PointOfSaleCouponsControllerProtocol,
cardPresentPaymentService: CardPresentPaymentFacade,
orderController: PointOfSaleOrderControllerProtocol,
analytics: Analytics = ServiceLocator.analytics,
Expand All @@ -89,6 +91,9 @@ protocol PointOfSaleAggregateModelProtocol {
self.analytics = analytics
self.collectOrderPaymentAnalyticsTracker = collectOrderPaymentAnalyticsTracker
self.paymentState = paymentState
// Initial, set to items (products)
self.currentViewState = itemsController.itemsViewState

publishCardReaderConnectionStatus()
publishPaymentMessages()
setupReaderReconnectionObservation()
Expand All @@ -98,22 +103,33 @@ protocol PointOfSaleAggregateModelProtocol {
// MARK: - ItemList
@available(iOS 17.0, *)
extension PointOfSaleAggregateModel {
func updateCurrentViewState(base: ItemListBaseItem) {
let viewState = base.itemType == .products ? itemsViewState : couponsViewState
currentViewState = viewState
}

@MainActor
func loadItems(base: ItemListBaseItem) async {
let controller = base.itemType == .products ? itemsController : couponsController

await controller.loadItems(base: base)
updateCurrentViewState(base: base)
}

@MainActor
func refreshItems(base: ItemListBaseItem) async {
let controller = base.itemType == .products ? itemsController : couponsController

await controller.refreshItems(base: base)
updateCurrentViewState(base: base)
}

@MainActor
func loadNextItems(base: ItemListBaseItem) async {
let controller = base.itemType == .products ? itemsController : couponsController

await controller.loadNextItems(base: base)
updateCurrentViewState(base: base)
}
}

Expand Down Expand Up @@ -157,6 +173,14 @@ extension PointOfSaleAggregateModel {
}
}

// MARK: - Coupons
@available(iOS 17.0, *)
extension PointOfSaleAggregateModel {
func enableCoupons() async {
await couponsController.enableCoupons()
}
}

// MARK: - Track events
@available(iOS 17.0, *)
private extension PointOfSaleAggregateModel {
Expand Down
93 changes: 76 additions & 17 deletions WooCommerce/Classes/POS/Models/PointOfSaleErrorState.swift
Original file line number Diff line number Diff line change
@@ -1,49 +1,108 @@
import Foundation

struct PointOfSaleErrorState: Equatable {
enum ErrorType: Equatable {
case productsLoadError
case variationsLoadError
case productsNextPageError
case variationsNextPageError
case couponsNotFound
case couponsLoadError
case couponsDisabled
}

let errorType: ErrorType
let title: String
let subtitle: String
let buttonText: String

static func errorOnLoadingProducts() -> Self {
PointOfSaleErrorState(title: Constants.failedToLoadProductsTitle,
subtitle: Constants.failedToLoadProductsSubtitle,
buttonText: Constants.failedToLoadProductsButtonTitle)
PointOfSaleErrorState(
errorType: .productsLoadError,
title: Constants.failedToLoadProductsTitle,
subtitle: Constants.failedToLoadProductsSubtitle,
buttonText: Constants.failedToLoadProductsButtonTitle)
}

static func errorOnLoadingVariations() -> Self {
PointOfSaleErrorState(title: Constants.failedToLoadVariationsTitle,
subtitle: Constants.failedToLoadVariationsSubtitle,
buttonText: Constants.failedToLoadVariationsButtonTitle)
PointOfSaleErrorState(
errorType: .variationsLoadError,
title: Constants.failedToLoadVariationsTitle,
subtitle: Constants.failedToLoadVariationsSubtitle,
buttonText: Constants.failedToLoadVariationsButtonTitle)
}

static func errorOnLoadingProductsNextPage() -> Self {
PointOfSaleErrorState(title: Constants.failedToLoadProductsNextPageTitle,
subtitle: Constants.failedToLoadProductsNextPageSubtitle,
buttonText: Constants.failedToLoadProductsNextPageButtonTitle)
PointOfSaleErrorState(
errorType: .productsNextPageError,
title: Constants.failedToLoadProductsNextPageTitle,
subtitle: Constants.failedToLoadProductsNextPageSubtitle,
buttonText: Constants.failedToLoadProductsNextPageButtonTitle)
}

static func errorOnLoadingVariationsNextPage() -> Self {
PointOfSaleErrorState(title: Constants.failedToLoadVariationsNextPageTitle,
subtitle: Constants.failedToLoadVariationsNextPageSubtitle,
buttonText: Constants.failedToLoadVariationsNextPageButtonTitle)
PointOfSaleErrorState(
errorType: .variationsNextPageError,
title: Constants.failedToLoadVariationsNextPageTitle,
subtitle: Constants.failedToLoadVariationsNextPageSubtitle,
buttonText: Constants.failedToLoadVariationsNextPageButtonTitle)
}

static func errorCouponsNotFound() -> Self {
PointOfSaleErrorState(title: Constants.noCouponsFoundTitle,
subtitle: Constants.noCouponsFoundSubtitle,
buttonText: Constants.noCouponsFoundButtonTitle)
PointOfSaleErrorState(
errorType: .couponsNotFound,
title: Constants.noCouponsFoundTitle,
subtitle: Constants.noCouponsFoundSubtitle,
buttonText: Constants.noCouponsFoundButtonTitle)
}

static func errorOnLoadingCoupons() -> Self {
PointOfSaleErrorState(title: "Error loading coupons", subtitle: "Error loading coupons", buttonText: "Retry")
PointOfSaleErrorState(
errorType: .couponsLoadError,
title: Constants.loadingCouponsErrorTitle,
subtitle: Constants.loadingCouponsErrorSubtitle,
buttonText: Constants.loadingCouponsErrorRetry)
}

static func errorCouponsDisabled() -> Self {
PointOfSaleErrorState(title: "Error loading coupons", subtitle: "Please enable coupons in WooCommerce Settings, and tap Retry", buttonText: "Retry")
PointOfSaleErrorState(
errorType: .couponsDisabled,
title: Constants.loadingCouponsDisabledTitle,
subtitle: Constants.loadingCouponsDisabledSubtitle,
buttonText: Constants.loadingCouponsDisabledAction)
}

enum Constants {
static let loadingCouponsErrorTitle = NSLocalizedString(
"pos.itemList.loadingCouponsErrorTitle",
value: "Error loading coupons",
comment: "Title appearing on the coupon list screen when there's an error loading coupons."
)
static let loadingCouponsErrorSubtitle = NSLocalizedString(
"pos.itemList.loadingCouponsErrorSubtitle",
value: "Error loading coupons",
comment: "Subtitle appearing on the coupon list screen when there's an error loading coupons."
)
static let loadingCouponsErrorRetry = NSLocalizedString(
"pos.itemList.loadingCouponsErrorRetry",
value: "Retry",
comment: "Text of the button appearing on the coupon list screen when there's an error loading coupons."
)
static let loadingCouponsDisabledTitle = NSLocalizedString(
"pos.itemList.loadingCouponsDisabledTitle",
value: "Error loading coupons",
comment: "Title appearing on the coupon list screen when coupons are disabled."
)
static let loadingCouponsDisabledSubtitle = NSLocalizedString(
"pos.itemList.loadingCouponsDisabledSubtitle",
value: "Please enable the use of coupon codes in your store.",
comment: "Subtitle appearing on the coupon list screen when coupons are disabled."
)
static let loadingCouponsDisabledAction = NSLocalizedString(
"pos.itemList.loadingCouponsDisabledAction",
value: "Enable",
comment: "Text of the button appearing on the coupon list screen when coupons are disabled."
)
static let noCouponsFoundTitle = NSLocalizedString(
"pos.itemList.noCouponsFoundTitle",
value: "No coupons found",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private extension CardReaderConnectionStatusView {
#Preview {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewCouponsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import SwiftUI
/// A view that displays an error message with a retry CTA when the list of POS items fails to load.
struct PointOfSaleItemListErrorView: View {
private let error: PointOfSaleErrorState
private let onRetry: (() -> Void)?
private let onAction: (() -> Void)?

init(error: PointOfSaleErrorState, onRetry: (() -> Void)? = nil) {
init(error: PointOfSaleErrorState, onAction: (() -> Void)? = nil) {
self.error = error
self.onRetry = onRetry
self.onAction = onAction
}

var body: some View {
Expand All @@ -33,7 +33,7 @@ struct PointOfSaleItemListErrorView: View {
Spacer().frame(height: PointOfSaleCardPresentPaymentLayout.textAndButtonSpacing)

Button(action: {
onRetry?()
onAction?()
}, label: {
Text(error.buttonText)
})
Expand All @@ -47,5 +47,5 @@ struct PointOfSaleItemListErrorView: View {
}

#Preview {
PointOfSaleItemListErrorView(error: .errorOnLoadingProducts(), onRetry: nil)
PointOfSaleItemListErrorView(error: .errorOnLoadingProducts(), onAction: nil)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ import SwiftUI
/// A view that displays an error message with a retry CTA when the list of products fails to load.
struct PointOfSaleItemListFullscreenErrorView: View {
private let error: PointOfSaleErrorState
private let onRetry: (() -> Void)?
private let onAction: (() -> Void)?

init(error: PointOfSaleErrorState, onRetry: (() -> Void)? = nil) {
init(error: PointOfSaleErrorState, onAction: (() -> Void)? = nil) {
self.error = error
self.onRetry = onRetry
self.onAction = onAction
}

var body: some View {
PointOfSaleItemListFullscreenView {
PointOfSaleItemListErrorView(error: error, onRetry: onRetry)
PointOfSaleItemListErrorView(error: error, onAction: onAction)
}
}
}

#Preview {
PointOfSaleItemListFullscreenErrorView(error: .errorOnLoadingProducts(), onRetry: nil)
PointOfSaleItemListFullscreenErrorView(error: .errorOnLoadingProducts(), onAction: nil)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ private enum Localization {
PointOfSaleItemListFullscreenView(
content: {
PointOfSaleItemListErrorView(
error: .init(title: "Error", subtitle: "Something went wrong", buttonText: "Fix it"))
error: .init(errorType: .productsLoadError, title: "Error", subtitle: "Something went wrong", buttonText: "Fix it"))
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ private extension PointOfSalePaymentSuccessView {
#Preview {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewCouponsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
Expand Down
4 changes: 2 additions & 2 deletions WooCommerce/Classes/POS/Presentation/CartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ private extension CartView {
#Preview {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewCouponsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
Expand All @@ -363,7 +363,7 @@ private extension CartView {
#Preview("Cart with one item") {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewCouponsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ private extension ChildItemList {
Spacer()
}

PointOfSaleItemListErrorView(error: error, onRetry: {
PointOfSaleItemListErrorView(error: error, onAction: {
Task {
await posModel.loadItems(base: node)
}
Expand Down Expand Up @@ -152,7 +152,7 @@ private extension ChildItemList {
], hasMoreItems: false)])
let posModel = PointOfSaleAggregateModel(
itemsController: itemsController,
couponsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewCouponsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
Expand All @@ -177,7 +177,7 @@ private extension ChildItemList {
])
let posModel = PointOfSaleAggregateModel(
itemsController: itemsController,
couponsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewCouponsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private extension ItemListRow {
#Preview("Loading") {
let posModel = PointOfSaleAggregateModel(
itemsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewItemsController(),
couponsController: PointOfSalePreviewCouponsController(),
cardPresentPaymentService: CardPresentPaymentPreviewService(),
orderController: PointOfSalePreviewOrderController(),
collectOrderPaymentAnalyticsTracker: POSCollectOrderPaymentAnalytics())
Expand Down
Loading