Skip to content

[Order Details] Display multiple shipping lines in order details #12823

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

Merged
merged 6 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return buildConfig == .localDeveloper || buildConfig == .alpha
case .dynamicDashboardM2:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .multipleShippingLines:
return buildConfig == .localDeveloper || buildConfig == .alpha
default:
return true
}
Expand Down
4 changes: 4 additions & 0 deletions Experiments/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,8 @@ public enum FeatureFlag: Int {
/// Enables new dashboard cards on the My Store screen.
///
case dynamicDashboardM2

/// Enables multiple shipping lines in order details and order creation/editing.
///
case multipleShippingLines
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import SwiftUI
import UIKit
import Yosemite
import Experiments
Expand Down Expand Up @@ -165,6 +166,12 @@ final class OrderDetailsDataSource: NSObject {
resultsControllers.addOnGroups
}

/// Shipping Methods list
///
var siteShippingMethods: [ShippingMethod] {
resultsControllers.siteShippingMethods
}

/// Shipping Labels for an Order
///
private(set) var shippingLabels: [ShippingLabel] = []
Expand All @@ -174,7 +181,7 @@ final class OrderDetailsDataSource: NSObject {
/// Shipping Lines from an Order
///
private var shippingLines: [ShippingLine] {
return order.shippingLines
return order.shippingLines.sorted(by: { $0.shippingID < $1.shippingID })
}

/// First Shipping method from an order
Expand Down Expand Up @@ -475,6 +482,8 @@ private extension OrderDetailsDataSource {
configureAttributionSessionPageViews(cell: cell, at: indexPath)
case let cell as WooBasicTableViewCell where row == .trashOrder:
configureTrashOrder(cell: cell, at: indexPath)
case let cell as HostingConfigurationTableViewCell<ShippingLineRowView> where row == .shippingLine:
configureShippingLine(cell: cell, at: indexPath)
default:
fatalError("Unidentified customer info row type")
}
Expand Down Expand Up @@ -1005,6 +1014,23 @@ private extension OrderDetailsDataSource {
cell.selectionStyle = .none
}

private func configureShippingLine(cell: HostingConfigurationTableViewCell<ShippingLineRowView>, at indexPath: IndexPath) {
let shippingLine = shippingLines[indexPath.row]
let shippingMethod = siteShippingMethods.first(where: { $0.methodID == shippingLine.methodID })?.title
let shippingTotal = currencyFormatter.formatAmount(shippingLine.total) ?? shippingLine.total

let view = ShippingLineRowView(shippingTitle: shippingLine.methodTitle,
shippingMethod: shippingMethod,
shippingAmount: shippingTotal,
editable: false)
let topMargin = indexPath.row == 0 ? Constants.cellDefaultMargin : 0 // Reduce cell padding between rows
let insets = UIEdgeInsets(top: topMargin, left: Constants.cellDefaultMargin, bottom: Constants.cellDefaultMargin, right: Constants.cellDefaultMargin)
cell.host(view, insets: insets)
cell.hideSeparator()
cell.selectionStyle = .none

}

private func configureSummary(cell: SummaryTableViewCell) {
let cellViewModel = SummaryTableViewCellViewModel(
order: order,
Expand Down Expand Up @@ -1140,7 +1166,9 @@ extension OrderDetailsDataSource {
let shippingNotice: Section? = {
// Hide the shipping method warning if order contains only virtual products
// or if the order contains only one shipping method
if isMultiShippingLinesAvailable(for: order) == false {
// or if multiple shipping lines are supported (feature flag)
if isMultiShippingLinesAvailable(for: order) == false ||
featureFlags.isFeatureFlagEnabled(.multipleShippingLines) {
return nil
}

Expand Down Expand Up @@ -1260,6 +1288,15 @@ extension OrderDetailsDataSource {
return sections
}()

let shippingLinesSection: Section? = {
guard shippingLines.count > 0
&& featureFlags.isFeatureFlagEnabled(.multipleShippingLines) else {
return nil
}

return Section(category: .shippingLines, title: Title.shippingLines, rows: Array(repeating: .shippingLine, count: shippingLines.count))
}()

let customerInformation: Section? = {
var rows: [Row] = []

Expand All @@ -1278,8 +1315,9 @@ extension OrderDetailsDataSource {
rows.append(.shippingAddress)
}

/// Shipping Lines
if shippingLines.count > 0 {
/// Shipping Lines (single shipping line)
if shippingLines.count > 0
&& !featureFlags.isFeatureFlagEnabled(.multipleShippingLines) {
rows.append(.shippingMethod)
}

Expand Down Expand Up @@ -1445,6 +1483,7 @@ extension OrderDetailsDataSource {
refundedProducts] +
shippingLabelSections +
[subscriptions,
shippingLinesSection,
payment,
customerInformation,
attribution,
Expand Down Expand Up @@ -1723,6 +1762,9 @@ extension OrderDetailsDataSource {
value: "Order attribution",
comment: "Title of Order Attribution Section in Order Details screen."
)
static let shippingLines = NSLocalizedString("orderDetailsDataSource.shippingLines.title",
value: "Shipping",
comment: "Title of Shipping Section in Order Details screen")
}

enum Footer {
Expand Down Expand Up @@ -1764,6 +1806,7 @@ extension OrderDetailsDataSource {
case customFields
case attribution
case trashOrder
case shippingLines
}

/// The table header style of a `Section`.
Expand Down Expand Up @@ -1864,6 +1907,7 @@ extension OrderDetailsDataSource {
case shippingLabelRefunded
case shippingLabelReprintButton
case shippingLabelTrackingNumber
case shippingLine
case shippingNotice
case addOrderNote
case orderNoteHeader
Expand Down Expand Up @@ -1958,6 +2002,8 @@ extension OrderDetailsDataSource {
return TitleAndValueTableViewCell.reuseIdentifier
case .trashOrder:
return WooBasicTableViewCell.reuseIdentifier
case .shippingLine:
return HostingConfigurationTableViewCell<ShippingLineRowView>.reuseIdentifier
}
}
}
Expand All @@ -1981,5 +2027,6 @@ extension OrderDetailsDataSource {
static let addOrderCell = 1
static let paymentCell = 1
static let paidByCustomerCell = 1
static let cellDefaultMargin: CGFloat = 16
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ final class OrderDetailsResultsControllers {
return ResultsController<StorageSitePlugin>(storageManager: storageManager, matching: predicate, sortedBy: [])
}()

/// Shipping Methods ResultsController.
///
private lazy var shippingMethodsResultsController: ResultsController<StorageShippingMethod> = {
let predicate = NSPredicate(format: "siteID == %lld", siteID)
return ResultsController<StorageShippingMethod>(storageManager: storageManager, matching: predicate, sortedBy: [])
}()

/// Order shipment tracking list
///
var orderTracking: [ShipmentTracking] {
Expand Down Expand Up @@ -138,6 +145,12 @@ final class OrderDetailsResultsControllers {
return feeLinesResultsController.fetchedObjects
}

/// Shipping methods list
///
var siteShippingMethods: [ShippingMethod] {
return shippingMethodsResultsController.fetchedObjects
}

/// Completion handler for when results controllers reload.
///
var onReload: (() -> Void)?
Expand All @@ -160,6 +173,7 @@ final class OrderDetailsResultsControllers {
configureAddOnGroupResultsController(onReload: onReload)
configureSitePluginsResultsController(onReload: onReload)
configureFeeLinesResultsController(onReload: onReload)
configureShippingMethodsResultsController(onReload: onReload)
}

func update(order: Order) {
Expand Down Expand Up @@ -344,6 +358,24 @@ private extension OrderDetailsResultsControllers {
}
}

private func configureShippingMethodsResultsController(onReload: @escaping () -> Void) {
shippingMethodsResultsController.onDidChangeContent = {
onReload()
}

shippingMethodsResultsController.onDidResetContent = { [weak self] in
guard let self else { return }
self.refetchAllResultsControllers()
onReload()
}

do {
try shippingMethodsResultsController.performFetch()
} catch {
DDLogError("⛔️ Unable to fetch Shipping Methods for Site \(siteID): \(error)")
}
}

/// Refetching all the results controllers is necessary after a storage reset in `onDidResetContent` callback and before reloading UI that
/// involves more than one results controller.
func refetchAllResultsControllers() {
Expand All @@ -355,5 +387,6 @@ private extension OrderDetailsResultsControllers {
try? shippingLabelResultsController.performFetch()
try? addOnGroupResultsController.performFetch()
try? sitePluginsResultsController.performFetch()
try? shippingMethodsResultsController.performFetch()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ extension OrderDetailsViewModel {
group.leave()
}

group.enter()
syncShippingMethods { _ in
onReloadSections?()
group.leave()
}

group.notify(queue: .main) { [weak self] in

/// Update state to synced
Expand Down Expand Up @@ -360,7 +366,7 @@ extension OrderDetailsViewModel {
/// Registers all of the available TableViewCells
///
func registerTableViewCells(_ tableView: UITableView) {
let cells = [
let cellsWithNib = [
LargeHeightLeftImageTableViewCell.self,
LeftImageTableViewCell.self,
CustomerNoteTableViewCell.self,
Expand All @@ -381,9 +387,17 @@ extension OrderDetailsViewModel {
TitleAndValueTableViewCell.self
]

for cellClass in cells {
let cellsWithoutNib = [
HostingConfigurationTableViewCell<ShippingLineRowView>.self
]

for cellClass in cellsWithNib {
tableView.registerNib(for: cellClass)
}

for cellClass in cellsWithoutNib {
tableView.register(cellClass)
}
}

/// Registers all of the available TableViewHeaderFooters
Expand Down Expand Up @@ -687,6 +701,19 @@ extension OrderDetailsViewModel {
}
}

func syncShippingMethods(onCompletion: ((Error?) -> ())? = nil) {
let action = ShippingMethodAction.synchronizeShippingMethods(siteID: order.siteID) { result in
switch result {
case .success:
onCompletion?(nil)
case let .failure(error):
DDLogError("⛔️ Error synchronizing shipping methods: \(error)")
onCompletion?(error)
}
}
stores.dispatch(action)
}

@MainActor
func checkShippingLabelCreationEligibility() async -> Bool {
guard await localRequirementsForShippingLabelsAreFulfilled() else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ struct ShippingLineRowView: View {
let shippingTitle: String

/// Name of the shipping method for the shipping line
let shippingMethod: String
let shippingMethod: String?

/// Amount for the shipping line
let shippingAmount: String
Expand All @@ -30,8 +30,10 @@ struct ShippingLineRowView: View {
// Avoids the shipping line name to be truncated when it's long enough
.fixedSize(horizontal: false, vertical: true)

Text(shippingMethod)
.subheadlineStyle()
if let shippingMethod {
Text(shippingMethod)
.subheadlineStyle()
}
}

Spacer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,40 @@ private extension HostingTableViewCell {
configureDefaultBackgroundConfiguration()
}
}

/// Use this cell to host a SwiftUI view into a `UITableViewCell` so it can be displayed in a `UITableView` instance
///
/// This cell can be used when the parent view controller is not available in the current context
///
class HostingConfigurationTableViewCell<Content: View>: UITableViewCell {
func host(_ view: Content, insets: UIEdgeInsets? = nil) {
var hostingConfiguration = UIHostingConfiguration {
view
}

// Override default hosting cell padding with custom insets
if let insets {
hostingConfiguration = hostingConfiguration
.margins(.top, insets.top)
.margins(.bottom, insets.bottom)
.margins(.leading, insets.left)
.margins(.trailing, insets.right)
}

self.contentConfiguration = hostingConfiguration
}

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureDefaultBackgroundConfiguration()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func updateConfiguration(using state: UICellConfigurationState) {
super.updateConfiguration(using: state)
updateDefaultBackgroundConfiguration(using: state)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct MockFeatureFlagService: FeatureFlagService {
private let sideBySideViewForOrderForm: Bool
private let isCustomersInHubMenuEnabled: Bool
private let isSubscriptionsInOrderCreationCustomersEnabled: Bool
private let isMultipleShippingLinesEnabled: Bool

init(isInboxOn: Bool = false,
isUpdateOrderOptimisticallyOn: Bool = false,
Expand All @@ -44,7 +45,8 @@ struct MockFeatureFlagService: FeatureFlagService {
isBackendReceiptsEnabled: Bool = false,
sideBySideViewForOrderForm: Bool = false,
isCustomersInHubMenuEnabled: Bool = false,
isSubscriptionsInOrderCreationCustomersEnabled: Bool = false) {
isSubscriptionsInOrderCreationCustomersEnabled: Bool = false,
isMultipleShippingLinesEnabled: Bool = false) {
self.isInboxOn = isInboxOn
self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn
self.shippingLabelsOnboardingM1 = shippingLabelsOnboardingM1
Expand All @@ -66,6 +68,7 @@ struct MockFeatureFlagService: FeatureFlagService {
self.sideBySideViewForOrderForm = sideBySideViewForOrderForm
self.isCustomersInHubMenuEnabled = isCustomersInHubMenuEnabled
self.isSubscriptionsInOrderCreationCustomersEnabled = isSubscriptionsInOrderCreationCustomersEnabled
self.isMultipleShippingLinesEnabled = isMultipleShippingLinesEnabled
}

func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool {
Expand Down Expand Up @@ -110,6 +113,8 @@ struct MockFeatureFlagService: FeatureFlagService {
return isCustomersInHubMenuEnabled
case .subscriptionsInOrderCreationCustomers:
return isSubscriptionsInOrderCreationCustomersEnabled
case .multipleShippingLines:
return isMultipleShippingLinesEnabled
default:
return false
}
Expand Down
Loading