Skip to content

Commit 20095c3

Browse files
Merge release/22.8 into trunk (#15949)
2 parents c1f0679 + 596ebcd commit 20095c3

File tree

40 files changed

+2068
-341
lines changed

40 files changed

+2068
-341
lines changed

RELEASE-NOTES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
- [*] Watch app: Fixed connection issue upon fresh install [https://github.com/woocommerce/woocommerce-ios/pull/15867]
3131
- [Internal] Shipping Labels: Optimize requests for syncing countries [https://github.com/woocommerce/woocommerce-ios/pull/15875]
3232
- [*] Shipping Labels: Display label size from account settings as default [https://github.com/woocommerce/woocommerce-ios/pull/15873]
33+
- [*] Shipping Labels: Ensured customs form validation enforces non-zero product weight to fix shipping rate loading failure. [https://github.com/woocommerce/woocommerce-ios/pull/15927]
34+
- [*] Shipping Labels: ITN number is now required for shipments with total value more than 2500. [https://github.com/woocommerce/woocommerce-ios/pull/15937]
3335

3436
22.7
3537
-----

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

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,13 @@ private extension WooShippingCustomsFormViewModel {
140140
.map { input -> ITNValidationError? in
141141
let (itn, items, hsTariffNumberTotalValueDictionary, countryCode) = input
142142
guard itn.isEmpty else {
143-
return itn.isValidITN ? nil : .invalidFormat
143+
return ITNNumberValidator.isValid(itn) ? nil : .invalidFormat
144144
}
145145

146146
let totalItemValue = items.reduce(0, { sum, item in
147147
sum + item.totalValue
148148
})
149-
if hsTariffNumberTotalValueDictionary.isEmpty,
150-
totalItemValue > Constants.minimumValueForRequiredITN {
149+
if totalItemValue > Constants.minimumValueForRequiredITN {
151150
return .missingForTotalShipmentValue
152151
}
153152

@@ -204,7 +203,7 @@ private extension WooShippingCustomsFormViewModel {
204203
restrictionType: restrictionType.toFormRestrictionType(),
205204
restrictionComments: restrictionType == .other ? restrictionDetails : "",
206205
nonDeliveryOption: returnToSenderIfNotDelivered ? .return : .abandon,
207-
itn: internationalTransactionNumber.isValidITN ? internationalTransactionNumber : "",
206+
itn: ITNNumberValidator.isValid(internationalTransactionNumber) ? internationalTransactionNumber : "",
208207
items: itemsViewModels.map {
209208
ShippingLabelCustomsForm.Item(
210209
description: $0.description,
@@ -246,9 +245,10 @@ extension WooShippingCustomsFormViewModel.ITNValidationError {
246245

247246
private enum Localization {
248247
static let itnInvalidFormat = NSLocalizedString(
249-
"wooShippingCustomsFormViewModel.ITNValidationError.invalidFormat",
250-
value: "Please enter a valid ITN in one of these formats: X12345678901234, AES X12345678901234, or NOEEI 30.37(a).",
251-
comment: "Message when the ITN field is invalid in the customs form of a shipping label"
248+
"wooShippingCustomsFormViewModel.ITNValidationError.invalidFormat.mandatoryAES",
249+
value: "Please enter a valid ITN in one of these formats: AES X12345678901234, or NOEEI 30.37(a).",
250+
comment: "Message when the ITN field is invalid in the customs form of a shipping label. " +
251+
"Doesn't contain X12345678901234 format example."
252252
)
253253
static let itnRequiredForTariffClass = NSLocalizedString(
254254
"wooShippingCustomsFormViewModel.ITNValidationError.missingForTariffClass",
@@ -385,18 +385,24 @@ extension WooShippingContentType {
385385
}
386386
}
387387

388-
private extension String {
389-
var isValidITN: Bool {
390-
guard self.isNotEmpty else {
388+
enum ITNNumberValidator {
389+
/// Validates AES/ITN (International Transaction Number) or NOEEI (No EEI) exemption codes
390+
/// Accepts formats like:
391+
/// - AES ITN: X12345678901234, AES 12345678901234 or AES ITN: 12345678901234
392+
/// - NOEEI exemptions: NOEEI 30.36 or NOEEI 30.36(a) or NOEEI 30.36(a)(1)
393+
/// AES/ITN numbers which are 14 digits long, optionally prefixed with 'X', 'AES', and/or 'ITN'
394+
/// NOEEI exemption codes in the format "NOEEI 30.XX" with optional subsection letters and numbers
395+
static func isValid(_ itnNumber: String) -> Bool {
396+
guard itnNumber.isNotEmpty else {
391397
return true
392398
}
393399

394-
let pattern = "^(?:(?:AES X\\d{14})|(?:NOEEI 30\\.\\d{1,2}(?:\\([a-z]\\)(?:\\(\\d\\))?)?))$"
400+
let pattern = "^(?:(?:AES(?!\\S)\\s*(?:ITN:?\\s*)?X?\\d{14})|(?:NOEEI\\s+30\\.\\d{2}(?:\\([a-z]\\)(?:\\(\\d\\))?)?))$"
395401

396402
do {
397-
let regex = try NSRegularExpression(pattern: pattern)
398-
let range = NSRange(self.startIndex..<self.endIndex, in: self)
399-
return regex.firstMatch(in: self, options: [], range: range) != nil
403+
let regex = try NSRegularExpression(pattern: pattern, options: .caseInsensitive)
404+
let range = NSRange(itnNumber.startIndex..<itnNumber.endIndex, in: itnNumber)
405+
return regex.firstMatch(in: itnNumber, options: [], range: range) != nil
400406
} catch {
401407
return false
402408
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ struct WooShippingCustomsItem: View {
1111
@State private var isShowingDescriptionInfoDialog = false
1212
@State private var isShowingOriginCountryInfoDialog = false
1313

14-
1514
@Environment(\.shippingWeightUnit) var weightUnit: String
1615

1716
var body: some View {
@@ -167,12 +166,12 @@ struct WooShippingCustomsItem: View {
167166
.padding(.trailing, Layout.unitsHorizontalSpacing)
168167
}
169168
.roundedBorder(cornerRadius: Layout.borderCornerRadius,
170-
lineColor: viewModel.weightPerUnit.isEmpty ? warningRedColor : Color(.separator),
169+
lineColor: viewModel.isValidWeight ? Color(.separator) : warningRedColor,
171170
lineWidth: Layout.borderLineWidth)
172171
Text(Localization.valueRequiredWarningText)
173172
.foregroundColor(warningRedColor)
174173
.footnoteStyle()
175-
.renderedIf(viewModel.weightPerUnit.isEmpty)
174+
.renderedIf(!viewModel.isValidWeight)
176175
}
177176
}
178177
.padding(.bottom, Layout.collapsibleViewVerticalSpacing)

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

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ final class WooShippingCustomsItemViewModel: ObservableObject {
5656
HSTariffNumberValidator.sanitize(hsTariffNumber)
5757
}
5858

59+
var isValidWeight: Bool {
60+
return Self.isWeightValid(weightPerUnit)
61+
}
62+
5963
@Published var requiredInformationIsEntered: Bool = false
6064

6165
private var cancellables = Set<AnyCancellable>()
@@ -73,7 +77,12 @@ final class WooShippingCustomsItemViewModel: ObservableObject {
7377
self.itemProductID = itemProductID
7478
self.itemQuantity = itemQuantity
7579
self.valuePerUnit = String(itemValue)
76-
self.weightPerUnit = String(itemWeight)
80+
81+
/// Skip zero weight
82+
if Self.isWeightNonZero(itemWeight) {
83+
self.weightPerUnit = String(itemWeight)
84+
}
85+
7786
self.currencySymbol = currencySymbol
7887
self.storageManager = storageManager
7988

@@ -121,7 +130,11 @@ private extension WooShippingCustomsItemViewModel {
121130
func combineRequiredInformationIsEntered() {
122131
Publishers.CombineLatest4($description, $valuePerUnit, $weightPerUnit, $selectedCountry)
123132
.sink { [weak self] description, valuePerUnit, weightPerUnit, selectedCountry in
124-
self?.requiredInformationIsEntered = description.isNotEmpty && valuePerUnit.isNotEmpty && weightPerUnit.isNotEmpty && selectedCountry != nil
133+
guard let self else { return }
134+
requiredInformationIsEntered = description.isNotEmpty &&
135+
valuePerUnit.isNotEmpty &&
136+
Self.isWeightValid(weightPerUnit) &&
137+
selectedCountry != nil
125138
}
126139
.store(in: &cancellables)
127140
}
@@ -143,6 +156,15 @@ private extension WooShippingCustomsItemViewModel {
143156
}
144157
.store(in: &cancellables)
145158
}
159+
160+
/// Specifically introduced to check for a `0` value
161+
static func isWeightValid(_ weightString: String) -> Bool {
162+
return isWeightNonZero(Double(weightString) ?? 0)
163+
}
164+
165+
static func isWeightNonZero(_ weightValue: Double) -> Bool {
166+
return weightValue > 0
167+
}
146168
}
147169

148170
/// Follows validation logic from `woocommerce-shipping/client/utils/customs.ts`

0 commit comments

Comments
 (0)