Skip to content

Commit 9c04b30

Browse files
authored
[Shipping labels] Update origin address remotely (#15049)
2 parents 2108999 + 589f4dd commit 9c04b30

File tree

7 files changed

+220
-22
lines changed

7 files changed

+220
-22
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ struct WooShippingEditAddressView: View {
166166
break
167167
}
168168
}
169-
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: viewModel.isRemotelyValidating))
169+
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: viewModel.isLoading))
170170
.disabled(viewModel.status == .missingInformation)
171171
}
172172
.padding()

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

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,18 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
146146

147147
// MARK: Remote validation
148148

149-
/// Whether the address is being remotely validated.
150-
/// This property is used to show a loading indicator while the remote validation is in progress.
151-
@Published private(set) var isRemotelyValidating: Bool = false
149+
/// Whether a remote action is in progress.
150+
@Published private(set) var isLoading: Bool = false
152151

153152
/// View model for normalizing the address.
154153
@Published var normalizeAddressVM: WooShippingNormalizeAddressViewModel?
155154

156155
/// Error from remote validation, if any.
157156
@Published private var remoteValidationError: String?
158157

158+
/// Closure called when an origin address is done being edited and the changes are confirmed.
159+
private(set) var onOriginAddressEdited: ((WooShippingOriginAddress) -> Void)?
160+
159161
init(type: AddressType,
160162
id: String,
161163
name: String,
@@ -173,7 +175,8 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
173175
phoneNumberRequired: Bool,
174176
stores: StoresManager = ServiceLocator.stores,
175177
storageManager: StorageManagerType = ServiceLocator.storageManager,
176-
debounceDelayInSeconds: Double = 1) {
178+
debounceDelayInSeconds: Double = 1,
179+
onOriginAddressEdited: ((WooShippingOriginAddress) -> Void)? = nil) {
177180
self.addressType = type
178181
self.id = id
179182
self.name = WooShippingAddressField(type: .name, value: name, required: company.isEmpty, validate: { _ in return nil })
@@ -203,6 +206,7 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
203206
self.siteID = stores.sessionManager.defaultStoreID ?? Int64.min
204207
self.storageManager = storageManager
205208
self.debounceDelay = debounceDelayInSeconds
209+
self.onOriginAddressEdited = onOriginAddressEdited
206210

207211
// Set validation rules for fields that rely on instance properties.
208212
self.name.validate = { [weak self] newName in
@@ -241,7 +245,7 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
241245
convenience init(address: WooShippingOriginAddress,
242246
stores: StoresManager = ServiceLocator.stores,
243247
storageManager: StorageManagerType = ServiceLocator.storageManager,
244-
onAddressEdited: ((WooShippingAddress) -> Void)? = nil) {
248+
onAddressEdited: ((WooShippingOriginAddress) -> Void)? = nil) {
245249
self.init(type: .origin,
246250
id: address.id,
247251
name: address.fullName,
@@ -258,7 +262,8 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
258262
isVerified: address.isVerified,
259263
phoneNumberRequired: true,
260264
stores: stores,
261-
storageManager: storageManager)
265+
storageManager: storageManager,
266+
onOriginAddressEdited: onAddressEdited)
262267
}
263268

264269
/// Validates the address remotely.
@@ -276,7 +281,13 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
276281
do {
277282
let validation = try await remotelyValidateAddress(addressToValidate)
278283
normalizeAddressVM = WooShippingNormalizeAddressViewModel(enteredAddress: validation.originalAddress,
279-
suggestedAddress: validation.normalizedAddress)
284+
suggestedAddress: validation.normalizedAddress,
285+
onConfirm: { [weak self] confirmedAddress in
286+
guard let self else { return }
287+
if addressType == .origin {
288+
updateConfirmedOriginAddress(confirmedAddress)
289+
}
290+
})
280291
} catch let error as WooShippingAddressValidationError {
281292
if let nameError = error.nameError {
282293
name.setError(nameError)
@@ -288,9 +299,43 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
288299
remoteValidationError = generalError
289300
}
290301
} catch {
302+
// TODO: Display error if validation request fails.
291303
DDLogError("⛔️ Error validating address for Woo Shipping label: \(error)")
292304
}
293305
}
306+
307+
/// Updates the origin address remotely with the provided (normalized) address and other edits.
308+
private func updateConfirmedOriginAddress(_ address: WooShippingAddress) {
309+
guard addressType == .origin else {
310+
return
311+
}
312+
313+
// Merge the provided (normalized) address with the edited address fields.
314+
let address = WooShippingOriginAddress(id: id,
315+
company: address.company,
316+
address1: address.address1,
317+
address2: address.address2,
318+
city: address.city,
319+
state: address.state,
320+
postcode: address.postcode,
321+
country: address.country,
322+
phone: address.phone,
323+
firstName: name.value,
324+
lastName: "",
325+
email: email.value,
326+
defaultAddress: isDefaultAddress,
327+
isVerified: true)
328+
329+
Task { @MainActor in
330+
do {
331+
let updatedOriginAddress = try await updateOriginAddress(with: address)
332+
onOriginAddressEdited?(updatedOriginAddress)
333+
} catch {
334+
// TODO: Display error if origin address update fails.
335+
DDLogError("⛔️ Error updating origin address for Woo Shipping label: \(error)")
336+
}
337+
}
338+
}
294339
}
295340

