Skip to content

Commit

Permalink
[HACK week] Adds favorite products filter (#14001)
Browse files Browse the repository at this point in the history
  • Loading branch information
selanthiraiyan authored Sep 26, 2024
2 parents 8dea352 + 0180b75 commit ed87ae5
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
case .blazeCampaignObjective:
return true
case .favoriteProducts:
return false
return buildConfig == .localDeveloper || buildConfig == .alpha
default:
return true
}
Expand Down
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [internal] Logging for app storage size added in Core Data crash logs [https://github.com/woocommerce/woocommerce-ios/pull/14008]
- [*] Payments: Prevent phone from sleeping during card reader updates [https://github.com/woocommerce/woocommerce-ios/pull/14021]
- [internal] Fixed concurrency issue with Blaze local notification scheduler. [https://github.com/woocommerce/woocommerce-ios/pull/14012]
- [internal] We added ability to mark and filter favorite products. This feature isn't released yet. But please smoke test product filtering work as expected. [https://github.com/woocommerce/woocommerce-ios/pull/14001]

20.5
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import Yosemite
import Experiments

protocol FavoriteProductsUseCase {
func markAsFavorite(productID: Int64)
Expand Down Expand Up @@ -34,11 +35,14 @@ private extension FavoriteProductsFilter {
struct DefaultFavoriteProductsUseCase: FavoriteProductsUseCase {
private let siteID: Int64
private let stores: StoresManager
private let featureFlagService: FeatureFlagService

init(siteID: Int64,
stores: StoresManager = ServiceLocator.stores) {
stores: StoresManager = ServiceLocator.stores,
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
self.siteID = siteID
self.stores = stores
self.featureFlagService = featureFlagService
}

@MainActor
Expand All @@ -64,6 +68,9 @@ struct DefaultFavoriteProductsUseCase: FavoriteProductsUseCase {

@MainActor
func favoriteProductIDs() async -> [Int64] {
guard featureFlagService.isFeatureFlagEnabled(.favoriteProducts) else {
return []
}
return await withCheckedContinuation { continuation in
stores.dispatch(AppSettingsAction.loadFavoriteProductIDs(siteID: siteID, onCompletion: { savedFavProductIDs in
continuation.resume(returning: savedFavProductIDs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,7 @@ private extension ProductFormViewController {
switch result {
case .success:
ServiceLocator.analytics.track(.productDetailProductDeleted)
self.viewModel.removeFromFavorite()
// Dismisses the in-progress UI.
self.navigationController?.dismiss(animated: true, completion: nil)
// Dismiss or Pop the Product Form
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ final class ProductSelectorViewModel: ObservableObject {

@Published var productVariationListViewModel: ProductVariationSelectorViewModel? = nil

private let favoriteProductsUseCase: FavoriteProductsUseCase
private(set) var favoriteProductIDs: [Int64] = []

init(siteID: Int64,
source: ProductSelectorSource,
selectedItemIDs: [Int64] = [],
Expand All @@ -241,6 +244,7 @@ final class ProductSelectorViewModel: ObservableObject {
stores: StoresManager = ServiceLocator.stores,
analytics: Analytics = ServiceLocator.analytics,
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
favoriteProductsUseCase: FavoriteProductsUseCase? = nil,
toggleAllVariationsOnSelection: Bool = true,
topProductsProvider: ProductSelectorTopProductsProviderProtocol? = nil,
pageFirstIndex: Int = PaginationTracker.Defaults.pageFirstIndex,
Expand Down Expand Up @@ -279,6 +283,11 @@ final class ProductSelectorViewModel: ObservableObject {
self.onConfigureProductRow = onConfigureProductRow
tracker = ProductSelectorViewModelTracker(analytics: analytics, trackProductsSource: topProductsProvider != nil)

self.favoriteProductsUseCase = favoriteProductsUseCase ?? DefaultFavoriteProductsUseCase(siteID: siteID)
Task { @MainActor [weak self] in
await self?.loadFavoriteProductIDs()
}

topProductsFromCachedOrders = topProductsProvider?.provideTopProducts(siteID: siteID) ?? .empty
tracker.viewModel = self

Expand Down Expand Up @@ -480,6 +489,13 @@ final class ProductSelectorViewModel: ObservableObject {
onAllSelectionsCleared?()
}

/// Loads favorite product IDs
///
@MainActor
func loadFavoriteProductIDs() async {
favoriteProductIDs = await favoriteProductsUseCase.favoriteProductIDs()
}

enum SyncApproach {
case external
case onButtonTap
Expand Down Expand Up @@ -511,6 +527,7 @@ extension ProductSelectorViewModel: PaginationTrackerDelegate {
productType: filtersSubject.value.promotableProductType?.productType,
productCategory: filtersSubject.value.productCategory,
sortOrder: .nameAscending,
productIDs: (filtersSubject.value.favoriteProduct != nil) ? favoriteProductIDs : [],
shouldDeleteStoredProductsOnFirstPage: shouldDeleteStoredProductsOnFirstPage) { [weak self] result in
guard let self = self else { return }

Expand Down Expand Up @@ -720,11 +737,22 @@ private extension ProductSelectorViewModel {
sections.append(ProductSelectorSection(type: type, products: products))
}

func updatePredicate(searchTerm: String, filters: FilterProductListViewModel.Filters, productSearchFilter: ProductSearchFilter) {
func updatePredicate(searchTerm: String, filters: FilterProductListViewModel.Filters, productSearchFilter: ProductSearchFilter) async {
let productIDs: [Int64]? = await {
guard filters.favoriteProduct != nil else {
return nil
}

await loadFavoriteProductIDs()
return favoriteProductIDs
}()

productsResultsController.updatePredicate(siteID: siteID,
stockStatus: filters.stockStatus,
productStatus: filters.productStatus,
productType: filters.promotableProductType?.productType)
productType: filters.promotableProductType?.productType,
productIDs: productIDs)

if searchTerm.isNotEmpty {
// When the search query changes, also includes the original results predicate in addition to the search keyword and filter key.
let searchResultsPredicate = NSPredicate(format: "SUBQUERY(searchResults, $result, $result.keyword = %@ AND $result.filterKey = %@).@count > 0",
Expand Down Expand Up @@ -770,10 +798,12 @@ private extension ProductSelectorViewModel {
Publishers.CombineLatest3(searchTermPublisher, filtersPublisher, searchFilterPublisher)
.sink { [weak self] searchTerm, filtersSubject, productSearchFilter in
guard let self = self else { return }
self.updateFilterButtonTitle(with: filtersSubject)
self.updatePredicate(searchTerm: searchTerm, filters: filtersSubject, productSearchFilter: productSearchFilter)
self.reloadData()
self.paginationTracker.resync()
Task { @MainActor in
self.updateFilterButtonTitle(with: filtersSubject)
await self.updatePredicate(searchTerm: searchTerm, filters: filtersSubject, productSearchFilter: productSearchFilter)
self.reloadData()
self.paginationTracker.resync()
}
}.store(in: &subscriptions)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,24 @@ final class ProductListViewModel: ProductsListViewModelProtocol {
private let barcodeSKUScannerItemFinder: BarcodeSKUScannerItemFinder
private let featureFlagService: FeatureFlagService

private let favoriteProductsUseCase: FavoriteProductsUseCase
private(set) var favoriteProductIDs: [Int64] = []

init(siteID: Int64,
stores: StoresManager = ServiceLocator.stores,
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
barcodeSKUScannerItemFinder: BarcodeSKUScannerItemFinder = BarcodeSKUScannerItemFinder()) {
favoriteProductsUseCase: FavoriteProductsUseCase? = nil,
barcodeSKUScannerItemFinder: BarcodeSKUScannerItemFinder = BarcodeSKUScannerItemFinder(),
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
self.siteID = siteID
self.stores = stores
self.featureFlagService = featureFlagService
self.wooSubscriptionProductsEligibilityChecker = WooSubscriptionProductsEligibilityChecker(siteID: siteID)
self.barcodeSKUScannerItemFinder = barcodeSKUScannerItemFinder
self.favoriteProductsUseCase = favoriteProductsUseCase ?? DefaultFavoriteProductsUseCase(siteID: siteID)

Task { @MainActor [weak self] in
await self?.loadFavoriteProductIDs()
}
}

var selectedProductsCount: Int {
Expand Down Expand Up @@ -208,6 +217,13 @@ final class ProductListViewModel: ProductsListViewModelProtocol {
})
}

/// Loads favorite product IDs
///
@MainActor
func loadFavoriteProductIDs() async {
favoriteProductIDs = await favoriteProductsUseCase.favoriteProductIDs()
}

private func isPluginActive(_ plugin: String, completion: @escaping (Bool) -> (Void)) {
let action = SystemStatusAction.fetchSystemPluginListWithNameList(siteID: siteID, systemPluginNameList: [plugin]) { plugin in
completion(plugin?.active == true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,21 +162,20 @@ final class ProductsViewController: UIViewController, GhostableViewController {

private var filters: FilterProductListViewModel.Filters = FilterProductListViewModel.Filters() {
didSet {
if filters != oldValue ||
categoryHasChangedRemotely {
updateLocalProductSettings(sort: sortOrder,
filters: filters)
updateFilterButtonTitle(filters: filters)
Task { @MainActor in
if filters != oldValue ||
categoryHasChangedRemotely {
updateLocalProductSettings(sort: sortOrder,
filters: filters)
updateFilterButtonTitle(filters: filters)

resultsController.updatePredicate(siteID: siteID,
stockStatus: filters.stockStatus,
productStatus: filters.productStatus,
productType: filters.promotableProductType?.productType)
await updatePredicate(filters: filters)

/// Reload because `updatePredicate` calls `performFetch` when creating a new predicate
tableView.reloadData()
/// Reload because `updatePredicate` calls `performFetch` when creating a new predicate
tableView.reloadData()

paginationTracker.resync()
paginationTracker.resync()
}
}
}
}
Expand Down Expand Up @@ -275,6 +274,8 @@ final class ProductsViewController: UIViewController, GhostableViewController {
}

navigationController?.navigationBar.removeShadow()

reloadFavoriteProductsIfNeeded()
}

override func viewWillDisappear(_ animated: Bool) {
Expand Down Expand Up @@ -1030,6 +1031,49 @@ private extension ProductsViewController {
}
return indexPathsForVisibleRows.contains(indexPath)
}

func reloadFavoriteProductsIfNeeded() {
Task { @MainActor [weak self] in
guard let self else { return }

// Reload only if favorite products filter is turned on
guard filters.favoriteProduct != nil else {
return
}

let previousFavoriteProductIDs = viewModel.favoriteProductIDs
await viewModel.loadFavoriteProductIDs()

// Reload only if favorite product IDs changed
guard viewModel.favoriteProductIDs != previousFavoriteProductIDs else {
return
}

await updatePredicate(filters: filters)

/// Reload because `updatePredicate` calls `performFetch` when creating a new predicate
tableView.reloadData()

paginationTracker.resync()
}
}

func updatePredicate(filters: FilterProductListViewModel.Filters) async {
let productIDs: [Int64]? = await {
guard filters.favoriteProduct != nil else {
return nil
}

await viewModel.loadFavoriteProductIDs()
return viewModel.favoriteProductIDs
}()

resultsController.updatePredicate(siteID: siteID,
stockStatus: filters.stockStatus,
productStatus: filters.productStatus,
productType: filters.promotableProductType?.productType,
productIDs: productIDs)
}
}

// MARK: - UITableViewDataSource Conformance
Expand Down Expand Up @@ -1175,10 +1219,15 @@ private extension ProductsViewController {
//
private extension ProductsViewController {
@objc private func pullToRefresh(sender: UIRefreshControl) {
ServiceLocator.analytics.track(.productListPulledToRefresh)
Task { @MainActor in

paginationTracker.resync {
sender.endRefreshing()
ServiceLocator.analytics.track(.productListPulledToRefresh)

await updatePredicate(filters: filters)

paginationTracker.resync {
sender.endRefreshing()
}
}
}

Expand Down Expand Up @@ -1374,7 +1423,8 @@ extension ProductsViewController: PaginationTrackerDelegate {
productStatus: filters.productStatus,
productType: filters.promotableProductType?.productType,
productCategory: filters.productCategory,
sortOrder: sortOrder) { [weak self] result in
sortOrder: sortOrder,
productIDs: (filters.favoriteProduct != nil) ? viewModel.favoriteProductIDs : []) { [weak self] result in
guard let self = self else {
return
}
Expand Down
8 changes: 6 additions & 2 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2893,6 +2893,7 @@
EEA1E2042AC1D22600A37ADD /* LegacyProductDetailPreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA1E2032AC1D22600A37ADD /* LegacyProductDetailPreviewViewModelTests.swift */; };
EEA1E20C2AC4639400A37ADD /* AddProductWithAIContainerViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA1E20B2AC4639400A37ADD /* AddProductWithAIContainerViewModelTests.swift */; };
EEA1E20E2AC55F2200A37ADD /* WooAnalyticsEvent+ProductCreationAI.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA1E20D2AC55F2200A37ADD /* WooAnalyticsEvent+ProductCreationAI.swift */; };
EEA3C2152CA3DB18000E82EC /* DefaultFavoriteProductsUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA3C2142CA3DB17000E82EC /* DefaultFavoriteProductsUseCaseTests.swift */; };
EEA6935C2B231C0C00BAECA6 /* ProductCreationAISurveyConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA6935B2B231C0C00BAECA6 /* ProductCreationAISurveyConfirmationView.swift */; };
EEA6935E2B231C6600BAECA6 /* ProductCreationAISurveyConfirmationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA6935D2B231C6500BAECA6 /* ProductCreationAISurveyConfirmationViewModel.swift */; };
EEA693602B23303A00BAECA6 /* ProductCreationAISurveyUseCaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA6935F2B23303A00BAECA6 /* ProductCreationAISurveyUseCaseTests.swift */; };
Expand Down Expand Up @@ -5955,6 +5956,7 @@
EEA1E2032AC1D22600A37ADD /* LegacyProductDetailPreviewViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyProductDetailPreviewViewModelTests.swift; sourceTree = "<group>"; };
EEA1E20B2AC4639400A37ADD /* AddProductWithAIContainerViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProductWithAIContainerViewModelTests.swift; sourceTree = "<group>"; };
EEA1E20D2AC55F2200A37ADD /* WooAnalyticsEvent+ProductCreationAI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WooAnalyticsEvent+ProductCreationAI.swift"; sourceTree = "<group>"; };
EEA3C2142CA3DB17000E82EC /* DefaultFavoriteProductsUseCaseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultFavoriteProductsUseCaseTests.swift; sourceTree = "<group>"; };
EEA6935B2B231C0C00BAECA6 /* ProductCreationAISurveyConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCreationAISurveyConfirmationView.swift; sourceTree = "<group>"; };
EEA6935D2B231C6500BAECA6 /* ProductCreationAISurveyConfirmationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCreationAISurveyConfirmationViewModel.swift; sourceTree = "<group>"; };
EEA6935F2B23303A00BAECA6 /* ProductCreationAISurveyUseCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCreationAISurveyUseCaseTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6239,6 +6241,7 @@
020B2F9723BDF2D000BD79AD /* Edit Product */ = {
isa = PBXGroup;
children = (
EEA3C2142CA3DB17000E82EC /* DefaultFavoriteProductsUseCaseTests.swift */,
CE91BE9E29FBE9BD00B6E1AF /* Quantity Rules */,
CCE64E3C29EEA9C500C1C937 /* Subscription */,
CC09A91029CB1AB700D6C4AD /* Composite Products */,
Expand Down Expand Up @@ -6330,6 +6333,7 @@
0262DA5523A23AA40029AF30 /* Shipping Settings */,
45B9C63B23A8E4DB007FC4C5 /* Edit Price */,
265BCA032430E5EA004E53EE /* Edit Categories */,
EE289AE12C988FF0004AB1A6 /* FavoriteProducts */,
4592A54824BF58A200BC3DE0 /* Edit Tags */,
45E9A6E124DAE18E00A600E8 /* Reviews */,
CC01CE5B29B2342E004FF537 /* Bundled Products */,
Expand Down Expand Up @@ -8897,7 +8901,6 @@
4592A54824BF58A200BC3DE0 /* Edit Tags */ = {
isa = PBXGroup;
children = (
EE289AE12C988FF0004AB1A6 /* FavoriteProducts */,
4592A54924BF58DD00BC3DE0 /* ProductTagsViewController.swift */,
4592A54A24BF58DD00BC3DE0 /* ProductTagsViewController.xib */,
450C2CAF24CF006A00D570DD /* ProductTagsDataSource.swift */,
Expand Down Expand Up @@ -13183,7 +13186,7 @@
EE289AE02C988FF0004AB1A6 /* FavoriteProductsUseCase.swift */,
);
name = FavoriteProducts;
path = Classes/ViewRelated/Products/FavoriteProducts;
path = "Classes/ViewRelated/Products/Edit Product/FavoriteProducts";
sourceTree = SOURCE_ROOT;
};
EE3272A229A88F670015F8D0 /* Onboarding */ = {
Expand Down Expand Up @@ -17041,6 +17044,7 @@
B90DACC22A31BBC800365897 /* BarcodeSKUScannerProductFinderTests.swift in Sources */,
D88D5A3B230B5D63007B6E01 /* MockAnalyticsProvider.swift in Sources */,
CE5757AF2B7E7F7400AEEB6D /* AnalyticsHubCustomizeViewModelTests.swift in Sources */,
EEA3C2152CA3DB18000E82EC /* DefaultFavoriteProductsUseCaseTests.swift in Sources */,
6885E2CE2C32B2E2004C8D70 /* TotalsViewModelTests.swift in Sources */,
EE1905822B50289100617C53 /* BlazeCampaignCreationFormViewModelTests.swift in Sources */,
B9C4AB29280031AB007008B8 /* PaymentReceiptEmailParameterDeterminerTests.swift in Sources */,
Expand Down
Loading

0 comments on commit ed87ae5

Please sign in to comment.