Skip to content

Commit a03c402

Browse files
authored
Merge pull request #6222 from woocommerce/issue/6024-add-shipping-line-input
Order Creation: Add shipping line UI
2 parents 2e433c8 + 127ea7c commit a03c402

File tree

9 files changed

+657
-96
lines changed

9 files changed

+657
-96
lines changed

WooCommerce/Classes/ViewRelated/Orders/Order Creation/NewOrderViewModel.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,15 +342,18 @@ extension NewOrderViewModel {
342342

343343
let shouldShowShippingTotal: Bool
344344
let shippingTotal: String
345+
let shippingMethodTitle: String
345346

346347
init(itemsTotal: String = "",
347348
shouldShowShippingTotal: Bool = false,
348349
shippingTotal: String = "",
350+
shippingMethodTitle: String = "",
349351
orderTotal: String = "",
350352
currencyFormatter: CurrencyFormatter = CurrencyFormatter(currencySettings: ServiceLocator.currencySettings)) {
351353
self.itemsTotal = currencyFormatter.formatAmount(itemsTotal) ?? ""
352354
self.shouldShowShippingTotal = shouldShowShippingTotal
353355
self.shippingTotal = currencyFormatter.formatAmount(shippingTotal) ?? ""
356+
self.shippingMethodTitle = shippingMethodTitle
354357
self.orderTotal = currencyFormatter.formatAmount(orderTotal) ?? ""
355358
}
356359
}
@@ -462,11 +465,14 @@ private extension NewOrderViewModel {
462465
.compactMap { self.currencyFormatter.convertToDecimal(from: $0) }
463466
.reduce(NSDecimalNumber(value: 0), { $0.adding($1) })
464467

468+
let shippingMethodTitle = order.shippingLines.first?.methodTitle ?? ""
469+
465470
let orderTotal = itemsTotal.adding(shippingTotal)
466471

467472
return PaymentDataViewModel(itemsTotal: itemsTotal.stringValue,
468473
shouldShowShippingTotal: order.shippingLines.isNotEmpty,
469474
shippingTotal: shippingTotal.stringValue,
475+
shippingMethodTitle: shippingMethodTitle,
470476
orderTotal: orderTotal.stringValue,
471477
currencyFormatter: self.currencyFormatter)
472478
}

WooCommerce/Classes/ViewRelated/Orders/Order Creation/PaymentSection/OrderPaymentSection.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import SwiftUI
2-
import Yosemite
2+
import struct Yosemite.ShippingLine
33

