Repository: woocommerce/woocommerce-ios
Pull request #15415: Shipping Labels: Add separate tabs for shipments
Author: login: itsmeichigo
Labels: name: type: task
description: An internally driven task.
name: feature: shipping labels
description: Related to creating, ordering, or printing shipping labels.
Created: 2025-03-25T11:25:59Z | Merged: 2025-03-27T04:24:59Z

<!-- Remember about a good descriptive title. -->

Part of #15307
<!-- Id number of the GitHub issue this PR addresses. -->
<!-- Part of: # -> use this if your PR is one of many related to one issue -->

<!-- 
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🛠 Need an AI review?  
Add a comment: `@coderabbitai review`
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
-->

## Description
<!-- Take the time to write a good summary. Why is it needed? What does it do? When fixing bugs try to avoid just writing “See original issue” – clarify what the problem was and how you’ve fixed it. -->
This PR updates functionalities for the split shipment screen. Changes include:
- Added logic for moving items between shipments.
- Added tab views when there is more than one shipment.
- Updated the move-to notice to support better accessibility.

The notice after splitting shipments will be handled in a subsequent PR.

## Steps to reproduce
<!-- Step-by-step testing instructions. For new user flows, consider instead stating the goal of the workflow and see if your PR reviewer can accomplish the workflow without specific steps! -->

1. Log in to a test store with the Woo Shipping plugin set up.
2. Open a completed order with more than one physical product with different quantities.
3. Select Create shipping label > Split shipments.
4. Test moving an item or a subset of one to a new shipment or an existing one.
5. Confirm that new shipments are created correctly and existing shipments are updated correctly.
6. Confirm that section headers are updated correctly after splitting items and when switching shipments.
7. Confirm that the move-to notice is updated correctly when switching between tabs.

## Testing information
<!-- This is your opportunity to break out individual scenarios that need testing (when necessary) and/or include a checklist for the reviewer to go through. Consider documenting the following from your own completed testing: devices used, alternate workflows, edge cases, affected areas, critical flows, areas not tested, and any remaining unknowns. Provide feedback on this new section of the PR template through Sept 30, 2024 to Apps Quality; additional context here: https://woomobilep2.wordpress.com/2024/05/06/woocommerce-mobile-quality-report-march-april/#comment-12036 -->
Tested and confirmed the above cases on simulator iPhone 16 Pro iOS 18.2.
Added unit tests to confirm the functionality.

## Screenshots
<!-- Include before and after images or gifs when appropriate. -->

https://github.com/user-attachments/assets/4976d5c6-ec5a-4ead-85fb-35aea77dfdcd



---
- [x] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

## Reviewer (or Author, in the case of optional code reviews):

Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement:

- [ ] The PR is small and has a clear, single focus, or a valid explanation is provided in the description. If needed, please request to split it into smaller PRs.
- [ ] Ensure Adequate Unit Test Coverage: The changes are reasonably covered by unit tests or an explanation is provided in the PR description.
- [ ] Manual Testing: The author listed all the tests they ran, including smoke tests when needed (e.g., for refactorings). The reviewer confirmed that the PR works as expected on all devices (phone/tablet) and no regressions are added.



