diff --git a/Modules/Sources/Networking/Model/Product/Product.swift b/Modules/Sources/Networking/Model/Product/Product.swift index 6f8415ca07f..c459d11e0eb 100644 --- a/Modules/Sources/Networking/Model/Product/Product.swift +++ b/Modules/Sources/Networking/Model/Product/Product.swift @@ -795,13 +795,6 @@ public struct Product: Codable, GeneratedCopiable, Equatable, GeneratedFakeable } -public extension Product { - /// Default product URL {site_url}?post_type=product&p={product_id} works for all sites. - func alternativePermalink(with siteURL: String) -> String { - String(format: "%@?post_type=product&p=%d", siteURL, productID) - } -} - /// Defines all of the Product CodingKeys /// private extension Product { diff --git a/Modules/Tests/NetworkingTests/Mapper/ProductMapperTests.swift b/Modules/Tests/NetworkingTests/Mapper/ProductMapperTests.swift index 0cde8fda0ba..69cd5518d81 100644 --- a/Modules/Tests/NetworkingTests/Mapper/ProductMapperTests.swift +++ b/Modules/Tests/NetworkingTests/Mapper/ProductMapperTests.swift @@ -29,8 +29,6 @@ final class ProductMapperTests: XCTestCase { XCTAssertEqual(product.name, "Book the Green Room") XCTAssertEqual(product.slug, "book-the-green-room") XCTAssertEqual(product.permalink, "https://example.com/product/book-the-green-room/") - XCTAssertEqual(product.alternativePermalink(with: "https://example.com"), - "https://example.com?post_type=product&p=\(dummyProductID)") let dateCreated = DateFormatter.Defaults.dateTimeFormatter.date(from: "2019-02-19T17:33:31") let dateModified = DateFormatter.Defaults.dateTimeFormatter.date(from: "2019-02-19T17:48:01") diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 7b04887e003..ab6882a2c86 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -7,6 +7,7 @@ - [*] Fix initialization of authenticator to avoid crashes during login [https://github.com/woocommerce/woocommerce-ios/pull/15953] - [*] Shipping Labels: Made HS tariff number field required in customs form for EU destinations [https://github.com/woocommerce/woocommerce-ios/pull/15946] - [*] Order Details: Attempt to improve performance by using a simplified version of product objects. [https://github.com/woocommerce/woocommerce-ios/pull/15959] +- [*] Use simple product objects in Shipping Labels and Blaze flows. [https://github.com/woocommerce/woocommerce-ios/pull/15965] - [*] Product List: Load list with simplified product objects to improve performance. [https://teamkiwip2.wordpress.com/2025/08/01/hack-week-improving-performance-when-loading-cached-products/] - [*] Order Details > Edit Shipping/Billing Address: Added map-based address lookup support for iOS 17+. [https://github.com/woocommerce/woocommerce-ios/pull/15964] - [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960] diff --git a/WooCommerce/Classes/Model/ShippingLabelPackageItem.swift b/WooCommerce/Classes/Model/ShippingLabelPackageItem.swift index 38276db26af..ba36666cafa 100644 --- a/WooCommerce/Classes/Model/ShippingLabelPackageItem.swift +++ b/WooCommerce/Classes/Model/ShippingLabelPackageItem.swift @@ -50,7 +50,7 @@ extension ShippingLabelPackageItem { self.imageURL = copy.imageURL } - init?(orderItem: OrderItem, products: [Product], productVariations: [ProductVariation]) { + init?(orderItem: OrderItem, products: [ShippingLabelProduct], productVariations: [ProductVariation]) { self.name = orderItem.name self.orderItemID = orderItem.itemID self.quantity = orderItem.quantity diff --git a/WooCommerce/Classes/Model/ShippingLabelProduct.swift b/WooCommerce/Classes/Model/ShippingLabelProduct.swift new file mode 100644 index 00000000000..562256480f6 --- /dev/null +++ b/WooCommerce/Classes/Model/ShippingLabelProduct.swift @@ -0,0 +1,27 @@ +import Foundation +import Yosemite + +/// Represents a Product Entity with basic details to display in the product section of shipping label creation form. +/// +struct ShippingLabelProduct: Equatable { + let productID: Int64 + let virtual: Bool + let weight: String? + let dimensions: ProductDimensions + + let imageURL: URL? + + init(storageProduct: StorageProduct) { + self.productID = storageProduct.productID + self.virtual = storageProduct.virtual + self.weight = storageProduct.weight + self.dimensions = { + guard let dimensions = storageProduct.dimensions else { + return ProductDimensions(length: "", width: "", height: "") + } + + return ProductDimensions(length: dimensions.length, width: dimensions.width, height: dimensions.height) + }() + self.imageURL = storageProduct.imagesArray.first?.toReadOnly().imageURL + } +} diff --git a/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignCreationFormViewModel.swift b/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignCreationFormViewModel.swift index 4ead8cd5fbf..f6a6c50efe8 100644 --- a/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignCreationFormViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignCreationFormViewModel.swift @@ -203,13 +203,7 @@ final class BlazeCampaignCreationFormViewModel: ObservableObject { @Published private(set) var isUsingAISuggestions: Bool = false private let storage: StorageManagerType - private var product: Product? { - guard let product = productsResultsController.fetchedObjects.first else { - assertionFailure("Unable to fetch product with ID: \(productID)") - return nil - } - return product - } + private var product: BlazeCampaignProduct? @Published private(set) var error: BlazeCampaignCreationError? private var suggestions: [BlazeAISuggestion] = [] @@ -232,9 +226,14 @@ final class BlazeCampaignCreationFormViewModel: ObservableObject { /// ResultController to get the product for the given product ID /// - private lazy var productsResultsController: ResultsController = { + private lazy var productsResultsController: GenericResultsController = { let predicate = \StorageProduct.siteID == siteID && \StorageProduct.productID == productID - let controller = ResultsController(storageManager: storage, matching: predicate, sortedBy: []) + let controller = GenericResultsController( + storageManager: storage, + matching: predicate, + sortedBy: [], + transformer: { BlazeCampaignProduct(storageProduct: $0) } + ) do { try controller.performFetch() } catch { @@ -306,6 +305,8 @@ final class BlazeCampaignCreationFormViewModel: ObservableObject { // sets isEvergreen = true by default if evergreen campaigns are supported self.isEvergreen = featureFlagService.isFeatureFlagEnabled(.blazeEvergreenCampaigns) + product = productsResultsController.fetchedObjects.first + initializeCampaignObjective() updateBudgetDetails() updateTargetLanguagesText() @@ -491,7 +492,7 @@ extension BlazeCampaignCreationFormViewModel { private extension BlazeCampaignCreationFormViewModel { @MainActor func loadProductImage() async -> MediaPickerImage? { - guard let firstImage = product?.images.first, + guard let firstImage = product?.firstImage, let image = try? await productImageLoader.requestImage(productImage: firstImage) else { return nil } diff --git a/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignProduct.swift b/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignProduct.swift new file mode 100644 index 00000000000..da522a3d36e --- /dev/null +++ b/WooCommerce/Classes/ViewRelated/Blaze/CampaignCreation/BlazeCampaignProduct.swift @@ -0,0 +1,27 @@ +import Foundation +import Yosemite + +/// Represents a Product Entity with basic details to display in the Blaze flow. +/// +struct BlazeCampaignProduct: Equatable { + let productID: Int64 + let name: String + let permalink: String + let fullDescription: String? + let shortDescription: String? + + let firstImage: ProductImage? + + func alternativePermalink(with siteURL: String) -> String { + String(format: "%@?post_type=product&p=%d", siteURL, productID) + } + + init(storageProduct: StorageProduct) { + self.productID = storageProduct.productID + self.name = storageProduct.name + self.permalink = storageProduct.permalink + self.fullDescription = storageProduct.fullDescription + self.shortDescription = storageProduct.briefDescription + self.firstImage = storageProduct.imagesArray.first?.toReadOnly() + } +} diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardView.swift index e154bd3b446..649cb1dae19 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardView.swift @@ -1,5 +1,4 @@ import SwiftUI -import struct Yosemite.Product import Kingfisher import struct Yosemite.DashboardCard @@ -294,15 +293,15 @@ private struct ProductInfoView: View { /// Scale of the view based on accessibility changes @ScaledMetric private var scale: CGFloat = 1.0 - private let product: Product + private let product: BlazeCampaignProduct - init(product: Product) { + init(product: BlazeCampaignProduct) { self.product = product } var body: some View { HStack(alignment: .center, spacing: Layout.contentSpacing) { - KFImage(product.imageURL) + KFImage(product.firstImage?.imageURL) .placeholder { Image(uiImage: .productPlaceholderImage) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardViewModel.swift index b1db5c6ac83..c3c11ca90e2 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardViewModel.swift @@ -14,7 +14,7 @@ final class BlazeCampaignDashboardViewModel: ObservableObject { /// Shows info about the latest Blaze campaign case showCampaign(campaign: BlazeCampaignListItem) /// Shows info about the latest published Product - case showProduct(product: Product) + case showProduct(product: BlazeCampaignProduct) /// When there is no campaign or published product case empty } @@ -90,19 +90,20 @@ final class BlazeCampaignDashboardViewModel: ObservableObject { }() /// Product ResultsController. - private lazy var productResultsController: ResultsController = { + private lazy var productResultsController: GenericResultsController = { let predicate = NSPredicate(format: "siteID == %lld AND statusKey ==[c] %@", siteID, ProductStatus.published.rawValue) - return ResultsController(storageManager: storageManager, - matching: predicate, - fetchLimit: 1, - sortOrder: .dateDescending) + return GenericResultsController( + storageManager: storageManager, + matching: predicate, + fetchLimit: 1, + sortedBy: [NSSortDescriptor(key: "date", ascending: false)], + transformer: { BlazeCampaignProduct(storageProduct: $0) } + ) }() - var latestPublishedProduct: Product? { - productResultsController.fetchedObjects.first - } + private(set) var latestPublishedProduct: BlazeCampaignProduct? private var subscriptions: Set = [] @@ -314,9 +315,14 @@ private extension BlazeCampaignDashboardViewModel { self?.updateResults() } + let productTransformer: (StorageProduct) -> BlazeCampaignProduct = { + BlazeCampaignProduct(storageProduct: $0) + } productResultsController.onDidChangeContent = { [weak self] in - self?.updateAvailability() - self?.updateResults() + guard let self else { return } + latestPublishedProduct = productResultsController.fetchedObjects.first + updateAvailability() + updateResults() } productResultsController.onDidResetContent = { [weak self] in self?.updateAvailability() @@ -326,6 +332,7 @@ private extension BlazeCampaignDashboardViewModel { do { try blazeCampaignResultsController.performFetch() try productResultsController.performFetch() + latestPublishedProduct = productResultsController.fetchedObjects.first updateResults() } catch { ServiceLocator.crashLogging.logError(error) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/Multi-package/ShippingLabelPackagesFormViewModel.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/Multi-package/ShippingLabelPackagesFormViewModel.swift index b8e030e3239..6bd6964c948 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/Multi-package/ShippingLabelPackagesFormViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/Multi-package/ShippingLabelPackagesFormViewModel.swift @@ -61,7 +61,7 @@ final class ShippingLabelPackagesFormViewModel: ObservableObject { /// Products contained inside the Order and fetched from Core Data /// - @Published private var products: [Product] = [] + @Published private var products: [ShippingLabelProduct] = [] /// ProductVariations contained inside the Order and fetched from Core Data /// diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/ShippingLabelPackageDetailsResultsControllers.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/ShippingLabelPackageDetailsResultsControllers.swift index 0b6558a9f90..783c5072eb1 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/ShippingLabelPackageDetailsResultsControllers.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/Create Shipping Label Form/Package Details/ShippingLabelPackageDetailsResultsControllers.swift @@ -7,12 +7,11 @@ final class ShippingLabelPackageDetailsResultsControllers { private let siteID: Int64 private let orderItems: [OrderItem] private let storageManager: StorageManagerType - private var onProductReload: (([Product]) -> Void)? private var onProductVariationsReload: (([ProductVariation]) -> Void)? /// Get the products found in Core Data and that match the predicate. /// - var products: [Product] { + var products: [ShippingLabelProduct] { try? productResultsController.performFetch() return productResultsController.fetchedObjects } @@ -33,11 +32,16 @@ final class ShippingLabelPackageDetailsResultsControllers { /// Product ResultsController. /// - private lazy var productResultsController: ResultsController = { + private lazy var productResultsController: GenericResultsController = { let predicate = NSPredicate(format: "siteID == %lld", siteID) let descriptor = NSSortDescriptor(key: "name", ascending: true) - return ResultsController(storageManager: storageManager, matching: predicate, sortedBy: [descriptor]) + return GenericResultsController( + storageManager: storageManager, + matching: predicate, + sortedBy: [descriptor], + transformer: { ShippingLabelProduct(storageProduct: $0) } + ) }() /// ProductVariation ResultsController. @@ -60,7 +64,7 @@ final class ShippingLabelPackageDetailsResultsControllers { init(siteID: Int64, orderItems: [OrderItem], storageManager: StorageManagerType = ServiceLocator.storageManager, - onProductReload: @escaping ([Product]) -> Void, + onProductReload: @escaping ([ShippingLabelProduct]) -> Void, onProductVariationsReload: @escaping ([ProductVariation]) -> Void) { self.siteID = siteID self.orderItems = orderItems @@ -69,7 +73,7 @@ final class ShippingLabelPackageDetailsResultsControllers { configureProductVariationResultsController(onReload: onProductVariationsReload) } - private func configureProductResultsController(onReload: @escaping ([Product]) -> ()) { + private func configureProductResultsController(onReload: @escaping ([ShippingLabelProduct]) -> ()) { productResultsController.onDidChangeContent = { [weak self] in guard let self = self else { return } onReload(self.productResultsController.fetchedObjects) diff --git a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsDataSource.swift b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsDataSource.swift index ff37009fa74..c740bfcefbc 100644 --- a/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsDataSource.swift +++ b/WooCommerce/Classes/ViewRelated/Orders/Order Details/Shipping Labels/WooShipping Create Shipping Labels/WooShipping Items Section/WooShippingItemsDataSource.swift @@ -30,7 +30,7 @@ final class DefaultWooShippingItemsDataSource: WooShippingItemsDataSource { /// Stored products that match the items in the order. /// - private var products: [Product] { + private var products: [ShippingLabelProduct] { try? productResultsController.performFetch() return productResultsController.fetchedObjects } @@ -44,12 +44,17 @@ final class DefaultWooShippingItemsDataSource: WooShippingItemsDataSource { /// Product ResultsController. /// - private lazy var productResultsController: ResultsController = { + private lazy var productResultsController: GenericResultsController = { let productIDs = order.items.map(\.productID) let predicate = NSPredicate(format: "siteID == %lld AND productID in %@", order.siteID, productIDs) let descriptor = NSSortDescriptor(key: "name", ascending: true) - return ResultsController(storageManager: storageManager, matching: predicate, sortedBy: [descriptor]) + return GenericResultsController( + storageManager: storageManager, + matching: predicate, + sortedBy: [descriptor], + transformer: { ShippingLabelProduct(storageProduct: $0) } + ) }() /// ProductVariation ResultsController. diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 0291363adcb..d065e0c6ac9 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -2762,6 +2762,8 @@ DEA357132ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA357122ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift */; }; DEA64C532E40B04700791018 /* OrderDetailsProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA64C522E40B04000791018 /* OrderDetailsProduct.swift */; }; DEA64C552E41A2D000791018 /* Product+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA64C542E41A2CA00791018 /* Product+Helpers.swift */; }; + DEA66A192E41DECF00791018 /* ShippingLabelProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA66A182E41DEC200791018 /* ShippingLabelProduct.swift */; }; + DEA66A1B2E41E0C000791018 /* BlazeCampaignProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA66A1A2E41E0B800791018 /* BlazeCampaignProduct.swift */; }; DEA65B372E41A65600791018 /* ProductListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA65B362E41A65100791018 /* ProductListItem.swift */; }; DEA6BCAF2BC6A9B10017D671 /* StoreStatsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA6BCAE2BC6A9B10017D671 /* StoreStatsChart.swift */; }; DEA6BCB12BC6AA040017D671 /* StoreStatsChartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA6BCB02BC6AA040017D671 /* StoreStatsChartViewModel.swift */; }; @@ -5963,6 +5965,8 @@ DEA357122ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignListViewModelTests.swift; sourceTree = ""; }; DEA64C522E40B04000791018 /* OrderDetailsProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailsProduct.swift; sourceTree = ""; }; DEA64C542E41A2CA00791018 /* Product+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Product+Helpers.swift"; sourceTree = ""; }; + DEA66A182E41DEC200791018 /* ShippingLabelProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelProduct.swift; sourceTree = ""; }; + DEA66A1A2E41E0B800791018 /* BlazeCampaignProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignProduct.swift; sourceTree = ""; }; DEA65B362E41A65100791018 /* ProductListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListItem.swift; sourceTree = ""; }; DEA6BCAE2BC6A9B10017D671 /* StoreStatsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsChart.swift; sourceTree = ""; }; DEA6BCB02BC6AA040017D671 /* StoreStatsChartViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsChartViewModel.swift; sourceTree = ""; }; @@ -10885,6 +10889,7 @@ FEDD70AE26A7223500194C3A /* StorageEligibilityErrorInfo+Woo.swift */, DEDB886A26E8531E00981595 /* ShippingLabelPackageAttributes.swift */, DE77889926FD7EF0008DFF44 /* ShippingLabelPackageItem.swift */, + DEA66A182E41DEC200791018 /* ShippingLabelProduct.swift */, DEF3300B270444060073AE29 /* ShippingLabelSelectedRate.swift */, DECE13FF279A595200816ECD /* Coupon+Woo.swift */, E1E649E82846188C0070B194 /* BetaFeature.swift */, @@ -13277,6 +13282,7 @@ DE57462D2B43EAF20034B10D /* CampaignCreation */ = { isa = PBXGroup; children = ( + DEA66A1A2E41E0B800791018 /* BlazeCampaignProduct.swift */, DE57462E2B43EB0B0034B10D /* BlazeCampaignCreationForm.swift */, DE5746302B43F6180034B10D /* BlazeCampaignCreationFormViewModel.swift */, EE1905832B579B6700617C53 /* BlazeCampaignCreationLoadingView.swift */, @@ -16333,6 +16339,7 @@ 31B0551E264B3C7A00134D87 /* CardPresentModalFoundReader.swift in Sources */, 4520A15E2722BA3E001FA573 /* OrderDateRangeFilter+Utils.swift in Sources */, DEF8CF0F29A890E900800A60 /* JetpackBenefitsViewModel.swift in Sources */, + DEA66A1B2E41E0C000791018 /* BlazeCampaignProduct.swift in Sources */, CE6E110B2C91DA5D00563DD4 /* WooShippingItemRow.swift in Sources */, B946880E29B627EB000646B0 /* SearchableActivityConvertable.swift in Sources */, 027ADB732D21812D009608DB /* POSItemImageView.swift in Sources */, @@ -16929,6 +16936,7 @@ 02A65301246AA63600755A01 /* ProductDetailsFactory.swift in Sources */, EE7E75AC2D84080D00E6FF5B /* WooShippingSplitShipmentsViewModel.swift in Sources */, D449C51D26DE6B5000D75B02 /* LargeTitle.swift in Sources */, + DEA66A192E41DECF00791018 /* ShippingLabelProduct.swift in Sources */, B6E7DB64293A7C390049B001 /* AnalyticsHubYesterdayRangeData.swift in Sources */, 456396B625C82691001F1A26 /* ShippingLabelFormStepTableViewCell.swift in Sources */, 03FBDA9D263AD49200ACE257 /* CouponListViewController.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeCampaignCreationFormViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeCampaignCreationFormViewModelTests.swift index b7bb59f4f72..cbfeda5c441 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeCampaignCreationFormViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Blaze/BlazeCampaignCreationFormViewModelTests.swift @@ -38,11 +38,6 @@ final class BlazeCampaignCreationFormViewModelTests: XCTestCase { /// Mock Storage: InMemory private var storageManager: StorageManagerType! - /// View storage for tests - private var storage: StorageType { - storageManager.viewStorage - } - private var stores: MockStoresManager! private var imageLoader: MockProductUIImageLoader! @@ -219,11 +214,11 @@ final class BlazeCampaignCreationFormViewModelTests: XCTestCase { // Given insertProduct(sampleProduct) mockDownloadImage(sampleImage) - var triggeredFetchAISuggestions = false + var fetchAISuggestionsTriggers = 0 stores.whenReceivingAction(ofType: BlazeAction.self) { action in switch action { case let .fetchAISuggestions(_, _, completion): - triggeredFetchAISuggestions = true + fetchAISuggestionsTriggers += 1 completion(.failure(MockError())) default: break @@ -238,13 +233,12 @@ final class BlazeCampaignCreationFormViewModelTests: XCTestCase { // Preload AI suggestions and reset the flag await viewModel.onLoad() - triggeredFetchAISuggestions = false // When await viewModel.onLoad() // Then - XCTAssertFalse(triggeredFetchAISuggestions) + XCTAssertEqual(fetchAISuggestionsTriggers, 1) } @MainActor @@ -1127,21 +1121,23 @@ private extension BlazeCampaignCreationFormViewModelTests { /// Insert a `Product` into storage. /// func insertProduct(_ readOnlyProduct: Product) { - let product = storage.insertNewObject(ofType: StorageProduct.self) - product.update(with: readOnlyProduct) - - for readOnlyImage in readOnlyProduct.images { - let productImage = storage.insertNewObject(ofType: StorageProductImage.self) - productImage.update(with: readOnlyImage) - productImage.product = product - } - storage.saveIfNeeded() + storageManager.performAndSave({ storage in + let product = storage.insertNewObject(ofType: StorageProduct.self) + product.update(with: readOnlyProduct) + + for readOnlyImage in readOnlyProduct.images { + let productImage = storage.insertNewObject(ofType: StorageProductImage.self) + productImage.update(with: readOnlyImage) + productImage.product = product + } + }, completion: {}, on: .main) } func insertCampaignObjective(_ readOnlyObjective: BlazeCampaignObjective) { - let objective = storage.insertNewObject(ofType: StorageBlazeCampaignObjective.self) - objective.update(with: readOnlyObjective) - storage.saveIfNeeded() + storageManager.performAndSave({ storage in + let objective = storage.insertNewObject(ofType: StorageBlazeCampaignObjective.self) + objective.update(with: readOnlyObjective) + }, completion: {}, on: .main) } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardViewModelTests.swift index a0cae42e891..0ed80d09a79 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Dashboard/Blaze/BlazeCampaignDashboardViewModelTests.swift @@ -17,11 +17,6 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { /// Mock Storage: InMemory private var storageManager: MockStorageManager! - /// View storage for tests - private var storage: StorageType { - storageManager.viewStorage - } - private var analyticsProvider: MockAnalyticsProvider! private var analytics: WooAnalytics! @@ -57,8 +52,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { blazeEligibilityChecker: checker) XCTAssertFalse(sut.canShowInDashboard) mockSynchronizeProducts(insertProductToStorage: .fake().copy(siteID: sampleSiteID, - statusKey: (ProductStatus.published.rawValue), - purchasable: true)) + statusKey: (ProductStatus.published.rawValue))) mockSynchronizeCampaignsList() @@ -117,14 +111,14 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { func test_it_shows_latest_published_product_in_dashboard() async { // Given let checker = MockBlazeEligibilityChecker(isSiteEligible: true) - let product1: Networking.Product = .fake().copy(siteID: sampleSiteID, - statusKey: (ProductStatus.published.rawValue), - purchasable: true) + let product1 = Networking.Product.fake().copy(siteID: sampleSiteID, + productID: 124, + statusKey: (ProductStatus.published.rawValue)) insertProduct(product1) - let product2: Networking.Product = .fake().copy(siteID: sampleSiteID, - statusKey: (ProductStatus.published.rawValue), - purchasable: true) + let product2 = Networking.Product.fake().copy(siteID: sampleSiteID, + productID: 123, + statusKey: (ProductStatus.published.rawValue)) let sut = BlazeCampaignDashboardViewModel(siteID: sampleSiteID, stores: stores, @@ -140,7 +134,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Then if case .showProduct(let product) = sut.state { - XCTAssertEqual(product, product2) + XCTAssertEqual(product.productID, product2.productID) } else { XCTFail("Wrong state") } @@ -221,8 +215,8 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Given let checker = MockBlazeEligibilityChecker(isSiteEligible: true) let fakeProduct = Product.fake().copy(siteID: sampleSiteID, - statusKey: (ProductStatus.published.rawValue), - purchasable: true) + productID: 123, + statusKey: (ProductStatus.published.rawValue)) let sut = BlazeCampaignDashboardViewModel(siteID: sampleSiteID, stores: stores, @@ -238,7 +232,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Then if case .showProduct(let product) = sut.state { - XCTAssertEqual(product, fakeProduct) + XCTAssertEqual(product.productID, fakeProduct.productID) } else { XCTFail("Wrong state") } @@ -359,8 +353,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Given let checker = MockBlazeEligibilityChecker(isSiteEligible: true) let fakeProduct = Product.fake().copy(siteID: sampleSiteID, - statusKey: (ProductStatus.published.rawValue), - purchasable: true) + statusKey: (ProductStatus.published.rawValue)) let sut = BlazeCampaignDashboardViewModel(siteID: sampleSiteID, stores: stores, @@ -574,8 +567,8 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Given let checker = MockBlazeEligibilityChecker(isSiteEligible: true) let fakeProduct = Product.fake().copy(siteID: sampleSiteID, - statusKey: (ProductStatus.published.rawValue), - purchasable: true) + productID: 123, + statusKey: (ProductStatus.published.rawValue)) let sut = BlazeCampaignDashboardViewModel(siteID: sampleSiteID, stores: stores, storageManager: storageManager, @@ -598,7 +591,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Then if case .showProduct(let product) = sut.state { - XCTAssertEqual(product, fakeProduct) + XCTAssertEqual(product.productID, fakeProduct.productID) } else { XCTFail("Wrong state") } @@ -611,16 +604,13 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Given insertProduct(Product.fake().copy(siteID: sampleSiteID, productID: 1, - statusKey: (ProductStatus.published.rawValue), - purchasable: true)) + statusKey: (ProductStatus.published.rawValue))) insertProduct(Product.fake().copy(siteID: sampleSiteID, productID: 2, - statusKey: (ProductStatus.draft.rawValue), - purchasable: true)) + statusKey: (ProductStatus.draft.rawValue))) insertProduct(Product.fake().copy(siteID: sampleSiteID, productID: 3, - statusKey: (ProductStatus.published.rawValue), - purchasable: true)) + statusKey: (ProductStatus.published.rawValue))) let checker = MockBlazeEligibilityChecker(isSiteEligible: true) let viewModel = BlazeCampaignDashboardViewModel(siteID: sampleSiteID, @@ -637,8 +627,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Given insertProduct(Product.fake().copy(siteID: sampleSiteID, productID: 1, - statusKey: (ProductStatus.draft.rawValue), - purchasable: true)) + statusKey: (ProductStatus.draft.rawValue))) let checker = MockBlazeEligibilityChecker(isSiteEligible: true) let viewModel = BlazeCampaignDashboardViewModel(siteID: sampleSiteID, @@ -784,8 +773,7 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { // Given let checker = MockBlazeEligibilityChecker(isSiteEligible: true) let fakeProduct = Product.fake().copy(siteID: sampleSiteID, - statusKey: (ProductStatus.published.rawValue), - purchasable: true) + statusKey: (ProductStatus.published.rawValue)) let sut = BlazeCampaignDashboardViewModel(siteID: sampleSiteID, stores: stores, @@ -897,18 +885,20 @@ final class BlazeCampaignDashboardViewModelTests: XCTestCase { } private extension BlazeCampaignDashboardViewModelTests { - func insertProduct(_ readOnlyProduct: Networking.Product) { - let newProduct = storage.insertNewObject(ofType: StorageProduct.self) - newProduct.update(with: readOnlyProduct) - storage.saveIfNeeded() + func insertProduct(_ listItem: Networking.Product) { + storageManager.performAndSave({ storage in + let newProduct = storage.insertNewObject(ofType: StorageProduct.self) + newProduct.update(with: listItem) + }, completion: {}, on: .main) } func insertCampaigns(_ readOnlyCampaigns: [BlazeCampaignListItem]) { - readOnlyCampaigns.forEach { campaign in - let newCampaign = storage.insertNewObject(ofType: StorageBlazeCampaignListItem.self) - newCampaign.update(with: campaign) - } - storage.saveIfNeeded() + storageManager.performAndSave({ storage in + readOnlyCampaigns.forEach { campaign in + let newCampaign = storage.insertNewObject(ofType: StorageBlazeCampaignListItem.self) + newCampaign.update(with: campaign) + } + }, completion: {}, on: .main) } func mockSynchronizeProducts(insertProductToStorage product: Networking.Product? = nil) {