Skip to content

Commit c773053

Browse files
authored
[Shipping labels] Add placeholder for shipping label rates when destination address is missing (#15084)
2 parents f774bf9 + 75038bf commit c773053

File tree

9 files changed

+140
-42
lines changed

9 files changed

+140
-42
lines changed

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,12 @@ extension UIImage {
11201120
return UIImage(named: "icon-widgets")!.withRenderingMode(.alwaysTemplate)
11211121
}
11221122

1123+
/// Woo Shipping - Placeholder image for shipping rates
1124+
///
1125+
static var wooShippingRatesPlaceholder: UIImage {
1126+
return UIImage(named: "woo-shipping-rates-placeholder")!
1127+
}
1128+
11231129
/// Variations Icon
11241130
///
11251131
static var variationsImage: UIImage {

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Package and Rate Selection/WooShippingServiceView.swift

Lines changed: 65 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,61 @@ struct WooShippingServiceView: View {
1414
}
1515

1616
var body: some View {
17-
VStack(alignment: .leading) {
18-
HStack {
19-
Text(Localization.shippingService)
20-
.headlineStyle()
21-
.frame(maxWidth: .infinity, alignment: .leading)
22-
Menu {
23-
ForEach(WooShippingServiceViewModel.SortOrder.allCases, id: \.self) { option in
24-
Button {
25-
viewModel.sortShipping(by: option)
26-
} label: {
27-
HStack {
28-
Text(option.displayName)
29-
if viewModel.sortOrder == option {
30-
Image(uiImage: .checkmarkStyledImage)
17+
if viewModel.hasDestinationAddress {
18+
VStack(alignment: .leading) {
19+
HStack {
20+
Text(Localization.shippingService)
21+
.headlineStyle()
22+
.frame(maxWidth: .infinity, alignment: .leading)
23+
Menu {
24+
ForEach(WooShippingServiceViewModel.SortOrder.allCases, id: \.self) { option in
25+
Button {
26+
viewModel.sortShipping(by: option)
27+
} label: {
28+
HStack {
29+
Text(option.displayName)
30+
if viewModel.sortOrder == option {
31+
Image(uiImage: .checkmarkStyledImage)
32+
}
3133
}
3234
}
3335
}
36+
} label: {
37+
HStack {
38+
Text(Localization.sortBy)
39+
Image(systemName: "chevron.up.chevron.down")
40+
}
41+
.foregroundStyle(Color(.primary))
3442
}
35-
} label: {
36-
HStack {
37-
Text(Localization.sortBy)
38-
Image(systemName: "chevron.up.chevron.down")
39-
}
40-
.foregroundStyle(Color(.primary))
43+
}
44+
TopTabView(tabs: carriers,
45+
tabsContainerHorizontalPadding: 16,
46+
unselectedStateColor: .secondary,
47+
tabsNameFont: .subheadline.bold(),
48+
tabItemContentHorizontalPadding: 6,
49+
tabItemContentVerticalPadding: 12)
50+
.redacted(reason: viewModel.loadingState == .loading ? .placeholder : [])
51+
.shimmering(active: viewModel.loadingState == .loading)
52+
.padding(.horizontal, Layout.padding * -1) // Offset the additional padding in TopTabView
53+
}
54+
.padding(.vertical, Layout.padding)
55+
} else {
56+
VStack(spacing: Layout.placeholderPadding) {
57+
Image(uiImage: .wooShippingRatesPlaceholder)
58+
VStack(spacing: Layout.innerSpacing) {
59+
Text(Localization.noDestinationAddressTitle)
60+
.font(.subheadline)
61+
.bold()
62+
Text(Localization.noDestinationAddressMessage)
63+
.subheadlineStyle()
4164
}
4265
}
43-
.padding(.horizontal)
44-
TopTabView(tabs: carriers,
45-
tabsContainerHorizontalPadding: 16,
46-
unselectedStateColor: .secondary,
47-
tabsNameFont: .subheadline.bold(),
48-
tabItemContentHorizontalPadding: 6,
49-
tabItemContentVerticalPadding: 12)
50-
.redacted(reason: viewModel.loadingState == .loading ? .placeholder : [])
51-
.shimmering(active: viewModel.loadingState == .loading)
66+
.frame(maxWidth: .infinity, alignment: .center)
67+
.multilineTextAlignment(.center)
68+
.padding(Layout.placeholderPadding)
69+
.roundedBorder(cornerRadius: 8, lineColor: Color(.border), lineWidth: 1, dashed: true)
70+
.padding(.vertical, Layout.padding)
5271
}
53-
.padding(.vertical)
5472
}
5573
}
5674

@@ -69,6 +87,14 @@ private struct WooShippingServiceCardListView: View {
6987
}
7088
}
7189

90+
private extension WooShippingServiceView {
91+
enum Layout {
92+
static let padding: CGFloat = 16
93+
static let innerSpacing: CGFloat = 8
94+
static let placeholderPadding: CGFloat = 32
95+
}
96+
}
97+
7298
private extension WooShippingServiceView {
7399
enum Localization {
74100
static let shippingService = NSLocalizedString("wooShipping.createLabels.rates.shippingService",
@@ -77,5 +103,14 @@ private extension WooShippingServiceView {
77103
static let sortBy = NSLocalizedString("wooShipping.createLabels.rates.sortBy",
78104
value: "Sort by",
79105
comment: "Label for the menu to select a sort order for shipping rates in the shipping label creation screen.")
106+
static let noDestinationAddressTitle = NSLocalizedString("wooShipping.createLabels.rates.noDestinationAddressTitle",
107+
value: "Add a destination address to get shipping rates",
108+
comment: "Title displayed when no destination address is provided " +
109+
"in the shipping label creation screen.")
110+
static let noDestinationAddressMessage = NSLocalizedString("wooShipping.createLabels.rates.noDestinationAddressMessage",
111+
value: "We need to know where this package is going " +
112+
"before we can show the available shipping rates.",
113+
comment: "Message displayed when no destination address is provided " +
114+
"in the shipping label creation screen.")
80115
}
81116
}

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Package and Rate Selection/WooShippingServiceViewModel.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ final class WooShippingServiceViewModel: ObservableObject {
77
private let destinationAddress: ShippingLabelAddress?
88
private let stores: StoresManager
99

10+
/// Whether the destination address is present and with non-empty fields.
11+
var hasDestinationAddress: Bool {
12+
destinationAddress?.formattedPostalAddress != nil
13+
}
14+
1015
/// List of tabs to display for the shipping services.
1116
/// Contains the data about available shipping rates, grouped by carrier.
1217
@Published private(set) var serviceTabs: [WooShippingServiceTab] = []
@@ -15,7 +20,7 @@ final class WooShippingServiceViewModel: ObservableObject {
1520
@Published private(set) var selectedRate: WooShippingSelectedRate?
1621

1722
/// State of loading shipping rates.
18-
private(set) var loadingState: LabelRatesState = .empty
23+
@Published private(set) var loadingState: LabelRatesState = .empty
1924

2025
/// Available standard shipping rates.
2126
private var standardRates: [ShippingLabelCarrierRate] = []
@@ -59,7 +64,7 @@ final class WooShippingServiceViewModel: ObservableObject {
5964

6065
/// Retrieves shipping label rates for this shipment from remote.
6166
func loadLabelRates(for selectedPackage: ShippingLabelPackageSelected) {
62-
guard let originAddress, let destinationAddress else {
67+
guard let originAddress, let destinationAddress, hasDestinationAddress else {
6368
return updateLoadingState(to: .error)
6469
}
6570
updateLoadingState(to: .loading)

WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ struct WooShippingCreateLabelsView: View {
6767
totalWeight: $viewModel.shipmentWeight,
6868
updateSelectedPackage: viewModel.selectPackage)
6969
WooShippingServiceView(viewModel: shippingService)
70-
.padding(.horizontal, -16)
7170
} else {
7271
WooShippingPackageAndRatePlaceholder(onSelectPackage: viewModel.selectPackage)
7372
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "woo-shipping-rates-placeholder.pdf",
5+
"idiom" : "universal"
6+
}
7+
],
8+
"info" : {
9+
"author" : "xcode",
10+
"version" : 1
11+
},
12+
"properties" : {
13+
"preserves-vector-representation" : true
14+
}
15+
}

WooCommerce/WooCommerceTests/Extensions/IconsTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,4 +812,8 @@ final class IconsTests: XCTestCase {
812812
func test_cardReaderLocationImage_is_not_nil() {
813813
XCTAssertNotNil(UIImage.cardReaderLocationImage)
814814
}
815+
816+
func test_wooShippingRatesPlaceholder_is_not_nil() {
817+
XCTAssertNotNil(UIImage.wooShippingRatesPlaceholder)
818+
}
815819
}

WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingCreateLabelsViewModelTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,8 @@ final class WooShippingCreateLabelsViewModelTests: XCTestCase {
426426
// Given
427427
let expectedWeight = 2.5
428428
let stores = MockStoresManager(sessionManager: .testingInstance)
429-
let viewModel = WooShippingCreateLabelsViewModel(order: Order.fake().copy(shippingAddress: Address.fake()),
429+
let address = Address.fake().copy(address1: "1 Main Street", city: "San Francisco", state: "CA", postcode: "12345", country: "US")
430+
let viewModel = WooShippingCreateLabelsViewModel(order: Order.fake().copy(shippingAddress: address),
430431
selectedOriginAddress: WooShippingOriginAddress.fake(),
431432
selectedPackage: samplePackageData(),
432433
stores: stores,

WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingServiceViewModelTests.swift

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class WooShippingServiceViewModelTests: XCTestCase {
3737
// Given
3838
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
3939
originAddress: ShippingLabelAddress.fake(),
40-
destinationAddress: ShippingLabelAddress.fake(),
40+
destinationAddress: sampleDestinationAddress(),
4141
stores: stores)
4242

4343
// When
@@ -106,7 +106,7 @@ final class WooShippingServiceViewModelTests: XCTestCase {
106106
}
107107
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
108108
originAddress: ShippingLabelAddress.fake(),
109-
destinationAddress: ShippingLabelAddress.fake(),
109+
destinationAddress: sampleDestinationAddress(),
110110
stores: stores)
111111

112112
// When
@@ -122,7 +122,7 @@ final class WooShippingServiceViewModelTests: XCTestCase {
122122
let standardRate = sampleStandardRates()[1]
123123
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
124124
originAddress: ShippingLabelAddress.fake(),
125-
destinationAddress: ShippingLabelAddress.fake(),
125+
destinationAddress: sampleDestinationAddress(),
126126
stores: stores)
127127

128128
// When
@@ -141,7 +141,7 @@ final class WooShippingServiceViewModelTests: XCTestCase {
141141
// Given
142142
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
143143
originAddress: ShippingLabelAddress.fake(),
144-
destinationAddress: ShippingLabelAddress.fake(),
144+
destinationAddress: sampleDestinationAddress(),
145145
stores: stores)
146146
// When
147147
viewModel.loadLabelRates(for: ShippingLabelPackageSelected.fake().copy(id: samplePackageID))
@@ -158,7 +158,7 @@ final class WooShippingServiceViewModelTests: XCTestCase {
158158
// Given
159159
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
160160
originAddress: ShippingLabelAddress.fake(),
161-
destinationAddress: ShippingLabelAddress.fake(),
161+
destinationAddress: sampleDestinationAddress(),
162162
stores: stores)
163163

164164
// When
@@ -177,7 +177,7 @@ final class WooShippingServiceViewModelTests: XCTestCase {
177177
var selectedRate: WooShippingSelectedRate?
178178
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
179179
originAddress: ShippingLabelAddress.fake(),
180-
destinationAddress: ShippingLabelAddress.fake(),
180+
destinationAddress: sampleDestinationAddress(),
181181
stores: stores) { rate in
182182
selectedRate = rate
183183
}
@@ -194,7 +194,7 @@ final class WooShippingServiceViewModelTests: XCTestCase {
194194
// Given
195195
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
196196
originAddress: ShippingLabelAddress.fake(),
197-
destinationAddress: ShippingLabelAddress.fake(),
197+
destinationAddress: sampleDestinationAddress(),
198198
stores: stores)
199199

200200
// When
@@ -211,7 +211,7 @@ final class WooShippingServiceViewModelTests: XCTestCase {
211211
// Given
212212
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
213213
originAddress: ShippingLabelAddress.fake(),
214-
destinationAddress: ShippingLabelAddress.fake(),
214+
destinationAddress: sampleDestinationAddress(),
215215
stores: stores)
216216

217217
// When
@@ -224,6 +224,27 @@ final class WooShippingServiceViewModelTests: XCTestCase {
224224
XCTAssertEqual(uspsCards?.first?.title, "USPS - Parcel Select Mail")
225225
}
226226

227+
func test_hasDestinationAddress_true_when_destination_address_is_complete() {
228+
// Given
229+
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
230+
originAddress: ShippingLabelAddress.fake(),
231+
destinationAddress: sampleDestinationAddress(),
232+
stores: stores)
233+
234+
// Then
235+
XCTAssertTrue(viewModel.hasDestinationAddress)
236+
}
237+
238+
func test_hasDestinationAddress_false_when_destination_address_is_empty() {
239+
// Given
240+
let viewModel = WooShippingServiceViewModel(order: Order.fake(),
241+
originAddress: ShippingLabelAddress.fake(),
242+
destinationAddress: ShippingLabelAddress.fake(),
243+
stores: stores)
244+
245+
// Then
246+
XCTAssertFalse(viewModel.hasDestinationAddress)
247+
}
227248
}
228249

229250
private extension WooShippingServiceViewModelTests {
@@ -307,4 +328,16 @@ private extension WooShippingServiceViewModelTests {
307328
deliveryDays: 2,
308329
deliveryDateGuaranteed: false)]
309330
}
331+
332+
func sampleDestinationAddress() -> ShippingLabelAddress {
333+
ShippingLabelAddress(company: "HEADQUARTERS",
334+
name: "JANE DOE",
335+
phone: "1-234-456-7890",
336+
country: "US",
337+
state: "NY",
338+
address1: "15 ALGONKIN ST STE 100",
339+
address2: "",
340+
city: "TICONDEROGA",
341+
postcode: "12883-1487")
342+
}
310343
}

0 commit comments

Comments
 (0)