Diff:
diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentItemCardViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentItemCardViewModel.swift
index a7026d39585..e5feeb19fc7 100644
--- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentItemCardViewModel.swift	
+++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentItemCardViewModel.swift	
@@ -6,6 +6,8 @@ import Yosemite
 final class CollapsibleShipmentItemCardViewModel: ObservableObject, Identifiable {
     let id = UUID()
 
+    let packageItem: ShippingLabelPackageItem
+
     /// The main item row.
     let mainItemRow: SelectableShipmentItemRowViewModel
 
@@ -14,39 +16,41 @@ final class CollapsibleShipmentItemCardViewModel: ObservableObject, Identifiable
 
     var onSelectionChange: (() -> Void)?
 
-    var selectedItemIds: [String] {
-        if mainItemRow.selected {
-            if childItemRows.isNotEmpty {
-                return childItemRows.map { $0.itemID }
-            } else {
-                return [mainItemRow.itemID]
-            }
+    var numberOfSelectedItems: Int {
+        if childItemRows.isEmpty {
+            mainItemRow.selected ? 1 : 0
+        } else {
+            childItemRows.count { $0.selected }
         }
-
-        return childItemRows
-            .filter { $0.selected }
-            .map(\.itemID)
     }
 
-    init(parentShipmentId: String,
-         childShipmentIds: [String],
-         item: ShippingLabelPackageItem,
+    init(item: ShippingLabelPackageItem,
          currency: String) {
-        let mainShippingItem = WooShippingItemRowViewModel(item: ShippingLabelPackageItem(copy: item, quantity: max(1.0, Decimal(childShipmentIds.count))),
+        self.packageItem = item
+
+        let mainShippingItem = WooShippingItemRowViewModel(item: item,
                                                            currency: currency)
         let childShippingItem = WooShippingItemRowViewModel(item: ShippingLabelPackageItem(copy: item, quantity: 1.0),
                                                             currency: currency)
 
-        self.mainItemRow = SelectableShipmentItemRowViewModel(itemID: parentShipmentId,
+        self.mainItemRow = SelectableShipmentItemRowViewModel(itemID: "\(item.productOrVariationID)",
                                                               isSelectable: true,
                                                               item: mainShippingItem,
                                                               showQuantity: true)
-        self.childItemRows = childShipmentIds.map({
-            SelectableShipmentItemRowViewModel(itemID: $0,
-                                               isSelectable: true,
-                                               item: childShippingItem,
-                                               showQuantity: false)
-        })
+
+        if item.quantity.intValue == 1 {
+            self.childItemRows = []
+        } else {
+            var childItemRows = [SelectableShipmentItemRowViewModel]()
+            for index in 0..<item.quantity.intValue {
+                let childShipmentId = "\(item.productOrVariationID)-sub-\(index)"
+                childItemRows.append(SelectableShipmentItemRowViewModel(itemID: childShipmentId,
+                                                                        isSelectable: true,
+                                                                        item: childShippingItem,
+                                                                        showQuantity: false))
+            }
+            self.childItemRows = childItemRows
+        }
 
         observeSelection()
     }
diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/MoveToShipmentNotice.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/MoveToShipmentNotice.swift
index 12e601c5e7a..085e855ffc0 100644
--- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/MoveToShipmentNotice.swift	
+++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/MoveToShipmentNotice.swift	
@@ -1,7 +1,7 @@
 import SwiftUI
 
 struct MoveToShipmentNoticeViewModel {
-    enum MoveTo {
+    enum Destination {
         case existingShipment(index: Int)
         case newShipment
     }
@@ -9,22 +9,23 @@ struct MoveToShipmentNoticeViewModel {
     let selectedItemsCount: Int
     let existingShipmentsCount: Int
     let currentShipmentIndex: Int?
-    let actionHandler: ((MoveTo) -> Void)
 }
 
 struct MoveToShipmentNotice: View {
     let viewModel: MoveToShipmentNoticeViewModel
+    let onMoving: ((MoveToShipmentNoticeViewModel.Destination) -> Void)
 
     var body: some View {
-        HStack {
+        AdaptiveStack(horizontalAlignment: .leading) {
             Text(String.localizedStringWithFormat(Localization.message, viewModel.selectedItemsCount))
                 .font(.subheadline)
                 .fontWeight(.semibold)
                 .foregroundColor(Color(uiColor: .text))
+                .frame(maxWidth: .infinity, alignment: .leading)
 
             Spacer()
 
-            if viewModel.existingShipmentsCount == 0 {
+            if viewModel.existingShipmentsCount == 1 {
                 moveToNewShipment
             } else {
                 menuWithExistingShipments
@@ -43,7 +44,7 @@ struct MoveToShipmentNotice: View {
 private extension MoveToShipmentNotice {
     var moveToNewShipment: some View {
         Button {
-            viewModel.actionHandler(.newShipment)
+            onMoving(.newShipment)
         } label: {
             HStack(spacing: Layout.horizontalSpacing) {
                 Text(Localization.moveToNewShipment)
@@ -59,13 +60,13 @@ private extension MoveToShipmentNotice {
             ForEach(0..<viewModel.existingShipmentsCount, id: \.self) { index in
                 if viewModel.currentShipmentIndex != index {
                     Button(String.localizedStringWithFormat(Localization.shipment, index + 1), action: {
-                        viewModel.actionHandler(.existingShipment(index: index))
+                        onMoving(.existingShipment(index: index))
                     })
                 }
             }
 
             Button(Localization.newShipment, action: {
-                viewModel.actionHandler(.newShipment)
+                onMoving(.newShipment)
             })
         } label: {
             HStack(spacing: Layout.horizontalSpacing) {
@@ -123,6 +124,6 @@ private extension MoveToShipmentNotice {
 #Preview {
     MoveToShipmentNotice(viewModel: MoveToShipmentNoticeViewModel(selectedItemsCount: 4,
                                                                   existingShipmentsCount: 3,
-                                                                  currentShipmentIndex: 1,
-                                                                  actionHandler: { _ in }))
+                                                                  currentShipmentIndex: 1),
+                         onMoving: { _ in })
 }
diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift
index 33844ceb118..6cfe6737bb2 100644
--- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift	
+++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsDetailView.swift	
@@ -8,7 +8,22 @@ struct WooShippingSplitShipmentsDetailView: View {
 
     var body: some View {
         NavigationView {
-            ZStack(alignment: .bottom) {
+            VStack {
+                if viewModel.shipments.count > 1 {
+                    TopTabView(tabs: viewModel.topTabItems,
+                               showContent: .constant(false),
+                               selectedTabIndex: $viewModel.selectedShipmentIndex,
+                               tabsContainerHorizontalPadding: nil,
+                               selectedStateColor: .accentColor,
+                               unselectedStateColor: .secondary,
+                               selectedTabIndicatorHeight: Layout.selectedTabIndicatorHeight,
+                               tabPadding: Layout.tabPadding,
+                               tabsNameFont: Font.subheadline.bold(),
+                               tabsIconSize: nil,
+                               tabItemContentHorizontalPadding: Layout.tabItemContentHorizontalPadding,
+                               tabItemContentVerticalPadding: Layout.tabItemContentVerticalPadding)
+                }
+
                 ScrollView {
                     VStack(alignment: .leading, spacing: Layout.contentPadding) {
                         AdaptiveStack(horizontalAlignment: .leading) {
@@ -20,7 +35,7 @@ struct WooShippingSplitShipmentsDetailView: View {
                         }
 
                         VStack(spacing: Layout.verticalSpacing) {
-                            ForEach(viewModel.shipmentCardViewModels) { item in
+                            ForEach(viewModel.currentShipment) { item in
                                 CollapsibleShipmentItemCard(viewModel: item)
                             }
                         }
@@ -28,6 +43,8 @@ struct WooShippingSplitShipmentsDetailView: View {
                     .padding(Layout.contentPadding)
                 }
 
+                Spacer()
+
                 noticeStack
                     .padding(Layout.contentPadding)
             }
@@ -62,7 +79,9 @@ private extension WooShippingSplitShipmentsDetailView {
             }
 
             if let moveTo = viewModel.moveToNoticeViewModel {
-                MoveToShipmentNotice(viewModel: moveTo)
+                MoveToShipmentNotice(viewModel: moveTo, onMoving: { destination in
+                    viewModel.moveSelectedItems(to: destination)
+                })
             }
         }
     }
@@ -110,6 +129,10 @@ fileprivate extension WooShippingSplitShipmentsDetailView {
         static let shadowYOffset: CGFloat = 2
         static let borderWidth: CGFloat = 0.5
         static let verticalSpacing: CGFloat = 8
+        static let selectedTabIndicatorHeight: CGFloat = 3.0
+        static let tabPadding: CGFloat = 9.0
+        static let tabItemContentHorizontalPadding: CGFloat = 16.0
+        static let tabItemContentVerticalPadding: CGFloat = 9.0
         static let cornerRadius: CGFloat = 8
     }
 
diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
index 44f69df1eec..026270c5775 100644
--- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift	
+++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift	
@@ -7,10 +7,21 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
     private let order: Order
     private let stores: StoresManager
     private let config: WooShippingConfig
-    private let items: [ShippingLabelPackageItem]
     private let currencySettings: CurrencySettings
     private let shippingSettingsService: ShippingSettingsService
 
+    typealias Shipment = [CollapsibleShipmentItemCardViewModel]
+
+    @Published private(set) var shipments: [Shipment]
+
+    @Published var selectedShipmentIndex: Int? = 0 {
+        didSet {
+            configureSectionHeader()
+            configureSelectionCallback()
+            updateMoveToNotice()
+        }
+    }
+
     /// Label with the total number of items to ship.
     @Published private(set) var itemsCountLabel: String = ""
 
@@ -26,7 +37,17 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         "\(itemsWeightLabel) • \(itemsPriceLabel)"
     }
 
-    let shipmentCardViewModels: [CollapsibleShipmentItemCardViewModel]
+    var topTabItems: [TopTabItem<EmptyView>] {
+        shipments.enumerated().map { (index, item) in
+            TopTabItem(name: String.localizedStringWithFormat(Localization.shipmentFormat, index + 1),
+                       content: { EmptyView() })
+        }
+    }
+
+    var currentShipment: [CollapsibleShipmentItemCardViewModel] {
+        let index = selectedShipmentIndex ?? 0
+        return shipments[index]
+    }
 
     @Published private(set) var moveToNoticeViewModel: MoveToShipmentNoticeViewModel?
 
@@ -42,36 +63,14 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
          shippingSettingsService: ShippingSettingsService = ServiceLocator.shippingSettingsService) {
         self.order = order
         self.config = config
-        self.items = items
         self.stores = stores
         self.currencySettings = currencySettings
         self.shippingSettingsService = shippingSettingsService
 
-        self.shipmentCardViewModels = {
-            var viewModels = [CollapsibleShipmentItemCardViewModel]()
-            for item in items {
-                // TODO: #15303 Set IDs based on web logic
-                let childShipmentIds: [String] = {
-                    guard item.quantity > 1 else {
-                        return []
-                    }
-
-                    var children: [String] = []
-                    for quantity in 0..<item.quantity.intValue {
-                        let childShipmentId = "\(item.productOrVariationID)-sub-\(quantity)"
-                        children.append(childShipmentId)
-                    }
-                    return children
-                }()
-
-                let viewModel = CollapsibleShipmentItemCardViewModel(parentShipmentId: "\(item.productOrVariationID)",
-                                                                     childShipmentIds: childShipmentIds,
-                                                                     item: item,
-                                                                     currency: order.currency)
-                viewModels.append(viewModel)
-            }
-            return viewModels
-        }()
+        let initialShipment = items.map { item in
+            CollapsibleShipmentItemCardViewModel(item: item, currency: order.currency)
+        }
+        self.shipments = [initialShipment]
 
         configureSectionHeader()
         configureSelectionCallback()
@@ -79,11 +78,11 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
 
     func onAppear() {
         showInstructionsNotice()
-        showMoveToNotice()
+        updateMoveToNotice()
     }
 
     func selectAll() {
-        shipmentCardViewModels.forEach {
+        currentShipment.forEach {
             $0.selectAll()
         }
     }
@@ -92,13 +91,89 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: order.currency)
+                )
+            } else if selectedQuantity > 0 {
+                let newItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(remainingQuantity))
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: order.currency)
+                )
+                let movedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(selectedQuantity))
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: movedItem, currency: order.currency)
+                )
+            } else if selectedQuantity == 0 {
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: order.currency)
+                )
+            }
+        }
+
+        // Step 2: Update the current shipment
+        let currentIndex = selectedShipmentIndex ?? 0
+        shipments[currentIndex] = newShipment
+
+        // Step 3: Add new or update existing shipment
+        switch destination {
+        case .newShipment:
+            shipments.append(movedItems)
+
+        case .existingShipment(let index):
+            var updatedShipment = Shipment()
+            for item in shipments[index] {
+                let matchingItemIndex = movedItems.firstIndex(where: {
+                    $0.packageItem.productOrVariationID == item.packageItem.productOrVariationID
+                })
+                if let matchingItemIndex {
+                    // Merge the quantity if the same item is moved to the shipment
+                    let updatedQuantity = item.packageItem.quantity + movedItems[matchingItemIndex].packageItem.quantity
+                    let updatedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: updatedQuantity)
+                    updatedShipment.append(
+                        CollapsibleShipmentItemCardViewModel(item: updatedItem, currency: order.currency)
+                    )
+                    movedItems.remove(at: matchingItemIndex)
+                } else {
+                    // Keep the item as-is
+                    updatedShipment.append(item)
+                }
+            }
+            // Add the rest of the new items to the shipment
+            updatedShipment.append(contentsOf: movedItems)
+            shipments[index] = updatedShipment
+        }
+
+        // Step 4: Remove the current shipment if it's empty.
+        // Then update the section header and selection callback
+        if currentShipment.isEmpty {
+            shipments.remove(at: currentIndex)
+            if shipments.count <= currentIndex {
+                selectedShipmentIndex = max(shipments.count - 1, 0)
+            }
+        }
+        configureSelectionCallback()
+        configureSectionHeader()
+    }
 }
 
 private extension WooShippingSplitShipmentsViewModel {
     func configureSelectionCallback() {
-        shipmentCardViewModels.forEach { viewModel in
+        currentShipment.forEach { viewModel in
             viewModel.onSelectionChange = { [weak self] in
-                self?.showMoveToNotice()
+                self?.updateMoveToNotice()
             }
         }
     }
@@ -109,37 +184,35 @@ private extension WooShippingSplitShipmentsViewModel {
         }
     }
 
-    func showMoveToNotice() {
-        let selectedItemsCount = {
-            shipmentCardViewModels.map { $0.selectedItemIds.count }.reduce(0, +)
-        }()
+    func updateMoveToNotice() {
+        let currentIndex = selectedShipmentIndex ?? 0
+        let selectedItemsCount = currentShipment
+            .map(\.numberOfSelectedItems)
+            .reduce(0, +)
 
         guard selectedItemsCount > 0 else {
-            return self.moveToNoticeViewModel = nil
+            return moveToNoticeViewModel = nil
+        }
+
+        let totalItemCount = currentShipment
+            .map { $0.packageItem.quantity }
+            .reduce(0, +).intValue
+
+        if shipments.count == 1 &&
+            selectedItemsCount == totalItemCount {
+            // do not allow moving all items if there is only one shipment at the moment
+            return moveToNoticeViewModel = nil
         }
 
-        // TODO: Use count and index values from shipment tabs
         moveToNoticeViewModel = MoveToShipmentNoticeViewModel(selectedItemsCount: selectedItemsCount,
-                                                               existingShipmentsCount: 3,
-                                                               currentShipmentIndex: 2,
-                                                               actionHandler: { [weak self] moveTo in
-            guard let self else { return }
-
-            self.moveToNoticeViewModel = nil
-            self.instructions = nil
-
-            switch moveTo {
-            case .existingShipment:
-                break
-            case .newShipment:
-                break
-            }
-        })
+                                                              existingShipmentsCount: shipments.count,
+                                                              currentShipmentIndex: currentIndex)
     }
 
     /// Configures the labels in the section header.
     ///
     func configureSectionHeader() {
+        let items = currentShipment.map(\.packageItem)
         let itemsCount = items.map(\.quantity).reduce(0, +)
         itemsCountLabel = Localization.itemsCount(itemsCount)
         itemsWeightLabel = formatWeight(for: items)
@@ -189,5 +262,10 @@ private extension WooShippingSplitShipmentsViewModel {
         static let itemsCountPluralFormat = NSLocalizedString("wooShipping.createLabels.splitShipment.items.count",
                                                               value: "%1$@ items",
                                                               comment: "Label for plural items to ship during shipping label creation. Reads like: '3 items'")
+        static let shipmentFormat = NSLocalizedString(
+            "wooShipping.createLabels.splitShipment.shipmentFormat",
+            value: "Shipment %1$d",
+            comment: "Label for a shipment during shipping label creation. The placeholder is the index of the shipment. Reads like: 'Shipment 1'"
+        )
     }
 }
diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TopTabView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TopTabView.swift
index e8548413c8a..46eda5d2f7d 100644
--- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TopTabView.swift	
+++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/TopTabView.swift	
@@ -122,6 +122,11 @@ struct TopTabView<Content: View>: View {
                                             Color.clear.onAppear {
                                                 if index < tabWidths.count {
                                                     tabWidths[index] = geometry.size.width
+                                                } else if index < tabs.count {
+                                                    /// Since `tabWidths` was initialized as a state for this view
+                                                    /// it would not be updated again when `tabs` change.
+                                                    /// Append a new width when the number of tabs increases.
+                                                    tabWidths.append(geometry.size.width)
                                                 }
                                             }
                                         })
diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift
index 928f7198d9f..7c984085b0d 100644
--- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift	
+++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift	
@@ -79,6 +79,334 @@ final class WooShippingSplitShipmentsViewModelTests: XCTestCase {
         assertEqual("13 kg • ₹22.50", viewModel.itemsDetailLabel)
     }
 
+    func test_shipments_is_correct_initially() throws {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+
+        // When
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // Then
+        XCTAssertEqual(viewModel.shipments.count, 1)
+        let shipment = try XCTUnwrap(viewModel.shipments.first)
+        XCTAssertEqual(shipment.count, items.count)
+    }
+
+    // MARK: - `moveToNoticeViewModel`
+
+    func test_moveToNoticeViewModel_is_nil_initially() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+
+        // When
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // Then
+        XCTAssertNil(viewModel.moveToNoticeViewModel)
+    }
+
+    func test_moveToNoticeViewModel_is_nil_when_there_exists_one_shipment_and_all_items_are_selected() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.selectAll()
+
+        // Then
+        XCTAssertNil(viewModel.moveToNoticeViewModel)
+    }
+
+    func test_moveToNoticeViewModel_is_correct_when_there_exists_one_shipment_and_not_all_items_are_selected() throws {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.first?.childItemRows.first?.handleTap()
+
+        // Then
+        let moveToNoticeViewModel = try XCTUnwrap(viewModel.moveToNoticeViewModel)
+        XCTAssertEqual(moveToNoticeViewModel.selectedItemsCount, 1)
+        XCTAssertEqual(moveToNoticeViewModel.existingShipmentsCount, 1)
+        XCTAssertEqual(moveToNoticeViewModel.currentShipmentIndex, 0)
+    }
+
+    func test_moveToNoticeViewModel_is_correct_when_there_exists_more_than_one_shipment_and_not_all_items_are_selected() throws {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1),
+                     sampleItem(id: 3, weight: 4, value: 5, quantity: 3)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.first?.childItemRows.first?.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+        viewModel.shipments.first?.last?.mainItemRow.handleTap()
+
+        // Then
+        let moveToNoticeViewModel = try XCTUnwrap(viewModel.moveToNoticeViewModel)
+        XCTAssertEqual(moveToNoticeViewModel.selectedItemsCount, 3)
+        XCTAssertEqual(moveToNoticeViewModel.existingShipmentsCount, 2)
+        XCTAssertEqual(moveToNoticeViewModel.currentShipmentIndex, 0)
+    }
+
+    func test_moveToNoticeViewModel_is_correct_when_there_exists_more_than_one_shipment_and_all_items_are_selected() throws {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1),
+                     sampleItem(id: 3, weight: 4, value: 5, quantity: 3)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.first?.childItemRows.first?.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+        viewModel.selectAll()
+
+        // Then
+        let moveToNoticeViewModel = try XCTUnwrap(viewModel.moveToNoticeViewModel)
+        XCTAssertEqual(moveToNoticeViewModel.selectedItemsCount, 5)
+        XCTAssertEqual(moveToNoticeViewModel.existingShipmentsCount, 2)
+        XCTAssertEqual(moveToNoticeViewModel.currentShipmentIndex, 0)
+    }
+
+    // MARK: - `moveSelectedItems`
+
+    func test_moveSelectedItems_to_new_shipments_works_correctly_when_moving_a_subset_of_items() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.first?.childItemRows.first?.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+
+        // Then
+        XCTAssertEqual(viewModel.shipments.count, 2)
+
+        XCTAssertEqual(viewModel.shipments[0].count, 2)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.productOrVariationID, items[0].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.quantity, 1)
+        XCTAssertEqual(viewModel.shipments[0][1].packageItem.productOrVariationID, items[1].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][1].packageItem.quantity, 1)
+
+        XCTAssertEqual(viewModel.shipments[1].count, 1)
+        XCTAssertEqual(viewModel.shipments[1][0].packageItem.productOrVariationID, items[0].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[1][0].packageItem.quantity, 1)
+    }
+
+    func test_moveSelectedItems_to_new_shipments_works_correctly_when_moving_whole_item() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.first?.mainItemRow.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+
+        // Then
+        XCTAssertEqual(viewModel.shipments.count, 2)
+
+        XCTAssertEqual(viewModel.shipments[0].count, 1)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.productOrVariationID, items[1].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.quantity, 1)
+
+        XCTAssertEqual(viewModel.shipments[1].count, 1)
+        XCTAssertEqual(viewModel.shipments[1][0].packageItem.productOrVariationID, items[0].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[1][0].packageItem.quantity, 2)
+    }
+
+    func test_moveSelectedItems_to_existing_shipments_works_correctly() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1),
+                     sampleItem(id: 3, weight: 4, value: 5, quantity: 3)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.first?.childItemRows.first?.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+        viewModel.shipments.first?.last?.mainItemRow.handleTap()
+        viewModel.moveSelectedItems(to: .existingShipment(index: 1))
+
+        // Then
+        XCTAssertEqual(viewModel.shipments.count, 2)
+
+        XCTAssertEqual(viewModel.shipments[0].count, 2)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.productOrVariationID, items[0].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.quantity, 1)
+        XCTAssertEqual(viewModel.shipments[0][1].packageItem.productOrVariationID, items[1].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][1].packageItem.quantity, 1)
+
+        XCTAssertEqual(viewModel.shipments[1].count, 2)
+        XCTAssertEqual(viewModel.shipments[1][0].packageItem.productOrVariationID, items[0].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[1][0].packageItem.quantity, 1)
+        XCTAssertEqual(viewModel.shipments[1][1].packageItem.productOrVariationID, items[2].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[1][1].packageItem.quantity, 3)
+    }
+
+    func test_moveSelectedItems_to_existing_shipments_merges_the_same_items() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1),
+                     sampleItem(id: 3, weight: 4, value: 5, quantity: 3)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.last?.childItemRows.first?.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+        viewModel.shipments.first?.last?.childItemRows.last?.handleTap()
+        viewModel.moveSelectedItems(to: .existingShipment(index: 1))
+
+        // Then
+        XCTAssertEqual(viewModel.shipments.count, 2)
+
+        XCTAssertEqual(viewModel.shipments[0].count, 3)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.productOrVariationID, items[0].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.quantity, 2)
+        XCTAssertEqual(viewModel.shipments[0][1].packageItem.productOrVariationID, items[1].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][1].packageItem.quantity, 1)
+        XCTAssertEqual(viewModel.shipments[0][2].packageItem.productOrVariationID, items[2].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][2].packageItem.quantity, 1)
+
+        XCTAssertEqual(viewModel.shipments[1].count, 1)
+        XCTAssertEqual(viewModel.shipments[1][0].packageItem.productOrVariationID, items[2].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[1][0].packageItem.quantity, 2)
+    }
+
+    func test_moveSelectedItems_removes_empty_shipment_after_moving() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.last?.mainItemRow.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+        viewModel.shipments.first?.first?.mainItemRow.handleTap()
+        viewModel.moveSelectedItems(to: .existingShipment(index: 1))
+
+        // Then
+        XCTAssertEqual(viewModel.shipments.count, 1)
+
+        XCTAssertEqual(viewModel.shipments[0].count, 2)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.productOrVariationID, items[1].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][0].packageItem.quantity, 1)
+        XCTAssertEqual(viewModel.shipments[0][1].packageItem.productOrVariationID, items[0].productOrVariationID)
+        XCTAssertEqual(viewModel.shipments[0][1].packageItem.quantity, 2)
+    }
+
+    func test_moveSelectedItems_to_new_shipments_updates_section_headers_for_current_shipment() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.first?.childItemRows.first?.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+
+        // Then
+        assertEqual("2 items", viewModel.itemsCountLabel)
+        assertEqual("₹12.50", viewModel.itemsPriceLabel)
+        assertEqual("8 kg • ₹12.50", viewModel.itemsDetailLabel)
+    }
+
+    func test_switching_shipments_updates_section_headers() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+
+        // When
+        viewModel.shipments.first?.first?.childItemRows.first?.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+        viewModel.selectedShipmentIndex = 1
+
+        // Then
+        assertEqual("1 item", viewModel.itemsCountLabel)
+        assertEqual("₹10.00", viewModel.itemsPriceLabel)
+        assertEqual("5 kg • ₹10.00", viewModel.itemsDetailLabel)
+    }
+
+    func test_topTabItems_are_updated_after_moving_items_to_new_shipment() {
+        // Given
+        let items = [sampleItem(id: 1, weight: 5, value: 10, quantity: 2),
+                     sampleItem(id: 2, weight: 3, value: 2.5, quantity: 1)]
+        let viewModel = WooShippingSplitShipmentsViewModel(order: sampleOrder,
+                                                           config: WooShippingConfig.fake(),
+                                                           items: items,
+                                                           currencySettings: currencySettings,
+                                                           shippingSettingsService: shippingSettingsService)
+        XCTAssertEqual(viewModel.topTabItems.count, 1)
+
+        // When
+        viewModel.shipments.first?.first?.childItemRows.first?.handleTap()
+        viewModel.moveSelectedItems(to: .newShipment)
+
+        // Then
+        XCTAssertEqual(viewModel.topTabItems.count, 2)
+    }
 }
 
 private extension WooShippingSplitShipmentsViewModelTests {


Reviews:
user: login: selanthiraiyan
body: Nice work, Huong! I left a couple of non-blocking comments. We can work on it in future PRs and merge this one. 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T00:42:05Z
user: login: itsmeichigo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T03:47:55Z
user: login: itsmeichigo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T04:05:16Z

Review comments:
diff_hunk: @@ -14,39 +16,41 @@ final class CollapsibleShipmentItemCardViewModel: ObservableObject, Identifiable
 
     var onSelectionChange: (() -> Void)?
 
-    var selectedItemIds: [String] {
-        if mainItemRow.selected {
-            if childItemRows.isNotEmpty {
-                return childItemRows.map { $0.itemID }
-            } else {
-                return [mainItemRow.itemID]
-            }
+    var numberOfSelectedItems: Int {
+        if childItemRows.isEmpty {
+            mainItemRow.selected ? 1 : 0
+        } else {
+            childItemRows.count { $0.selected }
         }
-
-        return childItemRows
-            .filter { $0.selected }
-            .map(\.itemID)
     }
 
-    init(parentShipmentId: String,
-         childShipmentIds: [String],
-         item: ShippingLabelPackageItem,
+    init(item: ShippingLabelPackageItem,
          currency: String) {
-        let mainShippingItem = WooShippingItemRowViewModel(item: ShippingLabelPackageItem(copy: item, quantity: max(1.0, Decimal(childShipmentIds.count))),
+        self.packageItem = item
+
+        let mainShippingItem = WooShippingItemRowViewModel(item: item,
                                                            currency: currency)
         let childShippingItem = WooShippingItemRowViewModel(item: ShippingLabelPackageItem(copy: item, quantity: 1.0),
                                                             currency: currency)
 
-        self.mainItemRow = SelectableShipmentItemRowViewModel(itemID: parentShipmentId,
+        self.mainItemRow = SelectableShipmentItemRowViewModel(itemID: "\(item.productOrVariationID)",
                                                               isSelectable: true,
                                                               item: mainShippingItem,
                                                               showQuantity: true)
-        self.childItemRows = childShipmentIds.map({
-            SelectableShipmentItemRowViewModel(itemID: $0,
-                                               isSelectable: true,
-                                               item: childShippingItem,
-                                               showQuantity: false)
-        })
+
+        if item.quantity.intValue == 1 {
+            self.childItemRows = []
+        } else {
+            var childItemRows = [SelectableShipmentItemRowViewModel]()
+            for quantity in 0..<item.quantity.intValue {
+                let childShipmentId = "\(item.productOrVariationID)-sub-\(quantity)"
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentItemCardViewModel.swift
user: login: selanthiraiyan
body: ```suggestion
            for index in 0..<item.quantity.intValue {
                let childShipmentId = "\(item.productOrVariationID)-sub-\(index)"
```
created_at: 2025-03-27T00:28:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,13 +91,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity > 0 {
+                let newItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(remainingQuantity))
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: self.order.currency)
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
user: login: selanthiraiyan
body: ```suggestion
                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: order.currency)
```
created_at: 2025-03-27T00:34:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,13 +91,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
user: login: selanthiraiyan
body: ```suggestion
                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: order.currency)
```
created_at: 2025-03-27T00:35:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,13 +91,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity > 0 {
+                let newItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(remainingQuantity))
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: self.order.currency)
+                )
+                let movedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(selectedQuantity))
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: movedItem, currency: self.order.currency)
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
user: login: selanthiraiyan
body: ```suggestion
                    CollapsibleShipmentItemCardViewModel(item: movedItem, currency: order.currency)
```
created_at: 2025-03-27T00:35:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,13 +91,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity > 0 {
+                let newItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(remainingQuantity))
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: self.order.currency)
+                )
+                let movedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(selectedQuantity))
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: movedItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity == 0 {
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
user: login: selanthiraiyan
body: ```suggestion
                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: order.currency)
```
created_at: 2025-03-27T00:35:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,13 +91,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity > 0 {
+                let newItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(remainingQuantity))
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: self.order.currency)
+                )
+                let movedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(selectedQuantity))
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: movedItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity == 0 {
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            }
+        }
+
+        // Step 2: Update the current shipment
+        let currentIndex = selectedShipmentIndex ?? 0
+        shipments[currentIndex] = newShipment
+        configureSelectionCallback()
+        configureSectionHeader()
+
+        // Step 3: Add new or update existing shipment
+        switch destination {
+        case .newShipment:
+            shipments.append(movedItems)
+
+        case .existingShipment(let index):
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
user: login: selanthiraiyan
body: I think we should check for existing items and merge the new items into existing collapsible card. Right now, we are creating more than one collapsible card for the same product. WDYT? 

https://github.com/user-attachments/assets/ed284fc8-0a10-4e80-bf64-73f342883801



created_at: 2025-03-27T00:39:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,13 +91,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity > 0 {
+                let newItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(remainingQuantity))
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: self.order.currency)
+                )
+                let movedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(selectedQuantity))
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: movedItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity == 0 {
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            }
+        }
+
+        // Step 2: Update the current shipment
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
user: login: selanthiraiyan
body: Should we remove the current shipment if all items are removed from it? 


