Skip to content
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

[Shipping labels] Remotely validate edited address #15023

Merged
merged 6 commits into from
Jan 31, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,29 @@ struct WooShippingEditAddressView: View {
.font(.subheadline)
.foregroundStyle(viewModel.status == .verified ? Constants.green : Constants.red)
Button(Localization.Button.label(for: viewModel.status)) {
if viewModel.status == .verified {
switch viewModel.status {
case .verified:
dismiss()
} else {
// TODO: Handle remote verification
case .unverified:
Task { @MainActor in
await viewModel.remotelyValidateAddress()
}
case .missingInformation:
break
}
}
.buttonStyle(PrimaryButtonStyle())
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: viewModel.isRemotelyValidating))
.disabled(viewModel.status == .missingInformation)
}
.padding()
}
.background(Color(uiColor: .systemBackground))
}
.sheet(item: $viewModel.normalizeAddressVM) { viewModel in
NavigationStack {
WooShippingNormalizeAddressView(viewModel: viewModel)
}
}
}

private struct AddressTextField: View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
statesOfSelectedCountry.isNotEmpty
}

// MARK: Remote validation

/// Whether the address is being remotely validated.
/// This property is used to show a loading indicator while the remote validation is in progress.
@Published private(set) var isRemotelyValidating: Bool = false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the comment, would the property be named isRemotelyValidated instead of validating?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this property is like an isLoading property, tracking when the remote validation is in progress (so we can show the loading indicator on the button). I'll update the comment to make that clearer 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thanks for the clarification


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

init(type: AddressType,
id: String,
name: String,
Expand Down Expand Up @@ -219,7 +228,8 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {

convenience init(address: WooShippingOriginAddress,
stores: StoresManager = ServiceLocator.stores,
storageManager: StorageManagerType = ServiceLocator.storageManager) {
storageManager: StorageManagerType = ServiceLocator.storageManager,
onAddressEdited: ((WooShippingAddress) -> Void)? = nil) {
self.init(type: .origin,
id: address.id,
name: address.fullName,
Expand All @@ -238,6 +248,28 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
stores: stores,
storageManager: storageManager)
}

/// Validates the address remotely.
@MainActor
func remotelyValidateAddress() async {
let addressToValidate = ShippingLabelAddress(company: company.value,
name: name.value,
phone: phone.value,
country: country.value,
state: state.value,
address1: address.value,
address2: "",
city: city.value,
postcode: postalCode.value)
do {
let validation = try await remotelyValidateAddress(addressToValidate)
normalizeAddressVM = WooShippingNormalizeAddressViewModel(enteredAddress: validation.originalAddress,
suggestedAddress: validation.normalizedAddress)
} catch {
// TODO: Handle `WooShippingAddressValidationError` errors.
DDLogError("⛔️ Error validating address for Woo Shipping label: \(error)")
}
}
}

// MARK: Validation
Expand Down Expand Up @@ -358,6 +390,26 @@ private extension WooShippingEditAddressViewModel {
selectedCountry = countries.first { $0.code == country.value }
selectedState = statesOfSelectedCountry.first { $0.code == stateCode }
}

/// Remotely validates the provided address.
@MainActor
func remotelyValidateAddress(_ address: ShippingLabelAddress) async throws -> WooShippingAddressValidationSuccess {
try await withCheckedThrowingContinuation { continuation in
isRemotelyValidating = true
let action = WooShippingAction.validateAddress(siteID: siteID,
address: address) { [weak self] result in
guard let self else { return }
switch result {
case let .success(validation):
continuation.resume(returning: validation)
case let .failure(error):
continuation.resume(throwing: error)
}
self.isRemotelyValidating = false
}
stores.dispatch(action)
}
}
}

// MARK: Constants
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import Yosemite

