Skip to content

Commit b12e18c

Browse files
authored
Merge pull request #3890 from woocommerce/issue/2974-displays-a-list-of-products-associated-with-the-order-under-a-single-package
Shipping Labels: displays a list of products associated with the order under a single package
2 parents a0ebe53 + 8ad6fd7 commit b12e18c

File tree

13 files changed

+567
-10
lines changed

13 files changed

+567
-10
lines changed

Networking/NetworkingTests/Mapper/ShippingLabelPackagesMapperTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ final class ShippingLabelPackagesMapperTests: XCTestCase {
1313

1414
XCTAssertEqual(shippingLabelPackages.storeOptions, sampleShippingLabelStoreOptions())
1515
XCTAssertEqual(shippingLabelPackages.customPackages, sampleShippingLabelCustomPackages())
16-
XCTAssertEqual(shippingLabelPackages.predefinedOptions, sampleShippingLabelPredefinedOptions())
16+
XCTAssertTrue(shippingLabelPackages.predefinedOptions.contains(sampleShippingLabelPredefinedOptions().first!))
17+
XCTAssertTrue(shippingLabelPackages.predefinedOptions.contains(sampleShippingLabelPredefinedOptions()[1]))
1718
}
1819
}
1920

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import SwiftUI
2+
import Yosemite
3+
4+
struct ShippingLabelPackageDetails: View {
5+
@State private var viewModel: ShippingLabelPackageDetailsViewModel
6+
7+
init(viewModel: ShippingLabelPackageDetailsViewModel) {
8+
_viewModel = State(initialValue: viewModel)
9+
}
10+
11+
var body: some View {
12+
ScrollView {
13+
VStack(spacing: 0) {
14+
ShippingLabelPackageNumberRow(packageNumber: 1, numberOfItems: viewModel.orderItems.count)
15+
16+
ListHeaderView(text: Localization.itemsToFulfillHeader, alignment: .left)
17+
.background(Color(.listBackground))
18+
19+
ForEach(viewModel.itemsRows) { productItemRow in
20+
Divider()
21+
productItemRow
22+
}
23+
24+
ListHeaderView(text: Localization.packageDetailsHeader, alignment: .left)
25+
.background(Color(.listBackground))
26+
27+
TitleAndValueRow(title: Localization.packageSelected, value: "To be implemented", selectable: true) {
28+
// TODO: open package selection screen
29+
print("Tapped")
30+
}
31+
32+
Divider()
33+
34+
TitleAndTextFieldRow(title: Localization.totalPackageWeight,
35+
placeholder: "0",
36+
text: .constant(""),
37+
symbol: "oz",
38+
keyboardType: .decimalPad)
39+
Divider()
40+
41+
ListHeaderView(text: Localization.footer, alignment: .left)
42+
.background(Color(.listBackground))
43+
}
44+
.background(Color(.systemBackground))
45+
}
46+
.background(Color(.listBackground))
47+
}
48+
}
49+
50+
private extension ShippingLabelPackageDetails {
51+
enum Localization {
52+
static let itemsToFulfillHeader = NSLocalizedString("ITEMS TO FULFILL", comment: "Header section items to fulfill in Shipping Label Package Detail")
53+
static let packageDetailsHeader = NSLocalizedString("PACKAGE DETAILS", comment: "Header section package details in Shipping Label Package Detail")
54+
static let packageSelected = NSLocalizedString("Package Selected",
55+
comment: "Title of the row for selecting a package in Shipping Label Package Detail screen")
56+
static let totalPackageWeight = NSLocalizedString("Total package weight",
57+
comment: "Title of the row for adding the package weight in Shipping Label Package Detail screen")
58+
static let footer = NSLocalizedString("Sum of products and package weight",
59+
comment: "Title of the footer in Shipping Label Package Detail screen")
60+
}
61+
}
62+
63+
struct ShippingLabelPackageDetails_Previews: PreviewProvider {
64+
65+
static var previews: some View {
66+
67+
let viewModel = ShippingLabelPackageDetailsViewModel(items: ShippingLabelPackageDetails_Previews.sampleItems(),
68+
currency: ShippingLabelPackageDetails_Previews.sampleCurrency())
69+
70+
ShippingLabelPackageDetails(viewModel: viewModel)
71+
.environment(\.colorScheme, .light)
72+
.previewDisplayName("Light")
73+
74+
ShippingLabelPackageDetails(viewModel: viewModel)
75+
.environment(\.colorScheme, .dark)
76+
.previewDisplayName("Dark")
77+
}
78+
}
79+
80+
81+
// MARK: - Private Methods
82+
//
83+
private extension ShippingLabelPackageDetails_Previews {
84+
85+
static func sampleItems() -> [OrderItem] {
86+
let item1 = OrderItem(itemID: 890,
87+
name: "Fruits Basket (Mix & Match Product)",
88+
productID: 52,
89+
variationID: 0,
90+
quantity: 2,
91+
price: NSDecimalNumber(integerLiteral: 30),
92+
sku: "",
93+
subtotal: "50.00",
94+
subtotalTax: "2.00",
95+
taxClass: "",
96+
taxes: [.init(taxID: 1, subtotal: "2", total: "1.2")],
97+
total: "30.00",
98+
totalTax: "1.20",
99+
attributes: [])
100+
101+
let item2 = OrderItem(itemID: 891,
102+
name: "Fruits Bundle",
103+
productID: 234,
104+
variationID: 0,
105+
quantity: 1.5,
106+
price: NSDecimalNumber(integerLiteral: 0),
107+
sku: "5555-A",
108+
subtotal: "10.00",
109+
subtotalTax: "0.40",
110+
taxClass: "",
111+
taxes: [.init(taxID: 1, subtotal: "0.4", total: "0")],
112+
total: "0.00",
113+
totalTax: "0.00",
114+
attributes: [])
115+
116+
return [item1, item2]
117+
}
118+
119+
static func sampleCurrency() -> String {
120+
return "USD"
121+
}
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import SwiftUI
2+
import UIKit
3+
import Yosemite
4+
5+
/// Displays the Shipping Label Package Details
6+
final class ShippingLabelPackageDetailsViewController: UIHostingController<ShippingLabelPackageDetails> {
7+
init(items: [OrderItem], currency: String) {
8+
let viewModel = ShippingLabelPackageDetailsViewModel(items: items, currency: currency)
9+
super.init(rootView: ShippingLabelPackageDetails(viewModel: viewModel))
10+
configureNavigationBar()
11+
}
12+
13+
required dynamic init?(coder aDecoder: NSCoder) {
14+
fatalError("init(coder:) has not been implemented")
15+
}
16+
}
17+
18+
private extension ShippingLabelPackageDetailsViewController {
19+
func configureNavigationBar() {
20+
navigationItem.title = Localization.navigationBarTitle
21+
}
22+
}
23+
24+
private extension ShippingLabelPackageDetailsViewController {
25+
enum Localization {
26+
static let navigationBarTitle =
27+
NSLocalizedString("Package Details",
28+
comment: "Navigation bar title of shipping label package details screen")
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import UIKit
2+
import Yosemite
3+
4+
/// View model for `ShippingLabelPackageDetails`.
5+
///
6+
struct ShippingLabelPackageDetailsViewModel {
7+
8+
let orderItems: [OrderItem]
9+
private let currency: String
10+
private let currencyFormatter: CurrencyFormatter
11+
12+
var itemsRows: [ItemToFulfillRow] {
13+
orderItems.map { (item) -> ItemToFulfillRow in
14+
let positivePrice = item.price.abs()
15+
let positiveQuantity = abs(item.quantity)
16+
let quantity = NumberFormatter.localizedString(from: positiveQuantity as NSDecimalNumber, number: .decimal)
17+
let itemPrice = currencyFormatter.formatAmount(positivePrice, with: currency) ?? String()
18+
let attributes = item.attributes.map { VariationAttributeViewModel(orderItemAttribute: $0) }
19+
let subtitle = Localization.subtitle(quantity: quantity, price: itemPrice, attributes: attributes)
20+
return ItemToFulfillRow(title: item.name, subtitle: subtitle)
21+
}
22+
}
23+
24+
init(items: [OrderItem], currency: String, formatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) {
25+
self.orderItems = items
26+
self.currency = currency
27+
self.currencyFormatter = formatter
28+
}
29+
}
30+
31+
private extension ShippingLabelPackageDetailsViewModel {
32+
enum Localization {
33+
static let subtitleFormat =
34+
NSLocalizedString("%1$@ x %2$@", comment: "In Shipping Labels Package Details,"
35+
+ " the pattern used to show the quantity multiplied by the price. For example, “23 x $400.00”."
36+
+ " The %1$@ is the quantity. The %2$@ is the formatted price with currency (e.g. $400.00).")
37+
static let subtitleWithAttributesFormat =
38+
NSLocalizedString("%1$@・%2$@ x %3$@", comment: "In Shipping Labels Package Details if the product has attributes,"
39+
+ " the pattern used to show the attributes and quantity multiplied by the price. For example, “purple, has logo・23 x $400.00”."
40+
+ " The %1$@ is the list of attributes (e.g. from variation)."
41+
+ " The %2$@ is the quantity. The %3$@ is the formatted price with currency (e.g. $400.00).")
42+
static func subtitle(quantity: String, price: String, attributes: [VariationAttributeViewModel]) -> String {
43+
let attributesText = attributes.map { $0.nameOrValue }.joined(separator: ", ")
44+
if attributes.isEmpty {
45+
return String.localizedStringWithFormat(subtitleFormat, quantity, price)
46+
} else {
47+
return String.localizedStringWithFormat(subtitleWithAttributesFormat, attributesText, quantity, price)
48+
}
49+
}
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import SwiftUI
2+
import Foundation
3+
4+
struct ShippingLabelPackageNumberRow: View {
5+
let packageNumber: Int
6+
let numberOfItems: Int
7+
8+
init(packageNumber: Int, numberOfItems: Int) {
9+
self.packageNumber = packageNumber
10+
self.numberOfItems = numberOfItems
11+
}
12+
13+
var body: some View {
14+
HStack {
15+
Text(String(format: Localization.package, packageNumber))
16+
.font(.headline)
17+
Text(String(format: Localization.numberOfItems, numberOfItems))
18+
.font(.body)
19+
Spacer()
20+
}
21+
.frame(height: Constants.height)
22+
.padding([.leading, .trailing], Constants.padding)
23+
}
24+
}
25+
26+
private extension ShippingLabelPackageNumberRow {
27+
enum Localization {
28+
static let package = NSLocalizedString("Package %1$d", comment: "Package term in Shipping Labels. Reads like Package 1")
29+
static let numberOfItems = NSLocalizedString("- %1$d items", comment: "Number of items in packages in Shipping Labels. Reads like - 10 items")
30+
}
31+
32+
enum Constants {
33+
static let height: CGFloat = 44
34+
static let padding: CGFloat = 44
35+
}
36+
}
37+
38+
39+
struct ShippingLabelPackageNumberRow_Previews: PreviewProvider {
40+
static var previews: some View {
41+
ShippingLabelPackageNumberRow(packageNumber: 1, numberOfItems: 10)
42+
.previewLayout(.fixed(width: 375, height: 50))
43+
44+
ShippingLabelPackageNumberRow(packageNumber: 7, numberOfItems: 1)
45+
.previewLayout(.fixed(width: 375, height: 50))
46+
}
47+
}

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewController.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ final class ShippingLabelFormViewController: UIViewController {
1212
init(order: Order) {
1313
viewModel = ShippingLabelFormViewModel(siteID: order.siteID,
1414
originAddress: nil,
15-
destinationAddress: order.shippingAddress)
15+
destinationAddress: order.shippingAddress,
16+
items: order.items,
17+
currency: order.currency)
1618
super.init(nibName: nil, bundle: nil)
1719
}
1820

@@ -185,8 +187,8 @@ private extension ShippingLabelFormViewController {
185187
icon: .productPlaceholderImage,
186188
title: Localization.packageDetailsCellTitle,
187189
body: "To be implemented",
188-
buttonTitle: Localization.continueButtonInCells) {
189-
190+
buttonTitle: Localization.continueButtonInCells) { [weak self] in
191+
self?.displayPackageDetailsVC()
190192
}
191193
}
192194

@@ -249,6 +251,11 @@ private extension ShippingLabelFormViewController {
249251
}
250252
navigationController?.pushViewController(vc, animated: true)
251253
}
254+
255+
func displayPackageDetailsVC() {
256+
let vc = ShippingLabelPackageDetailsViewController(items: viewModel.orderItems, currency: viewModel.currency)
257+
navigationController?.show(vc, sender: nil)
258+
}
252259
}
253260

254261
extension ShippingLabelFormViewController {

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/ShippingLabelFormViewModel.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ final class ShippingLabelFormViewModel {
2323
let siteID: Int64
2424
private(set) var originAddress: ShippingLabelAddress?
2525
private(set) var destinationAddress: ShippingLabelAddress?
26+
let orderItems: [OrderItem]
27+
let currency: String
2628

2729
private let stores: StoresManager
2830

@@ -38,9 +40,16 @@ final class ShippingLabelFormViewModel {
3840
}
3941
}
4042

41-
init(siteID: Int64, originAddress: Address?, destinationAddress: Address?, stores: StoresManager = ServiceLocator.stores) {
43+
init(siteID: Int64,
44+
originAddress: Address?,
45+
destinationAddress: Address?,
46+
items: [OrderItem],
47+
currency: String,
48+
stores: StoresManager = ServiceLocator.stores) {
4249

4350
self.siteID = siteID
51+
self.orderItems = items
52+
self.currency = currency
4453

4554
let accountSettings = ShippingLabelFormViewModel.getStoredAccountSettings()
4655
let company = ServiceLocator.stores.sessionManager.defaultSite?.name
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import SwiftUI
2+
3+
/// Represent a row of a Product Item that should be fulfilled
4+
///
5+
struct ItemToFulfillRow: View, Identifiable {
6+
let id = UUID()
7+
let title: String
8+
let subtitle: String
9+
10+
var body: some View {
11+
HStack {
12+
VStack(alignment: .leading,
13+
spacing: 8) {
14+
Text(title)
15+
.font(.body)
16+
Text(subtitle)
17+
.font(.footnote)
18+
.foregroundColor(Color(.textSubtle))
19+
}.padding([.leading, .trailing], Constants.vStackPadding)
20+
Spacer()
21+
}
22+
.padding([.top, .bottom], Constants.hStackPadding)
23+
.frame(minHeight: Constants.height)
24+
}
25+
}
26+
27+
private extension ItemToFulfillRow {
28+
enum Constants {
29+
static let vStackPadding: CGFloat = 16
30+
static let hStackPadding: CGFloat = 10
31+
static let height: CGFloat = 64
32+
}
33+
}
34+
35+
struct ItemToFulfillRow_Previews: PreviewProvider {
36+
static var previews: some View {
37+
ItemToFulfillRow(title: "Title", subtitle: "My subtitle")
38+
.previewLayout(.fixed(width: 375, height: 100))
39+
}
40+
}

0 commit comments

Comments
 (0)