296341
// MARK: Validation
@@ -416,7 +461,7 @@ private extension WooShippingEditAddressViewModel {
416461
@MainActor
417462
func remotelyValidateAddress(_ address: ShippingLabelAddress) async throws -> WooShippingAddressValidationSuccess {
418463
try await withCheckedThrowingContinuation { continuation in
419-
isRemotelyValidating = true
464+
isLoading = true
420465
let action = WooShippingAction.validateAddress(siteID: siteID,
421466
address: address) { [weak self] result in
422467
guard let self else { return }
@@ -426,7 +471,28 @@ private extension WooShippingEditAddressViewModel {
426471
case let .failure(error):
427472
continuation.resume(throwing: error)
428473
}
429-
self.isRemotelyValidating = false
474+
isLoading = false
475+
}
476+
stores.dispatch(action)
477+
}
478+
}
479+
480+
/// Updates an origin address remotely.
481+
@MainActor
482+
func updateOriginAddress(with address: WooShippingOriginAddress) async throws -> WooShippingOriginAddress {
483+
return try await withCheckedThrowingContinuation { continuation in
484+
isLoading = true
485+
let action = WooShippingAction.updateOriginAddress(siteID: siteID,
486+
address: address,
487+
isVerified: address.isVerified) { [weak self] result in
488+
guard let self else { return }
489+
switch result {
490+
case let .success(result):
491+
continuation.resume(returning: result.address)
492+
case .failure(let error):
493+
continuation.resume(throwing: error)
494+
}
495+
isLoading = false
430496
}
431497
stores.dispatch(action)
432498
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ struct WooShippingNormalizeAddressView: View {
4242
Divider().ignoresSafeArea(edges: [.horizontal])
4343
Button {
4444
viewModel.confirmSelectedAddress()
45+
dismiss()
4546
} label: {
4647
switch viewModel.selectedAddress {
4748
case .entered:

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import Yosemite
33

44
final class WooShippingOriginAddressListViewModel: ObservableObject {
5-
let addresses: [WooShippingOriginAddress]
5+
private(set) var addresses: [WooShippingOriginAddress]
66
@Published private(set) var selectedAddressID: String?
77

88
/// View model for address to edit.
@@ -32,8 +32,21 @@ final class WooShippingOriginAddressListViewModel: ObservableObject {
3232
onSelect?(address)
3333
}
3434

35+
/// Sets the `addressToEdit` property for editing the provided address.
36+
/// After the address is edited, the updated address is replaced in the list of addresses.
3537
func editAddress(_ address: WooShippingOriginAddress) {
36-
addressToEdit = WooShippingEditAddressViewModel(address: address)
38+
addressToEdit = WooShippingEditAddressViewModel(address: address, onAddressEdited: { [weak self] editedAddress in
39+
guard let self, let index = addresses.firstIndex(where: { $0.id == editedAddress.id }) else {
40+
return
41+
}
42+
addresses.remove(at: index)
43+
addresses.insert(editedAddress, at: index)
44+
// If the edited address was the selected address, update the selected address.
45+
if selectedAddressID == editedAddress.id {
46+
onSelect?(editedAddress)
47+
}
48+
addressToEdit = nil // Dismisses address edit screen
49+
})
3750
}
3851
}
3952

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

Lines changed: 118 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ final class WooShippingEditAddressViewModelTests: XCTestCase {
5656
XCTAssertEqual(viewModel.isDefaultAddress, saveAsDefault)
5757
XCTAssertEqual(viewModel.showCompanyField, showCompanyField)
5858
XCTAssertEqual(viewModel.status, .verified)
59-
XCTAssertFalse(viewModel.isRemotelyValidating)
59+
XCTAssertFalse(viewModel.isLoading)
6060
XCTAssertNil(viewModel.normalizeAddressVM)
6161
}
6262

@@ -687,9 +687,9 @@ final class WooShippingEditAddressViewModelTests: XCTestCase {
687687
}
688688

689689
@MainActor
690-
func test_isRemotelyValidating_set_during_and_after_remote_validation() async {
690+
func test_isLoading_set_during_and_after_remote_validation() async {
691691
// Given
692-
var isRemotelyValidatingDuringRemoteAction = false
692+
var isLoadingDuringRemoteAction = false
693693
let stores = MockStoresManager(sessionManager: .testingInstance)
694694
let viewModel = WooShippingEditAddressViewModel(type: .destination,
695695
id: "",
@@ -710,7 +710,7 @@ final class WooShippingEditAddressViewModelTests: XCTestCase {
710710
debounceDelayInSeconds: 0)
711711
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
712712
if case let .validateAddress(_, _, completion) = action {
713-
isRemotelyValidatingDuringRemoteAction = viewModel.isRemotelyValidating
713+
isLoadingDuringRemoteAction = viewModel.isLoading
714714
completion(.success(.init(normalizedAddress: .fake(), originalAddress: .fake(), isTrivialNormalization: true)))
715715
}
716716
}
@@ -719,8 +719,8 @@ final class WooShippingEditAddressViewModelTests: XCTestCase {
719719
await viewModel.remotelyValidateAddress()
720720

721721
// Then
722-
XCTAssertTrue(isRemotelyValidatingDuringRemoteAction)
723-
XCTAssertFalse(viewModel.isRemotelyValidating)
722+
XCTAssertTrue(isLoadingDuringRemoteAction)
723+
XCTAssertFalse(viewModel.isLoading)
724724
}
725725

726726
@MainActor
@@ -847,6 +847,118 @@ final class WooShippingEditAddressViewModelTests: XCTestCase {
847847
XCTAssertEqual(viewModel.address.errorMessage, expectedAddressError)
848848
XCTAssertEqual(viewModel.statusLabel, expectedGeneralError)
849849
}
850+
851+
@MainActor
852+
func test_isLoading_set_during_and_after_remote_origin_address_update() async {
853+
// Given
854+
let stores = MockStoresManager(sessionManager: .testingInstance)
855+
let viewModel = WooShippingEditAddressViewModel(address: .fake(), stores: stores)
856+
857+
// When
858+
let isLoadingDuringRemoteAction: Bool = await waitForAsync { promise in
859+
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
860+
switch action {
861+
case let .validateAddress(_, _, completion):
862+
completion(.success(.init(normalizedAddress: .fake(), originalAddress: .fake(), isTrivialNormalization: true)))
863+
case let .updateOriginAddress(_, _, _, completion):
864+
promise(viewModel.isLoading)
865+
completion(.success(WooShippingOriginAddressUpdate(address: .fake(), isVerified: true)))
866+
default:
867+
XCTFail("Unexpected action received: \(action)")
868+
}
869+
870+
}
871+
await viewModel.remotelyValidateAddress()
872+
viewModel.normalizeAddressVM?.confirmSelectedAddress()
873+
}
874+
875+
// Then
876+
XCTAssertTrue(isLoadingDuringRemoteAction)
877+
XCTAssertFalse(viewModel.isLoading)
878+
}
879+
880+
@MainActor
881+
func test_origin_address_update_sends_expected_origin_address_to_remote() async {
882+
// Given
883+
let originAddress = WooShippingOriginAddress.fake().copy(id: "origin",
884+
phone: "123-456-7890",
885+
firstName: "JANE",
886+
lastName: "DOE",
887+
888+
let suggestedAddress = WooShippingAddress(company: "HEADQUARTERS",
889+
name: "JANE DOE",
890+
phone: "123-456-7890",
891+
country: "US",
892+
state: "NY",
893+
address1: "15 ALGONKIN ST STE 100",
894+
address2: "",
895+
city: "TICONDEROGA",
896+
postcode: "12883-1487")
897+
let stores = MockStoresManager(sessionManager: .testingInstance)
898+
let viewModel = WooShippingEditAddressViewModel(address: originAddress, stores: stores)
899+
900+
// When
901+
let receivedAddress: WooShippingOriginAddress = await waitForAsync { promise in
902+
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
903+
switch action {
904+
case let .validateAddress(_, _, completion):
905+
completion(.success(.init(normalizedAddress: suggestedAddress, originalAddress: .fake(), isTrivialNormalization: true)))
906+
case let .updateOriginAddress(_, address, _, completion):
907+
promise(address)
908+
completion(.success(WooShippingOriginAddressUpdate(address: address, isVerified: true)))
909+
default:
910+
XCTFail("Unexpected action received: \(action)")
911+
}
912+
}
913+
await viewModel.remotelyValidateAddress()
914+
viewModel.normalizeAddressVM?.confirmSelectedAddress()
915+
}
916+
917+
// Then
918+
let expectedAddress = WooShippingOriginAddress(id: originAddress.id,
919+
company: suggestedAddress.company,
920+
address1: suggestedAddress.address1,
921+
address2: suggestedAddress.address2,
922+
city: suggestedAddress.city,
923+
state: suggestedAddress.state,
924+
postcode: suggestedAddress.postcode,
925+
country: suggestedAddress.country,
926+
phone: originAddress.phone,
927+
firstName: suggestedAddress.name,
928+
lastName: "",
929+
email: originAddress.email,
930+
defaultAddress: originAddress.defaultAddress,
931+
isVerified: true)
932+
XCTAssertEqual(receivedAddress, expectedAddress)
933+
}
934+
935+
@MainActor
936+
func test_origin_address_update_calls_onOriginAddressEdited_closure() async {
937+
// Given
938+
let expectedAddress = WooShippingOriginAddress.fake().copy(id: "origin")
939+
let stores = MockStoresManager(sessionManager: .testingInstance)
940+
let editedAddress: WooShippingOriginAddress = await waitForAsync { promise in
941+
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
942+
switch action {
943+
case let .validateAddress(_, _, completion):
944+
completion(.success(.init(normalizedAddress: .fake(), originalAddress: .fake(), isTrivialNormalization: true)))
945+
case let .updateOriginAddress(_, _, _, completion):
946+
completion(.success(WooShippingOriginAddressUpdate(address: expectedAddress, isVerified: true)))
947+
default:
948+
XCTFail("Unexpected action received: \(action)")
949+
}
950+
}
951+
// When
952+
let viewModel = WooShippingEditAddressViewModel(address: .fake(), stores: stores) { address in
953+
promise(address)
954+
}
955+
await viewModel.remotelyValidateAddress()
956+
viewModel.normalizeAddressVM?.confirmSelectedAddress()
957+
}
958+
959+
// Then
960+
XCTAssertEqual(editedAddress, expectedAddress)
961+
}
850962
}
851963

852964
private extension WooShippingEditAddressViewModel {

0 commit comments

Comments
 (0)