https://github.com/user-attachments/assets/1de61982-f455-4280-a651-2e29cf8f82f1


created_at: 2025-03-27T00:39:10Z
author_association: CONTRIBUTOR
path: WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/Split shipments/WooShippingSplitShipmentsViewModelTests.swift
user: login: selanthiraiyan
body: Nice tests. ❤️ 
created_at: 2025-03-27T00:41:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,13 +91,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity > 0 {
+                let newItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(remainingQuantity))
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: self.order.currency)
+                )
+                let movedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(selectedQuantity))
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: movedItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity == 0 {
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            }
+        }
+
+        // Step 2: Update the current shipment
+        let currentIndex = selectedShipmentIndex ?? 0
+        shipments[currentIndex] = newShipment
+        configureSelectionCallback()
+        configureSectionHeader()
+
+        // Step 3: Add new or update existing shipment
+        switch destination {
+        case .newShipment:
+            shipments.append(movedItems)
+
+        case .existingShipment(let index):
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
user: login: itsmeichigo
body: Good catch! I updated the logic in 7e5d3076bfba293f299586710351e3e7c8545776 and added a test for that in b20e8964fce342f4c96d5bf10212aa55cb7b7831.
created_at: 2025-03-27T03:47:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -92,13 +91,62 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject {
         instructions = nil
         dismissedInstructions = true
     }
+
+    func moveSelectedItems(to destination: MoveToShipmentNoticeViewModel.Destination) {
+        moveToNoticeViewModel = nil
+        instructions = nil
+
+        // Step 1: Split items
+        var newShipment = Shipment()
+        var movedItems = [CollapsibleShipmentItemCardViewModel]()
+        for item in currentShipment {
+            let initialQuantity = item.packageItem.quantity.intValue
+            let selectedQuantity = item.numberOfSelectedItems
+            let remainingQuantity = initialQuantity - selectedQuantity
+            if remainingQuantity == 0 {
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity > 0 {
+                let newItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(remainingQuantity))
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: newItem, currency: self.order.currency)
+                )
+                let movedItem = ShippingLabelPackageItem(copy: item.packageItem, quantity: Decimal(selectedQuantity))
+                movedItems.append(
+                    CollapsibleShipmentItemCardViewModel(item: movedItem, currency: self.order.currency)
+                )
+            } else if selectedQuantity == 0 {
+                newShipment.append(
+                    CollapsibleShipmentItemCardViewModel(item: item.packageItem, currency: self.order.currency)
+                )
+            }
+        }
+
+        // Step 2: Update the current shipment
path: WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/WooShippingSplitShipmentsViewModel.swift
user: login: itsmeichigo
body: You're right! I added the last step to remove empty shipment in de44457d37118029edf052a925de180ee03a3465. Tested in ff4652263620afdbf11c389ce364cad33804d22a.
created_at: 2025-03-27T04:05:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: wpmobilebot
created_at: 2025-03-25T11:35:38Z
body: <!-- REUSE_ID: prototype-build-link -->

