Skip to content

Commit 935914b

Browse files
authored
Merge pull request #5523 from woocommerce/issue/5483-error-notices-refactor
2 parents 174524c + 6c5b7e0 commit 935914b

File tree

6 files changed

+101
-33
lines changed

6 files changed

+101
-33
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import UIKit
22
import Yosemite
3+
import Combine
34

45
final class AddOrderCoordinator: Coordinator {
56
var navigationController: UINavigationController
@@ -70,10 +71,11 @@ private extension AddOrderCoordinator {
7071
/// Presents `SimplePaymentsAmountHostingController`.
7172
///
7273
func presentSimplePaymentsAmountController() {
73-
let viewModel = SimplePaymentsAmountViewModel(siteID: siteID)
74+
let presentNoticeSubject = PassthroughSubject<SimplePaymentsNotice, Never>()
75+
let viewModel = SimplePaymentsAmountViewModel(siteID: siteID, presentNoticeSubject: presentNoticeSubject)
7476
viewModel.onOrderCreated = onOrderCreated
7577

76-
let viewController = SimplePaymentsAmountHostingController(viewModel: viewModel)
78+
let viewController = SimplePaymentsAmountHostingController(viewModel: viewModel, presentNoticePublisher: presentNoticeSubject.eraseToAnyPublisher())
7779
let simplePaymentsNC = WooNavigationController(rootViewController: viewController)
7880
navigationController.present(simplePaymentsNC, animated: true)
7981

WooCommerce/Classes/ViewRelated/Orders/Simple Payments/Amount/SimplePaymentsAmount.swift

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,27 @@ final class SimplePaymentsAmountHostingController: UIHostingController<SimplePay
1414
///
1515
private lazy var modalNoticePresenter: NoticePresenter = {
1616
let presenter = DefaultNoticePresenter()
17-
presenter.presentingViewController = self
17+
presenter.presentingViewController = self.navigationController ?? self
1818
return presenter
1919
}()
2020

21-
init(viewModel: SimplePaymentsAmountViewModel) {
21+
init(viewModel: SimplePaymentsAmountViewModel, presentNoticePublisher: AnyPublisher<SimplePaymentsNotice, Never>) {
2222
super.init(rootView: SimplePaymentsAmount(viewModel: viewModel))
2323

2424
// Needed because a `SwiftUI` cannot be dismissed when being presented by a UIHostingController
2525
rootView.dismiss = { [weak self] in
2626
self?.dismiss(animated: true, completion: nil)
2727
}
2828

29-
// Observe the present notice intent and set it back to `nil` after presented.
30-
viewModel.$presentNotice
29+
// Observe the present notice intent.
30+
presentNoticePublisher
3131
.compactMap { $0 }
3232
.sink { [weak self] notice in
3333

34-
// To prevent keyboard to hide the notice
35-
self?.view.endEditing(true)
36-
3734
switch notice {
38-
case .error:
39-
self?.modalNoticePresenter.enqueue(notice: .init(title: SimplePaymentsAmount.Localization.error, feedbackType: .error))
35+
case .error(let description):
36+
self?.modalNoticePresenter.enqueue(notice: .init(title: description, feedbackType: .error))
4037
}
41-
42-
// Nullify the presentation intent.
43-
viewModel.presentNotice = nil
4438
}
4539
.store(in: &subscriptions)
4640
}
@@ -153,7 +147,6 @@ private extension SimplePaymentsAmount {
153147
static let title = NSLocalizedString("Take Payment", comment: "Title for the simple payments screen")
154148
static let instructions = NSLocalizedString("Enter Amount", comment: "Short instructions label in the simple payments screen")
155149
static let cancelTitle = NSLocalizedString("Cancel", comment: "Title for the button to cancel the simple payments screen")
156-
static let error = NSLocalizedString("There was an error creating the order", comment: "Notice text after failing to create a simple payments order.")
157150

158151
static func buttonTitle() -> String {
159152
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.simplePaymentsPrototype) {
@@ -171,3 +164,9 @@ private extension SimplePaymentsAmount {
171164
}
172165
}
173166
}
167+
168+
/// Representation of possible notices that can be displayed
169+
///
170+
enum SimplePaymentsNotice: Equatable {
171+
case error(String)
172+
}

WooCommerce/Classes/ViewRelated/Orders/Simple Payments/Amount/SimplePaymentsAmountViewModel.swift

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Yosemite
3+
import Combine
34
import Experiments
45

56
/// View Model for the `SimplePaymentsAmount` view.
@@ -19,11 +20,6 @@ final class SimplePaymentsAmountViewModel: ObservableObject {
1920
///
2021
@Published private(set) var loading: Bool = false
2122

22-
/// Defines the current notice that should be shown.
23-
/// Defaults to `nil`.
24-
///
25-
@Published var presentNotice: Notice?
26-
2723
/// Defines if the view should navigate to the summary view.
2824
/// Setting it to `false` will `nil` the summary view model.
2925
///
@@ -81,6 +77,10 @@ final class SimplePaymentsAmountViewModel: ObservableObject {
8177
///
8278
private let userLocale: Locale
8379

80+
/// Transmits notice presentation intents.
81+
///
82+
private let presentNoticeSubject: PassthroughSubject<SimplePaymentsNotice, Never>
83+
8484
/// Current store currency settings
8585
///
8686
private let storeCurrencySettings: CurrencySettings
@@ -100,12 +100,14 @@ final class SimplePaymentsAmountViewModel: ObservableObject {
100100
init(siteID: Int64,
101101
stores: StoresManager = ServiceLocator.stores,
102102
locale: Locale = Locale.autoupdatingCurrent,
103+
presentNoticeSubject: PassthroughSubject<SimplePaymentsNotice, Never> = PassthroughSubject(),
103104
storeCurrencySettings: CurrencySettings = ServiceLocator.currencySettings,
104105
analytics: Analytics = ServiceLocator.analytics,
105106
isDevelopmentPrototype: Bool = ServiceLocator.featureFlagService.isFeatureFlagEnabled(FeatureFlag.simplePaymentsPrototype)) {
106107
self.siteID = siteID
107108
self.stores = stores
108109
self.userLocale = locale
110+
self.presentNoticeSubject = presentNoticeSubject
109111
self.storeCurrencySettings = storeCurrencySettings
110112
self.storeCurrencySymbol = storeCurrencySettings.symbol(from: storeCurrencySettings.currencyCode)
111113
self.analytics = analytics
@@ -127,14 +129,16 @@ final class SimplePaymentsAmountViewModel: ObservableObject {
127129
switch result {
128130
case .success(let order):
129131
if self.isDevelopmentPrototype {
130-
self.summaryViewModel = SimplePaymentsSummaryViewModel(order: order, providedAmount: self.amount)
132+
self.summaryViewModel = SimplePaymentsSummaryViewModel(order: order,
133+
providedAmount: self.amount,
134+
presentNoticeSubject: self.presentNoticeSubject)
131135
} else {
132136
self.onOrderCreated(order)
133137
}
134138
self.analytics.track(event: WooAnalyticsEvent.SimplePayments.simplePaymentsFlowCompleted(amount: order.total))
135139

136140
case .failure(let error):
137-
self.presentNotice = .error
141+
self.presentNoticeSubject.send(.error(Localization.creationError))
138142
self.analytics.track(event: WooAnalyticsEvent.SimplePayments.simplePaymentsFlowFailed())
139143
DDLogError("⛔️ Error creating simple payments order: \(error)")
140144
}
@@ -190,10 +194,10 @@ private extension SimplePaymentsAmountViewModel {
190194
}
191195
}
192196

193-
// MARK: Definitions
194-
extension SimplePaymentsAmountViewModel {
195-
/// Representation of possible notices that can be displayed
196-
enum Notice: Equatable {
197-
case error
197+
// MARK: Constants
198+
private extension SimplePaymentsAmountViewModel {
199+
enum Localization {
200+
static let creationError = NSLocalizedString("There was an error creating the order",
201+
comment: "Notice text after failing to create a simple payments order.")
198202
}
199203
}

WooCommerce/Classes/ViewRelated/Orders/Simple Payments/Summary/SimplePaymentsSummaryViewModel.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Yosemite
3+
import Combine
34

45
/// `ViewModel` to drive the content of the `SimplePaymentsSummary` view.
56
///
@@ -61,6 +62,10 @@ final class SimplePaymentsSummaryViewModel: ObservableObject {
6162
///
6263
private let feeID: Int64
6364

65+
/// Transmits notice presentation intents.
66+
///
67+
private let presentNoticeSubject: PassthroughSubject<SimplePaymentsNotice, Never>
68+
6469
/// Stores Manager.
6570
///
6671
private let stores: StoresManager
@@ -76,11 +81,13 @@ final class SimplePaymentsSummaryViewModel: ObservableObject {
7681
siteID: Int64 = 0,
7782
orderID: Int64 = 0,
7883
feeID: Int64 = 0,
84+
presentNoticeSubject: PassthroughSubject<SimplePaymentsNotice, Never> = PassthroughSubject(),
7985
currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings),
8086
stores: StoresManager = ServiceLocator.stores) {
8187
self.siteID = siteID
8288
self.orderID = orderID
8389
self.feeID = feeID
90+
self.presentNoticeSubject = presentNoticeSubject
8491
self.currencyFormatter = currencyFormatter
8592
self.stores = stores
8693
self.providedAmount = currencyFormatter.formatAmount(providedAmount) ?? providedAmount
@@ -109,6 +116,7 @@ final class SimplePaymentsSummaryViewModel: ObservableObject {
109116

110117
convenience init(order: Order,
111118
providedAmount: String,
119+
presentNoticeSubject: PassthroughSubject<SimplePaymentsNotice, Never> = PassthroughSubject(),
112120
currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings),
113121
stores: StoresManager = ServiceLocator.stores) {
114122
self.init(providedAmount: providedAmount,
@@ -117,6 +125,7 @@ final class SimplePaymentsSummaryViewModel: ObservableObject {
117125
siteID: order.siteID,
118126
orderID: order.orderID,
119127
feeID: order.fees.first?.feeID ?? 0,
128+
presentNoticeSubject: presentNoticeSubject,
120129
currencyFormatter: currencyFormatter,
121130
stores: stores)
122131
}
@@ -147,11 +156,18 @@ final class SimplePaymentsSummaryViewModel: ObservableObject {
147156
// TODO: Analytics
148157
break
149158
case .failure:
150-
// TODO: Present notice
159+
self.presentNoticeSubject.send(.error(Localization.updateError))
151160
// TODO: Analytics
152-
break
153161
}
154162
}
155163
stores.dispatch(action)
156164
}
157165
}
166+
167+
// MARK: Constants
168+
private extension SimplePaymentsSummaryViewModel {
169+
enum Localization {
170+
static let updateError = NSLocalizedString("There was an error updating the order",
171+
comment: "Notice text after failing to update a simple payments order.")
172+
}
173+
}

WooCommerce/WooCommerceTests/ViewRelated/Orders/Simple Payments/SimplePaymentsAmountViewModelTests.swift

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import XCTest
3+
import Combine
34

45
@testable import WooCommerce
56
@testable import Yosemite
@@ -11,6 +12,8 @@ final class SimplePaymentsAmountViewModelTests: XCTestCase {
1112
private let usLocale = Locale(identifier: "en_US")
1213
private let usStoreSettings = CurrencySettings() // Default is US settings
1314

15+
private var subscriptions = Set<AnyCancellable>()
16+
1417
func test_view_model_prepends_currency_symbol() {
1518
// Given
1619
let viewModel = SimplePaymentsAmountViewModel(siteID: sampleSiteID, locale: usLocale, storeCurrencySettings: usStoreSettings)
@@ -280,7 +283,8 @@ final class SimplePaymentsAmountViewModelTests: XCTestCase {
280283
func test_view_model_attempts_error_notice_presentation_when_failing_to_crete_order() {
281284
// Given
282285
let testingStore = MockStoresManager(sessionManager: .testingInstance)
283-
let viewModel = SimplePaymentsAmountViewModel(siteID: sampleSiteID, stores: testingStore)
286+
let noticeSubject = PassthroughSubject<SimplePaymentsNotice, Never>()
287+
let viewModel = SimplePaymentsAmountViewModel(siteID: sampleSiteID, stores: testingStore, presentNoticeSubject: noticeSubject)
284288
testingStore.whenReceivingAction(ofType: OrderAction.self) { action in
285289
switch action {
286290
case let .createSimplePaymentsOrder(_, _, _, onCompletion):
@@ -291,13 +295,22 @@ final class SimplePaymentsAmountViewModelTests: XCTestCase {
291295
}
292296

293297
// When
294-
viewModel.createSimplePaymentsOrder()
298+
let receivedError: Bool = waitFor { promise in
299+
noticeSubject.sink { intent in
300+
switch intent {
301+
case .error:
302+
promise(true)
303+
}
304+
}
305+
.store(in: &self.subscriptions)
306+
viewModel.createSimplePaymentsOrder()
307+
}
295308

296309
// Then
297-
XCTAssertEqual(viewModel.presentNotice, .error)
310+
XCTAssertTrue(receivedError)
298311
}
299312

300-
func test_view_model_diable_cancel_button_while_creating_payment_order() {
313+
func test_view_model_disable_cancel_button_while_creating_payment_order() {
301314
// Given
302315
let testingStore = MockStoresManager(sessionManager: .testingInstance)
303316
let viewModel = SimplePaymentsAmountViewModel(siteID: sampleSiteID, stores: testingStore)

WooCommerce/WooCommerceTests/ViewRelated/Orders/Simple Payments/SimplePaymentsSummaryViewModelTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,38 @@ final class SimplePaymentsSummaryViewModelTests: XCTestCase {
108108
// Then
109109
XCTAssertEqual(loadingStates, [true, false]) // Loading, then not loading.
110110
}
111+
112+
func test_view_model_attempts_error_notice_presentation_when_failing_to_update_order() {
113+
// Given
114+
let mockStores = MockStoresManager(sessionManager: .testingInstance)
115+
let noticeSubject = PassthroughSubject<SimplePaymentsNotice, Never>()
116+
let viewModel = SimplePaymentsSummaryViewModel(providedAmount: "1.0",
117+
totalWithTaxes: "1.0",
118+
taxAmount: "0.0",
119+
presentNoticeSubject: noticeSubject,
120+
stores: mockStores)
121+
mockStores.whenReceivingAction(ofType: OrderAction.self) { action in
122+
switch action {
123+
case let .updateSimplePaymentsOrder(_, _, _, _, _, _, _, onCompletion):
124+
onCompletion(.failure(NSError(domain: "Error", code: 0)))
125+
default:
126+
XCTFail("Received unsupported action: \(action)")
127+
}
128+
}
129+
130+
// When
131+
let receivedError: Bool = waitFor { promise in
132+
noticeSubject.sink { intent in
133+
switch intent {
134+
case .error:
135+
promise(true)
136+
}
137+
}
138+
.store(in: &self.subscriptions)
139+
viewModel.updateOrder()
140+
}
141+
142+
// Then
143+
XCTAssertTrue(receivedError)
144+
}
111145
}

0 commit comments

Comments
 (0)