Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice wrapper!

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, GenerationError>) -> 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is hard to read and understand, but I don't have easy improvement suggestions 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, not a fan either. will keep it in the back of my head for improvements!


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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -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"]),
Expand All @@ -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)
}
}
Expand All @@ -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)
}
}