44
/// Represents the Payment section in an order
55
///
@@ -10,6 +10,10 @@ struct OrderPaymentSection: View {
1010
/// Closure to create/update the shipping line object
1111
let saveShippingLineClosure: (ShippingLine?) -> Void
1212

13+
/// Indicates if the shipping line details screen should be shown or not.
14+
///
15+
@State private var shouldShowShippingLineDetails: Bool = false
16+
1317
/// Environment safe areas
1418
///
1519
@Environment(\.safeAreaInsets) var safeAreaInsets: EdgeInsets
@@ -26,6 +30,11 @@ struct OrderPaymentSection: View {
2630

2731
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.orderCreation) {
2832
shippingRow
33+
.sheet(isPresented: $shouldShowShippingLineDetails) {
34+
ShippingLineDetails(viewModel: .init(inputData: viewModel, didSelectSave: { newShippingLine in
35+
saveShippingLineClosure(newShippingLine)
36+
}))
37+
}
2938
}
3039

3140
TitleAndValueRow(title: Localization.orderTotal, value: .content(viewModel.orderTotal), bold: true, selectionStyle: .none) {}
@@ -43,17 +52,11 @@ struct OrderPaymentSection: View {
4352
@ViewBuilder private var shippingRow: some View {
4453
if viewModel.shouldShowShippingTotal {
4554
TitleAndValueRow(title: Localization.shippingTotal, value: .content(viewModel.shippingTotal), selectionStyle: .highlight) {
46-
saveShippingLineClosure(nil)
55+
shouldShowShippingLineDetails = true
4756
}
4857
} else {
4958
Button(Localization.addShipping) {
50-
let testShippingLine = ShippingLine(shippingID: 0,
51-
methodTitle: "Flat Rate",
52-
methodID: "other",
53-
total: "10",
54-
totalTax: "",
55-
taxes: [])
56-
saveShippingLineClosure(testShippingLine)
59+
shouldShowShippingLineDetails = true
5760
}
5861
.buttonStyle(PlusButtonStyle())
5962
.padding()
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import SwiftUI
2+
3+
/// View to add/edit a single shipping line in an order, with the option to remove it.
4+
///
5+
struct ShippingLineDetails: View {
6+
7+
/// View model to drive the view content
8+
///
9+
@ObservedObject var viewModel: ShippingLineDetailsViewModel
10+
11+
/// Defines if the amount input text field should be focused. Defaults to `true`
12+
///
13+
@State private var focusAmountInput: Bool = true
14+
15+
@Environment(\.presentationMode) var presentation
16+
17+
@Environment(\.safeAreaInsets) var safeAreaInsets: EdgeInsets
18+
19+
init(viewModel: ShippingLineDetailsViewModel) {
20+
self.viewModel = viewModel
21+
}
22+
23+
var body: some View {
24+
NavigationView {
25+
ScrollView {
26+
VStack(spacing: .zero) {
27+
Section {
28+
Group {
29+
ZStack(alignment: .center) {
30+
// Hidden input text field
31+
BindableTextfield("", text: $viewModel.amount, focus: $focusAmountInput)
32+
.keyboardType(.decimalPad)
33+
.opacity(0)
34+
35+
// Visible & formatted field
36+
TitleAndTextFieldRow(title: Localization.amountField,
37+
placeholder: "",
38+
text: .constant(viewModel.formattedAmount),
39+
symbol: nil,
40+
keyboardType: .decimalPad)
41+
.foregroundColor(Color(viewModel.amountTextColor))
42+
.disabled(true)
43+
}
44+
.background(Color(.listForeground))
45+
.fixedSize(horizontal: false, vertical: true)
46+
.onTapGesture {
47+
focusAmountInput = true
48+
}
49+
50+
Divider()
51+
.padding(.leading, Layout.dividerPadding)
52+
53+
TitleAndTextFieldRow(title: Localization.nameField,
54+
placeholder: ShippingLineDetailsViewModel.Localization.namePlaceholder,
55+
text: $viewModel.methodTitle,
56+
symbol: nil,
57+
keyboardType: .default)
58+
}
59+
.padding(.horizontal, insets: safeAreaInsets)
60+
.addingTopAndBottomDividers()
61+
}
62+
.background(Color(.listForeground))
63+
64+
Spacer(minLength: Layout.sectionSpacing)
65+
66+
if viewModel.isExistingShippingLine {
67+
Section {
68+
Button(Localization.remove) {
69+
viewModel.didSelectSave(nil)
70+
presentation.wrappedValue.dismiss()
71+
}
72+
.padding()
73+
.frame(maxWidth: .infinity, alignment: .center)
74+
.foregroundColor(Color(.error))
75+
.padding(.horizontal, insets: safeAreaInsets)
76+
.addingTopAndBottomDividers()
77+
}
78+
.background(Color(.listForeground))
79+
}
80+
}
81+
}
82+
.background(Color(.listBackground))
83+
.ignoresSafeArea(.container, edges: [.horizontal, .bottom])
84+
.navigationTitle(viewModel.isExistingShippingLine ? Localization.shipping : Localization.addShipping)
85+
.navigationBarTitleDisplayMode(.inline)
86+
.toolbar {
87+
ToolbarItem(placement: .cancellationAction) {
88+
Button(Localization.close) {
89+
presentation.wrappedValue.dismiss()
90+
}
91+
}
92+
ToolbarItem(placement: .primaryAction) {
93+
Button(Localization.done) {
94+
viewModel.saveData()
95+
presentation.wrappedValue.dismiss()
96+
}
97+
.disabled(viewModel.shouldDisableDoneButton)
98+
}
99+
}
100+
}
101+
.wooNavigationBarStyle()
102+
}
103+
}
104+
105+
// MARK: Constants
106+
private extension ShippingLineDetails {
107+
enum Layout {
108+
static let sectionSpacing: CGFloat = 16.0
109+
static let dividerPadding: CGFloat = 16.0
110+
}
111+
112+
enum Localization {
113+
static let addShipping = NSLocalizedString("Add Shipping", comment: "Title for the Shipping Line screen during order creation")
114+
static let shipping = NSLocalizedString("Shipping", comment: "Title for the Shipping Line Details screen during order creation")
115+
116+
static let amountField = NSLocalizedString("Amount", comment: "Title for the amount field on the Shipping Line Details screen during order creation")
117+
static let nameField = NSLocalizedString("Name", comment: "Title for the name field on the Shipping Line Details screen during order creation")
118+
119+
static let close = NSLocalizedString("Close", comment: "Text for the close button in the Shipping Line Details screen")
120+
static let done = NSLocalizedString("Done", comment: "Text for the done button in the Shipping Line Details screen")
121+
static let remove = NSLocalizedString("Remove Shipping from Order",
122+
comment: "Text for the button to remove a shipping line from the order during order creation")
123+
}
124+
}
125+
126+
struct ShippingLineDetails_Previews: PreviewProvider {
127+
static var previews: some View {
128+
let viewModel = NewOrderViewModel.PaymentDataViewModel(itemsTotal: "5",
129+
shouldShowShippingTotal: true,
130+
shippingTotal: "10",
131+
shippingMethodTitle: "Shipping",
132+
orderTotal: "15")
133+
ShippingLineDetails(viewModel: .init(inputData: viewModel, didSelectSave: { _ in }))
134+
}
135+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import SwiftUI
2+
import struct Yosemite.ShippingLine
3+
4+
class ShippingLineDetailsViewModel: ObservableObject {
5+
6+
/// Closure to be invoked when the shipping line is updated.
7+
///
8+
var didSelectSave: ((ShippingLine?) -> Void)
9+
10+
/// Helper to format price field input.
11+
///
12+
private let priceFieldFormatter: PriceFieldFormatter
13+
14+
/// Formatted amount to display. When empty displays a placeholder value.
15+
///
16+
var formattedAmount: String {
17+
priceFieldFormatter.formattedAmount
18+
}
19+
20+
/// Stores the amount(unformatted) entered by the merchant.
21+
///
22+
@Published var amount: String = "" {
23+
didSet {
24+
guard amount != oldValue else { return }
25+
amount = priceFieldFormatter.formatAmount(amount)
26+
}
27+
}
28+
29+
/// Defines the amount text color.
30+
///
31+
var amountTextColor: UIColor {
32+
amount.isEmpty ? .textPlaceholder : .text
33+
}
34+
35+
/// Stores the method title entered by the merchant.
36+
///
37+
@Published var methodTitle: String
38+
39+
private let initialAmount: Decimal
40+
private let initialMethodTitle: String
41+
42+
/// Returns true when existing shipping line is edited.
43+
///
44+
let isExistingShippingLine: Bool
45+
46+
/// Method title entered by user or placeholder if it's empty.
47+
///
48+
private var finalMethodTitle: String {
49+
methodTitle.isNotEmpty ? methodTitle : Localization.namePlaceholder
50+
}
51+
52+
/// Returns true when there are no valid pending changes.
53+
///
54+
var shouldDisableDoneButton: Bool {
55+
guard let amountDecimal = priceFieldFormatter.amountDecimal, amountDecimal > .zero else {
56+
return true
57+
}
58+
59+
let amountUpdated = amountDecimal != initialAmount
60+
let methodTitleUpdated = finalMethodTitle != initialMethodTitle
61+
62+
return !(amountUpdated || methodTitleUpdated)
63+
}
64+
65+
init(inputData: NewOrderViewModel.PaymentDataViewModel,
66+
locale: Locale = Locale.autoupdatingCurrent,
67+
storeCurrencySettings: CurrencySettings = ServiceLocator.currencySettings,
68+
didSelectSave: @escaping ((ShippingLine?) -> Void)) {
69+
self.priceFieldFormatter = .init(locale: locale, storeCurrencySettings: storeCurrencySettings)
70+
71+
self.isExistingShippingLine = inputData.shouldShowShippingTotal
72+
self.initialMethodTitle = inputData.shippingMethodTitle
73+
self.methodTitle = initialMethodTitle
74+
75+
let currencyFormatter = CurrencyFormatter(currencySettings: storeCurrencySettings)
76+
if let initialAmount = currencyFormatter.convertToDecimal(from: inputData.shippingTotal) {
77+
self.initialAmount = initialAmount as Decimal
78+
} else {
79+
self.initialAmount = .zero
80+
}
81+
82+
if initialAmount > 0, let formattedInputAmount = currencyFormatter.formatAmount(initialAmount) {
83+
self.amount = priceFieldFormatter.formatAmount(formattedInputAmount)
84+
}
85+
86+
self.didSelectSave = didSelectSave
87+
}
88+
89+
func saveData() {
90+
let shippingLine = ShippingLine(shippingID: 0,
91+
methodTitle: finalMethodTitle,
92+
methodID: "other",
93+
total: amount,
94+
totalTax: "",
95+
taxes: [])
96+
didSelectSave(shippingLine)
97+
}
98+
}
99+
100+
// MARK: Constants
101+
102+
extension ShippingLineDetailsViewModel {
103+
enum Localization {
104+
static let namePlaceholder = NSLocalizedString("Shipping",
105+
comment: "Placeholder for the name field on the Shipping Line Details screen during order creation")
106+
}
107+
}

0 commit comments

Comments
 (0)