Skip to content

Commit a7e296c

Browse files
authored
Merge pull request #7960 from woocommerce/issue/7944-product-preview-flow
Product Preview: Add "Preview" button to product details
2 parents 1d7fc6f + dad2691 commit a7e296c

File tree

5 files changed

+181
-7
lines changed

5 files changed

+181
-7
lines changed

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,34 @@ final class ProductFormViewController<ViewModel: ProductFormViewModelProtocol>:
199199
saveProduct(status: .draft)
200200
}
201201

202+
// MARK: Product preview action handling
203+
204+
@objc private func saveDraftAndDisplayProductPreview() {
205+
guard viewModel.canSaveAsDraft() || viewModel.hasUnsavedChanges() else {
206+
displayProductPreview()
207+
return
208+
}
209+
210+
saveProduct(status: .draft) { [weak self] result in
211+
if result.isSuccess {
212+
self?.displayProductPreview()
213+
}
214+
}
215+
}
216+
217+
private func displayProductPreview() {
218+
var permalink = URLComponents(string: product.permalink)
219+
var updatedQueryItems = permalink?.queryItems ?? []
220+
updatedQueryItems.append(.init(name: "preview", value: "true"))
221+
permalink?.queryItems = updatedQueryItems
222+
guard let url = permalink?.url else {
223+
return
224+
}
225+
226+
// TODO: Show authenticated WebView
227+
WebviewHelper.launch(url, with: self)
228+
}
229+
202230
// MARK: Navigation actions
203231

