Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HACK week] Adds favorite products filter #14001

Merged
merged 20 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a6604e4
Remove product from favorite upon deletion.
selanthiraiyan Sep 19, 2024
b7ff0de
Add a way to load favorite product IDs.
selanthiraiyan Sep 19, 2024
928413c
Return favorite products only if feature flag is turned on.
selanthiraiyan Sep 19, 2024
0b2bc1c
Add favorite IDs to predicate.
selanthiraiyan Sep 19, 2024
988b945
New helper method to update predicate with favorite product IDs.
selanthiraiyan Sep 19, 2024
e2b6098
Update predicate with selected filters.
selanthiraiyan Sep 19, 2024
63b9770
Refresh predicate to reload favorite products upon pull to refresh.
selanthiraiyan Sep 19, 2024
c16256c
Pass favorite product IDs to remote if filter on.
selanthiraiyan Sep 19, 2024
069da08
Pass favorite product IDs to remote if filter on.
selanthiraiyan Sep 19, 2024
7eff56a
Test tha favorite product IDs are loaded correctly.
selanthiraiyan Sep 19, 2024
d290479
Test that it loads and sets favorite product IDs.
selanthiraiyan Sep 19, 2024
a7c10bb
Turn on feature flag for dev builds.
selanthiraiyan Sep 19, 2024
d32eb09
Add internal release notes for testing.
selanthiraiyan Sep 20, 2024
c468a3e
Reload products table view on view appear if we detect change in favo…
selanthiraiyan Sep 25, 2024
fa5c524
Reorder folders.
selanthiraiyan Sep 25, 2024
5563db9
Add back missing tests file.
selanthiraiyan Sep 25, 2024
1b9f83a
Update release notes.
selanthiraiyan Sep 25, 2024
9cc8d32
Merge branch 'feat/12274-favorite-remote-storage' into feat/12274-fav…
selanthiraiyan Sep 26, 2024
dbff344
Update release notes.
selanthiraiyan Sep 26, 2024
0180b75
Merge branch 'trunk' into feat/12274-favorite-products-logic
selanthiraiyan Sep 26, 2024
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
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
4 changes: 4 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
*** PLEASE FOLLOW THIS FORMAT: [<priority indicator, more stars = higher priority>] <description> [<PR URL>]
*** Use [*****] to indicate smoke tests of all critical flows should be run on the final IPA before release (e.g. major library or OS update).

20.6
-----
- [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
-----
- [*] Blaze: Schedule a reminder local notification asking to continue abandoned campaign creation flow. [https://github.com/woocommerce/woocommerce-ios/pull/13950]
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 @@ -2886,6 +2886,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 @@ -5941,6 +5942,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 @@ -6225,6 +6227,7 @@
020B2F9723BDF2D000BD79AD /* Edit Product */ = {
isa = PBXGroup;
children = (
EEA3C2142CA3DB17000E82EC /* DefaultFavoriteProductsUseCaseTests.swift */,
CE91BE9E29FBE9BD00B6E1AF /* Quantity Rules */,
CCE64E3C29EEA9C500C1C937 /* Subscription */,
CC09A91029CB1AB700D6C4AD /* Composite Products */,
Expand Down Expand Up @@ -6316,6 +6319,7 @@
0262DA5523A23AA40029AF30 /* Shipping Settings */,
45B9C63B23A8E4DB007FC4C5 /* Edit Price */,
265BCA032430E5EA004E53EE /* Edit Categories */,
EE289AE12C988FF0004AB1A6 /* FavoriteProducts */,
4592A54824BF58A200BC3DE0 /* Edit Tags */,
45E9A6E124DAE18E00A600E8 /* Reviews */,
CC01CE5B29B2342E004FF537 /* Bundled Products */,
Expand Down Expand Up @@ -8880,7 +8884,6 @@
4592A54824BF58A200BC3DE0 /* Edit Tags */ = {
isa = PBXGroup;
children = (
EE289AE12C988FF0004AB1A6 /* FavoriteProducts */,
4592A54924BF58DD00BC3DE0 /* ProductTagsViewController.swift */,
4592A54A24BF58DD00BC3DE0 /* ProductTagsViewController.xib */,
450C2CAF24CF006A00D570DD /* ProductTagsDataSource.swift */,
Expand Down Expand Up @@ -13146,7 +13149,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 @@ -16997,6 +17000,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