diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModel.swift index f4f50a07023..01b57f57656 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModel.swift @@ -17,7 +17,7 @@ final class WooShippingCustomsFormViewModel: ObservableObject { @Published var returnToSenderIfNotDelivered = false @Published var requiredInformationIsEntered = false - @Published var itemsRequiredInformationIsEntered = false + @Published private var itemsRequiredInformationIsEntered = false @Published var contentExplanation = "" @Published var restrictionDetails = "" @@ -30,15 +30,22 @@ final class WooShippingCustomsFormViewModel: ObservableObject { @Published private(set) var destinationCountryCode: String? private var cancellables = Set() - private let onCompletion: (ShippingLabelCustomsForm) -> () + + /// The callback that passes the `ShippingLabelCustomsForm` to outer environment + /// Called when: + /// - The customs form is closed + /// - The customs form is pre-filled with data and all required fields are completed. + private let onFormReady: (ShippingLabelCustomsForm) -> () + + @Published private(set) var itemsViewModels: [WooShippingCustomsItemViewModel] = [] init(order: Order, shipment: Shipment, originCountryCode: AnyPublisher? = nil, stores: StoresManager = ServiceLocator.stores, storageManager: StorageManagerType = ServiceLocator.storageManager, - onCompletion: @escaping (ShippingLabelCustomsForm) -> ()) { - self.onCompletion = onCompletion + onFormReady: @escaping (ShippingLabelCustomsForm) -> ()) { + self.onFormReady = onFormReady itemsViewModels = shipment.items.map { WooShippingCustomsItemViewModel(itemName: $0.name, @@ -55,31 +62,27 @@ final class WooShippingCustomsFormViewModel: ObservableObject { listenToItemsRequiredInformationValues() listenForRequiredInformation() listenForInternationalTransactionNumberIsRequired() + listenForRequiredInformationCompletedUponPreFill() } - @Published private(set) var itemsViewModels: [WooShippingCustomsItemViewModel] = [] + /// WOOMOB-734 + /// Solves the issue where a pre-filled form becomes complete without a manual submission + /// + /// Listens for the `requiredInformationIsEntered` state + /// As soon as all required info is entered, calls the `emitForm` just once + func listenForRequiredInformationCompletedUponPreFill() { + $requiredInformationIsEntered + .first { $0 == true } + .sink { [weak self] _ in + DispatchQueue.main.async { + self?.emitForm() + } + } + .store(in: &cancellables) + } func onDismiss() { - /// Ignoring `packageID` and `packageName` as these are not needed in WooShipping plugin, only in WCS&T - let form = ShippingLabelCustomsForm(packageID: "", - packageName: "", - contentsType: contentType.toFormContentsType(), - contentExplanation: contentType == .other ? contentExplanation : "", - restrictionType: restrictionType.toFormRestrictionType(), - restrictionComments: restrictionType == .other ? restrictionDetails : "", - nonDeliveryOption: returnToSenderIfNotDelivered ? .return : .abandon, - itn: internationalTransactionNumber.isValidITN ? internationalTransactionNumber : "", - items: itemsViewModels.map { - ShippingLabelCustomsForm.Item(description: $0.description, - quantity: $0.itemQuantity, - value: Double($0.valuePerUnit) ?? 0, - weight: Double($0.weightPerUnit) ?? 0, - hsTariffNumber: $0.isValidTariffNumber ? $0.hsTariffNumber : "", - originCountry: $0.selectedCountry?.code ?? "", - productID: $0.itemProductID) - } - ) - onCompletion(form) + emitForm() } func updateDestinationCountry(code: String) { @@ -192,6 +195,33 @@ private extension WooShippingCustomsFormViewModel { } return ServiceLocator.currencySettings.symbol(from: currencyCode) } + + private func emitForm() { + /// Ignoring `packageID` and `packageName` as these are not needed in WooShipping plugin, only in WCS&T + let form = ShippingLabelCustomsForm( + packageID: "", + packageName: "", + contentsType: contentType.toFormContentsType(), + contentExplanation: contentType == .other ? contentExplanation : "", + restrictionType: restrictionType.toFormRestrictionType(), + restrictionComments: restrictionType == .other ? restrictionDetails : "", + nonDeliveryOption: returnToSenderIfNotDelivered ? .return : .abandon, + itn: internationalTransactionNumber.isValidITN ? internationalTransactionNumber : "", + items: itemsViewModels.map { + ShippingLabelCustomsForm.Item( + description: $0.description, + quantity: $0.itemQuantity, + value: Double($0.valuePerUnit) ?? 0, + weight: Double($0.weightPerUnit) ?? 0, + hsTariffNumber: $0.isValidTariffNumber ? $0.hsTariffNumber : "", + originCountry: $0.selectedCountry?.code ?? "", + productID: $0.itemProductID + ) + } + ) + + onFormReady(form) + } } private extension WooShippingCustomsFormViewModel { diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsItemViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsItemViewModel.swift index f5f5b8ffbd0..3a5a6199822 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsItemViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsItemViewModel.swift @@ -103,6 +103,7 @@ private extension WooShippingCustomsItemViewModel { func combineToPreselectCountry() { $originCountryCode .compactMap { $0 } + .filter { !$0.isEmpty } .first() /// Make sure to only handle the initial value .combineLatest($countries) .map { code, countries in diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModelTests.swift index 7e4df1a5360..d63f529a55e 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShipping Customs/WooShippingCustomsFormViewModelTests.swift @@ -10,7 +10,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) } override func tearDown() { @@ -25,7 +25,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { var passedForm: ShippingLabelCustomsForm? viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: shipment, - onCompletion: { form in + onFormReady: { form in passedForm = form }) @@ -66,7 +66,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { var passedForm: ShippingLabelCustomsForm? viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { form in + onFormReady: { form in passedForm = form }) @@ -84,7 +84,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { var passedForm: ShippingLabelCustomsForm? viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { form in + onFormReady: { form in passedForm = form }) @@ -109,7 +109,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { var passedForm: ShippingLabelCustomsForm? viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { form in + onFormReady: { form in passedForm = form }) @@ -127,7 +127,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { let order = Order.fake().copy(currency: "USD") viewModel = WooShippingCustomsFormViewModel(order: order, shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // Then XCTAssertEqual(viewModel.itemsViewModels.first?.currencySymbol, "$") @@ -153,7 +153,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // Given viewModel = WooShippingCustomsFormViewModel(order: Order.fake().copy(currency: "USD"), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // When viewModel.itemsViewModels.forEach { item in @@ -176,7 +176,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // Given viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // When viewModel.itemsViewModels.first?.hsTariffNumberTotalValue = ("123456", 1000) @@ -189,7 +189,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // Given viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // When viewModel.itemsViewModels[0].requiredInformationIsEntered = true @@ -216,7 +216,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { let requiredDestinations = ["IR", "SY", "KP", "CU", "SD"] viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) viewModel.itemsViewModels.forEach { item in item.hsTariffNumber = "" item.valuePerUnit = "1000" @@ -241,7 +241,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // Given viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // When viewModel.itemsViewModels.first?.requiredInformationIsEntered = true @@ -255,7 +255,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // Given viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // When viewModel.internationalTransactionNumber = "NOEEI 30.37(a)" @@ -269,7 +269,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // Given viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // When viewModel.itemsViewModels.first?.requiredInformationIsEntered = true @@ -285,7 +285,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // Given viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // When viewModel.itemsViewModels.first?.requiredInformationIsEntered = true @@ -301,7 +301,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // Given viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: sampleShipment, - onCompletion: { _ in }) + onFormReady: { _ in }) // When viewModel.itemsViewModels.first?.requiredInformationIsEntered = true @@ -324,7 +324,7 @@ final class WooShippingCustomsFormViewModelTests: XCTestCase { // When viewModel = WooShippingCustomsFormViewModel(order: Order.fake(), shipment: shipment, - onCompletion: { _ in }) + onFormReady: { _ in }) let firstItemViewModel = viewModel.itemsViewModels.first // Then diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingShipmentDetailsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingShipmentDetailsViewModelTests.swift index 7024e27d002..22b634458bf 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingShipmentDetailsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingShipmentDetailsViewModelTests.swift @@ -817,6 +817,66 @@ final class WooShippingShipmentDetailsViewModelTests: XCTestCase { // Then XCTAssertEqual(customsItemViewModel.selectedCountry?.code, expectedCountry) } + + func test_package_contains_complete_customs_form_when_required_data_is_prefilled() throws { + // Setup + let originAddressSubject = PassthroughSubject() + let destinationAddressSubject = PassthroughSubject() + let stores = MockStoresManager(sessionManager: .testingInstance) + let storageManager = MockStorageManager() + + // Given + let originCountry = Country(code: "US", name: "United States", states: []) + let destinationCountry = Country(code: "CA", name: "Canada", states: []) + + let countries = [ + originCountry, + destinationCountry + ] + + stores.whenReceivingAction(ofType: DataAction.self) { action in + switch action { + case let .synchronizeCountries(_, onCompletion): + storageManager.insertSampleCountries(readOnlyCountries: countries) + onCompletion(.success(countries)) + } + } + + let shipment = sampleShipment + + let viewModel = WooShippingShipmentDetailsViewModel( + order: Order.fake(), + shipment: shipment, + shippingLabel: nil, + originAddress: originAddressSubject.eraseToAnyPublisher(), + destinationAddress: destinationAddressSubject.eraseToAnyPublisher(), + stores: stores, + storageManager: storageManager + ) + + // When + destinationAddressSubject.send(sampleDestinationAddress(country: destinationCountry.code, state: "")) + originAddressSubject.send(sampleOriginAddress(country: originCountry.code, state: "")) + + viewModel.selectPackage(samplePackageData()) + + // Then + XCTAssertTrue(viewModel.customsInformationIsCompleted) + + waitUntil { + guard let customsForm = viewModel.currentPackage?.customsForm else { + return false + } + + let customsFormItem = customsForm.items[0] + let shipmentItem = shipment.items[0] + + return customsFormItem.description == shipmentItem.name && + customsFormItem.value == shipmentItem.value && + customsFormItem.weight == shipmentItem.weight && + customsFormItem.originCountry == originCountry.code + } + } } private extension WooShippingShipmentDetailsViewModelTests {