Skip to content

Commit 1dc2a23

Browse files
authored
Merge pull request #9881 from woocommerce/issue/9819-retry-notice-upon-failure
[SKU Scan] Show Error Notice with Retry Button
2 parents 2286b0a + 8682618 commit 1dc2a23

File tree

5 files changed

+66
-21
lines changed

5 files changed

+66
-21
lines changed

WooCommerce/Classes/ViewRelated/Orders/Order Creation/EditableOrderViewModel.swift

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,15 @@ final class EditableOrderViewModel: ObservableObject {
8787
///
8888
@Published private(set) var performingNetworkRequest = false
8989

90-
/// Defines the current notice that should be shown.
90+
/// Defines the current notice that should be shown. It doesn't dismiss automatically
9191
/// Defaults to `nil`.
9292
///
93-
@Published var notice: Notice?
93+
@Published var fixedNotice: Notice?
94+
95+
/// Defines the current notice that should be shown. Autodismissable
96+
/// Defaults to `nil`.
97+
///
98+
@Published var autodismissableNotice: Notice?
9499

95100
// MARK: Status properties
96101

@@ -509,7 +514,7 @@ final class EditableOrderViewModel: ObservableObject {
509514
self.onFinished(newOrder)
510515
self.trackCreateOrderSuccess()
511516
case .failure(let error):
512-
self.notice = NoticeFactory.createOrderErrorNotice(error, order: self.orderSynchronizer.order)
517+
self.fixedNotice = NoticeFactory.createOrderErrorNotice(error, order: self.orderSynchronizer.order)
513518
self.trackCreateOrderFailure(error: error)
514519
DDLogError("⛔️ Error creating new order: \(error)")
515520
}
@@ -789,7 +794,7 @@ private extension EditableOrderViewModel {
789794
return nil
790795
}
791796
}
792-
.assign(to: &$notice)
797+
.assign(to: &$fixedNotice)
793798
}
794799

795800
/// Updates status badge viewmodel based on status order property.
@@ -1291,7 +1296,7 @@ extension EditableOrderViewModel {
12911296

12921297
/// Attempts to add a Product to the current Order by SKU search
12931298
///
1294-
func addScannedProductToOrder(barcode sku: String?, onCompletion: @escaping (Result<Void, Error>) -> Void) {
1299+
func addScannedProductToOrder(barcode sku: String?, onCompletion: @escaping (Result<Void, Error>) -> Void, onRetryRequested: @escaping () -> Void) {
12951300
guard let sku = sku else {
12961301
return onCompletion(.failure(ScannerError.nilSKU))
12971302
}
@@ -1304,6 +1309,12 @@ extension EditableOrderViewModel {
13041309
onCompletion(.success(()))
13051310
case .failure:
13061311
onCompletion(.failure(ScannerError.productNotFound))
1312+
self.autodismissableNotice = NoticeFactory.createProductNotFoundAfterSKUScanningErrorNotice(withRetryAction: { [weak self] in
1313+
self?.autodismissableNotice = nil
1314+
Task { @MainActor in
1315+
onRetryRequested()
1316+
}
1317+
})
13071318
}
13081319
}
13091320
}
@@ -1356,6 +1367,13 @@ extension EditableOrderViewModel {
13561367
return Notice(title: Localization.errorMessageOrderCreation, feedbackType: .error)
13571368
}
13581369

1370+
static func createProductNotFoundAfterSKUScanningErrorNotice(withRetryAction action: @escaping () -> Void) -> Notice {
1371+
Notice(title: Localization.scannedProductErrorNoticeMessage,
1372+
feedbackType: .error,
1373+
actionTitle: Localization.scannedProductErrorNoticeRetryActionTitle,
1374+
actionHandler: action)
1375+
}
1376+
13591377
/// Returns an order sync error notice.
13601378
///
13611379
static func syncOrderErrorNotice(_ error: Error, flow: Flow, with orderSynchronizer: OrderSynchronizer) -> Notice {
@@ -1441,6 +1459,11 @@ private extension EditableOrderViewModel {
14411459
static let multipleFeesAndShippingLines = NSLocalizedString("Fees & Shipping details are incomplete.\n" +
14421460
"To edit all the details, view the order in your WooCommerce store admin.",
14431461
comment: "Info message shown when the order contains multiple fees and shipping lines")
1462+
static let scannedProductErrorNoticeMessage = NSLocalizedString("Product not found. Failed to add product to order.",
1463+
comment: "Error message on the Order details view when the scanner cannot find a matching product")
1464+
static let scannedProductErrorNoticeRetryActionTitle = NSLocalizedString("Retry",
1465+
comment: "Retry button title on the Order details view when" +
1466+
"the scanner cannot find a matching product")
14441467

14451468
enum CouponSummary {
14461469
static let singular = NSLocalizedString("Coupon (%1$@)",

WooCommerce/Classes/ViewRelated/Orders/Order Creation/OrderForm.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ struct OrderForm: View {
178178
}
179179
}
180180
.wooNavigationBarStyle()
181-
.notice($viewModel.notice, autoDismiss: false)
181+
.notice($viewModel.autodismissableNotice)
182+
.notice($viewModel.fixedNotice, autoDismiss: false)
182183
}
183184
}
184185

@@ -330,6 +331,8 @@ private struct ProductsSection: View {
330331
ProductSKUInputScannerView(onBarcodeScanned: { detectedBarcode in
331332
viewModel.addScannedProductToOrder(barcode: detectedBarcode, onCompletion: { _ in
332333
showAddProductViaSKUScanner.toggle()
334+
}, onRetryRequested: {
335+
showAddProductViaSKUScanner = true
333336
})
334337
})
335338
})

WooCommerce/Classes/ViewRelated/Orders/OrderListViewController.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,7 @@ extension OrderListViewController {
330330
}
331331
}
332332

