Skip to content

Commit 00d8a10

Browse files
authored
[Shipping labels] Open address editing form for destination address (#15226)
2 parents c56e568 + f344e28 commit 00d8a10

File tree

8 files changed

+290
-77
lines changed

8 files changed

+290
-77
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Yosemite
2+
3+
struct WooShippingCustomsRequirements {
4+
/// Checks whether a customs form is required for the given origin country/state and destination country/state.
5+
///
6+
static func isCustomsRequired(originCountry: String?,
7+
originState: String?,
8+
destinationCountry: String?,
9+
destinationState: String?) -> Bool {
10+
// Special case: Any shipment from/to military addresses must have Customs
11+
if originCountry == Constants.usCountryCode,
12+
Constants.usMilitaryStates.contains(where: { $0 == originState }) {
13+
return true
14+
}
15+
if destinationCountry == Constants.usCountryCode,
16+
Constants.usMilitaryStates.contains(where: { $0 == destinationState }) {
17+
return true
18+
}
19+
20+
return originCountry != destinationCountry
21+
}
22+
}
23+
24+
private extension WooShippingCustomsRequirements {
25+
enum Constants {
26+
/// Country code for US - to check for international shipment
27+
///
28+
static let usCountryCode = "US"
29+
30+
/// These US states are a special case because they represent military bases. They're considered "domestic",
31+
/// but they require a Customs form to ship from/to them.
32+
static let usMilitaryStates = ["AA", "AE", "AP"]
33+
}
34+
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,7 @@ private extension WooShippingEditAddressView {
429429
phone: "",
430430
isDefaultAddress: true,
431431
showCompanyField: false,
432-
isVerified: true,
433-
phoneNumberRequired: true))
432+
isVerified: true))
434433
}
435434

436435
#Preview("With Company") {
@@ -447,6 +446,5 @@ private extension WooShippingEditAddressView {
447446
phone: "",
448447
isDefaultAddress: false,
449448
showCompanyField: true,
450-
isVerified: false,
451-
phoneNumberRequired: true))
449+
isVerified: false))
452450
}

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

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,13 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
6161
[name, company, country, address, city, state, postalCode, email, phone]
6262
}
6363

64-
/// Whether the phone number is required.
65-
private let phoneNumberRequired: Bool
64+
/// The origin address country code.
65+
/// This is used to determine whether the phone number is required when editing a destination address.
66+
private let originCountryCode: String?
67+
68+
/// The origin address state code.
69+
/// This is used to determine whether the phone number is required when editing a destination address.
70+
private let originStateCode: String?
6671

