diff --git a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift index 243972ee29e..431e4d77aea 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewController.swift @@ -626,6 +626,21 @@ private extension ProductVariationsViewController { let notice = Notice(title: error.errorTitle, message: error.errorDescription) noticePresenter.enqueue(notice: notice) } + + /// Asks the merchant for confirmation before generating all variations. + /// + private func presentGenerationConfirmation(numberOfVariations: Int, onCompletion: @escaping (_ confirmed: Bool) -> Void) { + let controller = UIAlertController(title: Localization.confirmationTitle, + message: Localization.confirmationDescription(variationCount: numberOfVariations), + preferredStyle: .alert) + controller.addDefaultActionWithTitle(Localization.ok) { _ in + onCompletion(true) + } + controller.addCancelActionWithTitle(Localization.cancel) { _ in + onCompletion(false) + } + present(controller, animated: true) + } } // MARK: - Placeholders @@ -702,18 +717,25 @@ extension ProductVariationsViewController: SyncingCoordinatorDelegate { /// Generates all possible variations for the product attributes. /// private func generateAllVariations() { - viewModel.generateAllVariations(for: product) { [weak self] result in - guard let self else { return } - switch result { - case .success: + viewModel.generateAllVariations(for: product) { [weak self] currentState in + switch currentState { + case .fetching: + break // TODO: Show fetching loading Indicator + case .confirmation(let variationCount, let onCompletion): + self?.presentGenerationConfirmation(numberOfVariations: variationCount, onCompletion: onCompletion) + case .creating: + break // TODO: Show creating loading Indicator + case .canceled: + break // TODO: Remove loading indicator + case .finished(let variationsCreated): + // TODO: Remove loading indicator + // TODO: Inform about created variations break - case .failure(let error): - self.presentGenerationError(error) + case .error(let error): + // TODO: Remove loading indicator + self?.presentGenerationError(error) } } - // TODO: - // - Show Loading Indicator - // - Hide Loading Indicator } } @@ -820,6 +842,17 @@ private extension ProductVariationsViewController { static let generateVariationError = NSLocalizedString("The variation couldn't be generated.", comment: "Error title when failing to generate a variation.") static let variationCreated = NSLocalizedString("Variation created", comment: "Text for the notice after creating the first variation.") + + static let confirmationTitle = NSLocalizedString("Generate all variations?", + comment: "Alert title to allow the user confirm if they want to generate all variations") + static func confirmationDescription(variationCount: Int) -> String { + let format = NSLocalizedString("This will create a variation for each and every possible combination of variation attributes (%d variations).", + comment: "Alert description to allow the user confirm if they want to generate all variations") + return String.localizedStringWithFormat(format, variationCount) + } + static let ok = NSLocalizedString("OK", comment: "Button text to confirm that we want to generate all variations") + static let cancel = NSLocalizedString("Cancel", comment: "Button text to confirm that we don't want to generate all variations") + } /// Localizated strings for the action sheet options diff --git a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift index 639bb4847b4..7e12d10bb89 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Variations/ProductVariationsViewModel.swift @@ -26,27 +26,49 @@ final class ProductVariationsViewModel { } /// Generates all missing variations for a product. Up to 100 variations. + /// Parameters: + /// - `Product`: Product on which we will be creating the variations + /// - `onStateChanged`: Closure invoked every time there is a significant state change in the generation process. /// - func generateAllVariations(for product: Product, onCompletion: @escaping (Result) -> Void) { + func generateAllVariations(for product: Product, onStateChanged: @escaping (GenerationState) -> Void) { + // Fetch Previous variations + onStateChanged(.fetching) fetchAllVariations(of: product) { [weak self] result in - guard let self else { return } switch result { case .success(let existingVariations): + // Generate variations locally let variationsToGenerate = ProductVariationGenerator.generateVariations(for: product, excluding: existingVariations) // Guard for 100 variation limit guard variationsToGenerate.count <= 100 else { - return onCompletion(.failure(.tooManyVariations(variationCount: variationsToGenerate.count))) + return onStateChanged(.error(.tooManyVariations(variationCount: variationsToGenerate.count))) } + // Guard for no variations to generate guard variationsToGenerate.count > 0 else { - // TODO: Inform user that no variation will be created - return onCompletion(.success(())) + return onStateChanged(.finished(false)) } - self.createVariationsRemotely(for: product, variations: variationsToGenerate, onCompletion: onCompletion) + // Confirm generation with merchant + onStateChanged(.confirmation(variationsToGenerate.count, { confirmed in + + guard confirmed else { + return onStateChanged(.canceled) + } + + // Create variations remotely + onStateChanged(.creating) + self?.createVariationsRemotely(for: product, variations: variationsToGenerate) { result in + switch result { + case .success: + onStateChanged(.finished(true)) + case .failure(let error): + onStateChanged(.error(error)) + } + } + })) case .failure: // TODO: Log and inform error @@ -106,7 +128,36 @@ extension ProductVariationsViewModel { } } +// MARK: Definitions for Generate All Variations extension ProductVariationsViewModel { + /// Type that represents the possible states while all variations are being created. + /// + enum GenerationState { + /// State while previous variations are being fetched + /// + case fetching + + /// State to allow merchant to confirm the variation generation + /// + case confirmation(_ numberOfVariations: Int, _ onCompletion: (_ confirmed: Bool) -> Void) + + /// State while the variations are being created remotely + /// + case creating + + ///State when the merchant decides to not continue with the generation process. + /// + case canceled + + /// State when the the process is finished. `variationsCreated` indicates if variations were created or not. + /// + case finished(_ variationsCreated: Bool) + + /// Error state in any part of the process. + /// + case error(GenerationError) + } + /// Type to represent known generation errors /// enum GenerationError: LocalizedError, Equatable { diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Products/Variations/ProductVariationsViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Products/Variations/ProductVariationsViewModelTests.swift index f74fabf00d6..282cdb01baa 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Products/Variations/ProductVariationsViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Products/Variations/ProductVariationsViewModelTests.swift @@ -137,8 +137,8 @@ final class ProductVariationsViewModelTests: XCTestCase { // When let error = waitFor { promise in - viewModel.generateAllVariations(for: product) { result in - if case let .failure(error) = result { + viewModel.generateAllVariations(for: product) { state in + if case let .error(error) = state { promise(error) } } @@ -148,7 +148,7 @@ final class ProductVariationsViewModelTests: XCTestCase { XCTAssertEqual(error, .tooManyVariations(variationCount: 125)) } - func test_generating_less_than_100_variations_invokes_create_action() { + func test_generating_less_than_100_variations_ask_for_confirmation_and_creates_variations() { // Given let product = Product.fake().copy(attributes: [ ProductAttribute.fake().copy(attributeID: 1, name: "Size", options: ["XS", "S", "M", "L", "XL"]), @@ -171,8 +171,11 @@ final class ProductVariationsViewModelTests: XCTestCase { // When let succeeded = waitFor { promise in - viewModel.generateAllVariations(for: product) { result in - if case .success = result { + viewModel.generateAllVariations(for: product) { state in + if case let .confirmation(_, onCompletion) = state { + onCompletion(true) + } + if case .finished = state { promise(true) } } @@ -181,4 +184,41 @@ final class ProductVariationsViewModelTests: XCTestCase { // Then XCTAssertTrue(succeeded) } + + func test_generating_less_than_100_variations_ask_for_confirmation_and_sends_cancel_state() { + // Given + let product = Product.fake().copy(attributes: [ + ProductAttribute.fake().copy(attributeID: 1, name: "Size", options: ["XS", "S", "M", "L", "XL"]), + ProductAttribute.fake().copy(attributeID: 2, name: "Color", options: ["Red", "Green", "Blue", "White", "Black"]), + ]) + + let stores = MockStoresManager(sessionManager: SessionManager.makeForTesting()) + stores.whenReceivingAction(ofType: ProductVariationAction.self) { action in + switch action { + case .synchronizeAllProductVariations(_, _, let onCompletion): + onCompletion(.success([])) + case .createProductVariations(_, _, _, let onCompletion): + onCompletion(.success([])) + default: + break + } + } + + let viewModel = ProductVariationsViewModel(stores: stores, formType: .edit) + + // When + let canceled = waitFor { promise in + viewModel.generateAllVariations(for: product) { state in + if case let .confirmation(_, onCompletion) = state { + onCompletion(false) + } + if case .canceled = state { + promise(true) + } + } + } + + // Then + XCTAssertTrue(canceled) + } }