Skip to content

Commit

Permalink
[Shipping labels] Display actual shipment data in shipment details bo…
Browse files Browse the repository at this point in the history
…ttom sheet (#14101)
  • Loading branch information
rachelmcr authored Oct 16, 2024
2 parents 1cf4736 + 849a911 commit d2a257e
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,11 @@ private extension OrderDetailsViewController {
return
}

let shippingLabelCreationVM = WooShippingCreateLabelsViewModel(order: viewModel.order)
let shippingLabelCreationVM = WooShippingCreateLabelsViewModel(order: viewModel.order, onLabelPurchase: { [weak self] markOrderComplete in
if markOrderComplete {
self?.markOrderCompleteFromShippingLabels()
}
})
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
Expand Up @@ -13,14 +13,22 @@ final class WooShippingItemsViewModel: ObservableObject {
private var dataSource: WooShippingItemsDataSource

/// Label with the total number of items to ship.
@Published var itemsCountLabel: String = ""
@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.
/// Include total weight and total price for all items in the shipment.
@Published var itemsDetailLabel: String = ""
/// Includes total weight and total price for all items in the shipment.
var itemsDetailLabel: String {
"\(itemsWeightLabel)\(itemsPriceLabel)"
}

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

init(dataSource: WooShippingItemsDataSource,
currencySettings: CurrencySettings = ServiceLocator.currencySettings,
Expand All @@ -38,8 +46,10 @@ private extension WooShippingItemsViewModel {
/// Configures the labels in the section header.
///
func configureSectionHeader() {
itemsCountLabel = generateItemsCountLabel()
itemsDetailLabel = generateItemsDetailLabel()
let itemsCount = dataSource.items.map(\.quantity).reduce(0, +)
itemsCountLabel = Localization.itemsCount(itemsCount)
itemsWeightLabel = formatWeight(for: dataSource.items)
itemsPriceLabel = formatPrice(for: dataSource.items)
}

/// Configures the item rows.
Expand All @@ -50,22 +60,6 @@ private extension WooShippingItemsViewModel {
}
}

/// Generates a label with the total number of items to ship.
///
func generateItemsCountLabel() -> String {
let itemsCount = dataSource.items.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 = formatWeight(for: dataSource.items)
let formattedPrice = formatPrice(for: dataSource.items)
return "\(formattedWeight)\(formattedPrice)"
}

/// Calculates and formats the total weight of the given items based on each item's weight and quantity.
///
func formatWeight(for items: [ShippingLabelPackageItem]) -> String {
Expand All @@ -91,12 +85,14 @@ private extension WooShippingItemsViewModel {
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)
return String.pluralize(count, singular: Localization.itemsCountSingularFormat, plural: Localization.itemsCountPluralFormat)
}
static let itemsCountFormat = NSLocalizedString("wooShipping.createLabels.items.count",
value: "%1$@ items",
comment: "Total number of items to ship during shipping label creation. Reads like: '3 items'")
static let itemsCountSingularFormat = NSLocalizedString("wooShipping.createLabels.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.items.count",
value: "%1$@ items",
comment: "Label for plural items to ship during shipping label creation. Reads like: '3 items'")
static let dimensionsFormat = NSLocalizedString("wooShipping.createLabels.items.dimensions",
value: "%1$@ x %2$@ x %3$@ %4$@",
comment: "Length, width, and height dimensions with the unit for an item to ship. "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,16 @@ struct WooShippingCreateLabelsView: View {
}) {
if isShipmentDetailsExpanded {
CollapsibleHStack(spacing: Layout.bottomSheetSpacing) {
Toggle(Localization.BottomSheet.markComplete, isOn: .constant(false)) // TODO: 14044 - Handle this toggle setting
Toggle(Localization.BottomSheet.markComplete, isOn: $viewModel.markOrderComplete)
.font(.subheadline)

Button {
// TODO: 13556 - Trigger purchase action
viewModel.purchaseLabel()
} label: {
Text(Localization.BottomSheet.purchase)
}
.buttonStyle(PrimaryButtonStyle())
.disabled(true) // TODO: 14044 - Enable button when shipping label is ready to purchase
.disabled(true) // TODO: 13556 - Enable button when shipping label is ready to purchase
}
.padding(.horizontal, Layout.bottomSheetPadding)
} else {
Expand All @@ -79,7 +79,7 @@ struct WooShippingCreateLabelsView: View {
HStack(alignment: .firstTextBaseline, spacing: Layout.bottomSheetSpacing) {
Text(Localization.BottomSheet.shipFrom)
.trackSize(size: $shipmentDetailsShipFromSize)
Text("417 MONTGOMERY ST, SAN FRANCISCO") // TODO: 14044 - Show real "ship from" address (store address)
Text(viewModel.originAddress)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
Expand All @@ -90,10 +90,12 @@ struct WooShippingCreateLabelsView: View {
Text(Localization.BottomSheet.shipTo)
.frame(width: shipmentDetailsShipFromSize.width, alignment: .leading)
VStack(alignment: .leading) {
Text("1 Infinite Loop") // TODO: 14044 - Show real "ship to" address (customer address)
.bold()
Text("Cupertino, CA 95014")
Text("USA")
ForEach(viewModel.destinationAddressLines, id: \.self) { addressLine in
Text(addressLine)
.if(addressLine == viewModel.destinationAddressLines.first) { line in
line.bold()
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
Expand Down Expand Up @@ -151,19 +153,21 @@ private extension WooShippingCreateLabelsView {
Text(viewModel.items.itemsCountLabel)
.bold()
Spacer()
Text("$148.50") // TODO: 14044 - Show real item total
Text(viewModel.items.itemsPriceLabel)
}
.frame(idealHeight: Layout.rowHeight)
AdaptiveStack {
Image(uiImage: .shippingIcon)
.frame(width: Layout.iconSize)
Text("Flat rate shipping") // TODO: 14044 - Show real shipping name
.bold()
.lineLimit(nil)
Spacer()
Text("$25.00") // TODO: 14044 - Show real shipping amount
ForEach(viewModel.shippingLines) { shippingLine in
AdaptiveStack {
Image(uiImage: .shippingIcon)
.frame(width: Layout.iconSize)
Text(shippingLine.title)
.bold()
.lineLimit(nil)
Spacer()
Text(shippingLine.formattedTotal)
}
.frame(idealHeight: Layout.rowHeight)
}
.frame(idealHeight: Layout.rowHeight)
}
}

Expand All @@ -175,8 +179,8 @@ private extension WooShippingCreateLabelsView {
AdaptiveStack {
Text(Localization.BottomSheet.subtotal)
Spacer()
Text("$0.00") // TODO: 13555 - Show real subtotal value
.if(true) { subtotal in // TODO: 14044 - Only show placeholder if subtotal is not available
Text("$0.00") // TODO: 13554 - Show subtotal value(s) for selected rate
.if(true) { subtotal in // TODO: 13554 - Only show placeholder if subtotal is not available
subtotal.redacted(reason: .placeholder)
}
}
Expand All @@ -185,8 +189,8 @@ private extension WooShippingCreateLabelsView {
Text(Localization.BottomSheet.total)
.bold()
Spacer()
Text("$0.00") // TODO: 13555 - Show real total value
.if(true) { total in // TODO: 14044 - Only show placeholder if total is not available
Text("$0.00") // TODO: 13554 - Show total value for selected shipping rate
.if(true) { total in // TODO: 13554 - Only show placeholder if total is not available
total.redacted(reason: .placeholder)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,60 @@
import Foundation
import Yosemite
import WooFoundation

/// 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) {
/// Address to ship from (store address), formatted for display.
let originAddress: String

/// Address to ship to (customer address), formatted for display and split into separate lines to allow additional formatting.
let destinationAddressLines: [String]

/// Shipping lines for the order, with formatted amount.
let shippingLines: [WooShipping_ShippingLineViewModel]

/// Whether to mark the order as complete after the label is purchased.
@Published var markOrderComplete: Bool = false

/// Closure to execute after the label is successfully purchased.
let onLabelPurchase: ((_ markOrderComplete: Bool) -> Void)?

init(order: Order,
siteAddress: SiteAddress = SiteAddress(),
onLabelPurchase: ((Bool) -> Void)? = nil) {
self.items = WooShippingItemsViewModel(dataSource: DefaultWooShippingItemsDataSource(order: order))
self.onLabelPurchase = onLabelPurchase
self.originAddress = Self.formatOriginAddress(siteAddress: siteAddress)
self.destinationAddressLines = (order.shippingAddress?.formattedPostalAddress ?? "").components(separatedBy: .newlines)
self.shippingLines = order.shippingLines.map({ WooShipping_ShippingLineViewModel(shippingLine: $0) })
}

/// Purchases a shipping label with the provided label details and settings.
func purchaseLabel() {
// TODO: 13556 - Add action to purchase label remotely
onLabelPurchase?(markOrderComplete) // TODO: 13556 - Only call this closure if the remote purchase is successful
}
}

private extension WooShippingCreateLabelsViewModel {
/// Formats the origin address from the provided `SiteAddress`.
static func formatOriginAddress(siteAddress: SiteAddress) -> String {
let address = Address(firstName: "",
lastName: "",
company: nil,
address1: siteAddress.address,
address2: siteAddress.address2,
city: siteAddress.city,
state: siteAddress.state,
postcode: siteAddress.postalCode,
country: siteAddress.countryCode.rawValue,
phone: nil,
email: nil)
let formattedPostalAddress = address.formattedPostalAddress?.replacingOccurrences(of: "\n", with: ", ")
return formattedPostalAddress ?? ""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Yosemite
import WooFoundation

/// Represents a shipping line in the Woo Shipping label creation flow.
struct WooShipping_ShippingLineViewModel: Identifiable {
/// Unique ID for the shipping line.
let id: Int64

/// Title for the shipping line.
let title: String

/// Formatted total amount for the shipping line.
let formattedTotal: String

init(shippingLine: ShippingLine,
currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) {
id = shippingLine.shippingID
title = shippingLine.methodTitle
formattedTotal = currencyFormatter.formatAmount(shippingLine.total) ?? shippingLine.total
}
}
Loading

0 comments on commit d2a257e

Please sign in to comment.