From fe87fde392da839640fa53a15787e77af718d5f9 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:40:49 +0530 Subject: [PATCH 01/10] Make title optional in Notice. --- .../Notices/DefaultNoticePresenter.swift | 4 +- .../Classes/Tools/Notices/Notice.swift | 41 ++++++++++++++++++- .../View Modifiers/View+NoticesModifier.swift | 14 ++++--- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/WooCommerce/Classes/Tools/Notices/DefaultNoticePresenter.swift b/WooCommerce/Classes/Tools/Notices/DefaultNoticePresenter.swift index 2f0b4c55a6c..06261bc2b9e 100644 --- a/WooCommerce/Classes/Tools/Notices/DefaultNoticePresenter.swift +++ b/WooCommerce/Classes/Tools/Notices/DefaultNoticePresenter.swift @@ -380,7 +380,9 @@ private extension UNMutableNotificationContent { convenience init(notice: Notice) { self.init() - title = notice.notificationInfo?.title ?? notice.title + if let title = notice.notificationInfo?.title ?? notice.title { + self.title = title + } if let body = notice.notificationInfo?.body { self.body = body diff --git a/WooCommerce/Classes/Tools/Notices/Notice.swift b/WooCommerce/Classes/Tools/Notices/Notice.swift index 904d32470a0..a92cff3029e 100644 --- a/WooCommerce/Classes/Tools/Notices/Notice.swift +++ b/WooCommerce/Classes/Tools/Notices/Notice.swift @@ -9,7 +9,7 @@ struct Notice { /// The title that contains the reason for the notice /// - let title: String + let title: String? /// An optional subtitle that contains a secondary description of the reason for the notice /// @@ -35,7 +35,6 @@ struct Notice { /// let actionHandler: (() -> Void)? - /// Designated Initializer /// init(title: String, @@ -65,3 +64,41 @@ extension Notice: Equatable { lhs.actionTitle == rhs.actionTitle } } + +/// Convenience initializers to init without a`title` +/// These initializers help to ensure that at least one of title/subtitle/message is non-nil. +/// +extension Notice { + /// To init using `subtitle` and/or `message` + /// + init(subtitle: String, + message: String? = nil, + feedbackType: UINotificationFeedbackGenerator.FeedbackType? = nil, + notificationInfo: NoticeNotificationInfo? = nil, + actionTitle: String? = nil, + actionHandler: ((() -> Void))? = nil) { + self.title = nil + self.subtitle = subtitle + self.message = message + self.feedbackType = feedbackType + self.notificationInfo = notificationInfo + self.actionTitle = actionTitle + self.actionHandler = actionHandler + } + + /// To init using only `message` + /// + init(message: String, + feedbackType: UINotificationFeedbackGenerator.FeedbackType? = nil, + notificationInfo: NoticeNotificationInfo? = nil, + actionTitle: String? = nil, + actionHandler: ((() -> Void))? = nil) { + self.title = nil + self.subtitle = nil + self.message = message + self.feedbackType = feedbackType + self.notificationInfo = notificationInfo + self.actionTitle = actionTitle + self.actionHandler = actionHandler + } +} diff --git a/WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift b/WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift index fdec18ef195..298ba3896de 100644 --- a/WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift +++ b/WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift @@ -64,13 +64,15 @@ struct NoticeModifier: ViewModifier { Spacer() HStack(spacing: 0.0) { VStack { - HStack { - Text(notice.title) - .lineLimit(notice.message.isNilOrEmpty ? 0 : 2) - Spacer() + if let title = notice.title { + HStack { + Text(title) + .lineLimit(notice.message.isNilOrEmpty ? 0 : 2) + Spacer() + } + .font(Constants.titleFont) + .foregroundColor(Constants.titleColor) } - .font(Constants.titleFont) - .foregroundColor(Constants.titleColor) if let subtitle = notice.subtitle { HStack { Text(subtitle) From 5c15f903fd2aafb89968b498651a4cbcc3325a73 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:41:08 +0530 Subject: [PATCH 02/10] Use `BoldableTextView` for message to be able to use markdown message in Notice. --- WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift b/WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift index 298ba3896de..b40e8b9fb0f 100644 --- a/WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift +++ b/WooCommerce/Classes/View Modifiers/View+NoticesModifier.swift @@ -83,7 +83,7 @@ struct NoticeModifier: ViewModifier { } if let message = notice.message { HStack { - Text(message) + BoldableTextView(message) Spacer() } .font(Constants.messageFont) From 9b304c37c476323a51f3b3128ff745a6cf427466 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:42:04 +0530 Subject: [PATCH 03/10] Add on selection callback. --- .../CollapsibleShipmentCardViewModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift index 002cb367854..21b2cb504b4 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift @@ -12,6 +12,8 @@ final class CollapsibleShipmentCardViewModel: ObservableObject, Identifiable { /// Child shipment rows, if the shipment has more than one quantity let childShipmentRows: [SelectableShipmentRowViewModel] + var onSelectionChange: (() -> Void)? + init(parentShipmentId: String, childShipmentIds: [String], item: ShippingLabelPackageItem, @@ -47,6 +49,7 @@ private extension CollapsibleShipmentCardViewModel { guard let self else { return } childShipmentRows.forEach({ $0.setSelected(row.selected) }) + onSelectionChange?() } childShipmentRows.forEach({ @@ -54,6 +57,7 @@ private extension CollapsibleShipmentCardViewModel { guard let self else { return } mainShipmentRow.setSelected(false) + onSelectionChange?() } }) } From 9bb99e0b97d64624435e504a2a5337b37ef7d543 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:42:28 +0530 Subject: [PATCH 04/10] Helper to know if an item is selected from the collapsible card. --- .../CollapsibleShipmentCardViewModel.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift index 21b2cb504b4..63a568f2e65 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Split Shipments/CollapsibleShipmentCardViewModel.swift @@ -14,6 +14,16 @@ final class CollapsibleShipmentCardViewModel: ObservableObject, Identifiable { var onSelectionChange: (() -> Void)? + var hasSelectedAnItem: Bool { + if mainShipmentRow.selected { + return true + } + + return childShipmentRows + .filter { $0.selected } + .isNotEmpty + } + init(parentShipmentId: String, childShipmentIds: [String], item: ShippingLabelPackageItem, From be0ef2fe12e62e1dee5dc3dc37e9aa117d3c6d4e Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:43:17 +0530 Subject: [PATCH 05/10] Strings for instructions notice. --- .../WooShippingSplitShipmentsViewModel.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 8e0c5a12c52..12e23be90c8 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 @@ -113,6 +113,16 @@ private extension WooShippingSplitShipmentsViewModel { // MARK: Constants private extension WooShippingSplitShipmentsViewModel { enum Localization { + enum SelectionInstructionsNotice { + static let message = NSLocalizedString("wooShipping.createLabels.splitShipment.SelectionInstructionsNotice.message", + value: "To split, select the items, and tap **move to new shipment** when the toolbar appears.", + comment: "Instructions to ask customer to select items to split during shipping label creation." + + " The content inside two double asterisks **...** denote bolded text.") + + static let dismiss = NSLocalizedString("wooShipping.createLabels.splitShipment.SelectionInstructionsNotice.dismiss", + value: "Dismiss", + comment: "Label of the button to dismiss the instructions notice in split shipments flow.") + } static func itemsCount(_ count: Decimal) -> String { return String.pluralize(count, singular: Localization.itemsCountSingularFormat, plural: Localization.itemsCountPluralFormat) } From 55d5c97d1495ca64ceed0fd6d63e5b374a325665 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:43:34 +0530 Subject: [PATCH 06/10] Show split shipments view only if label is not already purchased. --- .../WooShippingCreateLabelsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift index 8feb1d61f4f..fb4d77559c3 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShippingCreateLabelsView.swift @@ -104,7 +104,7 @@ private extension WooShippingCreateLabelsView { WooShippingPostPurchaseView(viewModel: postPurchase) } - if let splitShipmentsViewModel = viewModel.splitShipmentsViewModel { + if !viewModel.canViewLabel, let splitShipmentsViewModel = viewModel.splitShipmentsViewModel { WooShippingSplitShipmentsRow(viewModel: splitShipmentsViewModel) } From 26a867aab0af8700b683560f3c61df5e71de21ba Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:44:35 +0530 Subject: [PATCH 07/10] Show instructions notice. --- .../WooShippingSplitShipmentsDetailView.swift | 6 ++++- .../WooShippingSplitShipmentsViewModel.swift | 25 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) 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 e3565ddbc1a..50c1258f5ad 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 @@ -4,7 +4,7 @@ import Yosemite struct WooShippingSplitShipmentsDetailView: View { @Environment(\.dismiss) private var dismiss - let viewModel: WooShippingSplitShipmentsViewModel + @ObservedObject var viewModel: WooShippingSplitShipmentsViewModel var body: some View { NavigationView { @@ -41,6 +41,10 @@ struct WooShippingSplitShipmentsDetailView: View { } } } + .notice($viewModel.instructionsNotice, autoDismiss: false) + .onAppear { + viewModel.onAppear() + } } } 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 12e23be90c8..bcf9cbe015a 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 @@ -26,10 +26,10 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject { "\(itemsWeightLabel) • \(itemsPriceLabel)" } - @Published var selectedItemIDs: [String: [String]] = [:] - let shipmentCardViewModels: [CollapsibleShipmentCardViewModel] + @Published var instructionsNotice: Notice? + init(order: Order, config: WooShippingConfig, items: [ShippingLabelPackageItem], @@ -70,6 +70,11 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject { }() configureSectionHeader() + configureSelectionCallback() + } + + func onAppear() { + showInstructionsNotice() } func selectAll() { @@ -80,6 +85,22 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject { } private extension WooShippingSplitShipmentsViewModel { + + func showInstructionsNotice() { + if hasSelectedAnItem() == false { + instructionsNotice = Notice(message: Localization.SelectionInstructionsNotice.message, + feedbackType: .success, + actionTitle: Localization.SelectionInstructionsNotice.dismiss) { [weak self] in + self?.instructionsNotice = nil + } + } + } + + + func hasSelectedAnItem() -> Bool { + shipmentCardViewModels.map({ $0.hasSelectedAnItem }).contains(where: { $0 }) + } + /// Configures the labels in the section header. /// func configureSectionHeader() { From 62e0eea950b760ad197c7ab32395074fe0100d50 Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:44:53 +0530 Subject: [PATCH 08/10] Hide instructions notice upon selection. --- .../WooShippingSplitShipmentsViewModel.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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 bcf9cbe015a..9e860c30c5d 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 @@ -85,6 +85,13 @@ final class WooShippingSplitShipmentsViewModel: ObservableObject { } private extension WooShippingSplitShipmentsViewModel { + func configureSelectionCallback() { + shipmentCardViewModels.forEach { viewModel in + viewModel.onSelectionChange = { [weak self] in + self?.checkSelectionAndHideInstructions() + } + } + } func showInstructionsNotice() { if hasSelectedAnItem() == false { @@ -96,6 +103,11 @@ private extension WooShippingSplitShipmentsViewModel { } } + func checkSelectionAndHideInstructions() { + if hasSelectedAnItem() { + instructionsNotice = nil + } + } func hasSelectedAnItem() -> Bool { shipmentCardViewModels.map({ $0.hasSelectedAnItem }).contains(where: { $0 }) From 702c3827b0bf5153e2cc0d355f2fd2602cf3eb7f Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 14:54:43 +0530 Subject: [PATCH 09/10] Refactor code for simplicity. --- .../WooShippingSplitShipmentsViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9e860c30c5d..fd3d408e5ce 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 @@ -110,7 +110,7 @@ private extension WooShippingSplitShipmentsViewModel { } func hasSelectedAnItem() -> Bool { - shipmentCardViewModels.map({ $0.hasSelectedAnItem }).contains(where: { $0 }) + shipmentCardViewModels.contains(where: { $0.hasSelectedAnItem }) } /// Configures the labels in the section header. From 44b4cea3e4c078d090aabf53e8cab9344ee71faf Mon Sep 17 00:00:00 2001 From: Sharma Elanthiraiyan Date: Mon, 24 Mar 2025 19:22:52 +0530 Subject: [PATCH 10/10] Unwrap title in unit tests. --- .../Epilogue/SwitchStoreNoticePresenterTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/WooCommerce/WooCommerceTests/Authentication/Epilogue/SwitchStoreNoticePresenterTests.swift b/WooCommerce/WooCommerceTests/Authentication/Epilogue/SwitchStoreNoticePresenterTests.swift index e7c18c79ee3..fff39804dc8 100644 --- a/WooCommerce/WooCommerceTests/Authentication/Epilogue/SwitchStoreNoticePresenterTests.swift +++ b/WooCommerce/WooCommerceTests/Authentication/Epilogue/SwitchStoreNoticePresenterTests.swift @@ -66,7 +66,8 @@ final class SwitchStoreNoticePresenterTests: XCTestCase { XCTAssertEqual(noticePresenter.queuedNotices.count, 1) let notice = try XCTUnwrap(noticePresenter.queuedNotices.first) - assertThat(notice.title, contains: siteName) + let title = try XCTUnwrap(notice.title) + assertThat(title, contains: siteName) let expectedTitle = String.localizedStringWithFormat(SwitchStoreNoticePresenter.Localization.titleFormat, site.name) XCTAssertEqual(notice.title, expectedTitle) } @@ -92,7 +93,8 @@ final class SwitchStoreNoticePresenterTests: XCTestCase { XCTAssertEqual(noticePresenter.queuedNotices.count, 1) let notice = try XCTUnwrap(noticePresenter.queuedNotices.first) - assertThat(notice.title, contains: siteName) + let title = try XCTUnwrap(notice.title) + assertThat(title, contains: siteName) let expectedTitle = String.localizedStringWithFormat(SwitchStoreNoticePresenter.Localization.titleFormat, site.name) XCTAssertEqual(notice.title, expectedTitle) }