333-
func showErrorNotice(with message: String, in viewController: UIViewController) {
334-
let notice = Notice(title: message, feedbackType: .error)
333+
func showErrorNotice(_ notice: Notice, in viewController: UIViewController) {
335334
noticePresenter.presentingViewController = viewController
336335
noticePresenter.enqueue(notice: notice)
337336
}

WooCommerce/Classes/ViewRelated/Orders/OrdersRootViewController.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ final class OrdersRootViewController: UIViewController {
211211
case let .success(product):
212212
self.presentOrderCreationFlowWithScannedProduct(with: product.productID)
213213
case .failure:
214-
self.displayErrorNotice()
214+
self.displayScannedProductErrorNotice()
215215
}
216216
}
217217
})
@@ -238,9 +238,14 @@ final class OrdersRootViewController: UIViewController {
238238

239239
/// Presents an Error notice
240240
///
241-
private func displayErrorNotice() {
242-
let message = Localization.errorNoticeMessage
243-
ordersViewController.showErrorNotice(with: message, in: self)
241+
private func displayScannedProductErrorNotice() {
242+
let notice = Notice(title: Localization.scannedProductErrorNoticeMessage,
243+
feedbackType: .error,
244+
actionTitle: Localization.scannedProductErrorNoticeRetryActionTitle) { [weak self] in
245+
self?.presentOrderCreationFlowByProductScanning()
246+
}
247+
248+
ordersViewController.showErrorNotice(notice, in: self)
244249
}
245250

246251
/// Present `FilterListViewController`
@@ -505,8 +510,11 @@ private extension OrdersRootViewController {
505510
)
506511
static let emptyOrderDetails = NSLocalizedString("No order selected",
507512
comment: "Message on the detail view of the Orders tab before any order is selected")
508-
static let errorNoticeMessage = NSLocalizedString("Product not found. Failed to create a New Order",
513+
static let scannedProductErrorNoticeMessage = NSLocalizedString("Product not found. Failed to create a New Order",
509514
comment: "Error message on the Order list view when the scanner cannot find a matching product " +
510515
"and create a new order")
516+
static let scannedProductErrorNoticeRetryActionTitle = NSLocalizedString("Retry",
517+
comment: "Retry button title on the Order list view when the scanner cannot find" +
518+
"a matching product and create a new order")
511519
}
512520
}

WooCommerce/WooCommerceTests/ViewRelated/Orders/Order Creation/EditableOrderViewModelTests.swift

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ final class EditableOrderViewModelTests: XCTestCase {
160160
viewModel.createOrder()
161161

162162
// Then
163-
XCTAssertEqual(viewModel.notice, EditableOrderViewModel.NoticeFactory.createOrderErrorNotice(error, order: .fake()))
163+
XCTAssertEqual(viewModel.fixedNotice, EditableOrderViewModel.NoticeFactory.createOrderErrorNotice(error, order: .fake()))
164164
}
165165

166166
func test_view_model_fires_error_notice_when_order_sync_fails() {
@@ -186,21 +186,21 @@ final class EditableOrderViewModelTests: XCTestCase {
186186
}
187187

188188
// Then
189-
XCTAssertEqual(viewModel.notice, EditableOrderViewModel.NoticeFactory.syncOrderErrorNotice(error, flow: .creation, with: synchronizer))
189+
XCTAssertEqual(viewModel.fixedNotice, EditableOrderViewModel.NoticeFactory.syncOrderErrorNotice(error, flow: .creation, with: synchronizer))
190190
}
191191

192192
func test_view_model_clears_error_notice_when_order_is_syncing() {
193193
// Given
194194
let viewModel = EditableOrderViewModel(siteID: sampleSiteID, stores: stores)
195195
let error = NSError(domain: "Error", code: 0)
196-
viewModel.notice = EditableOrderViewModel.NoticeFactory.createOrderErrorNotice(error, order: .fake())
196+
viewModel.fixedNotice = EditableOrderViewModel.NoticeFactory.createOrderErrorNotice(error, order: .fake())
197197

198198
// When
199199
let notice: Notice? = waitFor { promise in
200200
self.stores.whenReceivingAction(ofType: OrderAction.self) { action in
201201
switch action {
202202
case .createOrder:
203-
promise(viewModel.notice)
203+
promise(viewModel.fixedNotice)
204204
default:
205205
XCTFail("Received unsupported action: \(action)")
206206
}
@@ -1614,13 +1614,13 @@ final class EditableOrderViewModelTests: XCTestCase {
16141614
default:
16151615
XCTFail("Expected failure, got success")
16161616
}
1617-
})
1617+
}, onRetryRequested: {})
16181618

