Skip to content

Commit 4cbcf7b

Browse files
authored
Shipping Labels: Allow proceeding with destination address when validation fails (#15928)
2 parents 44700a8 + 3f3ba1a commit 4cbcf7b

File tree

5 files changed

+211
-11
lines changed

5 files changed

+211
-11
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [*] POS: a POS tab in the tab bar is now available in the app for stores in countries eligible for Point of Sale, instead of the tab is only shown when the store is eligible for POS. [https://github.com/woocommerce/woocommerce-ios/pull/15918]
1111
- [*] Shipping Labels: Display base rate on selected shipping service cards [https://github.com/woocommerce/woocommerce-ios/pull/15916]
1212
- [*] Shipping Labels: Update mark order completed toggle on purchase form [https://github.com/woocommerce/woocommerce-ios/pull/15917]
13+
- [*] Shipping Labels: Allow confirming destination addresses when validation fails. [https://github.com/woocommerce/woocommerce-ios/pull/15928]
1314
- [*] Shipping Labels: Validate custom package dimensions [https://github.com/woocommerce/woocommerce-ios/pull/15925]
1415
- [*] Shipping Labels: Show UPS TOS modal in full length for better accessibility. [https://github.com/woocommerce/woocommerce-ios/pull/15926]
1516
- [*] Shipping Labels: Optimize data loading on purchase form [https://github.com/woocommerce/woocommerce-ios/pull/15919]

WooCommerce/Classes/Analytics/WooAnalyticsEvent+WooShipping.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ extension WooAnalyticsEvent {
1818
case validationFailed = "validation_failed"
1919
case validationSuccess = "validation_success"
2020
case confirmed
21+
case confirmedWithoutVerification = "confirmed_without_verification"
2122
}
2223

2324
enum PackageSelectionStep: String {

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ struct WooShippingEditAddressView: View {
2424

2525
@State private var isPresentingCountrySelector: Bool = false
2626
@State private var isPresentingStateSelector: Bool = false
27+
@State private var actionType: ActionType?
2728

2829
var body: some View {
2930
ScrollView {
@@ -207,7 +208,9 @@ struct WooShippingEditAddressView: View {
207208
}
208209
.font(.subheadline)
209210
.foregroundStyle(viewModel.status == .verified ? Constants.green : Constants.red)
211+
210212
Button(Localization.Button.label(for: viewModel.status)) {
213+
actionType = .validateOrConfirm
211214
switch viewModel.status {
212215
case .verified:
213216
dismiss()
@@ -219,8 +222,18 @@ struct WooShippingEditAddressView: View {
219222
break
220223
}
221224
}
222-
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: viewModel.isLoading))
225+
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: viewModel.isLoading &&
226+
actionType == .validateOrConfirm))
223227
.disabled(viewModel.status == .missingInformation)
228+
.renderedIf(!viewModel.canConfirmWithoutVerification)
229+
230+
Button(Localization.useAddressAsEntered) {
231+
actionType = .proceedWithoutValidation
232+
viewModel.proceedWithInputAddress()
233+
}
234+
.buttonStyle(SecondaryLoadingButtonStyle(isLoading: viewModel.isLoading &&
235+
actionType == .proceedWithoutValidation))
236+
.renderedIf(viewModel.canConfirmWithoutVerification)
224237
}
225238
.padding(isScrollViewEmbedded ? .vertical : .all)
226239
}
@@ -364,6 +377,11 @@ extension WooShippingEditAddressView {
364377
}
365378

366379
private extension WooShippingEditAddressView {
380+
enum ActionType {
381+
case validateOrConfirm
382+
case proceedWithoutValidation
383+
}
384+
367385
enum Constants {
368386
static let verticalSpacing: CGFloat = 16
369387
static let defaultPadding: CGFloat = 16
@@ -441,6 +459,11 @@ private extension WooShippingEditAddressView {
441459
static let done = NSLocalizedString("wooShipping.createLabels.editAddress.done",
442460
value: "Done",
443461
comment: "Button to dismiss the keyboard")
462+
static let useAddressAsEntered = NSLocalizedString(
463+
"wooShipping.createLabels.editAddress.useAddressAsEntered",
464+
value: "Use address as entered",
465+
comment: "Button to proceed with the input address even when validation fails"
466+
)
444467

445468
enum Button {
446469
static func label(for status: WooShippingAddressStatus) -> String {

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

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,14 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
8686
/// Status of the address, based on local validation and remote verification.
8787
var status: WooShippingAddressStatus {
8888
let isRemotelyVerified = originalAddressIsVerified && !hasChanges
89-
switch (isRemotelyVerified, isValid) {
90-
case (true, true): // Is a valid, remotely verified address.
89+
switch (isRemotelyVerified, isValid, canConfirmWithoutVerification) {
90+
case (true, true, _): // Is a valid, remotely verified address.
9191
return .verified
92-
case (false, true): // Is a valid, unverified address.
92+
case (false, true, _): // Is a valid, unverified address.
9393
return .unverified
94-
case (_, false): // Is an invalid address.
94+
case (_, false, true): // Validation fails but user can proceed.
95+
return .unverified
96+
case (_, false, false): // Is an invalid address.
9597
return .missingInformation
9698
}
9799
}
@@ -120,6 +122,9 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
120122
/// Selected state. We observe this to update the `state` property.
121123
@Published private(set) var selectedState: StateOfACountry?
122124

125+
/// Whether user can proceed with their input address even when validation fails.
126+
@Published private(set) var canConfirmWithoutVerification = false
127+
123128
/// View model for selecting a country from a list.
124129
var countrySelectorVM: CountrySelectorViewModel {
125130
let selectedCountryBinding = Binding<AreaSelectorCommandProtocol?>(
@@ -275,6 +280,7 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
275280
return self.isPhoneNumberValid ? nil : Localization.Validation.phone
276281
}
277282

283+
observeFieldValues()
278284
observeNameAndCompany()
279285
observeSelectedCountry()
280286
observeSelectedState()
@@ -343,6 +349,19 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
343349
onDestinationAddressEdited: onAddressEdited)
344350
}
345351

352+
func proceedWithInputAddress() {
353+
let address = WooShippingAddress(company: company.value,
354+
name: name.value,
355+
phone: phone.value,
356+
country: country.value,
357+
state: state.value,
358+
address1: address.value,
359+
address2: "",
360+
city: city.value,
361+
postcode: postalCode.value)
362+
updateConfirmedAddress(address, withoutVerification: true)
363+
}
364+
346365
/// Validates the address remotely.
347366
@MainActor
348367
func remotelyValidateAddress() async {
@@ -368,6 +387,10 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
368387
updateConfirmedAddress(confirmedAddress)
369388
})
370389
} catch let error as WooShippingAddressValidationError {
390+
/// Enables proceeding for destination addresses even when validation fails
391+
if case .destination = addressType {
392+
canConfirmWithoutVerification = true
393+
}
371394
if let nameError = error.nameError {
372395
name.setError(nameError)
373396
}
@@ -397,15 +420,15 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
397420
}
398421

399422
/// Update confirmed address remotely.
400-
@MainActor
401-
func updateConfirmedAddress(_ address: WooShippingAddress) {
423+
func updateConfirmedAddress(_ address: WooShippingAddress, withoutVerification: Bool = false) {
402424
switch addressType {
403425
case .origin:
404426
updateConfirmedOriginAddress(address)
405427
case .destination(let orderID):
406428
updateConfirmedDestinationAddress(
407429
for: orderID,
408-
with: address
430+
with: address,
431+
withoutVerification: withoutVerification
409432
)
410433
}
411434
}
@@ -454,7 +477,8 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
454477

455478
/// Updates the destination address remotely with the provided (normalized) address and other edits.
456479
private func updateConfirmedDestinationAddress(for orderID: Int64,
457-
with address: WooShippingAddress) {
480+
with address: WooShippingAddress,
481+
withoutVerification: Bool) {
458482
// Merge the provided (normalized) address with the edited address fields.
459483
let destinationAddress = WooShippingDestinationAddress(company: address.company,
460484
address1: address.address1,
@@ -474,7 +498,10 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
474498
let updatedDestinationAddress = try await updateDestinationAddress(for: orderID,
475499
with: destinationAddress)
476500
onDestinationAddressEdited?(updatedDestinationAddress.toWooShippingAddress(), email.value)
477-
analytics.track(event: .WooShipping.editingAddressStep(type: .destination, state: .confirmed))
501+
analytics.track(event: .WooShipping.editingAddressStep(
502+
type: .destination,
503+
state: withoutVerification ? .confirmedWithoutVerification : .confirmed
504+
))
478505
} catch {
479506
DDLogError("⛔️ Error updating destination address for Woo Shipping label: \(error)")
480507

@@ -483,7 +510,7 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
483510

484511
analytics.track(event: .WooShipping.editingAddressStep(
485512
type: .destination,
486-
state: .confirmed,
513+
state: withoutVerification ? .confirmedWithoutVerification : .confirmed,
487514
error: error
488515
))
489516
}
@@ -623,6 +650,13 @@ private extension WooShippingEditAddressViewModel {
623650
}
624651
.store(in: &cancellables)
625652
}
653+
654+
func observeFieldValues() {
655+
allFields.map { $0.$value.removeDuplicates() }
656+
.combineLatest()
657+
.map { _ in false }
658+
.assign(to: &$canConfirmWithoutVerification)
659+
}
626660
}
627661

628662
// MARK: Remote

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

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,147 @@ final class WooShippingEditAddressViewModelTests: XCTestCase {
14461446
XCTAssertEqual(viewModel.address.errorMessage, expectedAddressError)
14471447
XCTAssertEqual(viewModel.statusLabel, expectedGeneralError)
14481448
}
1449+
1450+
// MARK: - canConfirmWithoutVerification Tests
1451+
1452+
func test_canConfirmWithoutVerification_is_false_initially() {
1453+
// Given & When
1454+
let viewModel = WooShippingEditAddressViewModel(type: .destination(orderID: sampleOrderID),
1455+
id: "",
1456+
name: "JANE DOE",
1457+
company: "HEADQUARTERS",
1458+
country: "US",
1459+
address: "15 ALGONKIN ST",
1460+
city: "TICONDEROGA",
1461+
state: "NY",
1462+
postalCode: "12883-1487",
1463+
1464+
phone: "123-456-7890",
1465+
isDefaultAddress: false,
1466+
showCompanyField: true,
1467+
isVerified: false)
1468+
1469+
// Then
1470+
XCTAssertFalse(viewModel.canConfirmWithoutVerification)
1471+
}
1472+
1473+
@MainActor
1474+
func test_canConfirmWithoutVerification_is_enabled_for_destination_addresses_when_validation_fails() async {
1475+
// Given
1476+
let stores = MockStoresManager(sessionManager: .testingInstance)
1477+
let viewModel = WooShippingEditAddressViewModel(type: .destination(orderID: sampleOrderID),
1478+
id: "",
1479+
name: "",
1480+
company: "",
1481+
country: "US",
1482+
address: "ALGONKIN ST",
1483+
city: "TICONDEROGA",
1484+
state: "NY",
1485+
postalCode: "12883-1487",
1486+
1487+
phone: "123-456-7890",
1488+
isDefaultAddress: false,
1489+
showCompanyField: true,
1490+
isVerified: false,
1491+
stores: stores)
1492+
1493+
// Initial state
1494+
XCTAssertFalse(viewModel.canConfirmWithoutVerification)
1495+
1496+
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
1497+
if case let .validateAddress(_, _, completion) = action {
1498+
completion(.failure(WooShippingAddressValidationError(addressError: "House number is missing",
1499+
generalError: "Address not found",
1500+
nameError: nil)))
1501+
}
1502+
}
1503+
1504+
// When
1505+
await viewModel.remotelyValidateAddress()
1506+
1507+
// Then
1508+
XCTAssertTrue(viewModel.canConfirmWithoutVerification)
1509+
XCTAssertEqual(viewModel.status, .unverified)
1510+
}
1511+
1512+
@MainActor
1513+
func test_canConfirmWithoutVerification_remains_false_for_origin_addresses_even_when_validation_fails() async {
1514+
// Given
1515+
let stores = MockStoresManager(sessionManager: .testingInstance)
1516+
let viewModel = WooShippingEditAddressViewModel(type: .origin,
1517+
id: "",
1518+
name: "",
1519+
company: "",
1520+
country: "US",
1521+
address: "ALGONKIN ST",
1522+
city: "TICONDEROGA",
1523+
state: "NY",
1524+
postalCode: "12883-1487",
1525+
1526+
phone: "123-456-7890",
1527+
isDefaultAddress: true,
1528+
showCompanyField: true,
1529+
isVerified: false,
1530+
stores: stores)
1531+
1532+
// Initial state
1533+
XCTAssertFalse(viewModel.canConfirmWithoutVerification)
1534+
1535+
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
1536+
if case let .validateAddress(_, _, completion) = action {
1537+
completion(.failure(WooShippingAddressValidationError(addressError: "House number is missing",
1538+
generalError: "Address not found",
1539+
nameError: nil)))
1540+
}
1541+
}
1542+
1543+
// When
1544+
await viewModel.remotelyValidateAddress()
1545+
1546+
// Then
1547+
XCTAssertFalse(viewModel.canConfirmWithoutVerification)
1548+
XCTAssertEqual(viewModel.status, .missingInformation)
1549+
}
1550+
1551+
@MainActor
1552+
func test_canConfirmWithoutVerification_resets_to_false_when_address_fields_change() async {
1553+
// Given
1554+
let stores = MockStoresManager(sessionManager: .testingInstance)
1555+
let viewModel = WooShippingEditAddressViewModel(type: .destination(orderID: sampleOrderID),
1556+
id: "",
1557+
name: "",
1558+
company: "",
1559+
country: "US",
1560+
address: "ALGONKIN ST",
1561+
city: "TICONDEROGA",
1562+
state: "NY",
1563+
postalCode: "12883-1487",
1564+
1565+
phone: "123-456-7890",
1566+
isDefaultAddress: false,
1567+
showCompanyField: true,
1568+
isVerified: false,
1569+
stores: stores)
1570+
1571+
stores.whenReceivingAction(ofType: WooShippingAction.self) { action in
1572+
if case let .validateAddress(_, _, completion) = action {
1573+
completion(.failure(WooShippingAddressValidationError(addressError: "House number is missing",
1574+
generalError: "Address not found",
1575+
nameError: "Either Name or Company is required")))
1576+
}
1577+
}
1578+
1579+
await viewModel.remotelyValidateAddress()
1580+
XCTAssertTrue(viewModel.canConfirmWithoutVerification)
1581+
XCTAssertEqual(viewModel.status, .unverified)
1582+
1583+
// When any field value changes
1584+
viewModel.name.value = "JANE DOE"
1585+
1586+
// Then
1587+
XCTAssertFalse(viewModel.canConfirmWithoutVerification)
1588+
XCTAssertEqual(viewModel.status, .missingInformation)
1589+
}
14491590
}
14501591

14511592
private extension WooShippingEditAddressViewModel {

0 commit comments

Comments
 (0)