Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cb4ff85
Make NormalizationResponseDTO parameters nullable
irfano Aug 13, 2025
2959c0b
Add errors to address normalization model
irfano Aug 13, 2025
d198fe5
Handle errors parameter for normalizeAddress in the repository
irfano Aug 13, 2025
4f63487
Refactor NormalizeAddress error constants
irfano Aug 13, 2025
8c4b9b3
Refactor NormalizeAddressException to support multiple errors
irfano Aug 13, 2025
f363a1f
Refactor address normalization error handling
irfano Aug 13, 2025
68ee7b9
Refactor AddressStatus to be a sealed class
irfano Aug 13, 2025
0103472
Improve address normalization error handling and analytics
irfano Aug 13, 2025
cab0e5d
Update address with normalize exception
irfano Aug 13, 2025
3ac3773
Update address status according to normalize exception
irfano Aug 13, 2025
61893fb
Update getErrorState() logic to handle new normalize exception
irfano Aug 13, 2025
b62508d
Shorten WooShippingEditAddressViewModel.kt class
irfano Aug 13, 2025
4b1fc71
Update RELEASE-NOTES.txt
irfano Aug 13, 2025
0a60b8b
Merge branch 'trunk' into issue/WOOMOB-904-improve-address-validation…
irfano Aug 14, 2025
3feefd5
Merge branch 'trunk' into issue/WOOMOB-904-improve-address-validation…
irfano Aug 18, 2025
debbc11
Display individual field errors for address normalization API
irfano Aug 19, 2025
f42c834
Merge branch 'trunk' into issue/WOOMOB-904-improve-address-validation…
irfano Aug 19, 2025
8577a2b
Merge branch 'trunk' into issue/WOOMOB-904-improve-address-validation…
irfano Aug 20, 2025
105114e
Merge branch 'trunk' into issue/WOOMOB-904-improve-address-validation…
irfano Aug 20, 2025
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
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [*] Shipping Labels: Autofill the country field in the customs form and improve the form validation [https://github.com/woocommerce/woocommerce-android/pull/14469]
- [*] Shipping Labels: Fix UI issues on edit address screen [https://github.com/woocommerce/woocommerce-android/pull/14476]
- [Internal] Updated UserAgent of API requests to use `http.agent` System Property to fix performance issues related to WebView usage on app launch [https://github.com/woocommerce/woocommerce-android/pull/14431]
- [*] Shipping Labels: Improved handling of address validation errors [https://github.com/woocommerce/woocommerce-android/pull/14468]

23.0
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ fun ShipmentDetailsExpandedPreview() {
onEditDestinationAddress = {},
onEditOriginAddress = {},
onOriginAddressSelected = {},
destinationStatus = AddressStatus.VERIFIED,
destinationStatus = AddressStatus.Verified,
shipmentPurchased = false,
onPeekHeightChanged = {}
)
Expand Down Expand Up @@ -712,7 +712,7 @@ private fun ShipmentDetailsCollapsedPreview() {
onEditDestinationAddress = {},
onEditOriginAddress = {},
onOriginAddressSelected = {},
destinationStatus = AddressStatus.VERIFIED,
destinationStatus = AddressStatus.Verified,
shipmentPurchased = false,
onPeekHeightChanged = {},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,7 +876,7 @@ private fun WooShippingLabelCreationScreenPreview() {
),
onEditCustomsClick = {},
onEditDestinationAddress = {},
destinationStatus = AddressStatus.VERIFIED,
destinationStatus = AddressStatus.Verified,
onLabelPaperSizeOptionSelected = {},
onPrintShippingLabelClicked = {},
onTrackShipmentClicked = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.address.ObserveShippi
import com.woocommerce.android.ui.orders.wooshippinglabels.address.destination.VerifyDestinationAddress
import com.woocommerce.android.ui.orders.wooshippinglabels.address.origin.FetchOriginAddresses
import com.woocommerce.android.ui.orders.wooshippinglabels.address.origin.ObserveOriginAddresses
import com.woocommerce.android.ui.orders.wooshippinglabels.address.toAddress
import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeBannerUiState
import com.woocommerce.android.ui.orders.wooshippinglabels.components.NoticeType
import com.woocommerce.android.ui.orders.wooshippinglabels.components.ShippingLabelsSnackbarData
Expand All @@ -57,6 +56,7 @@ import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippableItemM
import com.woocommerce.android.ui.orders.wooshippinglabels.models.ShippingLabelStatus
import com.woocommerce.android.ui.orders.wooshippinglabels.models.StoreOptionsModel
import com.woocommerce.android.ui.orders.wooshippinglabels.models.WooShippingLabelPaperSize
import com.woocommerce.android.ui.orders.wooshippinglabels.models.toAddress
import com.woocommerce.android.ui.orders.wooshippinglabels.packages.ui.PackageData
import com.woocommerce.android.ui.orders.wooshippinglabels.purchased.ObserveShippingLabelStatus
import com.woocommerce.android.ui.orders.wooshippinglabels.purchased.printing.FetchShippingLabelFile
Expand Down Expand Up @@ -714,10 +714,10 @@ class WooShippingLabelCreationViewModel @Inject constructor(
val destinationStatus = when {
addressValidationHelper.isMissingDestinationAddress(
addresses[uiState.selectedIndex].shipTo.address
) -> AddressStatus.MISSING_ADDRESS
) -> AddressStatus.MissingAddress

addresses[uiState.selectedIndex].shipTo.isVerified -> AddressStatus.VERIFIED
else -> AddressStatus.UNVERIFIED
addresses[uiState.selectedIndex].shipTo.isVerified -> AddressStatus.Verified
else -> AddressStatus.Unverified
}

val shippingLineSummary = order.getShippingLinesSummary(currencyFormatter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ internal fun AddressSectionPortrait(
val barrier = createEndBarrier(shipFromLabel, shipToLabel)
val endBarrier = createStartBarrier(shipFromSelect)

val destinationStatusModifier = if (destinationStatus == AddressStatus.MISSING_ADDRESS) {
val destinationStatusModifier = if (destinationStatus == AddressStatus.MissingAddress) {
Modifier
.padding(start = dimensionResource(R.dimen.major_100))
.constrainAs(destinationAddressStatus) {
Expand Down Expand Up @@ -172,7 +172,7 @@ internal fun AddressSectionPortrait(
bottom = dimensionResource(R.dimen.major_100)
)
)
if (destinationStatus != AddressStatus.MISSING_ADDRESS) {
if (destinationStatus != AddressStatus.MissingAddress) {
Text(
text = shippingAddresses.shipTo.address.toString(),
modifier = Modifier
Expand Down Expand Up @@ -440,22 +440,25 @@ fun AddressStatusIndicator(
modifier: Modifier = Modifier
) {
val text = when (addressStatus) {
AddressStatus.VERIFIED -> stringResource(id = R.string.woo_shipping_address_verified)
AddressStatus.UNVERIFIED,
AddressStatus.VERIFY_FAILED -> stringResource(id = R.string.woo_shipping_address_unverified)
AddressStatus.Verified -> stringResource(id = R.string.woo_shipping_address_verified)
AddressStatus.Unverified,
is AddressStatus.VerifyFailed -> {
(addressStatus as? AddressStatus.VerifyFailed)?.exception?.generalError
?: stringResource(id = R.string.woo_shipping_address_unverified)
}

AddressStatus.MISSING_INFO -> stringResource(id = R.string.woo_shipping_address_missing_info)
AddressStatus.SAVE_CHANGES -> stringResource(id = R.string.woo_shipping_address_unsaved_changes)
AddressStatus.MISSING_ADDRESS -> stringResource(id = R.string.woo_shipping_address_missing)
AddressStatus.MissingInfo -> stringResource(id = R.string.woo_shipping_address_missing_info)
AddressStatus.SaveChanges -> stringResource(id = R.string.woo_shipping_address_unsaved_changes)
AddressStatus.MissingAddress -> stringResource(id = R.string.woo_shipping_address_missing)
}

val color = when (addressStatus) {
AddressStatus.VERIFIED -> colorResource(id = R.color.woo_shipping_label_success)
AddressStatus.Verified -> colorResource(id = R.color.woo_shipping_label_success)
else -> colorResource(id = R.color.woo_shipping_label_error)
}

val icon = when (addressStatus) {
AddressStatus.VERIFIED -> Icons.Outlined.CheckCircleOutline
AddressStatus.Verified -> Icons.Outlined.CheckCircleOutline
else -> Icons.Outlined.Info
}

Expand Down Expand Up @@ -503,7 +506,7 @@ private fun AddressSectionPortraitPreview() {
onEditOriginAddress = {},
onOriginAddressSelected = {},
isReadOnly = true,
destinationStatus = AddressStatus.VERIFIED
destinationStatus = AddressStatus.Verified
)
}
}
Expand All @@ -524,7 +527,7 @@ private fun AddressSectionPortraitMissingAddressPreview() {
onEditOriginAddress = {},
onOriginAddressSelected = {},
isReadOnly = false,
destinationStatus = AddressStatus.VERIFIED
destinationStatus = AddressStatus.Verified
)
}
}
Expand All @@ -545,7 +548,7 @@ private fun AddressSectionLandscapePreview() {
onEditDestinationAddress = {},
onEditOriginAddress = {},
onOriginAddressSelected = {},
destinationStatus = AddressStatus.VERIFIED
destinationStatus = AddressStatus.Verified
)
}
}
Expand All @@ -566,7 +569,7 @@ private fun AddressSectionLandscapeMissingAddressPreview() {
onEditDestinationAddress = {},
onEditOriginAddress = {},
onOriginAddressSelected = {},
destinationStatus = AddressStatus.VERIFIED
destinationStatus = AddressStatus.Verified
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.woocommerce.android.ui.orders.wooshippinglabels.address

import com.woocommerce.android.extensions.isNotNullOrEmpty
import com.woocommerce.android.model.Address
import com.woocommerce.android.model.AmbiguousLocation
import com.woocommerce.android.model.Location
import com.woocommerce.android.ui.orders.wooshippinglabels.models.AddressNormalizationModel

data class EditAddressViewState(
val isCompanyExpanded: Boolean,
val editableAddress: EditableAddress,
val loading: LoadingState,
val error: EditAddressError?,
val shouldUseStatesInput: Boolean,
val addressStatus: AddressStatus,
val addressValidationState: AddressValidationState
)

sealed class LoadingState {
data object Hidden : LoadingState()
data class DisplayLoading(val title: String, val message: String) : LoadingState()
}

data class EditAddressError(val message: String, val isIndefinite: Boolean = true, val onRetry: () -> Unit)

sealed class AddressStatus {
data object Verified : AddressStatus()
data object Unverified : AddressStatus()
data object MissingAddress : AddressStatus()
data object MissingInfo : AddressStatus()
data object SaveChanges : AddressStatus()
data class VerifyFailed(val exception: NormalizeAddressException? = null) : AddressStatus()
}

sealed class AddressValidationState {
data object NotStarted : AddressValidationState()
data object VerifyingAddress : AddressValidationState()
data class VerificationFailed(
val editableAddress: EditableAddress,
val exception: NormalizeAddressException? = null,
) : AddressValidationState()

data class AddressSelection(
val addressNormalization: AddressNormalizationModel,
val selectedAddress: Address
) : AddressValidationState()

data object UpdatingAddress : AddressValidationState()
data class AddressUpdateFailed(
val editableAddress: EditableAddress
) : AddressValidationState()

data class NormalizedAddressUpdateFailed(
val selection: AddressSelection,
) : AddressValidationState()
}

data class EditableAddress(
val name: InputValue = InputValue.EMPTY,
val company: InputValue = InputValue.EMPTY,
val country: Location = Location.EMPTY,
val address: InputValue = InputValue.EMPTY,
val city: InputValue = InputValue.EMPTY,
val state: Location = Location.EMPTY,
val postalCode: InputValue = InputValue.EMPTY,
val email: InputValue = InputValue.EMPTY,
val phone: InputValue = InputValue.EMPTY
) {
val hasIncorrectOrMissingData: Boolean
get() = address.error.isNotNullOrEmpty() ||
city.error.isNotNullOrEmpty() ||
postalCode.error.isNotNullOrEmpty() ||
email.error.isNotNullOrEmpty() ||
phone.error.isNotNullOrEmpty() ||
name.error.isNotNullOrEmpty() ||
company.error.isNotNullOrEmpty()
}

fun EditableAddress.toAddress() = Address(
firstName = name.value,
lastName = "",
company = company.value,
address1 = address.value,
address2 = "",
city = city.value,
state = AmbiguousLocation.Defined(state),
postcode = postalCode.value,
country = country,
email = email.value,
phone = phone.value
)

data class InputValue(val value: String, val error: String? = null, val isRequired: Boolean = false) {
companion object {
val EMPTY = InputValue("")
}
}

sealed class LocationState {
data object Loading : LocationState()
data object DisplayLoading : LocationState()
data object Error : LocationState()
data class Loaded(val locations: List<Location>) : LocationState()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.woocommerce.android.ui.orders.wooshippinglabels.address

import com.woocommerce.android.model.Address
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.orders.wooshippinglabels.address.NormalizeAddressException.Companion.GENERAL_ERROR
import com.woocommerce.android.ui.orders.wooshippinglabels.address.NormalizeAddressException.Companion.UNKNOWN_ERROR
import com.woocommerce.android.ui.orders.wooshippinglabels.models.AddressNormalizationModel
import com.woocommerce.android.ui.orders.wooshippinglabels.networking.WooShippingLabelRepository
import javax.inject.Inject
Expand All @@ -12,22 +12,48 @@ class NormalizeAddress @Inject constructor(
private val site: SelectedSite,
) {
suspend operator fun invoke(address: Address): Result<AddressNormalizationModel> {
return site.getOrNull()?.let {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed site.getOrNull() to site.get() to align with the approach we use for our domain classes in the shipping label screens.

val response = repository.normalizeAddress(it, address)
val result = response.model
when {
response.isError || result == null -> {
Result.failure(NormalizeAddressException(response.error.message ?: GENERAL_ERROR))
}

else -> Result.success(result)
val response = repository.normalizeAddress(site.get(), address)
val result = response.model
return when {
response.isError || result == null -> {
val message =
response.error.message ?: if (result == null) "Empty response" else UNKNOWN_ERROR
Result.failure(Exception(message))
}
} ?: Result.failure(NormalizeAddressException(GENERAL_ERROR))

result.errors != null -> Result.failure(NormalizeAddressException(errors = result.errors))
else -> Result.success(result)
}
}
}

class NormalizeAddressException(val error: String) : Exception(error) {
class NormalizeAddressException(val errors: Map<String, String>) : Exception() {
val generalError: String?
get() = errors[ERROR_GENERAL]
val nameError: String?
get() = errors[ERROR_NAME]
val companyError: String?
get() = errors[ERROR_COMPANY]
val addressError: String?
get() = errors[ERROR_ADDRESS]
val cityError: String?
get() = errors[ERROR_CITY]
val postcodeError: String?
get() = errors[ERROR_POSTCODE]
val emailError: String?
get() = errors[ERROR_EMAIL]
val phoneError: String?
get() = errors[ERROR_PHONE]

companion object {
const val GENERAL_ERROR = "general"
const val ERROR_GENERAL = "general"
const val ERROR_NAME = "name"
const val ERROR_COMPANY = "company"
const val ERROR_ADDRESS = "address"
const val ERROR_EMAIL = "email"
const val ERROR_PHONE = "phone"
const val ERROR_CITY = "city"
const val ERROR_POSTCODE = "postcode"
const val UNKNOWN_ERROR = "Unknown error"
}
}
Loading