Skip to content

Commit 816929d

Browse files
[Shipping Labels] Reset shipping rates on address change (#15795)
2 parents 3c3b24e + f535174 commit 816929d

File tree

5 files changed

+165
-21
lines changed

5 files changed

+165
-21
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
22.7
55
-----
6+
- [*] Shipping Labels: Fixed an issue where the purchase button would display a stale price after changing the origin address. [https://github.com/woocommerce/woocommerce-ios/pull/15795]
67
- [*] Order Details: Fix crash when reloading data [https://github.com/woocommerce/woocommerce-ios/pull/15764]
78
- [*] Shipping Labels: Improved shipment management UI by hiding remove/merge options instead of disabling them, hiding merge option for orders with 2 or fewer unfulfilled shipments, and hiding the ellipsis menu when no remove/merge actions are available [https://github.com/woocommerce/woocommerce-ios/pull/15760]
89
- [**] POS: a POS tab in the tab bar is now available in the app for stores eligible for Point of Sale, instead of the previous entry point in the Menu tab. [https://github.com/woocommerce/woocommerce-ios/pull/15766]

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

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -102,23 +102,7 @@ final class WooShippingShipmentDetailsViewModel: ObservableObject {
102102
}
103103

104104
/// Shipping rates for the purchased label, with formatted amount.
105-
var shippingRates: [(title: String, amount: String)] {
106-
if let shippingLabel {
107-
return [formatShippingRate(name: shippingLabel.serviceName, rate: shippingLabel.rate)]
108-
} else if let selectedRate {
109-
let baseRate = selectedRate.rate.rate
110-
let formattedBaseRate = formatShippingRate(name: Localization.baseRateLabel(for: selectedRate), rate: baseRate)
111-
let formattedSignatureRate = [
112-
selectedRate.signatureRate.map { self.formatShippingRate(name: Localization.signatureRequired, rate: $0.rate, basedOn: baseRate) },
113-
selectedRate.adultSignatureRate.map { self.formatShippingRate(name: Localization.adultSignatureRequired,
114-
rate: $0.rate,
115-
basedOn: baseRate) }
116-
].compacted()
117-
return [formattedBaseRate] + formattedSignatureRate
118-
} else {
119-
return []
120-
}
121-
}
105+
@Published private(set) var shippingRates: [(title: String, amount: String)] = []
122106

123107
var currentPackage: ShippingLabelPackageSelected? {
124108
guard let selectedPackage else {
@@ -171,6 +155,8 @@ final class WooShippingShipmentDetailsViewModel: ObservableObject {
171155
observeLabelRates()
172156
observeCustomsForm()
173157
observeHAZMATChanges()
158+
observeShippingRates()
159+
setupSelectedRateReset()
174160
}
175161

176162
/// Handles package selection for the shipping label.
@@ -370,6 +356,47 @@ private extension WooShippingShipmentDetailsViewModel {
370356
.assign(to: &$customsInformationIsCompleted)
371357
}
372358

359+
private func observeShippingRates() {
360+
$shippingLabel.combineLatest($selectedRate)
361+
.map { [weak self] shippingLabel, selectedRate -> [(title: String, amount: String)] in
362+
guard let self else { return [] }
363+
if let shippingLabel {
364+
return [self.formatShippingRate(name: shippingLabel.serviceName, rate: shippingLabel.rate)]
365+
} else if let selectedRate {
366+
let baseRate = selectedRate.rate.rate
367+
let formattedBaseRate = self.formatShippingRate(name: Localization.baseRateLabel(for: selectedRate), rate: baseRate)
368+
let formattedSignatureRate = [
369+
selectedRate.signatureRate.map { self.formatShippingRate(name: Localization.signatureRequired, rate: $0.rate, basedOn: baseRate) },
370+
selectedRate.adultSignatureRate.map { self.formatShippingRate(name: Localization.adultSignatureRequired,
371+
rate: $0.rate,
372+
basedOn: baseRate) }
373+
].compacted()
374+
return [formattedBaseRate] + formattedSignatureRate
375+
} else {
376+
return []
377+
}
378+
}
379+
.assign(to: &$shippingRates)
380+
}
381+
382+
/// Observes changes in shipment details and resets the selected rate.
383+
/// This is to prevent displaying a stale price when critical details that affect pricing have changed.
384+
func setupSelectedRateReset() {
385+
$destinationAddress
386+
.combineLatest($originAddress)
387+
.combineLatest($selectedPackage)
388+
.combineLatest($shipmentWeight)
389+
.combineLatest($hazmatCategory)
390+
.combineLatest($customsForm)
391+
// Drop the initial values set on initialization, so we only react to changes.
392+
.dropFirst()
393+
.sink { [weak self] _ in
394+
self?.selectedRate = nil
395+
self?.shippingService?.clearSelectedRate()
396+
}
397+
.store(in: &subscriptions)
398+
}
399+
373400
/// Converts the package data to a `ShippingLabelPackageSelected` object.
374401
func buildSelectedPackage(_ packageData: WooShippingPackageDataRepresentable,
375402
weight: Double,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ final class WooShippingServiceViewModel: ObservableObject {
7979
analytics.track(event: .WooShipping.rateSelectionStep(state: .selected))
8080
}
8181

82+
/// Clears the selected rate.
83+
func clearSelectedRate() {
84+
selectedRate = nil
85+
}
86+
8287
func refreshSelectedRate(from oldRate: WooShippingSelectedRate) -> WooShippingSelectedRate? {
8388
let updatedStandardRate = standardRates.first(where: {
8489
$0.serviceID == oldRate.rate.serviceID

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
4848
observeHAZMATNotices()
4949
observeSelectedPackage()
5050
observeSelectedRates()
51+
observeShippingRates()
5152
}
5253
}
5354

@@ -139,9 +140,7 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
139140
let shippingLines: [WooShipping_ShippingLineViewModel]
140141

141142
/// Shipping rates for the purchased label, with formatted amount.
142-
var shippingRates: [(title: String, amount: String)] {
143-
currentShipmentDetailsViewModel.shippingRates
144-
}
143+
@Published private(set) var shippingRates: [(title: String, amount: String)] = []
145144

146145
/// Total cost of the shipping label, formatted for display.
147146
var totalCost: String? {
@@ -488,6 +487,11 @@ private extension WooShippingCreateLabelsViewModel {
488487
.assign(to: &$selectedRate)
489488
}
490489

490+
func observeShippingRates() {
491+
currentShipmentDetailsViewModel.$shippingRates
492+
.assign(to: &$shippingRates)
493+
}
494+
491495
func updateShipmentDetailsViewModels() {
492496
shipmentDetailViewModels = shipments.map { shipment in
493497
let matchingShippingLabel = shippingLabels.first(where: { $0.shippingLabelID == shipment.purchasedLabelID })
@@ -510,6 +514,7 @@ private extension WooShippingCreateLabelsViewModel {
510514
observeHAZMATNotices()
511515
observeSelectedPackage()
512516
observeSelectedRates()
517+
observeShippingRates()
513518
}
514519

515520
func handleLabelPurchaseSuccess(newLabel: ShippingLabel, in shipment: Shipment) {

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

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ final class WooShippingShipmentDetailsViewModelTests: XCTestCase {
5757
XCTAssertFalse(viewModel.isPurchaseButtonEnabled)
5858

5959
// When
60-
viewModel.shippingService?.onSelectRate?(sampleSelectedRate())
6160
viewModel.selectPackage(samplePackageData())
61+
viewModel.shippingService?.onSelectRate?(sampleSelectedRate())
6262

6363
// Then
6464
XCTAssertTrue(viewModel.isPurchaseButtonEnabled)
@@ -615,6 +615,112 @@ final class WooShippingShipmentDetailsViewModelTests: XCTestCase {
615615
// Then
616616
XCTAssertEqual(viewModel.selectedPackage?.name, updatedPackage.name)
617617
}
618+
619+
func test_changing_origin_address_resets_selected_rate() throws {
620+
// Given
621+
let originAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleOriginAddress(country: "US", state: "NY"))
622+
let destinationAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleDestinationAddress(country: "US", state: "CA"))
623+
let viewModel = WooShippingShipmentDetailsViewModel(order: Order.fake(),
624+
shipment: sampleShipment,
625+
shippingLabel: nil,
626+
originAddress: originAddressSubject.eraseToAnyPublisher(),
627+
destinationAddress: destinationAddressSubject.eraseToAnyPublisher())
628+
629+
viewModel.shippingService?.onSelectRate?(sampleSelectedRate())
630+
XCTAssertNotNil(viewModel.selectedRate, "Precondition failed: selectedRate should not be nil")
631+
632+
// When
633+
originAddressSubject.send(sampleOriginAddress(country: "US", state: "FL"))
634+
635+
// Then
636+
XCTAssertNil(viewModel.selectedRate)
637+
XCTAssertNil(viewModel.shippingService?.selectedRate)
638+
}
639+
640+
func test_changing_destination_address_resets_selected_rate() throws {
641+
// Given
642+
let originAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleOriginAddress(country: "US", state: "NY"))
643+
let destinationAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleDestinationAddress(country: "US", state: "CA"))
644+
let viewModel = WooShippingShipmentDetailsViewModel(order: Order.fake(),
645+
shipment: sampleShipment,
646+
shippingLabel: nil,
647+
originAddress: originAddressSubject.eraseToAnyPublisher(),
648+
destinationAddress: destinationAddressSubject.eraseToAnyPublisher())
649+
650+
viewModel.shippingService?.onSelectRate?(sampleSelectedRate())
651+
XCTAssertNotNil(viewModel.selectedRate, "Precondition failed: selectedRate should not be nil")
652+
653+
// When
654+
destinationAddressSubject.send(sampleDestinationAddress(country: "US", state: "FL"))
655+
656+
// Then
657+
XCTAssertNil(viewModel.selectedRate)
658+
XCTAssertNil(viewModel.shippingService?.selectedRate)
659+
}
660+
661+
func test_changing_shipment_weight_resets_selected_rate() throws {
662+
// Given
663+
let originAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleOriginAddress(country: "US", state: "NY"))
664+
let destinationAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleDestinationAddress(country: "US", state: "CA"))
665+
let viewModel = WooShippingShipmentDetailsViewModel(order: Order.fake(),
666+
shipment: sampleShipment,
667+
shippingLabel: nil,
668+
originAddress: originAddressSubject.eraseToAnyPublisher(),
669+
destinationAddress: destinationAddressSubject.eraseToAnyPublisher())
670+
671+
viewModel.shippingService?.onSelectRate?(sampleSelectedRate())
672+
XCTAssertNotNil(viewModel.selectedRate, "Precondition failed: selectedRate should not be nil")
673+
674+
// When
675+
viewModel.shipmentWeight = "10"
676+
677+
// Then
678+
XCTAssertNil(viewModel.selectedRate)
679+
XCTAssertNil(viewModel.shippingService?.selectedRate)
680+
}
681+
682+
func test_changing_hazmat_category_resets_selected_rate() throws {
683+
// Given
684+
let originAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleOriginAddress(country: "US", state: "NY"))
685+
let destinationAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleDestinationAddress(country: "US", state: "CA"))
686+
let viewModel = WooShippingShipmentDetailsViewModel(order: Order.fake(),
687+
shipment: sampleShipment,
688+
shippingLabel: nil,
689+
originAddress: originAddressSubject.eraseToAnyPublisher(),
690+
destinationAddress: destinationAddressSubject.eraseToAnyPublisher())
691+
692+
viewModel.shippingService?.onSelectRate?(sampleSelectedRate())
693+
XCTAssertNotNil(viewModel.selectedRate, "Precondition failed: selectedRate should not be nil")
694+
695+
// When
696+
viewModel.hazmatCategory = .class1
697+
698+
// Then
699+
XCTAssertNil(viewModel.selectedRate)
700+
XCTAssertNil(viewModel.shippingService?.selectedRate)
701+
}
702+
703+
func test_changing_customs_form_resets_selected_rate() throws {
704+
// Given
705+
let originAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleOriginAddress(country: "US", state: "NY"))
706+
let destinationAddressSubject = CurrentValueSubject<WooShippingAddress?, Never>(sampleDestinationAddress(country: "US", state: "CA"))
707+
let viewModel = WooShippingShipmentDetailsViewModel(order: Order.fake(),
708+
shipment: sampleShipment,
709+
shippingLabel: nil,
710+
originAddress: originAddressSubject.eraseToAnyPublisher(),
711+
destinationAddress: destinationAddressSubject.eraseToAnyPublisher())
712+
713+
viewModel.shippingService?.onSelectRate?(sampleSelectedRate())
714+
XCTAssertNotNil(viewModel.selectedRate, "Precondition failed: selectedRate should not be nil")
715+
716+
// When
717+
viewModel.customsFormViewModel.returnToSenderIfNotDelivered = true
718+
viewModel.customsFormViewModel.onDismiss()
719+
720+
// Then
721+
XCTAssertNil(viewModel.selectedRate)
722+
XCTAssertNil(viewModel.shippingService?.selectedRate)
723+
}
618724
}
619725

620726
private extension WooShippingShipmentDetailsViewModelTests {

0 commit comments

Comments
 (0)