Skip to content

Commit 3a67956

Browse files
authored
Merge pull request #7065 from woocommerce/issue/6961-refactor-new-order-flow
[Unified Order Editing] Update New Order view and viewmodel to use existing order
2 parents 5dcdd44 + 25c07af commit 3a67956

File tree

6 files changed

+145
-20
lines changed

6 files changed

+145
-20
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ private extension AddOrderCoordinator {
7979
///
8080
func presentNewOrderController() {
8181
let viewModel = NewOrderViewModel(siteID: siteID)
82-
viewModel.onOrderCreated = onOrderCreated
82+
viewModel.onFinished = onOrderCreated
8383

8484
let viewController = NewOrderHostingController(viewModel: viewModel)
8585
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.splitViewInOrdersTab) {

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ final class NewOrderHostingController: UIHostingController<NewOrder> {
4444
///
4545
extension NewOrderHostingController {
4646
override func shouldPopOnBackButton() -> Bool {
47-
guard !viewModel.hasChanges else {
47+
guard viewModel.canBeDismissed else {
4848
presentDiscardChangesActionSheet(onDiscard: { [weak self] in
4949
self?.discardOrderAndPop()
5050
})
@@ -62,7 +62,7 @@ extension NewOrderHostingController {
6262
///
6363
extension NewOrderHostingController: UIAdaptivePresentationControllerDelegate {
6464
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
65-
return !viewModel.hasChanges
65+
return viewModel.canBeDismissed
6666
}
6767

6868
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
@@ -131,7 +131,7 @@ struct NewOrder: View {
131131
.ignoresSafeArea(.container, edges: [.horizontal])
132132
}
133133
}
134-
.navigationTitle(Localization.title)
134+
.navigationTitle(viewModel.title)
135135
.navigationBarTitleDisplayMode(.inline)
136136
.toolbar {
137137
ToolbarItem(placement: .cancellationAction) {
@@ -150,7 +150,10 @@ struct NewOrder: View {
150150
.id(navigationButtonID)
151151
.accessibilityIdentifier(Accessibility.createButtonIdentifier)
152152
.disabled(viewModel.disabled)
153-
153+
case .done:
154+
Button(Localization.doneButton) {
155+
dismissHandler()
156+
}
154157
case .loading:
155158
ProgressView()
156159
}
@@ -242,8 +245,8 @@ private extension NewOrder {
242245
}
243246

244247
enum Localization {
245-
static let title = NSLocalizedString("New Order", comment: "Title for the order creation screen")
246248
static let createButton = NSLocalizedString("Create", comment: "Button to create an order on the New Order screen")
249+
static let doneButton = NSLocalizedString("Done", comment: "Button to dismiss the Order Editing screen")
247250
static let cancelButton = NSLocalizedString("Cancel", comment: "Button to cancel the creation of an order on the New Order screen")
248251
static let products = NSLocalizedString("Products", comment: "Title text of the section that shows the Products when creating a new order")
249252
static let addProduct = NSLocalizedString("Add Product", comment: "Title text of the button that adds a product when creating a new order")

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

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,50 @@ final class NewOrderViewModel: ObservableObject {
1616

1717
private var cancellables: Set<AnyCancellable> = []
1818

19+
enum Flow: Equatable {
20+
case creation
21+
case editing(initialOrder: Order)
22+
}
23+
24+
/// Current flow. For editing stores existing order state prior to applying any edits.
25+
///
26+
let flow: Flow
27+
1928
/// Indicates whether user has made any changes
2029
///
2130
var hasChanges: Bool {
22-
orderSynchronizer.order != OrderFactory.emptyNewOrder
31+
switch flow {
32+
case .creation:
33+
return orderSynchronizer.order != OrderFactory.emptyNewOrder
34+
case .editing(let initialOrder):
35+
return orderSynchronizer.order != initialOrder
36+
}
37+
}
38+
39+
/// Indicates whether view can be dismissed.
40+
///
41+
var canBeDismissed: Bool {
42+
switch flow {
43+
case .creation: // Creation can be dismissed when there aren't changes pending to commit.
44+
return !hasChanges
45+
case .editing: // Editing can always be dismissed because changes are committed instantly.
46+
return true
47+
}
2348
}
2449

2550
/// Indicates whether the cancel button is visible.
2651
///
2752
var shouldShowCancelButton: Bool {
28-
featureFlagService.isFeatureFlagEnabled(.splitViewInOrdersTab)
53+
featureFlagService.isFeatureFlagEnabled(.splitViewInOrdersTab) && flow == .creation
54+
}
55+
56+
var title: String {
57+
switch flow {
58+
case .creation:
59+
return Localization.titleForNewOrder
60+
case .editing(let order):
61+
return String.localizedStringWithFormat(Localization.titleWithOrderNumber, order.number)
62+
}
2963
}
3064

3165
/// Active navigation bar trailing item.
@@ -46,9 +80,15 @@ final class NewOrderViewModel: ObservableObject {
4680

4781
/// Order creation date. For new order flow it's always current date.
4882
///
49-
let dateString: String = {
50-
DateFormatter.mediumLengthLocalizedDateFormatter.string(from: Date())
51-
}()
83+
var dateString: String {
84+
switch flow {
85+
case .creation:
86+
return DateFormatter.mediumLengthLocalizedDateFormatter.string(from: Date())
87+
case .editing(let order):
88+
let formatter = DateFormatter.dateAndTimeFormatter
89+
return formatter.string(from: order.dateCreated)
90+
}
91+
}
5292

5393
/// Representation of order status display properties.
5494
///
@@ -195,17 +235,23 @@ final class NewOrderViewModel: ObservableObject {
195235
private let orderSynchronizer: OrderSynchronizer
196236

197237
init(siteID: Int64,
238+
flow: Flow = .creation,
198239
stores: StoresManager = ServiceLocator.stores,
199240
storageManager: StorageManagerType = ServiceLocator.storageManager,
200241
currencySettings: CurrencySettings = ServiceLocator.currencySettings,
201242
analytics: Analytics = ServiceLocator.analytics,
202243
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
203244
self.siteID = siteID
245+
self.flow = flow
204246
self.stores = stores
205247
self.storageManager = storageManager
206248
self.currencyFormatter = CurrencyFormatter(currencySettings: currencySettings)
207249
self.analytics = analytics
208-
self.orderSynchronizer = RemoteOrderSynchronizer(siteID: siteID, stores: stores, currencySettings: currencySettings)
250+
if case let .editing(initialOrder) = flow {
251+
self.orderSynchronizer = RemoteOrderSynchronizer(siteID: siteID, initialOrder: initialOrder, stores: stores, currencySettings: currencySettings)
252+
} else {
253+
self.orderSynchronizer = RemoteOrderSynchronizer(siteID: siteID, initialOrder: nil, stores: stores, currencySettings: currencySettings)
254+
}
209255
self.featureFlagService = featureFlagService
210256

211257
// Set a temporary initial view model, as a workaround to avoid making it optional.
@@ -300,7 +346,7 @@ final class NewOrderViewModel: ObservableObject {
300346

301347
switch result {
302348
case .success(let newOrder):
303-
self.onOrderCreated(newOrder)
349+
self.onFinished(newOrder)
304350
self.trackCreateOrderSuccess()
305351
case .failure(let error):
306352
self.notice = NoticeFactory.createOrderErrorNotice(error, order: self.orderSynchronizer.order)
@@ -311,9 +357,11 @@ final class NewOrderViewModel: ObservableObject {
311357
trackCreateButtonTapped()
312358
}
313359

314-
/// Assign this closure to be notified when a new order is created
360+
/// Assign this closure to be notified when the flow has finished.
361+
/// For creation it means that the order has been created.
362+
/// For edition it means that the merchant has finished editing the order.
315363
///
316-
var onOrderCreated: (Order) -> Void = { _ in }
364+
var onFinished: (Order) -> Void = { _ in }
317365

318366
/// Updates the order status & tracks its event
319367
///
@@ -349,6 +397,7 @@ extension NewOrderViewModel {
349397
///
350398
enum NavigationItem: Equatable {
351399
case create
400+
case done
352401
case loading
353402
}
354403

@@ -500,13 +549,18 @@ private extension NewOrderViewModel {
500549
/// Calculates what navigation trailing item should be shown depending on our internal state.
501550
///
502551
func configureNavigationTrailingItem() {
503-
Publishers.CombineLatest(orderSynchronizer.orderPublisher, $performingNetworkRequest)
504-
.map { order, performingNetworkRequest -> NavigationItem in
552+
Publishers.CombineLatest3(orderSynchronizer.orderPublisher, $performingNetworkRequest, Just(flow))
553+
.map { order, performingNetworkRequest, flow -> NavigationItem in
505554
guard !performingNetworkRequest else {
506555
return .loading
507556
}
508557

509-
return .create
558+
switch flow {
559+
case .creation:
560+
return .create
561+
case .editing:
562+
return .done
563+
}
510564
}
511565
.assign(to: &$navigationTrailingItem)
512566
}
@@ -841,6 +895,8 @@ extension NewOrderViewModel {
841895

842896
private extension NewOrderViewModel {
843897
enum Localization {
898+
static let titleForNewOrder = NSLocalizedString("New Order", comment: "Title for the order creation screen")
899+
static let titleWithOrderNumber = NSLocalizedString("Order #%1$@", comment: "Order number title. Parameters: %1$@ - order number")
844900
static let errorMessageOrderCreation = NSLocalizedString("Unable to create new order", comment: "Notice displayed when order creation fails")
845901
static let errorMessageOrderSync = NSLocalizedString("Unable to load taxes for order",
846902
comment: "Notice displayed when taxes cannot be synced for new order")

WooCommerce/Classes/ViewRelated/Orders/Order Creation/Synchronizer/RemoteOrderSynchronizer.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,18 @@ final class RemoteOrderSynchronizer: OrderSynchronizer {
6363

6464
// MARK: Initializers
6565

66-
init(siteID: Int64, stores: StoresManager = ServiceLocator.stores, currencySettings: CurrencySettings = ServiceLocator.currencySettings) {
66+
init(siteID: Int64,
67+
initialOrder: Order? = nil,
68+
stores: StoresManager = ServiceLocator.stores,
69+
currencySettings: CurrencySettings = ServiceLocator.currencySettings) {
6770
self.siteID = siteID
6871
self.stores = stores
6972
self.currencyFormatter = CurrencyFormatter(currencySettings: currencySettings)
7073

74+
if let initialOrder = initialOrder {
75+
order = initialOrder
76+
}
77+
7178
updateBaseSyncOrderStatus()
7279
bindInputs()
7380
bindOrderSync()

WooCommerce/Classes/ViewRelated/Orders/Order Details/OrderDetailsViewController.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,14 @@ private extension OrderDetailsViewController {
353353
/// Presents the order edit form
354354
///
355355
private func editOrder() {
356-
// TODO: Implement
356+
let viewModel = NewOrderViewModel(siteID: viewModel.order.siteID, flow: .editing(initialOrder: viewModel.order))
357+
viewModel.onFinished = { [weak self] order in
358+
guard let self = self else { return }
359+
self.dismiss(animated: true)
360+
}
361+
let viewController = NewOrderHostingController(viewModel: viewModel)
362+
let navController = UINavigationController(rootViewController: viewController)
363+
present(navController, animated: true)
357364
}
358365
}
359366

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import WooFoundation
66
final class NewOrderViewModelTests: XCTestCase {
77

88
let sampleSiteID: Int64 = 123
9+
let sampleOrderID: Int64 = 1234
910
let sampleProductID: Int64 = 5
1011

1112
func test_view_model_inits_with_expected_values() {
@@ -21,6 +22,17 @@ final class NewOrderViewModelTests: XCTestCase {
2122
XCTAssertEqual(viewModel.productRows.count, 0)
2223
}
2324

25+
func test_edition_view_model_has_a_navigation_done_button() {
26+
// Given
27+
let stores = MockStoresManager(sessionManager: .testingInstance)
28+
29+
// When
30+
let viewModel = NewOrderViewModel(siteID: sampleSiteID, flow: .editing(initialOrder: .fake()), stores: stores)
31+
32+
// Then
33+
XCTAssertEqual(viewModel.navigationTrailingItem, .done)
34+
}
35+
2436
func test_loading_indicator_is_enabled_during_network_request() {
2537
// Given
2638
let stores = MockStoresManager(sessionManager: .testingInstance)
@@ -1012,6 +1024,46 @@ final class NewOrderViewModelTests: XCTestCase {
10121024
XCTAssertEqual(viewModel.addressFormViewModel.fields.lastName, "",
10131025
"Pending change was not discarded when address form was reset")
10141026
}
1027+
1028+
func test_canBeDismissed_is_true_when_creating_order_without_changes() {
1029+
// Given
1030+
let viewModel = NewOrderViewModel(siteID: sampleSiteID)
1031+
1032+
// Then
1033+
XCTAssertTrue(viewModel.canBeDismissed)
1034+
}
1035+
1036+
func test_canBeDismissed_is_false_when_creating_order_with_changes() {
1037+
// Given
1038+
let viewModel = NewOrderViewModel(siteID: sampleSiteID)
1039+
1040+
// When
1041+
viewModel.updateOrderStatus(newStatus: .failed)
1042+
1043+
// Then
1044+
XCTAssertFalse(viewModel.canBeDismissed)
1045+
}
1046+
1047+
func test_canBeDismissed_is_true_when_editing_order_without_changes() {
1048+
// Given
1049+
let order = Order.fake().copy(orderID: sampleOrderID)
1050+
let viewModel = NewOrderViewModel(siteID: sampleSiteID, flow: .editing(initialOrder: order))
1051+
1052+
// Then
1053+
XCTAssertTrue(viewModel.canBeDismissed)
1054+
}
1055+
1056+
func test_canBeDismissed_is_true_when_editing_order_with_changes() {
1057+
// Given
1058+
let order = Order.fake().copy(orderID: sampleOrderID)
1059+
let viewModel = NewOrderViewModel(siteID: sampleSiteID, flow: .editing(initialOrder: order))
1060+
1061+
// When
1062+
viewModel.updateOrderStatus(newStatus: .failed)
1063+
1064+
// Then
1065+
XCTAssertTrue(viewModel.canBeDismissed)
1066+
}
10151067
}
10161068

10171069
private extension MockStorageManager {

0 commit comments

Comments
 (0)