final class WooShippingNormalizeAddressViewModel: ObservableObject {
final class WooShippingNormalizeAddressViewModel: ObservableObject, Identifiable {
/// Unique ID for the view model.
let id = UUID()

/// The address entered by the merchant.
let enteredAddress: WooShippingAddress

Expand All @@ -12,11 +15,11 @@ final class WooShippingNormalizeAddressViewModel: ObservableObject {
@Published var selectedAddress: WooShippingSelectedAddressType = .suggested

/// Closure to perform when the address is confirmed.
var onConfirm: ((WooShippingAddress) -> Void)
var onConfirm: ((WooShippingAddress) -> Void)?

init(enteredAddress: WooShippingAddress,
suggestedAddress: WooShippingAddress,
onConfirm: @escaping ((WooShippingAddress) -> Void)) {
onConfirm: ((WooShippingAddress) -> Void)? = nil) {
self.enteredAddress = enteredAddress
self.suggestedAddress = suggestedAddress
self.onConfirm = onConfirm
Expand All @@ -25,7 +28,7 @@ final class WooShippingNormalizeAddressViewModel: ObservableObject {
/// Confirms the selected address.
func confirmSelectedAddress() {
let addressToConfirm = selectedAddress == .entered ? enteredAddress : suggestedAddress
onConfirm(addressToConfirm)
onConfirm?(addressToConfirm)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ final class WooShippingEditAddressViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.isDefaultAddress, saveAsDefault)
XCTAssertEqual(viewModel.showCompanyField, showCompanyField)
XCTAssertEqual(viewModel.status, .verified)
XCTAssertFalse(viewModel.isRemotelyValidating)
XCTAssertNil(viewModel.normalizeAddressVM)
}

func test_origin_address_inits_with_expected_values() {
Expand Down Expand Up @@ -683,6 +685,126 @@ final class WooShippingEditAddressViewModelTests: XCTestCase {
// Then
XCTAssertEqual(viewModel.status, .unverified)
}

@MainActor
func test_isRemotelyValidating_set_during_and_after_remote_validation() async {
// Given
var isRemotelyValidatingDuringRemoteAction = false
let stores = MockStoresManager(sessionManager: .testingInstance)
let viewModel = WooShippingEditAddressViewModel(type: .destination,
id: "",
name: "",
company: "",
country: "",
address: "",
city: "",
state: "",
postalCode: "",
email: "",
phone: "",
isDefaultAddress: true,
showCompanyField: true,
isVerified: true,
phoneNumberRequired: true,
stores: stores,
debounceDelayInSeconds: 0)
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
if case let .validateAddress(_, _, completion) = action {
isRemotelyValidatingDuringRemoteAction = viewModel.isRemotelyValidating
completion(.success(.init(normalizedAddress: .fake(), originalAddress: .fake(), isTrivialNormalization: true)))
}
}

// When
await viewModel.remotelyValidateAddress()

// Then
XCTAssertTrue(isRemotelyValidatingDuringRemoteAction)
XCTAssertFalse(viewModel.isRemotelyValidating)
}

@MainActor
func test_remotelyValidateAddress_sends_expected_address_to_validate() async {
// Given
let expectedAddress = ShippingLabelAddress(company: "HEADQUARTERS",
name: "JANE DOE",
phone: "1-234-456-7890",
country: "US",
state: "NY",
address1: "15 ALGONKIN ST STE 100",
address2: "",
city: "TICONDEROGA",
postcode: "12883-1487")
var receivedAddress: ShippingLabelAddress?
let storageManager = MockStorageManager()
let country = Country(code: "US", name: "United States", states: [StateOfACountry(code: "NY", name: "New York")])
storageManager.insertSampleCountries(readOnlyCountries: [country])
let stores = MockStoresManager(sessionManager: .testingInstance)
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
if case let .validateAddress(_, address, completion) = action {
receivedAddress = address
completion(.success(.init(normalizedAddress: .fake(), originalAddress: .fake(), isTrivialNormalization: true)))
}
}
let viewModel = WooShippingEditAddressViewModel(type: .destination,
id: "",
name: expectedAddress.name,
company: expectedAddress.company,
country: expectedAddress.country,
address: expectedAddress.address1,
city: expectedAddress.city,
state: expectedAddress.state,
postalCode: expectedAddress.postcode,
email: "",
phone: expectedAddress.phone,
isDefaultAddress: true,
showCompanyField: true,
isVerified: true,
phoneNumberRequired: true,
stores: stores,
storageManager: storageManager,
debounceDelayInSeconds: 0)

// When
await viewModel.remotelyValidateAddress()

// Then
XCTAssertEqual(receivedAddress, expectedAddress)
}

@MainActor
func test_remotelyValidateAddress_sets_normalizeAddressVM_on_success() async {
// Given
let stores = MockStoresManager(sessionManager: .testingInstance)
let viewModel = WooShippingEditAddressViewModel(type: .destination,
id: "",
name: "",
company: "",
country: "",
address: "",
city: "",
state: "",
postalCode: "",
email: "",
phone: "",
isDefaultAddress: true,
showCompanyField: true,
isVerified: true,
phoneNumberRequired: true,
stores: stores,
debounceDelayInSeconds: 0)
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
if case let .validateAddress(_, _, completion) = action {
completion(.success(.init(normalizedAddress: .fake(), originalAddress: .fake(), isTrivialNormalization: true)))
}
}

// When
await viewModel.remotelyValidateAddress()

// Then
XCTAssertNotNil(viewModel.normalizeAddressVM)
}
}

private extension WooShippingEditAddressViewModel {
Expand Down