Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Shipping labels] Add view models to provide data from order items to items section #13962

Merged
merged 7 commits into from
Sep 13, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,8 @@ private extension OrderDetailsViewController {
return
}

let shippingLabelCreationVC = WooShippingCreateLabelsViewHostingController()
let shippingLabelCreationVM = WooShippingCreateLabelsViewModel(order: viewModel.order)
let shippingLabelCreationVC = WooShippingCreateLabelsViewHostingController(viewModel: shippingLabelCreationVM)
shippingLabelCreationVC.modalPresentationStyle = .overFullScreen
navigationController?.present(shippingLabelCreationVC, animated: true)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Foundation

/// Provides view data for `WooShippingItemRow`.
///
struct WooShippingItemRowViewModel: Identifiable {
/// Unique ID for the item row
let id = UUID()

/// URL for item image
let imageUrl: URL?

/// Label for item quantity
let quantityLabel: String

/// Item name
let name: String

/// Label for item details
let detailsLabel: String

/// Label for item weight
let weightLabel: String

/// Label for item price
let priceLabel: String
}

/// Convenience extension to provide data to `WooShippingItemRow`
extension WooShippingItemRow {
init(viewModel: WooShippingItemRowViewModel) {
self.imageUrl = viewModel.imageUrl
self.quantityLabel = viewModel.quantityLabel
self.name = viewModel.name
self.detailsLabel = viewModel.detailsLabel
self.weightLabel = viewModel.weightLabel
self.priceLabel = viewModel.priceLabel
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ struct WooShippingItems: View {
/// Label for the total item details
let itemsDetailLabel: String

/// View models for items to ship
let items: [WooShippingItemRowViewModel]

/// Whether the item list is collapsed
@State private var isCollapsed: Bool = true

Expand All @@ -25,22 +28,11 @@ struct WooShippingItems: View {
},
content: {
VStack {
WooShippingItemRow(imageUrl: nil,
quantityLabel: "3",
name: "Little Nap Brazil 250g",
detailsLabel: "15×10×8cm • Espresso",
weightLabel: "275g",
priceLabel: "$60.00")
.padding()
.roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderWidth)
WooShippingItemRow(imageUrl: nil,
quantityLabel: "3",
name: "Little Nap Brazil 250g",
detailsLabel: "15×10×8cm • Espresso",
weightLabel: "275g",
priceLabel: "$60.00")
.padding()
.roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderWidth)
ForEach(items) { item in
WooShippingItemRow(viewModel: item)
.padding()
.roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderWidth)
}
}
})
.padding()
Expand All @@ -60,5 +52,18 @@ private extension WooShippingItems {
}

#Preview {
WooShippingItems(itemsCountLabel: "6 items", itemsDetailLabel: "825g · $135.00")
WooShippingItems(itemsCountLabel: "6 items",
itemsDetailLabel: "825g · $135.00",
items: [WooShippingItemRowViewModel(imageUrl: nil,
quantityLabel: "3",
name: "Little Nap Brazil 250g",
detailsLabel: "15×10×8cm • Espresso",
weightLabel: "275g",
priceLabel: "$60.00"),
WooShippingItemRowViewModel(imageUrl: nil,
quantityLabel: "3",
name: "Little Nap Brazil 250g",
detailsLabel: "15×10×8cm • Espresso",
weightLabel: "275g",
priceLabel: "$60.00")])
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import Foundation
import Yosemite
import WooFoundation

/// Provides view data for `WooShippingItems`.
///
final class WooShippingItemsViewModel: ObservableObject {
private let orderItems: [OrderItem]
private let currencyFormatter: CurrencyFormatter

/// Label with the total number of items to ship.
@Published var itemsCountLabel: String = ""

/// Label with the details of the items to ship.
@Published var itemsDetailLabel: String = ""

/// View models for rows of items to ship.
@Published var itemRows: [WooShippingItemRowViewModel] = []

init(orderItems: [OrderItem],
currencySettings: CurrencySettings = ServiceLocator.currencySettings) {
self.orderItems = orderItems
self.currencyFormatter = CurrencyFormatter(currencySettings: currencySettings)

configureSectionHeader()
configureItemRows()
}
}