<p><img alt='WooCommerce iOS' align='top' src='https://appcenter-filemanagement-distrib4ede6f06e.azureedge.net/1c82e9c2-9564-4c67-9f0d-59d36b7286c4/AppIcon60x60%402x.png?sv=2019-07-07&sr=c&sig=2uxma6SB9QsG7rNqkUsqP3D6ZYPAmN1wiu3h79awl1U%3D&se=2025-04-03T04%3A15%3A43Z&sp=r' width='20px' />📲 You can test the changes from this Pull Request in <b>WooCommerce iOS</b> by scanning the QR code below to install the corresponding build.</p>
<table>
<tr>
  <td rowspan='6' width='260px'><img src='https://api.qrserver.com/v1/create-qr-code/?size=500x500&qzone=4&data=https%3A%2F%2Finstall.appcenter.ms%2Forgs%2Fautomattic%2Fapps%2FWooCommerce-Installable-Builds%2Freleases%2F13432' width='250' height='250' /></td>
  <td><b>App Name</b></td><td><img alt='WooCommerce iOS' align='top' src='https://appcenter-filemanagement-distrib4ede6f06e.azureedge.net/1c82e9c2-9564-4c67-9f0d-59d36b7286c4/AppIcon60x60%402x.png?sv=2019-07-07&sr=c&sig=2uxma6SB9QsG7rNqkUsqP3D6ZYPAmN1wiu3h79awl1U%3D&se=2025-04-03T04%3A15%3A43Z&sp=r' width='20px' /> WooCommerce iOS</td>
</tr>
<tr><td><b>Build Number</b></td><td>pr15415-ff46522</td></tr>
<tr><td><b>Version</b></td><td>22.0</td></tr>
<tr><td><b>Bundle ID</b></td><td>com.automattic.alpha.woocommerce</td></tr>
<tr><td><b>Commit</b></td><td>ff4652263620afdbf11c389ce364cad33804d22a</td></tr>
<tr><td><b>App Center Build</b></td><td><a href='https://install.appcenter.ms/orgs/automattic/apps/WooCommerce-Installable-Builds/releases/13432'>WooCommerce - Prototype Builds #13432</a></td></tr>
</table>
<em>Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.</em>

author_association: COLLABORATOR
user: login: itsmeichigo
created_at: 2025-03-27T04:06:14Z
body: Thank you @selanthiraiyan for the reviews and catching issues with the moving logic! I updated the logic as mentioned in the threads above. I'll enable auto-merge now, but please feel free the leave any comment if you find any other issues.

author_association: CONTRIBUTOR
user: login: selanthiraiyan
created_at: 2025-03-27T04:20:10Z
body: Thanks for the updates! The changes look good to me. 🚀 
author_association: CONTRIBUTOR