diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Post-Purchase/WooShippingPostPurchaseView.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Post-Purchase/WooShippingPostPurchaseView.swift index 707eb5624b1..e13af9d5e76 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Post-Purchase/WooShippingPostPurchaseView.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Post-Purchase/WooShippingPostPurchaseView.swift @@ -3,6 +3,8 @@ import SwiftUI struct WooShippingPostPurchaseView: View { @ObservedObject private(set) var viewModel: WooShippingPostPurchaseViewModel + @State private var isPrintingLabel = false + var body: some View { VStack(alignment: .leading, spacing: 8) { Text(Localization.readyToPrint) @@ -35,12 +37,16 @@ struct WooShippingPostPurchaseView: View { .roundedBorder(cornerRadius: 8, lineColor: Color(.separator), lineWidth: 1) } Button { - // TODO: Request label from remote and open print dialog + isPrintingLabel = true + Task { @MainActor in + await viewModel.printLabel() + isPrintingLabel = false + } } label: { Text(Localization.printButton) .bold() } - .buttonStyle(HighlightButtonStyle(background: Layout.panelHighlight, backgroundPressed: Layout.buttonPressed)) + .buttonStyle(HighlightLoadingButtonStyle(isLoading: isPrintingLabel, background: Layout.panelHighlight, backgroundPressed: Layout.buttonPressed)) NavigationLink { ShippingLabelPrintingInstructionsView() .navigationTitle(Localization.infoTitle) @@ -144,13 +150,19 @@ private extension WooShippingPostPurchaseView { } #Preview { - WooShippingPostPurchaseView(viewModel: WooShippingPostPurchaseViewModel(labelSizes: [.label, .legal, .a4], + WooShippingPostPurchaseView(viewModel: WooShippingPostPurchaseViewModel(siteID: 123, + labelID: 1, + labelSizes: [.label, .legal, .a4], trackingURL: URL(string: "https://woocommerce.com"), pickupURL: WooShippingCarrier.usps.pickupURL)) .padding() } #Preview("Label without links") { - WooShippingPostPurchaseView(viewModel: WooShippingPostPurchaseViewModel(labelSizes: [.label, .legal, .a4], trackingURL: nil, pickupURL: nil)) + WooShippingPostPurchaseView(viewModel: WooShippingPostPurchaseViewModel(siteID: 123, + labelID: 1, + labelSizes: [.label, .legal, .a4], + trackingURL: nil, + pickupURL: nil)) .padding() } diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Post-Purchase/WooShippingPostPurchaseViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Post-Purchase/WooShippingPostPurchaseViewModel.swift index eac0e877839..47e705e3a43 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Post-Purchase/WooShippingPostPurchaseViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Post-Purchase/WooShippingPostPurchaseViewModel.swift @@ -2,6 +2,10 @@ import Yosemite import WooFoundation final class WooShippingPostPurchaseViewModel: ObservableObject { + private let stores: StoresManager + private let siteID: Int64 + private let labelID: Int64 + /// Available paper sizes for printing the shipping label. let labelSizes: [ShippingLabelPaperSize] @@ -14,16 +18,23 @@ final class WooShippingPostPurchaseViewModel: ObservableObject { /// Shipment pickup URL for the shipping label. let pickupURL: URL? - init(labelSizes: [ShippingLabelPaperSize], + init(siteID: Int64, + labelID: Int64, + labelSizes: [ShippingLabelPaperSize], trackingURL: URL?, - pickupURL: URL?) { + pickupURL: URL?, + stores: StoresManager = ServiceLocator.stores) { + self.siteID = siteID + self.labelID = labelID self.labelSizes = labelSizes self.trackingURL = trackingURL self.pickupURL = pickupURL + self.stores = stores } convenience init(shippingLabel: ShippingLabel, - siteAddress: SiteAddress = SiteAddress()) { + siteAddress: SiteAddress = SiteAddress(), + stores: StoresManager = ServiceLocator.stores) { // Label sizes aren't provided by the API, so we can hard-code them to match the extension behavior: let labelSizes = { var availableLabelSizes: [ShippingLabelPaperSize] = [.label, .letter] @@ -35,8 +46,42 @@ final class WooShippingPostPurchaseViewModel: ObservableObject { let trackingURL = ShippingLabelTrackingURLGenerator.url(for: shippingLabel) let pickupURL = WooShippingCarrier(rawValue: shippingLabel.carrierID)?.pickupURL - self.init(labelSizes: labelSizes, + self.init(siteID: shippingLabel.siteID, + labelID: shippingLabel.shippingLabelID, + labelSizes: labelSizes, trackingURL: trackingURL, - pickupURL: pickupURL) + pickupURL: pickupURL, + stores: stores) + } + + /// Fetches the shipping label in the selected paper size and presents the print dialog. + @MainActor + func printLabel() async { + do { + let printData = try await requestPrintData() + presentPrintDialog(with: printData) + } catch { + DDLogError("Error generating shipping label document for printing: \(error)") + } + } +} + +private extension WooShippingPostPurchaseViewModel { + /// Requests the shipping label data for printing. + @MainActor + func requestPrintData() async throws -> ShippingLabelPrintData { + try await withCheckedThrowingContinuation { continuation in + let action = WooShippingAction.printLabel(siteID: siteID, labelIDs: [labelID], paperSize: selectedLabelSize) { result in + continuation.resume(with: result) + } + stores.dispatch(action) + } + } + + /// Presents the print dialog with the provided print data. + func presentPrintDialog(with printData: ShippingLabelPrintData) { + let printController = UIPrintInteractionController() + printController.printingItem = printData.data + printController.present(animated: true) } } diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift index bf8685e1b1a..f291b20cf05 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/ButtonStyles.swift @@ -195,8 +195,46 @@ struct HighlightButtonStyle: ButtonStyle { /// Background color when the button is pressed. let backgroundPressed: Color + /// Defines if the content should be hidden. + /// Useful for when we want to show an overlay on top of the bottom without hiding its decoration. Like showing a progress view. + /// + private(set) var hideContent = false + func makeBody(configuration: Configuration) -> some View { - HighlightButton(configuration: configuration, background: background, backgroundPressed: backgroundPressed) + HighlightButton(configuration: configuration, background: background, backgroundPressed: backgroundPressed, hideContent: hideContent) + } +} + +/// Adds a highlight button style while showing a progress view on top of the button when required. +/// +struct HighlightLoadingButtonStyle: PrimitiveButtonStyle { + /// Set to `true` to show a progress view within the button. + var isLoading: Bool + + /// Background color for the button. + let background: Color + + /// Background color when the button is pressed. + let backgroundPressed: Color + + /// Returns a `ProgressView` if the view is loading. Return nil otherwise + /// + private var progressViewOverlay: ProgressView? { + isLoading ? ProgressView() : nil + } + + func makeBody(configuration: Configuration) -> some View { + return Button(configuration) + .buttonStyle(HighlightButtonStyle(background: background, backgroundPressed: backgroundPressed, hideContent: isLoading)) + .disabled(isLoading) + .overlay(progressViewOverlay.tint(Color(.primaryButtonTitle))) + } + + /// Only dispatch events while the view is not loading. + /// + private func dispatchTrigger(_ configuration: Configuration) { + guard !isLoading else { return } + configuration.trigger() } } @@ -481,8 +519,14 @@ private struct HighlightButton: View { /// Background color when the button is pressed. let backgroundPressed: Color + /// Defines if the content should be hidden. + /// Useful for when we want to show an overlay on top of the bottom without hiding its decoration. Like showing a progress view. + /// + private(set) var hideContent = false + var body: some View { BaseButton(configuration: configuration) + .opacity(contentOpacity) .foregroundColor(Color(.primaryButtonTitle)) .font(.headline) .background( @@ -498,6 +542,10 @@ private struct HighlightButton: View { return background } } + + var contentOpacity: Double { + hideContent ? 0.0 : 1.0 + } } private enum Style { diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingPostPurchaseViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingPostPurchaseViewModelTests.swift index aba54f3a8ac..c93b98f8c1c 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingPostPurchaseViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Shipping Label/WooShipping Create Shipping Labels/WooShippingPostPurchaseViewModelTests.swift @@ -11,7 +11,7 @@ final class WooShippingPostPurchaseViewModelTests: XCTestCase { let pickupURL = WooShippingCarrier.usps.pickupURL // When - let viewModel = WooShippingPostPurchaseViewModel(labelSizes: labelSizes, trackingURL: trackingURL, pickupURL: pickupURL) + let viewModel = WooShippingPostPurchaseViewModel(siteID: 123, labelID: 1, labelSizes: labelSizes, trackingURL: trackingURL, pickupURL: pickupURL) // Then XCTAssertEqual(viewModel.labelSizes, labelSizes) @@ -68,4 +68,27 @@ final class WooShippingPostPurchaseViewModelTests: XCTestCase { XCTAssertEqual(viewModel.pickupURL, WooShippingCarrier.usps.pickupURL) } + @MainActor + func test_printLabel_fetches_label_data_from_remote() async { + // Given + var printData: ShippingLabelPrintData? + let stores = MockStoresManager(sessionManager: .testingInstance) + stores.whenReceivingAction(ofType: WooShippingAction.self) { action in + switch action { + case let .printLabel(_, _, _, completion): + let data = ShippingLabelPrintData.fake() + printData = data + completion(.success(data)) + default: + XCTFail("Unexpected action: \(action)") + } + } + let viewModel = WooShippingPostPurchaseViewModel(shippingLabel: ShippingLabel.fake(), stores: stores) + + // When + await viewModel.printLabel() + + // Then + XCTAssertNotNil(printData) + } }