Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d4395d1
Use ProductListItem in Blaze dashboard
itsmeichigo Jul 31, 2025
72dbdcc
Add missing properties for ProductListItem
itsmeichigo Jul 31, 2025
88cc5e2
Replace product type in BlazeCampaignCreationFormViewModel
itsmeichigo Jul 31, 2025
f7c4ac9
Replace Product object in shipping label
itsmeichigo Jul 31, 2025
9a6f736
Update release notes
itsmeichigo Jul 31, 2025
2f5e88f
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 1, 2025
52cc176
Add missing date property to fix failed test
itsmeichigo Aug 1, 2025
a761e14
Replace computed variables for fetched objects
itsmeichigo Aug 1, 2025
f31c7f2
Revert change to WooShippingItemsDataSource's product
itsmeichigo Aug 1, 2025
53d1168
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 1, 2025
cc3d894
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 5, 2025
63ef710
Use separate product types for blaze and shipping labels
itsmeichigo Aug 5, 2025
f2af267
Fix swiftlint issue
itsmeichigo Aug 5, 2025
7327737
Merge branch 'woomob-619-xcode-warnings-performing-io-on-the-main-thr…
itsmeichigo Aug 5, 2025
57b8589
Use GenericResultsController for Blaze and shipping label flows
itsmeichigo Aug 5, 2025
1a5f9b7
Merge branch 'trunk' into woomob-619-simplified-product-objects
itsmeichigo Aug 7, 2025
c48cd9f
Merge branch 'trunk' into woomob-619-simplified-product-objects
itsmeichigo Aug 7, 2025
bcf3ebd
Remove unused properties
itsmeichigo Aug 7, 2025
c0ff9f5
Remove outdated test
itsmeichigo Aug 7, 2025
b0623c3
Fix failed tests
itsmeichigo Aug 7, 2025
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
7 changes: 0 additions & 7 deletions Modules/Sources/Networking/Model/Product/Product.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 0 additions & 2 deletions Modules/Tests/NetworkingTests/Mapper/ProductMapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion WooCommerce/Classes/Model/ShippingLabelPackageItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions WooCommerce/Classes/Model/ShippingLabelProduct.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand All @@ -232,9 +226,14 @@ final class BlazeCampaignCreationFormViewModel: ObservableObject {

/// ResultController to get the product for the given product ID
///
private lazy var productsResultsController: ResultsController<StorageProduct> = {
private lazy var productsResultsController: GenericResultsController<StorageProduct, BlazeCampaignProduct> = {
let predicate = \StorageProduct.siteID == siteID && \StorageProduct.productID == productID
let controller = ResultsController<StorageProduct>(storageManager: storage, matching: predicate, sortedBy: [])
let controller = GenericResultsController<StorageProduct, BlazeCampaignProduct>(
storageManager: storage,
matching: predicate,
sortedBy: [],
transformer: { BlazeCampaignProduct(storageProduct: $0) }
)
do {
try controller.performFetch()
} catch {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import SwiftUI
import struct Yosemite.Product
import Kingfisher
import struct Yosemite.DashboardCard

Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -90,19 +90,20 @@ final class BlazeCampaignDashboardViewModel: ObservableObject {
}()

/// Product ResultsController.
private lazy var productResultsController: ResultsController<StorageProduct> = {
private lazy var productResultsController: GenericResultsController<StorageProduct, BlazeCampaignProduct> = {
let predicate = NSPredicate(format: "siteID == %lld AND statusKey ==[c] %@",
siteID,
ProductStatus.published.rawValue)
return ResultsController<StorageProduct>(storageManager: storageManager,
matching: predicate,
fetchLimit: 1,
sortOrder: .dateDescending)
return GenericResultsController<StorageProduct, BlazeCampaignProduct>(
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<AnyCancellable> = []

Expand Down Expand Up @@ -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()
Expand All @@ -326,6 +332,7 @@ private extension BlazeCampaignDashboardViewModel {
do {
try blazeCampaignResultsController.performFetch()
try productResultsController.performFetch()
latestPublishedProduct = productResultsController.fetchedObjects.first
updateResults()
} catch {
ServiceLocator.crashLogging.logError(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -33,11 +32,16 @@ final class ShippingLabelPackageDetailsResultsControllers {

/// Product ResultsController.
///
private lazy var productResultsController: ResultsController<StorageProduct> = {
private lazy var productResultsController: GenericResultsController<StorageProduct, ShippingLabelProduct> = {
let predicate = NSPredicate(format: "siteID == %lld", siteID)
let descriptor = NSSortDescriptor(key: "name", ascending: true)

return ResultsController<StorageProduct>(storageManager: storageManager, matching: predicate, sortedBy: [descriptor])
return GenericResultsController<StorageProduct, ShippingLabelProduct>(
storageManager: storageManager,
matching: predicate,
sortedBy: [descriptor],
transformer: { ShippingLabelProduct(storageProduct: $0) }
)
}()

/// ProductVariation ResultsController.
Expand All @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -44,12 +44,17 @@ final class DefaultWooShippingItemsDataSource: WooShippingItemsDataSource {

/// Product ResultsController.
///
private lazy var productResultsController: ResultsController<StorageProduct> = {
private lazy var productResultsController: GenericResultsController<StorageProduct, ShippingLabelProduct> = {
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<StorageProduct>(storageManager: storageManager, matching: predicate, sortedBy: [descriptor])
return GenericResultsController<StorageProduct, ShippingLabelProduct>(
storageManager: storageManager,
matching: predicate,
sortedBy: [descriptor],
transformer: { ShippingLabelProduct(storageProduct: $0) }
)
}()

/// ProductVariation ResultsController.
Expand Down
8 changes: 8 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -5963,6 +5965,8 @@
DEA357122ADCC4C9006380BA /* BlazeCampaignListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignListViewModelTests.swift; sourceTree = "<group>"; };
DEA64C522E40B04000791018 /* OrderDetailsProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderDetailsProduct.swift; sourceTree = "<group>"; };
DEA64C542E41A2CA00791018 /* Product+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Product+Helpers.swift"; sourceTree = "<group>"; };
DEA66A182E41DEC200791018 /* ShippingLabelProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelProduct.swift; sourceTree = "<group>"; };
DEA66A1A2E41E0B800791018 /* BlazeCampaignProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignProduct.swift; sourceTree = "<group>"; };
DEA65B362E41A65100791018 /* ProductListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListItem.swift; sourceTree = "<group>"; };
DEA6BCAE2BC6A9B10017D671 /* StoreStatsChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsChart.swift; sourceTree = "<group>"; };
DEA6BCB02BC6AA040017D671 /* StoreStatsChartViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsChartViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -13277,6 +13282,7 @@
DE57462D2B43EAF20034B10D /* CampaignCreation */ = {
isa = PBXGroup;
children = (
DEA66A1A2E41E0B800791018 /* BlazeCampaignProduct.swift */,
DE57462E2B43EB0B0034B10D /* BlazeCampaignCreationForm.swift */,
DE5746302B43F6180034B10D /* BlazeCampaignCreationFormViewModel.swift */,
EE1905832B579B6700617C53 /* BlazeCampaignCreationLoadingView.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
Loading