204232
@objc func closeNavigationBarButtonTapped() {
@@ -716,14 +744,14 @@ private extension ProductFormViewController {
716744
// MARK: Navigation actions
717745
//
718746
private extension ProductFormViewController {
719-
func saveProduct(status: ProductStatus? = nil) {
747+
func saveProduct(status: ProductStatus? = nil, onCompletion: @escaping (Result<Void, ProductUpdateError>) -> Void = { _ in }) {
720748
let productStatus = status ?? product.status
721749
let messageType = viewModel.saveMessageType(for: productStatus)
722750
showSavingProgress(messageType)
723-
saveProductRemotely(status: status)
751+
saveProductRemotely(status: status, onCompletion: onCompletion)
724752
}
725753

726-
func saveProductRemotely(status: ProductStatus?) {
754+
func saveProductRemotely(status: ProductStatus?, onCompletion: @escaping (Result<Void, ProductUpdateError>) -> Void = { _ in }) {
727755
viewModel.saveProductRemotely(status: status) { [weak self] result in
728756
switch result {
729757
case .failure(let error):
@@ -732,6 +760,7 @@ private extension ProductFormViewController {
732760
// Dismisses the in-progress UI then presents the error alert.
733761
self?.navigationController?.dismiss(animated: true) {
734762
self?.displayError(error: error)
763+
onCompletion(.failure(error))
735764
}
736765
case .success:
737766
// Dismisses the in-progress UI, then presents the confirmation alert.
@@ -741,6 +770,7 @@ private extension ProductFormViewController {
741770
// Show linked products promo banner after product save
742771
(self?.viewModel as? ProductFormViewModel)?.isLinkedProductsPromoEnabled = true
743772
self?.reloadLinkedPromoCellAnimated()
773+
onCompletion(.success(()))
744774
}
745775
}
746776
}
@@ -908,6 +938,8 @@ private extension ProductFormViewController {
908938
// Create action buttons based on view model
909939
let rightBarButtonItems: [UIBarButtonItem] = viewModel.actionButtons.reversed().map { buttonType in
910940
switch buttonType {
941+
case .preview:
942+
return createPreviewBarButtonItem()
911943
case .publish:
912944
return createPublishBarButtonItem()
913945
case .save:
@@ -934,6 +966,10 @@ private extension ProductFormViewController {
934966
return UIBarButtonItem(title: Localization.saveTitle, style: .done, target: self, action: #selector(saveProductAndLogEvent))
935967
}
936968

969+
func createPreviewBarButtonItem() -> UIBarButtonItem {
970+
return UIBarButtonItem(title: Localization.previewTitle, style: .done, target: self, action: #selector(saveDraftAndDisplayProductPreview))
971+
}
972+
937973
func createMoreOptionsBarButtonItem() -> UIBarButtonItem {
938974
let moreButton = UIBarButtonItem(image: .moreImage,
939975
style: .plain,
@@ -1535,6 +1571,7 @@ private extension ProductFormViewController {
15351571
private enum Localization {
15361572
static let publishTitle = NSLocalizedString("Publish", comment: "Action for creating a new product remotely with a published status")
15371573
static let saveTitle = NSLocalizedString("Save", comment: "Action for saving a Product remotely")
1574+
static let previewTitle = NSLocalizedString("Preview", comment: "Action for previewing draft Product changes in the webview")
15381575
static let groupedProductsViewTitle = NSLocalizedString("Grouped Products",
15391576
comment: "Navigation bar title for editing linked products for a grouped product")
15401577
static let unnamedProduct = NSLocalizedString("Unnamed product",

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewModel.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Combine
2+
import protocol Experiments.FeatureFlagService
23
import Yosemite
34

45
import protocol Storage.StorageManagerType
@@ -151,6 +152,14 @@ final class ProductFormViewModel: ProductFormViewModelProtocol {
151152
}
152153
}()
153154

155+
if featureFlagService.isFeatureFlagEnabled(.productsOnboarding),
156+
// Preview existing drafts or new products, that can be saved as a draft
157+
(canSaveAsDraft() || originalProductModel.status == .draft),
158+
// Do not preview new blank products without any changes
159+
!(formType == .add && !hasUnsavedChanges()) {
160+
buttons.insert(.preview, at: 0)
161+
}
162+
154163
// Add more button if needed
155164
if shouldShowMoreOptionsMenu() {
156165
buttons.append(.more)
@@ -170,13 +179,16 @@ final class ProductFormViewModel: ProductFormViewModelProtocol {
170179

171180
private let analytics: Analytics
172181

182+
private let featureFlagService: FeatureFlagService
183+
173184
init(product: EditableProductModel,
174185
formType: ProductFormType,
175186
productImageActionHandler: ProductImageActionHandler,
176187
stores: StoresManager = ServiceLocator.stores,
177188
storageManager: StorageManagerType = ServiceLocator.storageManager,
178189
productImagesUploader: ProductImageUploaderProtocol = ServiceLocator.productImageUploader,
179-
analytics: Analytics = ServiceLocator.analytics) {
190+
analytics: Analytics = ServiceLocator.analytics,
191+
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
180192
self.formType = formType
181193
self.productImageActionHandler = productImageActionHandler
182194
self.originalProduct = product
@@ -186,6 +198,7 @@ final class ProductFormViewModel: ProductFormViewModelProtocol {
186198
self.storageManager = storageManager
187199
self.productImagesUploader = productImagesUploader
188200
self.analytics = analytics
201+
self.featureFlagService = featureFlagService
189202

190203
self.cancellable = productImageActionHandler.addUpdateObserver(self) { [weak self] allStatuses in
191204
guard let self = self else { return }

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewModelProtocol.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ enum ProductFormType {
1010

1111
/// The type of action that can be performed in the product.
1212
enum ActionButtonType {
13+
case preview
1314
case publish
1415
case save
1516
case more

WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,24 @@ struct MockFeatureFlagService: FeatureFlagService {
99
private let isLoginPrologueOnboardingEnabled: Bool
1010
private let isSimplifiedLoginFlowI1Enabled: Bool
1111
private let isStoreCreationMVPEnabled: Bool
12+
private let isProductsOnboardingEnabled: Bool
1213

1314
init(isInboxOn: Bool = false,
1415
isSplitViewInOrdersTabOn: Bool = false,
1516
isUpdateOrderOptimisticallyOn: Bool = false,
1617
shippingLabelsOnboardingM1: Bool = false,
1718
isLoginPrologueOnboardingEnabled: Bool = false,
1819
isSimplifiedLoginFlowI1Enabled: Bool = false,
19-
isStoreCreationMVPEnabled: Bool = false) {
20+
isStoreCreationMVPEnabled: Bool = false,
21+
isProductsOnboardingEnabled: Bool = false) {
2022
self.isInboxOn = isInboxOn
2123
self.isSplitViewInOrdersTabOn = isSplitViewInOrdersTabOn
2224
self.isUpdateOrderOptimisticallyOn = isUpdateOrderOptimisticallyOn
2325
self.shippingLabelsOnboardingM1 = shippingLabelsOnboardingM1
2426
self.isLoginPrologueOnboardingEnabled = isLoginPrologueOnboardingEnabled
2527
self.isSimplifiedLoginFlowI1Enabled = isSimplifiedLoginFlowI1Enabled
2628
self.isStoreCreationMVPEnabled = isStoreCreationMVPEnabled
29+
self.isProductsOnboardingEnabled = isProductsOnboardingEnabled
2730
}
2831

2932
func isFeatureFlagEnabled(_ featureFlag: FeatureFlag) -> Bool {
@@ -42,6 +45,8 @@ struct MockFeatureFlagService: FeatureFlagService {
4245
return isSimplifiedLoginFlowI1Enabled
4346
case .storeCreationMVP:
4447
return isStoreCreationMVPEnabled
48+
case .productsOnboarding:
49+
return isProductsOnboardingEnabled
4550
default:
4651
return false
4752
}

WooCommerce/WooCommerceTests/ViewRelated/Products/Edit Product/ProductFormViewModelTests.swift

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import XCTest
33
@testable import WooCommerce
44
import Yosemite
55
import TestKit
6+
import protocol Experiments.FeatureFlagService
67

78
final class ProductFormViewModelTests: XCTestCase {
89

@@ -532,19 +533,136 @@ final class ProductFormViewModelTests: XCTestCase {
532533
let hasLinkedProducts = try XCTUnwrap(analyticsProvider.receivedProperties.first?["has_linked_products"] as? Bool)
533534
XCTAssertTrue(hasLinkedProducts)
534535
}
536+
537+
// MARK: Preview button tests (with enabled Product Onboarding feature flag)
538+
539+
func test_no_preview_button_for_new_blank_product_without_any_changes() {
540+
// Given
541+
let product = Product.fake().copy(statusKey: ProductStatus.published.rawValue)
542+
let viewModel = createViewModel(product: product, formType: .add, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
543+
544+
// When
545+
let actionButtons = viewModel.actionButtons
546+
547+
// Then
548+
XCTAssertEqual(actionButtons, [.publish, .more])
549+
}
550+
551+
func test_preview_button_for_new_product_with_pending_changes() {
552+
// Given
553+
let product = Product.fake().copy(statusKey: ProductStatus.published.rawValue)
554+
let viewModel = createViewModel(product: product, formType: .add, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
555+
viewModel.updateName("new name")
556+
557+
// When
558+
let actionButtons = viewModel.actionButtons
559+
560+
// Then
561+
XCTAssertEqual(actionButtons, [.preview, .publish, .more])
562+
}
563+
564+
func test_no_preview_button_for_existing_published_product_without_any_changes() {
565+
// Given
566+
let product = Product.fake().copy(productID: 123, statusKey: ProductStatus.published.rawValue)
567+
let viewModel = createViewModel(product: product, formType: .edit, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
568+
viewModel.updateName("new name")
569+
570+
// When
571+
let actionButtons = viewModel.actionButtons
572+
573+
// Then
574+
XCTAssertEqual(actionButtons, [.save, .more])
575+
}
576+
577+
func test_no_preview_button_for_existing_published_product_with_pending_changes() {
578+
// Given
579+
let product = Product.fake().copy(productID: 123, statusKey: ProductStatus.published.rawValue)
580+
let viewModel = createViewModel(product: product, formType: .edit, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
581+
582+
// When
583+
let actionButtons = viewModel.actionButtons
584+
585+
// Then
586+
XCTAssertEqual(actionButtons, [.more])
587+
}
588+
589+
func test_preview_button_for_existing_draft_product_without_any_changes() {
590+
// Given
591+
let product = Product.fake().copy(productID: 123, statusKey: ProductStatus.draft.rawValue)
592+
let viewModel = createViewModel(product: product, formType: .edit, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
593+
594+
// When
595+
let actionButtons = viewModel.actionButtons
596+
597+
// Then
598+
XCTAssertEqual(actionButtons, [.preview, .publish, .more])
599+
}
600+
601+
func test_preview_button_for_existing_draft_product_with_pending_changes() {
602+
// Given
603+
let product = Product.fake().copy(productID: 123, statusKey: ProductStatus.draft.rawValue)
604+
let viewModel = createViewModel(product: product, formType: .edit, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
605+
viewModel.updateName("new name")
606+
607+
// When
608+
let actionButtons = viewModel.actionButtons
609+
610+
// Then
611+
XCTAssertEqual(actionButtons, [.preview, .save, .more])
612+
}
613+
614+
func test_no_preview_button_for_existing_product_with_other_status_and_without_any_changes() {
615+
// Given
616+
let product = Product.fake().copy(productID: 123, statusKey: "other")
617+
let viewModel = createViewModel(product: product, formType: .edit, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
618+
619+
// When
620+
let actionButtons = viewModel.actionButtons
621+
622+
// Then
623+
XCTAssertEqual(actionButtons, [.publish, .more])
624+
}
625+
626+
func test_no_preview_button_for_existing_product_with_other_status_and_pending_changes() {
627+
// Given
628+
let product = Product.fake().copy(productID: 123, statusKey: "other")
629+
let viewModel = createViewModel(product: product, formType: .edit, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
630+
viewModel.updateName("new name")
631+
632+
// When
633+
let actionButtons = viewModel.actionButtons
634+
635+
// Then
636+
XCTAssertEqual(actionButtons, [.save, .more])
637+
}
638+
639+
func test_no_preview_button_for_any_product_in_read_only_mode() {
640+
// Given
641+
let product = Product.fake().copy(productID: 123, statusKey: ProductStatus.published.rawValue)
642+
let viewModel = createViewModel(product: product, formType: .readonly, featureFlagService: MockFeatureFlagService(isProductsOnboardingEnabled: true))
643+
viewModel.updateName("new name")
644+
645+
// When
646+
let actionButtons = viewModel.actionButtons
647+
648+
// Then
649+
XCTAssertEqual(actionButtons, [.more])
650+
}
535651
}
536652

537653
private extension ProductFormViewModelTests {
538654
func createViewModel(product: Product,
539655
formType: ProductFormType,
540656
stores: StoresManager = ServiceLocator.stores,
541-
analytics: Analytics = ServiceLocator.analytics) -> ProductFormViewModel {
657+
analytics: Analytics = ServiceLocator.analytics,
658+
featureFlagService: FeatureFlagService = MockFeatureFlagService()) -> ProductFormViewModel {
542659
let model = EditableProductModel(product: product)
543660
let productImageActionHandler = ProductImageActionHandler(siteID: 0, product: model)
544661
return ProductFormViewModel(product: model,
545662
formType: formType,
546663
productImageActionHandler: productImageActionHandler,
547664
stores: stores,
548-
analytics: analytics)
665+
analytics: analytics,
666+
featureFlagService: featureFlagService)
549667
}
550668
}

0 commit comments

Comments
 (0)