Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
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]
- [*] Order Creation: Prevent subscription products to be added to an order [https://github.com/woocommerce/woocommerce-ios/pull/15960]
- [internal] Replace COTS_DEVICE reader model name with TAP_TO_PAY_DEVICE. [https://github.com/woocommerce/woocommerce-ios/pull/15961]

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
34 changes: 34 additions & 0 deletions WooCommerce/Classes/Model/ShippingLabelProduct.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 siteID: Int64
let productID: Int64
let name: String

let price: String
let virtual: Bool
let weight: String?
let dimensions: ProductDimensions

let imageURL: URL?

init(storageProduct: StorageProduct) {
self.siteID = storageProduct.siteID
self.productID = storageProduct.productID
self.name = storageProduct.name
self.price = storageProduct.price
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,32 @@
import Foundation
import Yosemite

/// Represents a Product Entity with basic details to display in the Blaze flow.
///
struct BlazeCampaignProduct: Equatable {
let siteID: Int64
let productID: Int64
let name: String
let permalink: String
let fullDescription: String?
let shortDescription: String?

let price: String

let firstImage: ProductImage?

func alternativePermalink(with siteURL: String) -> String {
String(format: "%@?post_type=product&p=%d", siteURL, productID)
}

init(storageProduct: StorageProduct) {
self.siteID = storageProduct.siteID
self.productID = storageProduct.productID
self.name = storageProduct.name
self.price = storageProduct.price
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,12 @@ final class ShippingLabelPackageDetailsResultsControllers {
private let siteID: Int64
private let orderItems: [OrderItem]
private let storageManager: StorageManagerType
private var onProductReload: (([Product]) -> Void)?
private var onProductReload: (([ShippingLabelProduct]) -> 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 +33,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 +65,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 +74,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 @@ -2755,6 +2755,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 */; };
DEA6BCAF2BC6A9B10017D671 /* StoreStatsChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA6BCAE2BC6A9B10017D671 /* StoreStatsChart.swift */; };
DEA6BCB12BC6AA040017D671 /* StoreStatsChartViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA6BCB02BC6AA040017D671 /* StoreStatsChartViewModel.swift */; };
DEA88F502AA9D0100037273B /* AddEditProductCategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEA88F4F2AA9D0100037273B /* AddEditProductCategoryViewModel.swift */; };
Expand Down Expand Up @@ -5948,6 +5950,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>"; };
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>"; };
DEA88F4F2AA9D0100037273B /* AddEditProductCategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditProductCategoryViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -10862,6 +10866,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 @@ -13253,6 +13258,7 @@
DE57462D2B43EAF20034B10D /* CampaignCreation */ = {
isa = PBXGroup;
children = (
DEA66A1A2E41E0B800791018 /* BlazeCampaignProduct.swift */,
DE57462E2B43EB0B0034B10D /* BlazeCampaignCreationForm.swift */,
DE5746302B43F6180034B10D /* BlazeCampaignCreationFormViewModel.swift */,
EE1905832B579B6700617C53 /* BlazeCampaignCreationLoadingView.swift */,
Expand Down Expand Up @@ -16306,6 +16312,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 @@ -16901,6 +16908,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