diff --git a/Modules/Sources/Networking/Remote/MerchantGenerativeContentRemote.swift b/Modules/Sources/Networking/Remote/MerchantGenerativeContentRemote.swift new file mode 100644 index 00000000000..dc66b9e463e --- /dev/null +++ b/Modules/Sources/Networking/Remote/MerchantGenerativeContentRemote.swift @@ -0,0 +1,201 @@ +import Foundation +import NetworkingCore + +/// Generative content remote that uses merchant's API key for OpenAI instead of Jetpack AI +/// +public final class MerchantGenerativeContentRemote: GenerativeContentRemoteProtocol { + private let apiKey: String + + public init(apiKey: String) { + self.apiKey = apiKey + } + + public func generateText(siteID: Int64, + base: String, + feature: GenerativeContentRemoteFeature, + responseFormat: GenerativeContentRemoteResponseFormat) async throws -> String { + guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else { + throw NSError(domain: "Invalid URL", code: 0) + } + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let parameters: [String: Any] = [ + "model": "gpt-4", + "messages": [ + [ + "role": "user", + "content": base + ] + ], + "max_tokens": 1000, + "temperature": 0.7 + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: parameters) + + let (data, _) = try await URLSession.shared.data(for: request) + + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let choices = json["choices"] as? [[String: Any]], + let firstChoice = choices.first, + let message = firstChoice["message"] as? [String: Any], + let content = message["content"] as? String else { + throw MerchantGenerativeContentRemoteError.invalidResponse + } + + return content.trimmingCharacters(in: .whitespacesAndNewlines) + } + + public func identifyLanguage(siteID: Int64, + string: String, + feature: GenerativeContentRemoteFeature) async throws -> String { + let prompt = String(format: AIRequestPrompts.identifyLanguage, string) + return try await generateText(siteID: siteID, base: prompt, feature: feature, responseFormat: .text) + } + + public func generateAIProduct(siteID: Int64, + productName: String?, + keywords: String, + language: String, + tone: String, + currencySymbol: String, + dimensionUnit: String?, + weightUnit: String?, + categories: [ProductCategory], + tags: [ProductTag]) async throws -> AIProduct { + + // Build input components + var inputComponents = [String(format: AIRequestPrompts.inputComponents, keywords, tone)] + + // Name will be added only if `productName` is available + if let productName = productName, !productName.isEmpty { + inputComponents.insert(String(format: AIRequestPrompts.productNameTemplate, productName), at: 1) + } + + let input = inputComponents.joined(separator: "\n") + + // Build JSON response format dictionary + let jsonResponseFormatDict: [String: Any] = { + let tagsPrompt: String = { + guard !tags.isEmpty else { + return AIRequestPrompts.defaultTagsPrompt + } + return String(format: AIRequestPrompts.existingTagsPrompt, tags.map { $0.name }.joined(separator: ", ")) + }() + + let categoriesPrompt: String = { + guard !categories.isEmpty else { + return AIRequestPrompts.defaultCategoriesPrompt + } + return String(format: AIRequestPrompts.existingCategoriesPrompt, categories.map { $0.name }.joined(separator: ", ")) + }() + + let shippingPrompt = { + var dict = [String: String]() + if let weightUnit { + dict["weight"] = String(format: AIRequestPrompts.weightPrompt, weightUnit) + } + + if let dimensionUnit { + dict["length"] = String(format: AIRequestPrompts.lengthPrompt, dimensionUnit) + dict["width"] = String(format: AIRequestPrompts.widthPrompt, dimensionUnit) + dict["height"] = String(format: AIRequestPrompts.heightPrompt, dimensionUnit) + } + return dict + }() + + return ["names": String(format: AIRequestPrompts.namesFormat, language), + "descriptions": String(format: AIRequestPrompts.descriptionsFormat, tone, language), + "short_descriptions": String(format: AIRequestPrompts.shortDescriptionsFormat, tone, language), + "virtual": AIRequestPrompts.virtualFormat, + "shipping": shippingPrompt, + "price": String(format: AIRequestPrompts.priceFormat, currencySymbol), + "tags": tagsPrompt, + "categories": categoriesPrompt] + }() + + let expectedJsonFormat = String(format: AIRequestPrompts.jsonFormatInstructions, + jsonResponseFormatDict.toJSONEncoded() ?? "") + + let prompt = input + "\n" + expectedJsonFormat + + // Make OpenAI API request + guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else { + throw MerchantGenerativeContentRemoteError.invalidResponse + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + let parameters: [String: Any] = [ + "model": "gpt-4", + "messages": [ + [ + "role": "user", + "content": prompt + ] + ], + // gpt-4 does not support response_format parameter with json_object type, which does work with gpt-3 and the Jetpack tunnel + // This may change further based on the selected model, and the AI provider, so has to be handled here. + // ie: + // "response_format": ["type": "json_object"], + "max_tokens": 4000, + "temperature": 0.7 + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: parameters) + + let (data, _) = try await URLSession.shared.data(for: request) + + // Parse OpenAI response + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let choices = json["choices"] as? [[String: Any]], + let firstChoice = choices.first, + let message = firstChoice["message"] as? [String: Any], + let content = message["content"] as? String else { + throw MerchantGenerativeContentRemoteError.invalidResponse + } + + // Parse the AI-generated product JSON + guard let contentData = content.data(using: .utf8), + let productJson = try JSONSerialization.jsonObject(with: contentData) as? [String: Any] else { + throw MerchantGenerativeContentRemoteError.invalidResponse + } + + // Extract and create AIProduct manually + let names = productJson["names"] as? [String] ?? [] + let descriptions = productJson["descriptions"] as? [String] ?? [] + let shortDescriptions = productJson["short_descriptions"] as? [String] ?? [] + let virtual = productJson["virtual"] as? Bool ?? false + let price = productJson["price"] as? String ?? "" + let aiTags = productJson["tags"] as? [String] ?? [] + let aiCategories = productJson["categories"] as? [String] ?? [] + + // Extract shipping info + let shippingDict = productJson["shipping"] as? [String: Any] ?? [:] + let length = shippingDict["length"] as? String ?? "" + let weight = shippingDict["weight"] as? String ?? "" + let width = shippingDict["width"] as? String ?? "" + let height = shippingDict["height"] as? String ?? "" + + let shipping = AIProduct.Shipping(length: length, weight: weight, width: width, height: height) + + return AIProduct(names: names, + descriptions: descriptions, + shortDescriptions: shortDescriptions, + virtual: virtual, + shipping: shipping, + tags: aiTags, + price: price, + categories: aiCategories) + } +} + +private enum MerchantGenerativeContentRemoteError: Error { + case invalidResponse +} diff --git a/Modules/Sources/Yosemite/Actions/ProductAction.swift b/Modules/Sources/Yosemite/Actions/ProductAction.swift index e6b1c9b276f..0bbc4e96267 100644 --- a/Modules/Sources/Yosemite/Actions/ProductAction.swift +++ b/Modules/Sources/Yosemite/Actions/ProductAction.swift @@ -1,6 +1,11 @@ import Foundation import Networking +public enum AISource { + case jetpack + case merchant +} + public enum ItemIdentifierSearchResult { case product(Product) case variation(ProductVariation) @@ -130,6 +135,7 @@ public enum ProductAction: Action { /// case identifyLanguage(siteID: Int64, string: String, + aiSource: AISource, feature: GenerativeContentRemoteFeature, completion: (Result) -> Void) @@ -194,6 +200,7 @@ public enum ProductAction: Action { weightUnit: String?, categories: [ProductCategory], tags: [ProductTag], + AISource: AISource, completion: (Result) -> Void) /// Fetches stock based on the given status for a site diff --git a/Modules/Sources/Yosemite/Stores/ProductStore.swift b/Modules/Sources/Yosemite/Stores/ProductStore.swift index 1a73dba5a2e..bbd9715ff43 100644 --- a/Modules/Sources/Yosemite/Stores/ProductStore.swift +++ b/Modules/Sources/Yosemite/Stores/ProductStore.swift @@ -122,9 +122,11 @@ public class ProductStore: Store { replaceProductLocally(product: product, onCompletion: onCompletion) case let .checkIfStoreHasProducts(siteID, status, onCompletion): checkIfStoreHasProducts(siteID: siteID, status: status, onCompletion: onCompletion) - case let .identifyLanguage(siteID, string, feature, completion): + case let .identifyLanguage(siteID, string, aiSource, feature, completion): identifyLanguage(siteID: siteID, - string: string, feature: feature, + string: string, + aisource: aiSource, + feature: feature, completion: completion) case let .generateProductDescription(siteID, name, features, language, completion): generateProductDescription(siteID: siteID, name: name, features: features, language: language, completion: completion) @@ -146,6 +148,7 @@ public class ProductStore: Store { weightUnit, categories, tags, + aiSource, completion): generateAIProduct(siteID: siteID, productName: productName, @@ -157,6 +160,7 @@ public class ProductStore: Store { weightUnit: weightUnit, categories: categories, tags: tags, + aiSource: aiSource, completion: completion) case let .fetchStockReport(siteID, stockType, pageNumber, pageSize, order, completion): fetchStockReport(siteID: siteID, @@ -586,15 +590,28 @@ private extension ProductStore { func identifyLanguage(siteID: Int64, string: String, + aisource: AISource, feature: GenerativeContentRemoteFeature, completion: @escaping (Result) -> Void) { Task { @MainActor in - let result = await Result { - try await generativeContentRemote.identifyLanguage(siteID: siteID, - string: string, - feature: feature) + switch aisource { + case .jetpack: + let result = await Result { + try await generativeContentRemote.identifyLanguage(siteID: siteID, + string: string, + feature: feature) + } + completion(result) + case .merchant: + let result = await Result { + // Temporary. This will come from the KeyChain rather than the environment + let key = ProcessInfo.processInfo.environment["openai-debug-api-key"] ?? "api key not found" + return try await MerchantGenerativeContentRemote(apiKey: key).identifyLanguage(siteID: siteID, + string: string, + feature: feature) + } + completion(result) } - completion(result) } } @@ -730,22 +747,41 @@ private extension ProductStore { weightUnit: String?, categories: [ProductCategory], tags: [ProductTag], + aiSource: AISource, completion: @escaping (Result) -> Void) { Task { @MainActor in - let result = await Result { - let product = try await generativeContentRemote.generateAIProduct(siteID: siteID, - productName: productName, - keywords: keywords, - language: language, - tone: tone, - currencySymbol: currencySymbol, - dimensionUnit: dimensionUnit, - weightUnit: weightUnit, - categories: categories, - tags: tags) - return product + switch aiSource { + case .jetpack: + let result = await Result { + let product = try await generativeContentRemote.generateAIProduct(siteID: siteID, + productName: productName, + keywords: keywords, + language: language, + tone: tone, + currencySymbol: currencySymbol, + dimensionUnit: dimensionUnit, + weightUnit: weightUnit, + categories: categories, + tags: tags) + return product + } + completion(result) + case .merchant: + let result = await Result { + let key = ProcessInfo.processInfo.environment["openai-debug-api-key"] ?? "api key not found" + return try await MerchantGenerativeContentRemote(apiKey: key).generateAIProduct(siteID: siteID, + productName: productName, + keywords: keywords, + language: language, + tone: tone, + currencySymbol: currencySymbol, + dimensionUnit: dimensionUnit, + weightUnit: weightUnit, + categories: categories, + tags: tags) + } + completion(result) } - completion(result) } } diff --git a/Modules/Tests/YosemiteTests/Stores/ProductStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/ProductStoreTests.swift index f3d6c466dd0..dde6d0da4fc 100644 --- a/Modules/Tests/YosemiteTests/Stores/ProductStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/ProductStoreTests.swift @@ -2226,6 +2226,7 @@ final class ProductStoreTests: XCTestCase { let result = waitFor { promise in productStore.onAction(ProductAction.identifyLanguage(siteID: self.sampleSiteID, string: "Woo is awesome", + aiSource: .jetpack, feature: .productSharing) { result in promise(result) }) @@ -2251,6 +2252,7 @@ final class ProductStoreTests: XCTestCase { let result = waitFor { promise in productStore.onAction(ProductAction.identifyLanguage(siteID: self.sampleSiteID, string: "Woo is awesome", + aiSource: .jetpack, feature: .productSharing) { result in promise(result) }) @@ -2942,7 +2944,8 @@ final class ProductStoreTests: XCTestCase { dimensionUnit: "cm", weightUnit: "kg", categories: [ProductCategory.fake(), ProductCategory.fake()], - tags: [ProductTag.fake(), ProductTag.fake()]) { result in + tags: [ProductTag.fake(), ProductTag.fake()], + AISource: .jetpack) { result in promise(result) }) } @@ -2973,7 +2976,8 @@ final class ProductStoreTests: XCTestCase { dimensionUnit: "cm", weightUnit: "kg", categories: [ProductCategory.fake(), ProductCategory.fake()], - tags: [ProductTag.fake(), ProductTag.fake()]) { result in + tags: [ProductTag.fake(), ProductTag.fake()], + AISource: .jetpack) { result in promise(result) }) } diff --git a/WooCommerce/Classes/ViewRelated/Products/AI/ProductDescriptionGenerationViewModel.swift b/WooCommerce/Classes/ViewRelated/Products/AI/ProductDescriptionGenerationViewModel.swift index 29621658f01..fa1c840eff1 100644 --- a/WooCommerce/Classes/ViewRelated/Products/AI/ProductDescriptionGenerationViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Products/AI/ProductDescriptionGenerationViewModel.swift @@ -50,6 +50,10 @@ final class ProductDescriptionGenerationViewModel: ObservableObject { /// private var languageIdentifiedUsingAI: String? + private var aiSource: AISource { + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) ? .merchant : .jetpack + } + init(siteID: Int64, name: String, description: String, @@ -139,6 +143,7 @@ private extension ProductDescriptionGenerationViewModel { let language = try await withCheckedThrowingContinuation { continuation in stores.dispatch(ProductAction.identifyLanguage(siteID: siteID, string: name + " " + features, + aiSource: aiSource, feature: .productDescription, completion: { result in continuation.resume(with: result) diff --git a/WooCommerce/Classes/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModel.swift b/WooCommerce/Classes/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModel.swift index b3f4b25e969..bfa08452cb5 100644 --- a/WooCommerce/Classes/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModel.swift @@ -52,6 +52,10 @@ final class ProductSharingMessageGenerationViewModel: ObservableObject { /// private var languageIdentifiedUsingAI: String? + private var aiSource: AISource { + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) ? .merchant : .jetpack + } + init(siteID: Int64, url: String, productName: String, @@ -152,6 +156,7 @@ private extension ProductSharingMessageGenerationViewModel { let language = try await withCheckedThrowingContinuation { continuation in stores.dispatch(ProductAction.identifyLanguage(siteID: siteID, string: productName + " " + productDescription, + aiSource: aiSource, feature: .productSharing, completion: { result in continuation.resume(with: result) diff --git a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/EntryPoint/AddProductWithAIActionSheet.swift b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/EntryPoint/AddProductWithAIActionSheet.swift index 1747659f574..38720c40401 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/EntryPoint/AddProductWithAIActionSheet.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/EntryPoint/AddProductWithAIActionSheet.swift @@ -1,4 +1,5 @@ import SwiftUI +import enum Yosemite.AISource /// Hosting controller for `AddProductWithAIActionSheet`. /// @@ -39,6 +40,10 @@ struct AddProductWithAIActionSheet: View { self.onProductTypeOption = onProductTypeOption } + private var aiSource: AISource { + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) ? .merchant : .jetpack + } + var body: some View { ScrollView { VStack(alignment: .leading) { @@ -70,8 +75,16 @@ struct AddProductWithAIActionSheet: View { VStack(alignment: .leading, spacing: Constants.verticalSpacing) { Text(Localization.CreateProductWithAI.aiTitle) .bodyStyle() - Text(Localization.CreateProductWithAI.aiDescription) - .subheadlineStyle() + + switch aiSource { + case .merchant: + Text(Localization.CreateProductWithAI.merchantAIDescription) + .subheadlineStyle() + case .jetpack: + Text(Localization.CreateProductWithAI.aiDescription) + .subheadlineStyle() + } + AdaptiveStack(horizontalAlignment: .leading) { Text(Localization.CreateProductWithAI.legalText) Text(.init(Localization.CreateProductWithAI.learnMore)) @@ -161,6 +174,11 @@ private extension AddProductWithAIActionSheet { value: "Let us generate product details for you", comment: "Description of the option to add new product with AI assistance" ) + static let merchantAIDescription = NSLocalizedString( + "addProductWithAIActionSheet.createProductWithAI.merchantAiDescription", + value: "Generate product details using AI. Enter your API key under Settings > AI Settings", + comment: "Description of the option to add new product with AI assistance" + ) static let legalText = NSLocalizedString( "addProductWithAIActionSheet.createProductWithAI.legalText", value: "Powered by AI.", diff --git a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/Preview/ProductDetailPreviewViewModel.swift b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/Preview/ProductDetailPreviewViewModel.swift index 0a592fde6ee..b23090a5123 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/Preview/ProductDetailPreviewViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/Preview/ProductDetailPreviewViewModel.swift @@ -153,6 +153,10 @@ final class ProductDetailPreviewViewModel: ObservableObject { return productImageUploader.actionHandler(key: key, originalStatuses: []) }() + private var aiSource: AISource { + ServiceLocator.featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) ? .merchant : .jetpack + } + init(siteID: Int64, productFeatures: String, imageState: ImageState, @@ -529,6 +533,7 @@ private extension ProductDetailPreviewViewModel { let language = try await withCheckedThrowingContinuation { continuation in stores.dispatch(ProductAction.identifyLanguage(siteID: siteID, string: productInfo, + aiSource: aiSource, feature: .productCreation, completion: { result in continuation.resume(with: result) @@ -549,6 +554,7 @@ private extension ProductDetailPreviewViewModel { return try await generateAIProduct(language: language, tone: tone, + aiSource: aiSource, existingCategories: existingCategories, existingTags: existingTags) } @@ -556,6 +562,7 @@ private extension ProductDetailPreviewViewModel { @MainActor func generateAIProduct(language: String, tone: AIToneVoice, + aiSource: AISource, existingCategories: [ProductCategory], existingTags: [ProductTag]) async throws -> AIProduct { try await withCheckedThrowingContinuation { continuation in @@ -569,6 +576,7 @@ private extension ProductDetailPreviewViewModel { weightUnit: weightUnit, categories: existingCategories, tags: existingTags, + AISource: aiSource, completion: { result in continuation.resume(with: result) })) diff --git a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/ProductCreationAIEligibilityChecker.swift b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/ProductCreationAIEligibilityChecker.swift index 50c3335d130..b7bbefc31ba 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/ProductCreationAIEligibilityChecker.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Add Product/AddProductWithAI/ProductCreationAIEligibilityChecker.swift @@ -20,6 +20,11 @@ final class ProductCreationAIEligibilityChecker: ProductCreationAIEligibilityChe return false } - return site.isWordPressComStore || site.isAIAssistantFeatureActive + if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.allowMerchantAIAPIKey) { + // Temporary: Always allow for AI usage if the flag is enabled, bypassing eligibility criteria + return true + } else { + return site.isWordPressComStore || site.isAIAssistantFeatureActive + } } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Products/AI/ProductDescriptionGenerationViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Products/AI/ProductDescriptionGenerationViewModelTests.swift index 423c480d295..4a2141c6b86 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Products/AI/ProductDescriptionGenerationViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Products/AI/ProductDescriptionGenerationViewModelTests.swift @@ -211,7 +211,7 @@ final class ProductDescriptionGenerationViewModelTests: XCTestCase { switch action { case let .generateProductDescription(_, _, _, _, completion): completion(.success("Must buy")) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) identifyLanguageRequestCounter += 1 default: @@ -253,7 +253,7 @@ final class ProductDescriptionGenerationViewModelTests: XCTestCase { switch action { case let .generateProductDescription(_, _, _, _, completion): completion(.success("Must buy")) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) identifyLanguageRequestCounter += 1 default: @@ -450,7 +450,7 @@ private extension ProductDescriptionGenerationViewModelTests { switch action { case let .generateProductDescription(_, _, _, _, completion): completion(generatedDescription) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(identifyLaunguage) default: return XCTFail("Unexpected action: \(action)") diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModelTests.swift index 9080dc57e7c..712bbfcec1a 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Products/AI/ProductSharingMessageGenerationViewModelTests.swift @@ -47,7 +47,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { case let .generateProductSharingMessage(_, _, _, _, _, completion): XCTAssertTrue(viewModel.generationInProgress) completion(.success("Check this out!")) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) default: return @@ -74,7 +74,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.success(expectedString)) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) default: return @@ -101,7 +101,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.failure(NSError(domain: "Test", code: 500))) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) default: return @@ -126,7 +126,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { stores: stores) stores.whenReceivingAction(ofType: ProductAction.self) { action in switch action { - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.failure(NSError(domain: "Test", code: 500))) default: return @@ -156,7 +156,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.success("Test")) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success(expectedLanguage)) default: return @@ -209,7 +209,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.failure(NSError(domain: "Test", code: 500))) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) default: return @@ -242,7 +242,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.success("Test")) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.failure(NSError(domain: "Test", code: 500))) default: return @@ -295,7 +295,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.success(expectedString)) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) default: return @@ -394,7 +394,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.success(expectedString)) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) default: return @@ -424,7 +424,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.success(expectedString)) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) default: return @@ -459,7 +459,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.success("Must buy")) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) identifyLanguageRequestCounter += 1 default: @@ -497,7 +497,7 @@ final class ProductSharingMessageGenerationViewModelTests: XCTestCase { switch action { case let .generateProductSharingMessage(_, _, _, _, _, completion): completion(.success("Must buy")) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success("en")) identifyLanguageRequestCounter += 1 default: diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductWithAI/ProductDetailPreviewViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductWithAI/ProductDetailPreviewViewModelTests.swift index 55995927fbb..fdf1c563570 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductWithAI/ProductDetailPreviewViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Products/Add Product/AddProductWithAI/ProductDetailPreviewViewModelTests.swift @@ -184,9 +184,9 @@ final class ProductDetailPreviewViewModelTests: XCTestCase { stores.whenReceivingAction(ofType: ProductAction.self) { action in switch action { - case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, completion): + case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, _, completion): completion(.success(.fake())) - case let .identifyLanguage(_, string, _, completion): + case let .identifyLanguage(_, string, _, _, completion): // Then XCTAssertEqual(string, productFeatures) completion(.success("en")) @@ -219,10 +219,10 @@ final class ProductDetailPreviewViewModelTests: XCTestCase { stores.whenReceivingAction(ofType: ProductAction.self) { action in switch action { - case let .generateAIProduct(_, _, _, language, _, _, _, _, _, _, completion): + case let .generateAIProduct(_, _, _, language, _, _, _, _, _, _, _, completion): XCTAssertEqual(language, expectedLanguage) completion(.success(.fake())) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): identifyingLanguageRequestCount += 1 completion(.success(expectedLanguage)) default: @@ -364,6 +364,7 @@ final class ProductDetailPreviewViewModelTests: XCTestCase { weightUnit, categories, tags, + _, completion): // Then XCTAssertEqual(siteID, sampleSiteID) @@ -376,7 +377,7 @@ final class ProductDetailPreviewViewModelTests: XCTestCase { XCTAssertEqual(categories, sampleCategories) XCTAssertEqual(tags, sampleTags) completion(.success(.fake())) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success(sampleLanguage)) default: break @@ -405,10 +406,10 @@ final class ProductDetailPreviewViewModelTests: XCTestCase { // When stores.whenReceivingAction(ofType: ProductAction.self) { action in switch action { - case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, completion): + case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, _, completion): XCTAssertTrue(viewModel.isGeneratingDetails) completion(.success(self.sampleAIProduct)) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): XCTAssertTrue(viewModel.isGeneratingDetails) completion(.success("en")) default: @@ -444,10 +445,10 @@ final class ProductDetailPreviewViewModelTests: XCTestCase { // When stores.whenReceivingAction(ofType: ProductAction.self) { action in switch action { - case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, completion): + case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, _, completion): XCTAssertEqual(viewModel.errorState, .none) completion(.failure(expectedError)) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): XCTAssertEqual(viewModel.errorState, .none) completion(.success("en")) default: @@ -1080,10 +1081,10 @@ final class ProductDetailPreviewViewModelTests: XCTestCase { // When stores.whenReceivingAction(ofType: ProductAction.self) { action in switch action { - case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, completion): + case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, _, completion): XCTAssertFalse(viewModel.isSavingProduct) completion(.success(aiProduct)) - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): XCTAssertFalse(viewModel.isSavingProduct) completion(.success("en")) case let .addProduct(_, onCompletion): @@ -1457,13 +1458,13 @@ private extension ProductDetailPreviewViewModelTests { addedProductResult: (Result)? = nil) { stores.whenReceivingAction(ofType: ProductAction.self) { action in switch action { - case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, completion): + case let .generateAIProduct(_, _, _, _, _, _, _, _, _, _, _, completion): if let aiGeneratedProductResult { completion(aiGeneratedProductResult) } else { completion(.success(self.sampleAIProduct)) } - case let .identifyLanguage(_, _, _, completion): + case let .identifyLanguage(_, _, _, _, completion): completion(.success(identifiedLanguage)) case let .addProduct(product, onCompletion): if let addedProductResult {