6772
/// Status of the address, based on local validation and remote verification.
6873
var status: WooShippingAddressStatus {
@@ -158,6 +163,10 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
158163
/// Closure called when an origin address is done being edited and the changes are confirmed.
159164
private(set) var onOriginAddressEdited: ((WooShippingOriginAddress) -> Void)?
160165

166+
/// Closure called when a destination address is done being edited and the changes are confirmed.
167+
/// Returns the updated address and email address.
168+
private(set) var onDestinationAddressEdited: ((WooShippingAddress, String?) -> Void)?
169+
161170
init(type: AddressType,
162171
id: String,
163172
name: String,
@@ -172,11 +181,13 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
172181
isDefaultAddress: Bool,
173182
showCompanyField: Bool,
174183
isVerified: Bool,
175-
phoneNumberRequired: Bool,
184+
originCountryCode: String? = nil,
185+
originStateCode: String? = nil,
176186
stores: StoresManager = ServiceLocator.stores,
177187
storageManager: StorageManagerType = ServiceLocator.storageManager,
178188
debounceDelayInSeconds: Double = 1,
179-
onOriginAddressEdited: ((WooShippingOriginAddress) -> Void)? = nil) {
189+
onOriginAddressEdited: ((WooShippingOriginAddress) -> Void)? = nil,
190+
onDestinationAddressEdited: ((WooShippingAddress, String?) -> Void)? = nil) {
180191
self.addressType = type
181192
self.id = id
182193
self.name = WooShippingAddressField(type: .name, value: name, required: company.isEmpty, validate: { _ in return nil })
@@ -197,11 +208,17 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
197208
self.email = WooShippingAddressField(type: .email, value: email, required: true, validate: { newEmail in
198209
newEmail.isEmpty ? Localization.Validation.email : nil
199210
})
211+
let phoneNumberRequired = Self.phoneNumberRequired(addressType: type,
212+
selectedCountryCode: country,
213+
selectedState: state,
214+
originCountryCode: originCountryCode,
215+
originStateCode: originStateCode)
200216
self.phone = WooShippingAddressField(type: .phone, value: phone, required: phoneNumberRequired, validate: { _ in return nil})
201217
self.isDefaultAddress = isDefaultAddress
202218
self.showCompanyField = showCompanyField
203219
self.originalAddressIsVerified = isVerified
204-
self.phoneNumberRequired = phoneNumberRequired
220+
self.originCountryCode = originCountryCode
221+
self.originStateCode = originStateCode
205222
self.stores = stores
206223
self.siteID = stores.sessionManager.defaultStoreID ?? Int64.min
207224
self.storageManager = storageManager
@@ -242,6 +259,7 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
242259
validateAddress()
243260
}
244261

262+
/// Used to initialize the view model with an origin address.
245263
convenience init(address: WooShippingOriginAddress,
246264
stores: StoresManager = ServiceLocator.stores,
247265
storageManager: StorageManagerType = ServiceLocator.storageManager,
@@ -260,12 +278,41 @@ final class WooShippingEditAddressViewModel: ObservableObject, Identifiable {
260278
isDefaultAddress: address.defaultAddress,
261279
showCompanyField: address.company.isNotEmpty,
262280
isVerified: address.isVerified,
263-
phoneNumberRequired: true,
264281
stores: stores,
265282
storageManager: storageManager,
266283
onOriginAddressEdited: onAddressEdited)
267284
}
268285

286+
/// Used to initialize the view model with a destination address.
287+
convenience init(address: WooShippingAddress?,
288+
email: String?,
289+
isVerified: Bool,
290+
originCountryCode: String?,
291+
originStateCode: String?,
292+
stores: StoresManager = ServiceLocator.stores,
293+
storageManager: StorageManagerType = ServiceLocator.storageManager,
294+
onAddressEdited: ((WooShippingAddress, String?) -> Void)? = nil) {
295+
self.init(type: .destination,
296+
id: UUID().uuidString,
297+
name: address?.name ?? "",
298+
company: address?.company ?? "",
299+
country: address?.country ?? "",
300+
address: address?.combinedAddress ?? "",
301+
city: address?.city ?? "",
302+
state: address?.state ?? "",
303+
postalCode: address?.postcode ?? "",
304+
email: email ?? "",
305+
phone: address?.phone ?? "",
306+
isDefaultAddress: false,
307+
showCompanyField: address?.company.isNotEmpty == true,
308+
isVerified: isVerified,
309+
originCountryCode: originCountryCode,
310+
originStateCode: originStateCode,
311+
stores: stores,
312+
storageManager: storageManager,
313+
onDestinationAddressEdited: onAddressEdited)
314+
}
315+
269316
/// Validates the address remotely.
270317
@MainActor
271318
func remotelyValidateAddress() async {
@@ -357,7 +404,7 @@ extension WooShippingEditAddressViewModel {
357404
///
358405
private var isPhoneNumberValid: Bool {
359406
guard phone.value.isNotEmpty else {
360-
return !phoneNumberRequired
407+
return !phoneNumberRequired(for: country.value, and: state.value)
361408
}
362409
guard isUSAddress else {
363410
return true
@@ -369,6 +416,39 @@ extension WooShippingEditAddressViewModel {
369416
return phoneDigits.count == 10
370417
}
371418
}
419+
420+
/// Whether the phone number is required.
421+
///
422+
private func phoneNumberRequired(for country: String?, and state: String?) -> Bool {
423+
Self.phoneNumberRequired(addressType: addressType,
424+
selectedCountryCode: country,
425+
selectedState: state,
426+
originCountryCode: originCountryCode,
427+
originStateCode: originStateCode)
428+
}
429+
430+
/// Whether the phone number is required.
431+
/// - Parameters:
432+
/// - addressType: Type of address being edited.
433+
/// - selectedCountryCode: Country code of the selected country for the address being edited.
434+
/// - selectedState: Selected state for the address being edited.
435+
/// - originCountryCode: Country code of the origin address.
436+
/// - originStateCode: State code of the origin address.
437+
private static func phoneNumberRequired(addressType: AddressType,
438+
selectedCountryCode: String?,
439+
selectedState: String?,
440+
originCountryCode: String?,
441+
originStateCode: String?) -> Bool {
442+
switch addressType {
443+
case .origin:
444+
return true
445+
case .destination:
446+
return WooShippingCustomsRequirements.isCustomsRequired(originCountry: originCountryCode,
447+
originState: originStateCode,
448+
destinationCountry: selectedCountryCode,
449+
destinationState: selectedState)
450+
}
451+
}
372452
}
373453

374454
private extension WooShippingEditAddressViewModel {
@@ -405,6 +485,10 @@ private extension WooShippingEditAddressViewModel {
405485
country.setDisplayValue(selectedCountry.name)
406486
selectedState = nil
407487
state.required = stateRequired
488+
489+
// Update phone number requirement based on selected country.
490+
phone.required = phoneNumberRequired(for: selectedCountry.code, and: selectedState?.code)
491+
phone.validateField()
408492
}
409493
.store(in: &cancellables)
410494
}
@@ -416,6 +500,10 @@ private extension WooShippingEditAddressViewModel {
416500
guard let self else { return }
417501
state.value = selectedState?.code ?? ""
418502
state.setDisplayValue(selectedState?.name ?? "")
503+
504+
// Update phone number requirement based on selected state.
505+
phone.required = phoneNumberRequired(for: country.value, and: selectedState?.code)
506+
phone.validateField()
419507
}
420508
.store(in: &cancellables)
421509
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,13 @@ struct WooShippingCreateLabelsView: View {
136136
}
137137
}
138138
}
139+
.sheet(item: $viewModel.addressToEdit) { addressToEdit in
140+
NavigationStack {
141+
WooShippingEditAddressView(viewModel: addressToEdit)
142+
.navigationTitle(Localization.BottomSheet.editDestination)
143+
.navigationBarTitleDisplayMode(.inline)
144+
}
145+
}
139146
}
140147
}
141148
}
@@ -223,6 +230,10 @@ private extension WooShippingCreateLabelsView {
223230
addressVerificationLabel
224231
}
225232
.frame(maxWidth: .infinity, alignment: .leading)
233+
PencilEditButton {
234+
viewModel.editDestinationAddress()
235+
}
236+
.buttonStyle(TextButtonStyle())
226237
}
227238
.padding(Layout.bottomSheetPadding)
228239
}
@@ -333,6 +344,11 @@ private extension WooShippingCreateLabelsView {
333344
.padding(.vertical, 12)
334345
.background(RoundedRectangle(cornerRadius: Layout.cornerRadius)
335346
.fill(Color(uiColor: isDestinationAddressVerified ? .withColorStudio(.green, shade: .shade0) : .withColorStudio(.red, shade: .shade0))))
347+
.onTapGesture {
348+
if !isDestinationAddressVerified {
349+
viewModel.editDestinationAddress()
350+
}
351+
}
336352
}
337353
}
338354
}
@@ -427,6 +443,9 @@ private extension WooShippingCreateLabelsView {
427443
value: "Purchase Label · %1$@",
428444
comment: "Label for button to purchase the shipping label on the shipping label creation screen, " +
429445
"including the label price. Reads like: 'Purchase Label · $7.63'")
446+
static let editDestination = NSLocalizedString("wooShipping.createLabels.bottomSheet.editDestination",
447+
value: "Edit Destination",
448+
comment: "Title for the edit destination address screen in the shipping label creation flow")
430449
}
431450

