Skip to content

Commit 606477b

Browse files
authored
Merge pull request #7141 from woocommerce/issue/7051-add-non-editable-order-banner
[Unified Order Editing] Add non-editable banner
2 parents 0eae2a7 + ef5bab2 commit 606477b

File tree

9 files changed

+164
-5
lines changed

9 files changed

+164
-5
lines changed

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,12 @@ extension UIImage {
981981
static var welcomeImage: UIImage {
982982
UIImage(imageLiteralResourceName: "img-welcome")
983983
}
984+
985+
/// Lock Image
986+
///
987+
static var lockImage: UIImage {
988+
UIImage.gridicon(.lock, size: CGSize(width: 24, height: 24))
989+
}
984990
}
985991

986992
private extension UIImage {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ final class EditableOrderViewModel: ObservableObject {
101101
/// Defines if the view should be disabled.
102102
@Published private(set) var disabled: Bool = false
103103

104+
/// Defines if the non editable banner should be shown.
105+
@Published private(set) var shouldShowNonEditableBanner: Bool = false
106+
104107
/// Status Results Controller.
105108
///
106109
private lazy var statusResultsController: ResultsController<StorageOrderStatus> = {
@@ -266,6 +269,7 @@ final class EditableOrderViewModel: ObservableObject {
266269
configureCustomerDataViewModel()
267270
configurePaymentDataViewModel()
268271
configureCustomerNoteDataViewModel()
272+
configureNonEditableBanner()
269273
resetAddressForm()
270274
}
271275

@@ -703,6 +707,21 @@ private extension EditableOrderViewModel {
703707
.assign(to: &$paymentDataViewModel)
704708
}
705709

710+
/// Binds the order state to the `shouldShowNonEditableBanner` property.
711+
///
712+
func configureNonEditableBanner() {
713+
Publishers.CombineLatest(orderSynchronizer.orderPublisher, Just(flow))
714+
.map { order, flow in
715+
switch flow {
716+
case .creation:
717+
return false
718+
case .editing:
719+
return !order.isEditable
720+
}
721+
}
722+
.assign(to: &$shouldShowNonEditableBanner)
723+
}
724+
706725
/// Tracks when customer details have been added
707726
///
708727
func trackCustomerDetailsAdded() {

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,14 @@ struct OrderForm: View {
107107
ScrollViewReader { scroll in
108108
ScrollView {
109109
VStack(spacing: Layout.noSpacing) {
110-
OrderStatusSection(viewModel: viewModel)
110+
111+
Group {
112+
Divider() // Needed because `NonEditableOrderBanner` does not have a top divider
113+
NonEditableOrderBanner(width: geometry.size.width)
114+
}
115+
.renderedIf(viewModel.shouldShowNonEditableBanner)
116+
117+
OrderStatusSection(viewModel: viewModel, topDivider: !viewModel.shouldShowNonEditableBanner)
111118

112119
Spacer(minLength: Layout.sectionSpacing)
113120

WooCommerce/Classes/ViewRelated/Orders/Order Creation/StatusSection/OrderStatusSection.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ import Yosemite
44
/// Represents the Status section with date label, status badge and edit button.
55
///
66
struct OrderStatusSection: View {
7+
78
@ObservedObject var viewModel: EditableOrderViewModel
89

910
@Environment(\.safeAreaInsets) var safeAreaInsets: EdgeInsets
1011

12+
/// Set false to not render the top divider.
13+
/// Useful when there is a content on top that has its own divider.
14+
///
15+
private(set) var topDivider: Bool = true
16+
1117
var body: some View {
1218
Divider()
19+
.renderedIf(topDivider)
1320

1421
VStack(alignment: .leading, spacing: .zero) {
1522
Text(viewModel.dateString)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
/// Banner to inform that an order is not editable.
5+
///
6+
struct NonEditableOrderBanner: UIViewRepresentable {
7+
typealias Callback = () -> ()
8+
9+
/// Desired `width` of the view.
10+
///
11+
private let width: CGFloat
12+
13+
/// Create a view with the desired `width`. Needed to calculate a correct view `height` later.
14+
///
15+
init(width: CGFloat) {
16+
self.width = width
17+
}
18+
19+
func makeCoordinator() -> Coordinator {
20+
Coordinator(bannerWrapper: TopBannerWrapperView())
21+
}
22+
23+
func makeUIView(context: Context) -> UIView {
24+
let expandButton = TopBannerViewModel.TopButtonType.chevron {
25+
context.coordinator.bannerWrapper.invalidateIntrinsicContentSize() // Forces the view to recalculate it's size as it collapses/expands
26+
}
27+
28+
let viewModel = TopBannerViewModel(title: Localization.title,
29+
infoText: Localization.description,
30+
icon: UIImage.lockImage,
31+
iconTintColor: .brand,
32+
isExpanded: false,
33+
topButton: expandButton)
34+
let mainBanner = TopBannerView(viewModel: viewModel)
35+
36+
// Set the current super view width and the real view to be displayed inside the wrapper.
37+
context.coordinator.bannerWrapper.width = width
38+
context.coordinator.bannerWrapper.setBanner(mainBanner)
39+
return context.coordinator.bannerWrapper
40+
}
41+
42+
func updateUIView(_ uiView: UIView, context: Context) {
43+
context.coordinator.bannerWrapper.width = width
44+
}
45+
}
46+
47+
// MARK: Coordinator
48+
extension NonEditableOrderBanner {
49+
/// Hold state across `SwiftUI` lifecycle passes.
50+
///
51+
struct Coordinator {
52+
/// Banner wrapper that will contain a `TopBannerView`.
53+
///
54+
let bannerWrapper: TopBannerWrapperView
55+
}
56+
}
57+
58+
// MARK: Localization
59+
private extension NonEditableOrderBanner {
60+
enum Localization {
61+
static let title = NSLocalizedString("Parts of this order are not currently editable", comment: "Title of the banner when the order is not editable")
62+
static let description = NSLocalizedString("To edit Products or Payment Details, change the status to Pending Payment.",
63+
comment: "Content of the banner when the order is not editable")
64+
}
65+
}

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,7 @@
562562
26CCBE0B2523B3650073F94D /* RefundProductsTotalTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CCBE0A2523B3650073F94D /* RefundProductsTotalTableViewCell.swift */; };
563563
26CCBE0D2523C2560073F94D /* RefundProductsTotalTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 26CCBE0C2523C2560073F94D /* RefundProductsTotalTableViewCell.xib */; };
564564
26CFDB2727357E8000AB940B /* SimplePaymentsSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26CFDB2627357E8000AB940B /* SimplePaymentsSummary.swift */; };
565+
26DB7E3528636D2200506173 /* NonEditableOrderBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26DB7E3428636D2200506173 /* NonEditableOrderBanner.swift */; };
565566
26E0ADF12631D94D00A5EB3B /* TopBannerWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E0ADF02631D94D00A5EB3B /* TopBannerWrapperView.swift */; };
566567
26E0AE13263359F900A5EB3B /* View+Conditionals.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E0AE12263359F900A5EB3B /* View+Conditionals.swift */; };
567568
26E0AE1926335AA900A5EB3B /* Survey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26E0AE1826335AA900A5EB3B /* Survey.swift */; };
@@ -2328,6 +2329,7 @@
23282329
26CCBE0A2523B3650073F94D /* RefundProductsTotalTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefundProductsTotalTableViewCell.swift; sourceTree = "<group>"; };
23292330
26CCBE0C2523C2560073F94D /* RefundProductsTotalTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefundProductsTotalTableViewCell.xib; sourceTree = "<group>"; };
23302331
26CFDB2627357E8000AB940B /* SimplePaymentsSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplePaymentsSummary.swift; sourceTree = "<group>"; };
2332+
26DB7E3428636D2200506173 /* NonEditableOrderBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonEditableOrderBanner.swift; sourceTree = "<group>"; };
23312333
26E0ADF02631D94D00A5EB3B /* TopBannerWrapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopBannerWrapperView.swift; sourceTree = "<group>"; };
23322334
26E0AE12263359F900A5EB3B /* View+Conditionals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Conditionals.swift"; sourceTree = "<group>"; };
23332335
26E0AE1826335AA900A5EB3B /* Survey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Survey.swift; sourceTree = "<group>"; };
@@ -4908,6 +4910,22 @@
49084910
path = CountrySelector;
49094911
sourceTree = "<group>";
49104912
};
4913+
26DB7E3228636CF200506173 /* Order Edition */ = {
4914+
isa = PBXGroup;
4915+
children = (
4916+
26DB7E3328636D1300506173 /* Banners */,
4917+
);
4918+
path = "Order Edition";
4919+
sourceTree = "<group>";
4920+
};
4921+
26DB7E3328636D1300506173 /* Banners */ = {
4922+
isa = PBXGroup;
4923+
children = (
4924+
26DB7E3428636D2200506173 /* NonEditableOrderBanner.swift */,
4925+
);
4926+
path = Banners;
4927+
sourceTree = "<group>";
4928+
};
49114929
26E1BEC7251BE50C0096D0A1 /* Issue Refunds */ = {
49124930
isa = PBXGroup;
49134931
children = (
@@ -7032,6 +7050,7 @@
70327050
02C1853927FED8BE00ABD764 /* Refund */,
70337051
2678897A270E6E3C00BD249E /* Simple Payments */,
70347052
CCFC50532743BBBF001E505F /* Order Creation */,
7053+
26DB7E3228636CF200506173 /* Order Edition */,
70357054
0206483923FA4160008441BB /* OrdersRootViewController.swift */,
70367055
45B0DF39273C20120026DC61 /* OrdersRootViewController.xib */,
70377056
57C5FF7925091A350074EC26 /* OrderListSyncActionUseCase.swift */,
@@ -9126,6 +9145,7 @@
91269145
D8652E582630BFF500350F37 /* OrderDetailsPaymentAlerts.swift in Sources */,
91279146
B5A56BF0219F2CE90065A902 /* VerticalButton.swift in Sources */,
91289147
D831E2DC230E0558000037D0 /* Authentication.swift in Sources */,
9148+
26DB7E3528636D2200506173 /* NonEditableOrderBanner.swift in Sources */,
91299149
314DC4BF268D183600444C9E /* CardReaderSettingsKnownReaderStorage.swift in Sources */,
91309150
2662D90826E15D6E00E25611 /* CountrySelectorCommand.swift in Sources */,
91319151
024A543422BA6F8F00F4F38E /* DeveloperEmailChecker.swift in Sources */,

WooCommerce/WooCommerceTests/Extensions/IconsTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,4 +648,8 @@ final class IconsTests: XCTestCase {
648648
func test_rectangle_on_rectangle_angled_is_not_nil() {
649649
XCTAssertNotNil(UIImage.rectangleOnRectangleAngled)
650650
}
651+
652+
func test_lock_icon_is_not_nil() {
653+
XCTAssertNotNil(UIImage.lockImage)
654+
}
651655
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,36 @@ final class EditableOrderViewModelTests: XCTestCase {
11161116
// Then
11171117
XCTAssertTrue(isCallbackCalled)
11181118
}
1119+
1120+
func test_creating_order_does_not_shows_banner() {
1121+
// Given
1122+
let viewModel = EditableOrderViewModel(siteID: sampleSiteID)
1123+
1124+
// When & Then
1125+
XCTAssertFalse(viewModel.shouldShowNonEditableBanner)
1126+
}
1127+
1128+
func test_editing_a_non_editable_order_shows_banner() {
1129+
// Given
1130+
let order = Order.fake().copy(isEditable: false)
1131+
1132+
// When
1133+
let viewModel = EditableOrderViewModel(siteID: sampleSiteID, flow: .editing(initialOrder: order))
1134+
1135+
// Then
1136+
XCTAssertTrue(viewModel.shouldShowNonEditableBanner)
1137+
}
1138+
1139+
func test_editing_an_editable_order_does_not_shows_banner() {
1140+
// Given
1141+
let order = Order.fake().copy(isEditable: true)
1142+
1143+
// When
1144+
let viewModel = EditableOrderViewModel(siteID: sampleSiteID, flow: .editing(initialOrder: order))
1145+
1146+
// Then
1147+
XCTAssertFalse(viewModel.shouldShowNonEditableBanner)
1148+
}
11191149
}
11201150

11211151
private extension MockStorageManager {

Yosemite/Yosemite/Stores/OrderStore.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,11 +355,12 @@ private extension OrderStore {
355355
/// Optimistically update the Status
356356
let oldStatus = updateOrderStatus(siteID: siteID, orderID: orderID, statusKey: status)
357357

358-
remote.updateOrder(from: siteID, orderID: orderID, statusKey: status) { [weak self] (_, error) in
358+
remote.updateOrder(from: siteID, orderID: orderID, statusKey: status) { [weak self] (order, error) in
359359
guard let error = error else {
360-
// NOTE: We're *not* actually updating the whole entity here. Reason: Prevent UI inconsistencies!!
361-
onCompletion(nil)
362-
return
360+
if let order = order {
361+
self?.upsertStoredOrder(readOnlyOrder: order)
362+
}
363+
return onCompletion(nil)
363364
}
364365

365366
/// Revert Optimistic Update

0 commit comments

Comments
 (0)