private extension WooShippingItemsViewModel {
/// Configures the labels in the section header.
///
func configureSectionHeader() {
itemsCountLabel = generateItemsCountLabel()
itemsDetailLabel = generateItemsDetailLabel()
}

/// Configures the item rows.
///
func configureItemRows() {
itemRows = generateItemRows()
}

/// Generates a label with the total number of items to ship.
///
func generateItemsCountLabel() -> String {
let itemsCount = orderItems.map(\.quantity).reduce(0, +)
return Localization.itemsCount(itemsCount)
}

/// Generates a label with the details of the items to ship.
/// This includes the total weight and total price of all items.
///
func generateItemsDetailLabel() -> String {
let formattedWeight = "1 kg" // TODO-13550: Get the total weight (each product/variation * item quantity) and weight unit
let itemsTotal = orderItems.map { $0.price.decimalValue * $0.quantity }.reduce(0, +)
let formattedPrice = currencyFormatter.formatAmount(itemsTotal) ?? itemsTotal.description

return "\(formattedWeight) • \(formattedPrice)"
}

/// Generates an item row view model for each order item.
///
func generateItemRows() -> [WooShippingItemRowViewModel] {
orderItems.map { item in
WooShippingItemRowViewModel(imageUrl: nil, // TODO-13550: Get the product/variation imageURL
quantityLabel: item.quantity.description,
name: item.name,
detailsLabel: generateItemRowDetailsLabel(for: item),
weightLabel: "", // TODO-13550: Get the product/variation weight
priceLabel: currencyFormatter.formatAmount(item.price.decimalValue) ?? item.price.description)
}
}

/// Generates a details label for an item row.
///
func generateItemRowDetailsLabel(for item: OrderItem) -> String {
let formattedDimensions: String? = nil // TODO-13550: Get the product/variation dimensions

let attributes: String? = {
guard item.attributes.isNotEmpty else {
return nil
}
return item.attributes.map { VariationAttributeViewModel(orderItemAttribute: $0) }.map(\.nameOrValue).joined(separator: ", ")
}()

return [formattedDimensions, attributes].compacted().joined(separator: " • ")
}
}

// MARK: Constants
private extension WooShippingItemsViewModel {
enum Localization {
static func itemsCount(_ count: Decimal) -> String {
let formattedCount = NumberFormatter.localizedString(from: count as NSDecimalNumber, number: .decimal)
return String(format: Localization.itemsCountFormat, formattedCount)
}
static let itemsCountFormat = NSLocalizedString("wooShipping.createLabels.items.count",
value: "%1$@ items",
comment: "Total number of items to ship during shipping label creation.")
}
}