432451
enum AddressVerification {

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

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import Combine
88
final class WooShippingCreateLabelsViewModel: ObservableObject {
99
private let currencyFormatter: CurrencyFormatter
1010
private let itemsDataSource: WooShippingItemsDataSource
11-
private let destinationAddress: WooShippingAddress?
11+
private var destinationAddress: WooShippingAddress?
12+
private var destinationEmail: String?
1213
private let stores: StoresManager
1314
private var subscriptions: Set<AnyCancellable> = []
1415
private var debounceDuration: Double = 1
@@ -79,6 +80,10 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
7980
/// This property can be set to display a notice with the provided label about the destination address status.
8081
@Published var destinationAddressStatusNoticeLabel: String?
8182

83+
/// View model for address to edit.
84+
/// Setting this property will navigate to the address edit screen.
85+
@Published var addressToEdit: WooShippingEditAddressViewModel?
86+
8287
/// Shipping lines for the order, with formatted amount.
8388
let shippingLines: [WooShipping_ShippingLineViewModel]
8489

@@ -147,17 +152,10 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
147152
let destinationAddress = destinationAddress else {
148153
return false
149154
}
150-
// Special case: Any shipment from/to military addresses must have Customs
151-
if originAddress.country == Constants.usCountryCode,
152-
Constants.usMilitaryStates.contains(where: { $0 == originAddress.state }) {
153-
return true
154-
}
155-
if destinationAddress.country == Constants.usCountryCode,
156-
Constants.usMilitaryStates.contains(where: { $0 == destinationAddress.state }) {
157-
return true
158-
}
159-
160-
return originAddress.country != destinationAddress.country
155+
return WooShippingCustomsRequirements.isCustomsRequired(originCountry: originAddress.country,
156+
originState: originAddress.state,
157+
destinationCountry: destinationAddress.country,
158+
destinationState: destinationAddress.state)
161159
}
162160

163161
/// Initialize the view model without an existing shipping label.
@@ -178,6 +176,7 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
178176
self.currencyFormatter = CurrencyFormatter(currencySettings: currencySettings)
179177
self.onLabelPurchase = onLabelPurchase
180178
self.destinationAddress = Self.getDestinationAddress(order: order, address: order.shippingAddress)
179+
self.destinationEmail = order.shippingAddress?.email ?? order.billingAddress?.email
181180
self.shippingLines = order.shippingLines.map({ WooShipping_ShippingLineViewModel(shippingLine: $0, currency: order.currency) })
182181
self.selectedOriginAddress = selectedOriginAddress
183182
self.selectedPackage = selectedPackage
@@ -273,6 +272,24 @@ final class WooShippingCreateLabelsViewModel: ObservableObject {
273272
func onCustomsFormFilled(form: ShippingLabelCustomsForm) {
274273
customsForm = form
275274
}
275+
276+
/// Sets the `addressToEdit` property for editing the destination address.
277+
/// After the address is edited, the destination address is replaced with the updated address.
278+
func editDestinationAddress() {
279+
addressToEdit = WooShippingEditAddressViewModel(address: destinationAddress,
280+
email: destinationEmail,
281+
isVerified: destinationAddressStatus == .verified,
282+
originCountryCode: selectedOriginAddress?.country,
283+
originStateCode: selectedOriginAddress?.state,
284+
onAddressEdited: { [weak self] editedAddress, editedEmail in
285+
guard let self else {
286+
return
287+
}
288+
destinationAddress = editedAddress
289+
destinationEmail = editedEmail
290+
addressToEdit = nil // Dismisses address edit screen
291+
})
292+
}
276293
}
277294

278295
// MARK: Remote
@@ -455,16 +472,6 @@ private extension WooShippingCreateLabelsViewModel {
455472
comment: "Notice when a destination address is missing on the shipping label creation screen")
456473
}
457474
}
458-
459-
enum Constants {
460-
/// Country code for US - to check for international shipment
461-
///
462-
static let usCountryCode = "US"
463-
464-
/// These US states are a special case because they represent military bases. They're considered "domestic",
465-
/// but they require a Customs form to ship from/to them.
466-
static let usMilitaryStates = ["AA", "AE", "AP"]
467-
}
468475
}
469476

470477
private extension WooShippingOriginAddress {

0 commit comments

Comments
 (0)