diff --git a/Modules/Sources/Fakes/Networking.generated.swift b/Modules/Sources/Fakes/Networking.generated.swift index 45f3a9b179b..b67a0f1102d 100644 --- a/Modules/Sources/Fakes/Networking.generated.swift +++ b/Modules/Sources/Fakes/Networking.generated.swift @@ -343,7 +343,8 @@ extension Networking.Booking { attendanceStatusKey: .fake(), localTimezone: .fake(), currency: .fake(), - orderInfo: .fake() + orderInfo: .fake(), + note: .fake() ) } } diff --git a/Modules/Sources/Networking/Model/Bookings/Booking.swift b/Modules/Sources/Networking/Model/Bookings/Booking.swift index c53bc714f8c..e7f647cd6a8 100644 --- a/Modules/Sources/Networking/Model/Bookings/Booking.swift +++ b/Modules/Sources/Networking/Model/Bookings/Booking.swift @@ -24,6 +24,7 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { public let localTimezone: String public let currency: String public let orderInfo: BookingOrderInfo? + public let note: String public var bookingStatus: BookingStatus { return BookingStatus(rawValue: statusKey) ?? .unknown @@ -54,7 +55,8 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { attendanceStatusKey: String, localTimezone: String, currency: String, - orderInfo: BookingOrderInfo?) { + orderInfo: BookingOrderInfo?, + note: String) { self.siteID = siteID self.bookingID = bookingID self.allDay = allDay @@ -75,6 +77,7 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { self.localTimezone = localTimezone self.currency = currency self.orderInfo = orderInfo + self.note = note } /// The public initializer for Booking. @@ -129,6 +132,7 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { let localTimezone = try container.decode(String.self, forKey: .localTimezone) let currency = try container.decode(String.self, forKey: .currency) let orderInfo: BookingOrderInfo? = nil // to be prefilled when synced + let note = try container.decode(String.self, forKey: .note) self.init(siteID: siteID, bookingID: bookingID, @@ -149,7 +153,8 @@ public struct Booking: Codable, GeneratedCopiable, Hashable, GeneratedFakeable { attendanceStatusKey: attendanceStatusKey, localTimezone: localTimezone, currency: currency, - orderInfo: orderInfo) + orderInfo: orderInfo, + note: note) } public func encode(to encoder: Encoder) throws { @@ -203,6 +208,7 @@ private extension Booking { case attendanceStatusKey = "attendance_status" case localTimezone = "local_timezone" case currency + case note } } diff --git a/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift b/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift index c20e19dc571..b02490bf551 100644 --- a/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift +++ b/Modules/Sources/Networking/Model/Copiable/Models+Copiable.generated.swift @@ -450,7 +450,8 @@ extension Networking.Booking { attendanceStatusKey: CopiableProp = .copy, localTimezone: CopiableProp = .copy, currency: CopiableProp = .copy, - orderInfo: NullableCopiableProp = .copy + orderInfo: NullableCopiableProp = .copy, + note: CopiableProp = .copy ) -> Networking.Booking { let siteID = siteID ?? self.siteID let bookingID = bookingID ?? self.bookingID @@ -472,6 +473,7 @@ extension Networking.Booking { let localTimezone = localTimezone ?? self.localTimezone let currency = currency ?? self.currency let orderInfo = orderInfo ?? self.orderInfo + let note = note ?? self.note return Networking.Booking( siteID: siteID, @@ -493,7 +495,8 @@ extension Networking.Booking { attendanceStatusKey: attendanceStatusKey, localTimezone: localTimezone, currency: currency, - orderInfo: orderInfo + orderInfo: orderInfo, + note: note ) } } diff --git a/Modules/Sources/Storage/Model/Booking/Booking+CoreDataProperties.swift b/Modules/Sources/Storage/Model/Booking/Booking+CoreDataProperties.swift index 23e8e8eb9b7..5aa9cbe9aff 100644 --- a/Modules/Sources/Storage/Model/Booking/Booking+CoreDataProperties.swift +++ b/Modules/Sources/Storage/Model/Booking/Booking+CoreDataProperties.swift @@ -23,4 +23,5 @@ extension Booking { @NSManaged public var localTimezone: String? @NSManaged public var currency: String? @NSManaged public var orderInfo: BookingOrderInfo? + @NSManaged public var note: String? } diff --git a/Modules/Sources/Storage/Model/MIGRATIONS.md b/Modules/Sources/Storage/Model/MIGRATIONS.md index dd85daf3e41..798fbabb19e 100644 --- a/Modules/Sources/Storage/Model/MIGRATIONS.md +++ b/Modules/Sources/Storage/Model/MIGRATIONS.md @@ -2,6 +2,10 @@ This file documents changes in the WCiOS Storage data model. Please explain any changes to the data model as well as any custom migrations. +## Model 130 (Release 23.7) +- @adborbas 2025-11-06 + - Added `note` attribute to `Booking` entity. + ## Model 129 (Release X.X.X.X) - @rafaelkayumov 2025-10-17 - Added `attendanceStatusKey` attribute to `Booking` entity. diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion index 051471ed320..f6128c44773 100644 --- a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - Model 129.xcdatamodel + Model 130.xcdatamodel diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 129.xcdatamodel/contents b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 129.xcdatamodel/contents index d030970308a..30557bf6665 100644 --- a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 129.xcdatamodel/contents +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 129.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 130.xcdatamodel/contents b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 130.xcdatamodel/contents new file mode 100644 index 00000000000..a242f13b827 --- /dev/null +++ b/Modules/Sources/Storage/Resources/WooCommerce.xcdatamodeld/Model 130.xcdatamodel/contents @@ -0,0 +1,1177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift b/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift index db92ed7c8de..a2b926fde39 100644 --- a/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift +++ b/Modules/Sources/Yosemite/Model/Booking/Booking+ReadOnlyConvertible.swift @@ -32,6 +32,7 @@ extension Storage.Booking: ReadOnlyConvertible { attendanceStatusKey = booking.attendanceStatusKey localTimezone = booking.localTimezone currency = booking.currency + note = booking.note } /// Returns a ReadOnly version of the receiver. @@ -56,7 +57,8 @@ extension Storage.Booking: ReadOnlyConvertible { attendanceStatusKey: attendanceStatusKey ?? "", localTimezone: localTimezone ?? "", currency: currency ?? "USD", - orderInfo: orderInfo?.toReadOnly()) + orderInfo: orderInfo?.toReadOnly(), + note: note ?? "") } } diff --git a/Modules/Tests/NetworkingTests/Responses/booking-list.json b/Modules/Tests/NetworkingTests/Responses/booking-list.json index 5532948f4ad..af8356812d1 100644 --- a/Modules/Tests/NetworkingTests/Responses/booking-list.json +++ b/Modules/Tests/NetworkingTests/Responses/booking-list.json @@ -19,6 +19,7 @@ "parent_id": 0, "person_counts": [], "local_timezone": "", + "note": "", }, { "id": 77, @@ -39,6 +40,7 @@ "parent_id": 0, "person_counts": [], "local_timezone": "", + "note": "", } ] } diff --git a/Modules/Tests/StorageTests/CoreData/MigrationTests.swift b/Modules/Tests/StorageTests/CoreData/MigrationTests.swift index a634d620938..2ab77b12df3 100644 --- a/Modules/Tests/StorageTests/CoreData/MigrationTests.swift +++ b/Modules/Tests/StorageTests/CoreData/MigrationTests.swift @@ -2312,6 +2312,27 @@ final class MigrationTests: XCTestCase { let updatedValue = migratedBooking.value(forKey: "attendanceStatusKey") as? String XCTAssertEqual(updatedValue, "checked_in") } + + func test_migrating_from_129_to_130_adds_new_note_attribute_to_booking() throws { + // Given + let sourceContainer = try startPersistentContainer("Model 129") + let sourceContext = sourceContainer.viewContext + + let booking = insertBooking(to: sourceContext) + try sourceContext.save() + + XCTAssertNil(booking.entity.attributesByName["note"], "Precondition. Attribute does not exist.") + + // When + let targetContainer = try migrate(sourceContainer, to: "Model 130") + + // Then + let targetContext = targetContainer.viewContext + let migratedBooking = try XCTUnwrap(targetContext.first(entityName: "Booking")) + + // `note` should be present in `migratedBooking` + XCTAssertNotNil(migratedBooking.entity.attributesByName["note"]) + } } // MARK: - Persistent Store Setup and Migrations diff --git a/WooCommerce/Classes/Bookings/BookingsTabView.swift b/WooCommerce/Classes/Bookings/BookingsTabView.swift index f9a8101fb1c..0fe671944a5 100644 --- a/WooCommerce/Classes/Bookings/BookingsTabView.swift +++ b/WooCommerce/Classes/Bookings/BookingsTabView.swift @@ -50,7 +50,9 @@ struct BookingsTabView: View { } detail: { if let selectedBooking { let viewModel = BookingDetailsViewModel(booking: selectedBooking) - BookingDetailsView(viewModel) + NavigationStack { + BookingDetailsView(viewModel) + } } else { Text("Select a booking to see details.") } diff --git a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift index e26793b0ea5..a20b9a3bce8 100644 --- a/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift +++ b/WooCommerce/Classes/ViewModels/Booking Details/BookingDetailsViewModel.swift @@ -33,6 +33,8 @@ final class BookingDetailsViewModel: ObservableObject { booking.attendanceStatus } + var note: String { booking.note } + init(booking: Booking, stores: StoresManager = ServiceLocator.stores, storage: StorageManagerType = ServiceLocator.storageManager) { @@ -69,7 +71,8 @@ private extension BookingDetailsViewModel { ) let bookingNotes = Section( - header: .title(Localization.bookingNotesSectionHeaderTitle.uppercased()), + header: .title(Localization.bookingNoteSectionHeaderTitle.uppercased()), + footerText: Localization.bookingNoteSectionFooterText, content: .bookingNotes ) @@ -378,10 +381,16 @@ private extension BookingDetailsViewModel { comment: "Header title for the 'Payment' section in the booking details screen." ) - static let bookingNotesSectionHeaderTitle = NSLocalizedString( - "BookingDetailsView.bookingNotes.headerTitle", - value: "Booking notes", - comment: "Header title for the 'Booking notes' section in the booking details screen." + static let bookingNoteSectionHeaderTitle = NSLocalizedString( + "BookingDetailsView.bookingNote.headerTitle", + value: "Booking note", + comment: "Header title for the 'Booking note' section in the booking details screen." + ) + + static let bookingNoteSectionFooterText = NSLocalizedString( + "BookingDetailsView.bookingNote.footerText", + value: "This is a private note. It'll not be shared with the customer.", + comment: "Footer text for the `Booking note` section in the booking details screen." ) static let cancelBookingAlertMessage = NSLocalizedString( diff --git a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift index 6191a146fda..4516fa873da 100644 --- a/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift +++ b/WooCommerce/Classes/ViewRelated/Bookings/Booking Details/BookingDetailsView.swift @@ -239,18 +239,9 @@ private extension BookingDetailsView { } func bookingNotesView() -> some View { - HStack(spacing: Layout.contentSidePadding) { - Image(systemName: "plus") - .font(.title3.weight(.medium)) - Text(Localization.bookingNotesRowText) - .rowTextStyle() - Spacer() - } - .foregroundStyle(Color.accentColor) - .padding(.vertical, Layout.rowTextVerticalPadding) - .tappable { - print("On Add a note tap") - } + MultilineEditableTextRow(value: viewModel.note, + placeholder: Localization.bookingNotesRowText, + detailTitle: Localization.bookingNoteNavbarText) } } @@ -305,9 +296,15 @@ extension BookingDetailsView { /// Booking notes static let bookingNotesRowText = NSLocalizedString( - "BookingDetailsView.bookingNotes.addANoteRow.title", - value: "Add a note", - comment: "Add a note row title in booking notes section in booking details view." + "BookingDetailsView.bookingNote.addNoteRow.title", + value: "Add note", + comment: "Add a booking note section in booking details view." + ) + + static let bookingNoteNavbarText = NSLocalizedString( + "BookingDetailsView.bookingNote.navbar.title", + value: "Booking note", + comment: "Title of navigation bar when editing a booking note." ) } } @@ -337,7 +334,8 @@ struct BookingDetailsView_Previews: PreviewProvider { attendanceStatusKey: "booked", localTimezone: "America/New_York", currency: "USD", - orderInfo: nil + orderInfo: nil, + note: "" ) let viewModel = BookingDetailsViewModel(booking: sampleBooking) return BookingDetailsView(viewModel) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift new file mode 100644 index 00000000000..9bc920d5513 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextDetailView.swift @@ -0,0 +1,104 @@ +import SwiftUI + +struct MultilineEditableTextDetailView: View { + @Environment(\.dismiss) private var dismiss + + @Binding var text: String + @State private var editedText: String + @State private var showDiscardChangesDialog = false + @FocusState private var isFocused: Bool + + let title: String? + + init(text: Binding, title: String? = nil) { + self._text = text + self._editedText = State(initialValue: text.wrappedValue) + self.title = title + } + + var body: some View { + VStack { + TextEditor(text: $editedText) + .focused($isFocused) + .padding(.horizontal, Layout.horizontalPadding) + .padding(.vertical, Layout.verticalPadding) + } + .navigationTitle(title ?? "") + .navigationBarTitleDisplayMode(.inline) + .navigationBarBackButtonHidden(true) + .toolbar { toolbar } + .wooNavigationBarStyle() + .onAppear { isFocused = true } + } + + private var toolbar: some ToolbarContent { + Group { + ToolbarItem(placement: .topBarLeading) { + Button(action: handleBackButtonTap) { + Image(systemName: "chevron.backward") + .font(.body.weight(.semibold)) + } + .confirmationDialog( + Localization.discardChangesAlertTitle, + isPresented: $showDiscardChangesDialog, + titleVisibility: .visible + ) { + Button(Localization.discardChangesActionTitle, role: .destructive) { + dismiss() + } + Button(Localization.cancelActionTitle, role: .cancel) {} + } + } + + if editedText != text { + ToolbarItem(placement: .primaryAction) { + Button(Localization.doneButtonTitle) { + text = editedText + dismiss() + } + .fontWeight(.medium) + } + } + } + } + + private func handleBackButtonTap() { + if editedText != text { + showDiscardChangesDialog = true + } else { + dismiss() + } + } +} + +fileprivate extension MultilineEditableTextDetailView { + enum Layout { + static let horizontalPadding: CGFloat = 16 + static let verticalPadding: CGFloat = 12 + } +} + +extension MultilineEditableTextDetailView { + enum Localization { + static let discardChangesAlertTitle = NSLocalizedString( + "MultilineEditableTextDetailView.discardChanges.alert.title", + value: "Are you sure you want to discard these changes?", + comment: "Title for the confirmation dialog when the user attempts to discard changes in the multiline text editor." + ) + static let discardChangesActionTitle = NSLocalizedString( + "MultilineEditableTextDetailView.discardChanges.alert.discardAction", + value: "Discard changes", + comment: "Destructive action button title to discard changes in the multiline text editor." + ) + static let cancelActionTitle = NSLocalizedString( + "MultilineEditableTextDetailView.discardChanges.alert.cancelAction", + value: "Cancel", + comment: "Cancel button title for the discard changes confirmation dialog in the multiline text editor." + ) + static let doneButtonTitle = NSLocalizedString( + "MultilineEditableTextDetailView.doneButton.title", + value: "Done", + comment: "Navigation bar button title used to save changes and close the multiline text editor." + ) + } +} diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift new file mode 100644 index 00000000000..a42fe8f7f62 --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/MultilineEditableTextRow/MultilineEditableTextRow.swift @@ -0,0 +1,66 @@ +import SwiftUI + +struct MultilineEditableTextRow: View { + @State var value: String + let placeholder: String + let detailTitle: String? + + init(value: String, placeholder: String, detailTitle: String? = nil) { + self.value = value + self.placeholder = placeholder + self.detailTitle = detailTitle + } + + var body: some View { + NavigationLink { + MultilineEditableTextDetailView(text: $value, title: detailTitle) + } label: { + content + } + } + + @ViewBuilder + private var content: some View { + HStack(alignment: .center, spacing: Layout.spacing) { + label + + Spacer() + + DisclosureIndicator() + } + .padding(.vertical, Layout.padding) + } + + @ViewBuilder + private var label: some View { + if value.isEmpty { + Text(placeholder) + .rowTextStyle() + .foregroundStyle(Color(.textSubtle)) + + } else { + Text(value) + .multilineTextAlignment(.leading) + .foregroundStyle(Color.primary) + } + } +} + +fileprivate extension MultilineEditableTextRow { + enum Layout { + static let spacing: CGFloat = 10 + static let padding: CGFloat = 12 + } +} + +#if DEBUG +#Preview { + @Previewable @State var text: String = "" + + NavigationStack { + MultilineEditableTextRow(value: text, placeholder: "Add note") + .padding(.horizontal, 16) + } + .preferredColorScheme(.dark) +} +#endif diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index b88c6969faa..7da3197cf18 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -5785,6 +5785,7 @@ 3FD9BFBE2E0A2533004A8DC8 /* WooCommerceScreenshots */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3FD9BFC42E0A2534004A8DC8 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WooCommerceScreenshots; sourceTree = ""; }; 646A2C682E9FCD7E003A32A1 /* Routing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Routing; sourceTree = ""; }; 6489D8522EA667AC00D96802 /* Routing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Routing; sourceTree = ""; }; + 64EA08E42EC214FA00050202 /* MultilineEditableTextRow */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = MultilineEditableTextRow; sourceTree = ""; }; DEDB5D342E7A68950022E5A1 /* Bookings */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Bookings; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ @@ -8487,6 +8488,7 @@ 45D875D72611EA3D00226C3F /* SwiftUI Components */ = { isa = PBXGroup; children = ( + 64EA08E42EC214FA00050202 /* MultilineEditableTextRow */, 2D880B482DFB2F3D00A6FB2C /* OptionalBinding.swift */, EE3B17B52AA03837004D3E0C /* CelebrationView.swift */, DE7E5E8B2B4E9353002E28D2 /* ErrorStateView.swift */, @@ -13262,6 +13264,7 @@ fileSystemSynchronizedGroups = ( 2D33E6B02DD1453E000C7198 /* WooShippingPaymentMethod */, 646A2C682E9FCD7E003A32A1 /* Routing */, + 64EA08E42EC214FA00050202 /* MultilineEditableTextRow */, DEDB5D342E7A68950022E5A1 /* Bookings */, ); name = WooCommerce;