/// Convenience extension to provide data to `WooShippingItemRow`
extension WooShippingItems {
init(viewModel: WooShippingItemsViewModel) {
self.itemsCountLabel = viewModel.itemsCountLabel
self.itemsDetailLabel = viewModel.itemsDetailLabel
self.items = viewModel.itemRows
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import SwiftUI
/// Hosting controller for `WooShippingCreateLabelsView`.
///
final class WooShippingCreateLabelsViewHostingController: UIHostingController<WooShippingCreateLabelsView> {
init() {
super.init(rootView: WooShippingCreateLabelsView())
let viewModel: WooShippingCreateLabelsViewModel

init(viewModel: WooShippingCreateLabelsViewModel) {
self.viewModel = viewModel
super.init(rootView: WooShippingCreateLabelsView(viewModel: viewModel))
}

required dynamic init?(coder aDecoder: NSCoder) {
Expand All @@ -15,12 +18,14 @@ final class WooShippingCreateLabelsViewHostingController: UIHostingController<Wo
/// View to create shipping labels with the Woo Shipping extension.
///
struct WooShippingCreateLabelsView: View {
@ObservedObject var viewModel: WooShippingCreateLabelsViewModel

@Environment(\.dismiss) private var dismiss

var body: some View {
NavigationStack {
ScrollView {
WooShippingItems(itemsCountLabel: "6 items", itemsDetailLabel: "825g · $135.00")
WooShippingItems(viewModel: viewModel.items)
.padding()
}
.navigationTitle(Localization.title)
Expand All @@ -46,7 +51,3 @@ private extension WooShippingCreateLabelsView {
comment: "Title of the button to dismiss the shipping label creation screen")
}
}

#Preview {
WooShippingCreateLabelsView()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation
import Yosemite

/// Provides view data for `WooShippingCreateLabelsView`.
///
final class WooShippingCreateLabelsViewModel: ObservableObject {
/// View model for the items to ship.
@Published private(set) var items: WooShippingItemsViewModel

init(order: Order) {
self.items = WooShippingItemsViewModel(orderItems: order.items)
}
}
24 changes: 24 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2274,6 +2274,10 @@
CEA7C32D2C47C9BD00528450 /* GoogleAdsCampaignStatsTotals+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA7C32C2C47C9BD00528450 /* GoogleAdsCampaignStatsTotals+UI.swift */; };
CEA9C8E02B6D323A000FE114 /* AnalyticsWebReportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA9C8DF2B6D323A000FE114 /* AnalyticsWebReportTests.swift */; };
CEAB739C2C81E3F600A7EB39 /* WooShippingCreateLabelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEAB739B2C81E3F600A7EB39 /* WooShippingCreateLabelsView.swift */; };
CEC3CC6B2C92FDB700B93FBE /* WooShippingItemRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC6A2C92FDB700B93FBE /* WooShippingItemRowViewModel.swift */; };
CEC3CC6D2C93127300B93FBE /* WooShippingItemsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC6C2C93127300B93FBE /* WooShippingItemsViewModel.swift */; };
CEC3CC6F2C93146700B93FBE /* WooShippingCreateLabelsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC6E2C93146700B93FBE /* WooShippingCreateLabelsViewModel.swift */; };
CEC3CC742C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC3CC732C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift */; };
CEC8188C2A3B7C8B00459843 /* AppStartupWaitingTimeTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8188B2A3B7C8B00459843 /* AppStartupWaitingTimeTracker.swift */; };
CEC8188E2A3C75DD00459843 /* AppStartupWaitingTimeTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC8188D2A3C75DD00459843 /* AppStartupWaitingTimeTrackerTests.swift */; };
CECC758623D21AC200486676 /* AggregateOrderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CECC758523D21AC200486676 /* AggregateOrderItem.swift */; };
Expand Down Expand Up @@ -5314,6 +5318,10 @@
CEA7C32C2C47C9BD00528450 /* GoogleAdsCampaignStatsTotals+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GoogleAdsCampaignStatsTotals+UI.swift"; sourceTree = "<group>"; };
CEA9C8DF2B6D323A000FE114 /* AnalyticsWebReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsWebReportTests.swift; sourceTree = "<group>"; };
CEAB739B2C81E3F600A7EB39 /* WooShippingCreateLabelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingCreateLabelsView.swift; sourceTree = "<group>"; };
CEC3CC6A2C92FDB700B93FBE /* WooShippingItemRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingItemRowViewModel.swift; sourceTree = "<group>"; };
CEC3CC6C2C93127300B93FBE /* WooShippingItemsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingItemsViewModel.swift; sourceTree = "<group>"; };
CEC3CC6E2C93146700B93FBE /* WooShippingCreateLabelsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingCreateLabelsViewModel.swift; sourceTree = "<group>"; };
CEC3CC732C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingItemsViewModelTests.swift; sourceTree = "<group>"; };
CEC8188B2A3B7C8B00459843 /* AppStartupWaitingTimeTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStartupWaitingTimeTracker.swift; sourceTree = "<group>"; };
CEC8188D2A3C75DD00459843 /* AppStartupWaitingTimeTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStartupWaitingTimeTrackerTests.swift; sourceTree = "<group>"; };
CECA64B020D9990E005A44C4 /* WooCommerce-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WooCommerce-Bridging-Header.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7486,6 +7494,7 @@
02F67FF325806DF000C3BAD2 /* Shipping Label */ = {
isa = PBXGroup;
children = (
CEC3CC722C9343BC00B93FBE /* WooShipping Create Shipping Labels */,
02464064258B122A00C10A7D /* Reprint Shipping Label */,
02F67FF425806E0100C3BAD2 /* ShippingLabelTrackingURLGeneratorTests.swift */,
027F240B258371150021DB06 /* RefundShippingLabelViewModelTests.swift */,
Expand Down Expand Up @@ -11739,7 +11748,9 @@
isa = PBXGroup;
children = (
CE6E110A2C91DA5D00563DD4 /* WooShippingItemRow.swift */,
CEC3CC6A2C92FDB700B93FBE /* WooShippingItemRowViewModel.swift */,
CE6E110C2C91E5FF00563DD4 /* WooShippingItems.swift */,
CEC3CC6C2C93127300B93FBE /* WooShippingItemsViewModel.swift */,
);
path = "WooShipping Items Section";
sourceTree = "<group>";
Expand Down Expand Up @@ -11841,6 +11852,15 @@
children = (
CE6E11092C91DA3D00563DD4 /* WooShipping Items Section */,
CEAB739B2C81E3F600A7EB39 /* WooShippingCreateLabelsView.swift */,
CEC3CC6E2C93146700B93FBE /* WooShippingCreateLabelsViewModel.swift */,
);
path = "WooShipping Create Shipping Labels";
sourceTree = "<group>";
};
CEC3CC722C9343BC00B93FBE /* WooShipping Create Shipping Labels */ = {
isa = PBXGroup;
children = (
CEC3CC732C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift */,
);
path = "WooShipping Create Shipping Labels";
sourceTree = "<group>";
Expand Down Expand Up @@ -15396,6 +15416,7 @@
02EA6BFA2435E92600FFF90A /* KingfisherImageDownloader+ImageDownloadable.swift in Sources */,
7E7C5F8F2719BA7300315B61 /* ProductCategoryCellViewModel.swift in Sources */,
DE8AA0B32BBE55E40084D2CC /* DashboardViewHostingController.swift in Sources */,
CEC3CC6F2C93146700B93FBE /* WooShippingCreateLabelsViewModel.swift in Sources */,
DEC2962326BD4E6E005A056B /* ShippingLabelCustomsFormInput.swift in Sources */,
E1E649EB28461EDF0070B194 /* BetaFeaturesConfiguration.swift in Sources */,
26309F17277D0AEA0012797F /* SafeAreaInsetsKey.swift in Sources */,
Expand Down Expand Up @@ -16065,6 +16086,7 @@
B6F3796C293794A000718561 /* AnalyticsHubYearToDateRangeData.swift in Sources */,
2667BFE52530DCF4008099D4 /* RefundItemsValuesCalculationUseCase.swift in Sources */,
203163BB2C1C5F72001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedUpdatePostalCodeView.swift in Sources */,
CEC3CC6B2C92FDB700B93FBE /* WooShippingItemRowViewModel.swift in Sources */,
AEE9A880293A3E5500227C92 /* RefreshablePlainList.swift in Sources */,
203163B72C1C5EDF001C96DA /* PointOfSaleCardPresentPaymentConnectingFailedChargeReaderView.swift in Sources */,
2004E2ED2C0F5DD800D62521 /* CardPresentPaymentCollectOrderPaymentUseCaseAdaptor.swift in Sources */,
Expand Down Expand Up @@ -16162,6 +16184,7 @@
CE0F17CF22A8105800964A63 /* ReadMoreTableViewCell.swift in Sources */,
02E3B62F2906322B007E0F13 /* AuthenticationFormFieldView.swift in Sources */,
029106C42BE34AA900C2248B /* CollapsibleCustomerCardViewModel.swift in Sources */,
CEC3CC6D2C93127300B93FBE /* WooShippingItemsViewModel.swift in Sources */,
03EF24FE28C0B356006A033E /* CardPresentPaymentsPlugin+CashOnDelivery.swift in Sources */,
DEC2961F26BD1605005A056B /* ShippingLabelCustomsFormListViewModel.swift in Sources */,
8625C5132BF20CC6007F1901 /* ReviewsDashboardCardViewModel.swift in Sources */,
Expand Down Expand Up @@ -16492,6 +16515,7 @@
EEA693602B23303A00BAECA6 /* ProductCreationAISurveyUseCaseTests.swift in Sources */,
DE4B3B2C2692DC2200EEF2D8 /* ReviewOrderViewModelTests.swift in Sources */,
D89C009425B4E9E2000E4683 /* ULAccountMismatchViewControllerTests.swift in Sources */,
CEC3CC742C9343DF00B93FBE /* WooShippingItemsViewModelTests.swift in Sources */,
26DDA4AB2C49627F005FBEBF /* DashboardTimestampStoreTests.swift in Sources */,
573A960324F433DD0091F3A5 /* ProductsTopBannerFactoryTests.swift in Sources */,
DEF657AA2C8AC25C00ACD61E /* BlazeCampaignObjectivePickerViewModelTests.swift in Sources */,
Expand Down
Loading