16191619
// Then
16201620
XCTAssertEqual(capturedErrors, [.nilSKU])
16211621
}
16221622

1623-
func test_addScannedProductToOrder_when_sku_is_not_found_then_fails_to_add_product_and_returns_productNotFound_error() {
1623+
func test_addScannedProductToOrder_when_sku_is_not_found_then_returns_productNotFound_error_and_shows_autodismissable_notice_with_retry_action() {
16241624
// Given
16251625
stores.whenReceivingAction(ofType: ProductAction.self, thenCall: { action in
16261626
switch action {
@@ -1632,6 +1632,7 @@ final class EditableOrderViewModelTests: XCTestCase {
16321632
})
16331633

16341634
// When
1635+
var onRetryRequested = false
16351636
let expectedError = waitFor { promise in
16361637
self.viewModel.addScannedProductToOrder(barcode: "nonExistingSKU", onCompletion: { expectedError in
16371638
switch expectedError {
@@ -1640,11 +1641,22 @@ final class EditableOrderViewModelTests: XCTestCase {
16401641
default:
16411642
XCTFail("Expected failure, got success")
16421643
}
1644+
}, onRetryRequested: {
1645+
onRetryRequested = true
16431646
})
16441647
}
16451648

1649+
let expectedNotice = EditableOrderViewModel.NoticeFactory.createProductNotFoundAfterSKUScanningErrorNotice(withRetryAction: {})
1650+
16461651
// Then
16471652
XCTAssertEqual(expectedError, .productNotFound)
1653+
XCTAssertEqual(viewModel.autodismissableNotice, expectedNotice)
1654+
1655+
viewModel.autodismissableNotice?.actionHandler?()
1656+
1657+
waitUntil {
1658+
onRetryRequested == true
1659+
}
16481660
}
16491661

16501662
func test_addScannedProductToOrder_when_existing_sku_is_found_then_retrieving_a_matching_product_returns_success() {
@@ -1668,7 +1680,7 @@ final class EditableOrderViewModelTests: XCTestCase {
16681680
default:
16691681
XCTFail("Expected success, got failure")
16701682
}
1671-
})
1683+
}, onRetryRequested: {})
16721684
}
16731685

16741686
// Then
@@ -1706,7 +1718,7 @@ final class EditableOrderViewModelTests: XCTestCase {
17061718
default:
17071719
XCTFail("Expected success, got failure")
17081720
}
1709-
})
1721+
}, onRetryRequested: {})
17101722
}
17111723
let viewModel = EditableOrderViewModel(siteID: sampleSiteID, storageManager: storageManager, initialProductID: initialProductID)
17121724

0 commit comments

Comments
 (0)