diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsViewModel.swift index cefaa4ea87f..5eb7f76993c 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsViewModel.swift @@ -10,7 +10,7 @@ final class WooShippingItemsViewModel: ObservableObject { private let currencySettings: CurrencySettings /// Data source for items to be shipped. - private var dataSource: WooShippingItemsDataSource + private(set) var dataSource: WooShippingItemsDataSource /// Label with the total number of items to ship. @Published private(set) var itemsCountLabel: String = "" diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCard.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCard.swift new file mode 100644 index 00000000000..da9e9817c75 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCard.swift @@ -0,0 +1,97 @@ +import Yosemite +import SwiftUI + +/// Displays a single collapsible shipment item row or grouped parent and child shipment item rows +struct CollapsibleShipmentCard: View { + @State private var isCollapsed: Bool = true + + private let viewModel: CollapsibleShipmentCardViewModel + + init(viewModel: CollapsibleShipmentCardViewModel) { + self.viewModel = viewModel + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + mainShipmentRow + .padding(.horizontal, Layout.horizontalPadding) + .padding(.vertical, Layout.verticalPadding) + .background( + mainShipmentRowBackground + ) + + if !isCollapsed { + VStack(spacing: 0) { + ForEach(Array(viewModel.childShipmentRows.enumerated()), id: \.element.id) { index, item in + VStack(spacing: 0) { + Divider() + + SelectableShipmentRow(viewModel: item) + .padding(.leading, Layout.horizontalPadding * 2) + .padding(.trailing, Layout.horizontalPadding) + .padding(.vertical, Layout.verticalPadding) + .background(backgroundForChildShipmentRow(isFinalRow: index == viewModel.childShipmentRows.count - 1)) + } + } + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + .roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderWidth) + } +} + +private extension CollapsibleShipmentCard { + @ViewBuilder + var mainShipmentRow: some View { + if viewModel.childShipmentRows.isEmpty { + SelectableShipmentRow(viewModel: viewModel.mainShipmentRow) + } else { + Button(action: { + withAnimation { + isCollapsed.toggle() + } + }, label: { + ZStack(alignment: .topTrailing) { + SelectableShipmentRow(viewModel: viewModel.mainShipmentRow) + .contentShape(Rectangle()) + + Image(uiImage: isCollapsed ? .chevronDownImage : .chevronUpImage) + .foregroundColor(Color(.accent)) + } + }) + .buttonStyle(PlainButtonStyle()) + } + } + + @ViewBuilder + var mainShipmentRowBackground: some View { + if isCollapsed { + RoundedRectangle(cornerRadius: Layout.borderCornerRadius) + .fill(Color(.listForeground(modal: false))) + } else { + UnevenRoundedRectangle(cornerRadii: .init(topLeading: Layout.borderCornerRadius, topTrailing: Layout.borderCornerRadius)) + .fill(Color(.listForeground(modal: false))) + } + } + + @ViewBuilder + func backgroundForChildShipmentRow(isFinalRow: Bool) -> some View { + if isFinalRow { + UnevenRoundedRectangle(cornerRadii: .init(bottomLeading: Layout.borderCornerRadius, bottomTrailing: Layout.borderCornerRadius)) + .fill(Color(.listForeground(modal: false))) + } else { + Color(.listForeground(modal: false)) + } + } +} + +private extension CollapsibleShipmentCard { + enum Layout { + static let borderCornerRadius: CGFloat = 8 + static let borderWidth: CGFloat = 0.5 + static let borderLineWidth: CGFloat = 1 + static let horizontalPadding: CGFloat = 16 + static let verticalPadding: CGFloat = 8 + } +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift new file mode 100644 index 00000000000..002cb367854 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift @@ -0,0 +1,60 @@ +import SwiftUI +import WooFoundation +import Yosemite + +/// View model for `CollapsibleShipmentCard`. +final class CollapsibleShipmentCardViewModel: ObservableObject, Identifiable { + let id = UUID() + + /// The main shipment row. + let mainShipmentRow: SelectableShipmentRowViewModel + + /// Child shipment rows, if the shipment has more than one quantity + let childShipmentRows: [SelectableShipmentRowViewModel] + + init(parentShipmentId: String, + childShipmentIds: [String], + item: ShippingLabelPackageItem, + currency: String) { + let mainShippingItem = WooShippingItemRowViewModel(item: ShippingLabelPackageItem(copy: item, quantity: max(1.0, Decimal(childShipmentIds.count))), + currency: currency) + let childShippingItem = WooShippingItemRowViewModel(item: ShippingLabelPackageItem(copy: item, quantity: 1.0), + currency: currency) + + self.mainShipmentRow = SelectableShipmentRowViewModel(shipmentId: parentShipmentId, + isSelectable: true, + item: mainShippingItem, + showQuantity: true) + self.childShipmentRows = childShipmentIds.map({ + SelectableShipmentRowViewModel(shipmentId: $0, + isSelectable: true, + item: childShippingItem, + showQuantity: false) + }) + + observeSelection() + } + + func selectAll() { + mainShipmentRow.setSelected(true) + childShipmentRows.forEach({ $0.setSelected(true) }) + } +} + +private extension CollapsibleShipmentCardViewModel { + func observeSelection() { + mainShipmentRow.onSelectedChange = { [weak self] row in + guard let self else { return } + + childShipmentRows.forEach({ $0.setSelected(row.selected) }) + } + + childShipmentRows.forEach({ + $0.onSelectedChange = { [weak self] row in + guard let self else { return } + + mainShipmentRow.setSelected(false) + } + }) + } +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/SelectableShipmentRow.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/SelectableShipmentRow.swift new file mode 100644 index 00000000000..964554f35ab --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/SelectableShipmentRow.swift @@ -0,0 +1,96 @@ +import Yosemite +import SwiftUI + +/// Row for a selectable shipment item to ship with the Woo Shipping extension. +struct SelectableShipmentRow: View { + @ObservedObject private var viewModel: SelectableShipmentRowViewModel + + init(viewModel: SelectableShipmentRowViewModel) { + self.viewModel = viewModel + } + + @ScaledMetric private var scale: CGFloat = 1 + + var body: some View { + AdaptiveStack(spacing: Layout.horizontalSpacing) { + if viewModel.isSelectable { + selectionCircle(selected: viewModel.selected) + .contentShape(Rectangle()) + .onTapGesture { + viewModel.handleTap() + } + } + + ProductImageThumbnail(productImageURL: viewModel.item.imageUrl, + productImageSize: Layout.imageSize, + scale: scale, + productImageCornerRadius: Layout.imageCornerRadius, + foregroundColor: Color(UIColor.listSmallIcon)) + .overlay(alignment: .topTrailing) { + if viewModel.showQuantity { + BadgeView(text: viewModel.item.quantityLabel, + customizations: .init(textColor: .white, backgroundColor: .black), + backgroundShape: badgeStyle) + .offset(x: Layout.badgeOffset, y: -Layout.badgeOffset) + } + } + VStack(alignment: .leading) { + Text(viewModel.item.name) + .bodyStyle() + Text(viewModel.item.detailsLabel) + .subheadlineStyle() + AdaptiveStack(verticalAlignment: .lastTextBaseline) { + Text(viewModel.item.weightLabel) + .subheadlineStyle() + Spacer() + Text(viewModel.item.priceLabel) + .font(.subheadline) + .foregroundStyle(Color(.text)) + } + } + } + .frame(maxWidth: .infinity) + } +} + +private extension SelectableShipmentRow { + @ViewBuilder + func selectionCircle(selected: Bool) -> some View { + if selected { + Image(uiImage: .checkCircleImage.withRenderingMode(.alwaysTemplate)) + .foregroundStyle(Color(.primary)) + } else { + Image(uiImage: .checkEmptyCircleImage) + } + } + + /// Displays a different badge background shape based on the item quantity + /// Circular for 2-character quantities, rounded for 3-character quantities or more + var badgeStyle: BadgeView.BackgroundShape { + if viewModel.item.quantityLabel.count < 3 { + return .circle + } else { + return .roundedRectangle(cornerRadius: Layout.badgeOffset) + } + } +} + +private extension SelectableShipmentRow { + enum Layout { + static let horizontalSpacing: CGFloat = 16 + static let imageSize: CGFloat = 56.0 + static let imageCornerRadius: CGFloat = 4.0 + static let badgeOffset: CGFloat = 8.0 + } +} + +#Preview { + SelectableShipmentRow(viewModel: SelectableShipmentRowViewModel(shipmentId: "123", + isSelectable: false, + item: WooShippingItemRowViewModel(imageUrl: nil, + quantityLabel: "3", + name: "Little Nap Brazil 250g", + detailsLabel: "15×10×8cm • Espresso", + weightLabel: "275g", + priceLabel: "$60.00"))) +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/SelectableShipmentRowViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/SelectableShipmentRowViewModel.swift new file mode 100644 index 00000000000..071070de0a4 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/SelectableShipmentRowViewModel.swift @@ -0,0 +1,41 @@ +import SwiftUI +import WooFoundation +import Yosemite + +/// View model for `SelectableShipmentRow`. +final class SelectableShipmentRowViewModel: ObservableObject, Identifiable { + let id = UUID() + + let item: WooShippingItemRowViewModel + + @Published private(set) var selected: Bool = false + + let isSelectable: Bool + + let shipmentId: String + + let showQuantity: Bool + + var onSelectedChange: ((SelectableShipmentRowViewModel) -> Void)? + + init(shipmentId: String, + isSelectable: Bool, + item: WooShippingItemRowViewModel, + showQuantity: Bool = true) { + self.shipmentId = shipmentId + self.isSelectable = isSelectable + self.item = item + self.showQuantity = showQuantity + } + + func handleTap() { + selected = !selected + if let onSelectedChange { + onSelectedChange(self) + } + } + + func setSelected(_ selected: Bool) { + self.selected = selected + } +} diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift index 16b9ef6d2ed..e3565ddbc1a 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift @@ -1,4 +1,5 @@ import SwiftUI +import Yosemite struct WooShippingSplitShipmentsDetailView: View { @Environment(\.dismiss) private var dismiss @@ -17,11 +18,9 @@ struct WooShippingSplitShipmentsDetailView: View { .foregroundStyle(Color(.textSubtle)) } - VStack { - ForEach(viewModel.items) { item in - WooShippingItemRow(viewModel: item) - .padding() - .roundedBorder(cornerRadius: Layout.borderCornerRadius, lineColor: Color(.separator), lineWidth: Layout.borderWidth) + VStack(spacing: Layout.verticalSpacing) { + ForEach(viewModel.shipmentCardViewModels) { item in + CollapsibleShipmentCard(viewModel: item) } } } @@ -32,7 +31,7 @@ struct WooShippingSplitShipmentsDetailView: View { .toolbar { ToolbarItem(placement: .cancellationAction) { Button(Localization.selectAll) { - + viewModel.selectAll() } } ToolbarItem(placement: .confirmationAction) { @@ -50,6 +49,7 @@ private extension WooShippingSplitShipmentsDetailView { static let contentPadding: CGFloat = 16 static let borderCornerRadius: CGFloat = 8 static let borderWidth: CGFloat = 0.5 + static let verticalSpacing: CGFloat = 8 } enum Localization { static let title = NSLocalizedString( @@ -73,6 +73,16 @@ private extension WooShippingSplitShipmentsDetailView { #if DEBUG #Preview { WooShippingSplitShipmentsDetailView(viewModel: WooShippingSplitShipmentsViewModel(order: ShippingLabelSampleData.sampleOrder(), - config: ShippingLabelSampleData.sampleWooShippingConfig())) + config: ShippingLabelSampleData.sampleWooShippingConfig(), + items: [ShippingLabelPackageItem(productOrVariationID: 1, + name: "Shirt", + weight: 0.5, + quantity: 2, + value: 9.99, + dimensions: ProductDimensions(length: "", + width: "", + height: ""), + attributes: [], + imageURL: nil)])) } #endif diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsRow.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsRow.swift index f2faf50226b..1c011b3afc0 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsRow.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsRow.swift @@ -46,7 +46,17 @@ private extension WooShippingSplitShipmentsRow { #if DEBUG #Preview { WooShippingSplitShipmentsRow(viewModel: WooShippingSplitShipmentsViewModel(order: ShippingLabelSampleData.sampleOrder(), - config: ShippingLabelSampleData.sampleWooShippingConfig())) + config: ShippingLabelSampleData.sampleWooShippingConfig(), + items: [ShippingLabelPackageItem(productOrVariationID: 1, + name: "Shirt", + weight: 0.5, + quantity: 2, + value: 9.99, + dimensions: ProductDimensions(length: "", + width: "", + height: ""), + attributes: [], + imageURL: nil)])) .padding() } #endif diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift index 6fd43887c9b..8e0c5a12c52 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift @@ -2,34 +2,125 @@ import SwiftUI import Yosemite import WooFoundation +/// ViewModel for `WooShippingSplitShipmentsDetailView` final class WooShippingSplitShipmentsViewModel: ObservableObject { private let order: Order private let stores: StoresManager private let config: WooShippingConfig + private let items: [ShippingLabelPackageItem] + private let currencySettings: CurrencySettings + private let shippingSettingsService: ShippingSettingsService + + /// Label with the total number of items to ship. + @Published private(set) var itemsCountLabel: String = "" + + /// Label with the total price for all items in the shipment. + @Published private(set) var itemsPriceLabel = "" + + /// Label with the total weight for all items in the shipment. + private var itemsWeightLabel = "" + + /// Label with the details of the items to ship. + /// Includes total weight and total price for all items in the current shipment. + var itemsDetailLabel: String { + "\(itemsWeightLabel) • \(itemsPriceLabel)" + } + + @Published var selectedItemIDs: [String: [String]] = [:] + + let shipmentCardViewModels: [CollapsibleShipmentCardViewModel] - /// Label for the total number of items - let itemsCountLabel = "6 items" - - /// Label for the total item details - let itemsDetailLabel = "825g · $135.00" - - let items: [WooShippingItemRowViewModel] = [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")] init(order: Order, config: WooShippingConfig, - stores: StoresManager = ServiceLocator.stores) { + items: [ShippingLabelPackageItem], + stores: StoresManager = ServiceLocator.stores, + currencySettings: CurrencySettings = ServiceLocator.currencySettings, + shippingSettingsService: ShippingSettingsService = ServiceLocator.shippingSettingsService) { self.order = order self.config = config + self.items = items self.stores = stores + self.currencySettings = currencySettings + self.shippingSettingsService = shippingSettingsService + + self.shipmentCardViewModels = { + var viewModels = [CollapsibleShipmentCardViewModel]() + for item in items { + // TODO: #15303 Set IDs based on web logic + let childShipmentIds: [String] = { + guard item.quantity > 1 else { + return [] + } + + var children: [String] = [] + for quantity in 0.. String { + let totalWeight = items + .map { item in + item.weight * Double(truncating: item.quantity as NSDecimalNumber) + } + .reduce(0, +) + let weightFormatter = WeightFormatter(weightUnit: shippingSettingsService.weightUnit ?? "") + return weightFormatter.formatWeight(weight: totalWeight) + } + + /// Calculates and formats the price of the given item based on the item quantity and unit price. + /// + func formatPrice(for items: [ShippingLabelPackageItem]) -> String { + let totalPrice = items.map { Decimal($0.value) * $0.quantity }.reduce(0, +) + let currencyFormatter = CurrencyFormatter(currencySettings: currencySettings) + return currencyFormatter.formatAmount(totalPrice, with: order.currency) ?? totalPrice.description + } +} + +// MARK: Constants +private extension WooShippingSplitShipmentsViewModel { + enum Localization { + static func itemsCount(_ count: Decimal) -> String { + return String.pluralize(count, singular: Localization.itemsCountSingularFormat, plural: Localization.itemsCountPluralFormat) + } + static let itemsCountSingularFormat = NSLocalizedString("wooShipping.createLabels.splitShipment.items.countSingular", + value: "%1$@ item", + comment: "Label for singular item to ship during shipping label creation. Reads like: '1 item'") + static let itemsCountPluralFormat = NSLocalizedString("wooShipping.createLabels.splitShipment.items.count", + value: "%1$@ items", + comment: "Label for plural items to ship during shipping label creation. Reads like: '3 items'") } } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift index 8378e1d641b..62aa7479199 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModel.swift @@ -446,6 +446,7 @@ private extension WooShippingCreateLabelsViewModel { if let config { splitShipmentsViewModel = WooShippingSplitShipmentsViewModel(order: order, config: config, + items: items.dataSource.items, stores: stores) } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index caf14efd206..432e74091c5 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -3032,6 +3032,11 @@ EEB4E2DE29B61AAD00371C3C /* CouponInputTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB4E2DD29B61AAD00371C3C /* CouponInputTransformer.swift */; }; EEBA02A32ADD6005001FE8E4 /* BlazeCampaignDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBA02A22ADD6005001FE8E4 /* BlazeCampaignDashboardView.swift */; }; EEBA02A52ADD606D001FE8E4 /* BlazeCampaignDashboardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBA02A42ADD606D001FE8E4 /* BlazeCampaignDashboardViewModel.swift */; }; + EEBB816F2D8C082B008D6CE5 /* CollapsibleShipmentCardViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBB816E2D8C0821008D6CE5 /* CollapsibleShipmentCardViewModel.swift */; }; + EEBB81712D8C0839008D6CE5 /* CollapsibleShipmentCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBB81702D8C0834008D6CE5 /* CollapsibleShipmentCard.swift */; }; + EEBB9B3B2D8E5071008D6CE5 /* SelectableShipmentRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBB9B3A2D8E5058008D6CE5 /* SelectableShipmentRow.swift */; }; + EEBB9B3D2D8E5099008D6CE5 /* SelectableShipmentRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBB9B3C2D8E508E008D6CE5 /* SelectableShipmentRowViewModel.swift */; }; + EEBB9B402D8FE5B6008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBB9B3F2D8FE5B4008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift */; }; EEBDF7DA2A2EF69B00EFEF47 /* ShareProductCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBDF7D92A2EF69B00EFEF47 /* ShareProductCoordinator.swift */; }; EEBDF7DF2A2F674100EFEF47 /* ShareProductAIEligibilityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBDF7DE2A2F674100EFEF47 /* ShareProductAIEligibilityChecker.swift */; }; EEBDF7E22A2F685C00EFEF47 /* DefaultShareProductAIEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBDF7E12A2F685C00EFEF47 /* DefaultShareProductAIEligibilityCheckerTests.swift */; }; @@ -6241,6 +6246,11 @@ EEB4E2DD29B61AAD00371C3C /* CouponInputTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponInputTransformer.swift; sourceTree = ""; }; EEBA02A22ADD6005001FE8E4 /* BlazeCampaignDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignDashboardView.swift; sourceTree = ""; }; EEBA02A42ADD606D001FE8E4 /* BlazeCampaignDashboardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignDashboardViewModel.swift; sourceTree = ""; }; + EEBB816E2D8C0821008D6CE5 /* CollapsibleShipmentCardViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleShipmentCardViewModel.swift; sourceTree = ""; }; + EEBB81702D8C0834008D6CE5 /* CollapsibleShipmentCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleShipmentCard.swift; sourceTree = ""; }; + EEBB9B3A2D8E5058008D6CE5 /* SelectableShipmentRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableShipmentRow.swift; sourceTree = ""; }; + EEBB9B3C2D8E508E008D6CE5 /* SelectableShipmentRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableShipmentRowViewModel.swift; sourceTree = ""; }; + EEBB9B3F2D8FE5B4008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingSplitShipmentsViewModelTests.swift; sourceTree = ""; }; EEBDF7D92A2EF69B00EFEF47 /* ShareProductCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareProductCoordinator.swift; sourceTree = ""; }; EEBDF7DE2A2F674100EFEF47 /* ShareProductAIEligibilityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareProductAIEligibilityChecker.swift; sourceTree = ""; }; EEBDF7E12A2F685C00EFEF47 /* DefaultShareProductAIEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultShareProductAIEligibilityCheckerTests.swift; sourceTree = ""; }; @@ -12502,6 +12512,7 @@ CEC3CC722C9343BC00B93FBE /* WooShipping Create Shipping Labels */ = { isa = PBXGroup; children = ( + EEBB9B3E2D8FE5AC008D6CE5 /* Split shipments */, B9A5317D2D2FCC3900208304 /* WooShipping Customs */, CE2207032CA5C55100E16D9B /* WooShippingCreateLabelsViewModelTests.swift */, CE7B4A572CA191F400F764EB /* WooShippingItemRowViewModelTests.swift */, @@ -13996,6 +14007,10 @@ EE7E75A62D83EAD200E6FF5B /* WooShipping Split Shipments */ = { isa = PBXGroup; children = ( + EEBB9B3C2D8E508E008D6CE5 /* SelectableShipmentRowViewModel.swift */, + EEBB9B3A2D8E5058008D6CE5 /* SelectableShipmentRow.swift */, + EEBB81702D8C0834008D6CE5 /* CollapsibleShipmentCard.swift */, + EEBB816E2D8C0821008D6CE5 /* CollapsibleShipmentCardViewModel.swift */, EE7E75AB2D84080A00E6FF5B /* WooShippingSplitShipmentsViewModel.swift */, EE7E75A92D84066800E6FF5B /* WooShippingSplitShipmentsDetailView.swift */, EE7E75A72D83EB0700E6FF5B /* WooShippingSplitShipmentsRow.swift */, @@ -14057,6 +14072,14 @@ path = Blaze; sourceTree = ""; }; + EEBB9B3E2D8FE5AC008D6CE5 /* Split shipments */ = { + isa = PBXGroup; + children = ( + EEBB9B3F2D8FE5B4008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift */, + ); + path = "Split shipments"; + sourceTree = ""; + }; EEBDF7D62A2EF65D00EFEF47 /* ShareProduct */ = { isa = PBXGroup; children = ( @@ -15862,6 +15885,7 @@ 0211259F2578DE310075AD2A /* ShippingLabelPrintingStepView.swift in Sources */, B58B4AB22108F01700076FDD /* NoticeView.swift in Sources */, 20D2CCA52C7E328300051705 /* POSModalCloseButton.swift in Sources */, + EEBB816F2D8C082B008D6CE5 /* CollapsibleShipmentCardViewModel.swift in Sources */, DA25ADDD2C86145E00AE81FE /* MarkOrderAsReadUseCase.swift in Sources */, 01BB6C072D09DC560094D55B /* CardPresentModalLocationPreAlert.swift in Sources */, 74B5713621CD7604008F9B8E /* SharingHelper.swift in Sources */, @@ -16524,6 +16548,7 @@ 02DE39D92968647100BB31D4 /* DomainSettingsViewModel.swift in Sources */, 576EA39225264C7400AFC0B3 /* RefundConfirmationViewController.swift in Sources */, DE36E0982A8634FF00B98496 /* StoreNameSetupView.swift in Sources */, + EEBB9B3B2D8E5071008D6CE5 /* SelectableShipmentRow.swift in Sources */, 2688641B25D3202B00821BA5 /* EditAttributesViewController.swift in Sources */, 7E6A01972725B811001668D5 /* FilterProductCategoryListViewController.swift in Sources */, E10DFC7A2673595A0083AFF2 /* ShareSheet.swift in Sources */, @@ -16751,6 +16776,7 @@ 453DBF8E2387F34A006762A5 /* UICollectionViewCell+Helpers.swift in Sources */, 45B9C63E23A8E50D007FC4C5 /* ProductPriceSettingsViewController.swift in Sources */, 318853362639FC9C00F66A9C /* PaymentSettingsFlowPresentingViewController.swift in Sources */, + EEBB9B3D2D8E5099008D6CE5 /* SelectableShipmentRowViewModel.swift in Sources */, 452FE64B25657EC100EB54A0 /* LinkedProductsViewController.swift in Sources */, B99686E32A13C98200D1AF62 /* ScanToPayViewModel.swift in Sources */, 450C6EEA286F4334002DB168 /* SitePlugin+Woo.swift in Sources */, @@ -16765,6 +16791,7 @@ 021125992578D9C20075AD2A /* ShippingLabelPrintingInstructionsView.swift in Sources */, 03E471D42942096B001A58AD /* BuiltInCardReaderPaymentAlertsProvider.swift in Sources */, 026826C72BF59E410036F959 /* PointOfSaleCardPresentPaymentScanningForReadersView.swift in Sources */, + EEBB81712D8C0839008D6CE5 /* CollapsibleShipmentCard.swift in Sources */, 68E952CC287536010095A23D /* SafariView.swift in Sources */, D449C51C26DE6B5000D75B02 /* IconListItem.swift in Sources */, EE9D03182B89E2B10077CED1 /* OrderStatusEnum+Analytics.swift in Sources */, @@ -17891,6 +17918,7 @@ 269B46642A16D6ED00ADA872 /* UpdateAnalyticsSettingsUseCaseTests.swift in Sources */, AEFF77A829786A2900667F7A /* PriceInputViewControllerTests.swift in Sources */, 02C2756F24F5F5EE00286C04 /* ProductShippingSettingsViewModel+ProductVariationTests.swift in Sources */, + EEBB9B402D8FE5B6008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift in Sources */, 571FDDAE24C768DC00D486A5 /* MockZendeskManager.swift in Sources */, 45FBDF3C238D4EA800127F77 /* ExtendedAddProductImageCollectionViewCellTests.swift in Sources */, 2609797C2A13D31500442249 /* PrivacyBannerPresentationUseCaseTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift new file mode 100644 index 00000000000..928f7198d9f --- /dev/null +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift @@ -0,0 +1,106 @@ +import XCTest +@testable import WooCommerce +import WooFoundation +import Yosemite + +final class WooShippingSplitShipmentsViewModelTests: XCTestCase { + + private var currencySettings: CurrencySettings! + private var shippingSettingsService: MockShippingSettingsService! + + let sampleOrder = Order.fake().copy(currency: "INR") + + override func setUp() { + currencySettings = CurrencySettings() + shippingSettingsService = MockShippingSettingsService(dimensionUnit: "cm", + weightUnit: "kg") + } + + func test_inits_with_expected_values() throws { + // Given + let items = [sampleItem(id: 1, weight: 4, value: 10, quantity: 1), + sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)] + + // When + let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder, + config: WooShippingConfig.fake(), + items: items, + currencySettings: currencySettings, + shippingSettingsService: shippingSettingsService) + + // Then + assertEqual("2 items", viewModel.itemsCountLabel) + assertEqual("₹12.50", viewModel.itemsPriceLabel) + assertEqual("7 kg • ₹12.50", viewModel.itemsDetailLabel) + } + + func test_total_items_count_label_handles_single_item() { + // Given + let items = [sampleItem(id: 1, weight: 1, value: 1, quantity: 1)] + + // When + let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder, + config: WooShippingConfig.fake(), + items: items, + currencySettings: currencySettings, + shippingSettingsService: shippingSettingsService) + // Then + assertEqual("1 item", viewModel.itemsCountLabel) + } + + func test_total_items_count_label_handles_items_with_quantity_greater_than_one() { + // Given + let items = [sampleItem(id: 1, weight: 1, value: 1, quantity: 1), + sampleItem(id: 2, weight: 1, value: 1, quantity: 2)] + + // When + let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder, + config: WooShippingConfig.fake(), + items: items, + currencySettings: currencySettings, + shippingSettingsService: shippingSettingsService) + // Then + assertEqual("3 items", viewModel.itemsCountLabel) + } + + func test_total_items_detail_label_handles_items_with_quantity_greater_than_one() { + // Given + let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2), + sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)] + + // When + let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder, + config: WooShippingConfig.fake(), + items: items, + currencySettings: currencySettings, + shippingSettingsService: shippingSettingsService) + + // Then + assertEqual("13 kg • ₹22.50", viewModel.itemsDetailLabel) + } + +} + +private extension WooShippingSplitShipmentsViewModelTests { + func sampleItem(id: Int64, weight: Double, value: Double, quantity: Decimal) -> ShippingLabelPackageItem { + ShippingLabelPackageItem(productOrVariationID: id, + name: "Item", + weight: weight, + quantity: quantity, + value: value, + dimensions: ProductDimensions(length: "20", width: "35", height: "5"), + attributes: [], + imageURL: nil) + } +} + +private final class MockDataSource: WooShippingItemsDataSource { + var items: [ShippingLabelPackageItem] + var currency: String + + init(items: [ShippingLabelPackageItem], + currency: String = "GBP") { + self.items = items + self.currency = currency + } +}