Skip to content

Commit 5719ed7

Browse files
authored
CIAB Bookings: Add eligibility check for Bookings tab (#16139)
2 parents 3b620a9 + bd9d56d commit 5719ed7

File tree

15 files changed

+466
-12
lines changed

15 files changed

+466
-12
lines changed

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
104104
return buildConfig == .localDeveloper || buildConfig == .alpha
105105
case .pointOfSaleLocalCatalogi1:
106106
return buildConfig == .localDeveloper || buildConfig == .alpha
107+
case .ciabBookings:
108+
return buildConfig == .localDeveloper || buildConfig == .alpha
107109
default:
108110
return true
109111
}

Modules/Sources/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,8 @@ public enum FeatureFlag: Int {
215215
/// It syncs products and variations to local storage and display them in POS for quick access.
216216
///
217217
case pointOfSaleLocalCatalogi1
218+
219+
/// Enables a new Bookings tab for CIAB sites
220+
///
221+
case ciabBookings
218222
}

Modules/Sources/Networking/Remote/ProductsRemote.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public protocol ProductsRemoteProtocol {
4848
pageNumber: Int,
4949
pageSize: Int,
5050
productStatus: ProductStatus?,
51+
productType: ProductType?,
5152
completion: @escaping (Result<[Int64], Error>) -> Void)
5253
func loadNumberOfProducts(siteID: Int64) async throws -> Int64
5354

@@ -593,12 +594,14 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
593594
pageNumber: Int = Default.pageNumber,
594595
pageSize: Int = Default.pageSize,
595596
productStatus: ProductStatus? = nil,
597+
productType: ProductType? = nil,
596598
completion: @escaping (Result<[Int64], Error>) -> Void) {
597599
let parameters = [
598600
ParameterKey.page: String(pageNumber),
599601
ParameterKey.perPage: String(pageSize),
600602
ParameterKey.fields: ParameterKey.id,
601-
ParameterKey.productStatus: productStatus?.rawValue ?? ""
603+
ParameterKey.productStatus: productStatus?.rawValue ?? "",
604+
ParameterKey.productType: productType?.rawValue ?? ""
602605
].filter({ $0.value.isEmpty == false })
603606

604607
let path = Path.products

Modules/Sources/Storage/Tools/StorageType+Extensions.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,20 @@ public extension StorageType {
271271
return allObjects(ofType: Product.self, matching: predicate, sortedBy: [descriptor])
272272
}
273273

274+
/// Has stored Products for the provided siteID and optional requirements.
275+
///
276+
func hasProducts(siteID: Int64, status: String?, type: String?) -> Bool {
277+
var predicates: [NSPredicate] = [\Product.siteID == siteID]
278+
if let status {
279+
predicates.append(\Product.statusKey == status)
280+
}
281+
if let type {
282+
predicates.append(\Product.productTypeKey == type)
283+
}
284+
let combinedPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
285+
return firstObject(ofType: Product.self, matching: combinedPredicate) != nil
286+
}
287+
274288
/// Retrieves all of the stored Products matching the provided array products ids from the provided SiteID
275289
///
276290
func loadProducts(siteID: Int64, productsIDs: [Int64]) -> [Product] {

Modules/Sources/Yosemite/Actions/ProductAction.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ public enum ProductAction: Action {
124124
///
125125
case checkIfStoreHasProducts(siteID: Int64,
126126
status: ProductStatus? = nil,
127+
type: ProductType? = nil,
127128
onCompletion: (Result<Bool, Error>) -> Void)
128129

129130
/// Identifies the language from the given string

Modules/Sources/Yosemite/Stores/ProductStore.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ public class ProductStore: Store {
120120
validateProductSKU(sku, siteID: siteID, onCompletion: onCompletion)
121121
case let .replaceProductLocally(product, onCompletion):
122122
replaceProductLocally(product: product, onCompletion: onCompletion)
123-
case let .checkIfStoreHasProducts(siteID, status, onCompletion):
124-
checkIfStoreHasProducts(siteID: siteID, status: status, onCompletion: onCompletion)
123+
case let .checkIfStoreHasProducts(siteID, status, productType, onCompletion):
124+
checkIfStoreHasProducts(siteID: siteID, status: status, productType: productType, onCompletion: onCompletion)
125125
case let .identifyLanguage(siteID, string, feature, completion):
126126
identifyLanguage(siteID: siteID,
127127
string: string, feature: feature,
@@ -562,19 +562,18 @@ private extension ProductStore {
562562
/// Checks if the store already has any products with the given status.
563563
/// Returns `false` if the store has no products.
564564
///
565-
func checkIfStoreHasProducts(siteID: Int64, status: ProductStatus?, onCompletion: @escaping (Result<Bool, Error>) -> Void) {
565+
func checkIfStoreHasProducts(siteID: Int64,
566+
status: ProductStatus?,
567+
productType: ProductType?,
568+
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
566569
// Check for locally stored products first.
567570
let storage = storageManager.viewStorage
568-
if let products = storage.loadProducts(siteID: siteID), !products.isEmpty {
569-
if let status, (products.filter { $0.statusKey == status.rawValue }.isEmpty) == false {
570-
return onCompletion(.success(true))
571-
} else if status == nil {
572-
return onCompletion(.success(true))
573-
}
571+
if storage.hasProducts(siteID: siteID, status: status?.rawValue, type: productType?.rawValue) {
572+
return onCompletion(.success(true))
574573
}
575574

576575
// If there are no locally stored products, then check remote.
577-
remote.loadProductIDs(for: siteID, pageNumber: 1, pageSize: 1, productStatus: status) { result in
576+
remote.loadProductIDs(for: siteID, pageNumber: 1, pageSize: 1, productStatus: status, productType: productType) { result in
578577
switch result {
579578
case .success(let ids):
580579
onCompletion(.success(ids.isEmpty == false))

Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockProductsRemote.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ extension MockProductsRemote: ProductsRemoteProtocol {
370370
pageNumber: Int,
371371
pageSize: Int,
372372
productStatus: ProductStatus?,
373+
productType: ProductType?,
373374
completion: @escaping (Result<[Int64], Error>) -> Void) {
374375
// no-op
375376
}

Modules/Tests/YosemiteTests/Stores/ProductStoreTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,6 +1898,26 @@ final class ProductStoreTests: XCTestCase {
18981898
XCTAssertFalse(hasPublishedProducts)
18991899
}
19001900

1901+
func test_checkIfStoreHasProducts_returns_expected_result_when_local_storage_has_no_product_of_given_product_type_and_remote_returns_empty_array() throws {
1902+
// Given
1903+
storageManager.insertSampleProduct(readOnlyProduct: Product.fake().copy(siteID: sampleSiteID, productTypeKey: "simple"))
1904+
let productStore = ProductStore(dispatcher: dispatcher, storageManager: storageManager, network: network)
1905+
network.simulateResponse(requestUrlSuffix: "products", filename: "products-ids-only-empty")
1906+
1907+
// When
1908+
let result: Result<Bool, Error> = waitFor { promise in
1909+
let action = ProductAction.checkIfStoreHasProducts(siteID: self.sampleSiteID, type: .booking) { result in
1910+
promise(result)
1911+
}
1912+
productStore.onAction(action)
1913+
}
1914+
1915+
// Then
1916+
XCTAssertTrue(result.isSuccess)
1917+
let hasBookableProducts = try XCTUnwrap(result.get())
1918+
XCTAssertFalse(hasBookableProducts)
1919+
}
1920+
19011921
// MARK: - ProductAction.generateProductDescription
19021922

19031923
func test_generateProductDescription_returns_text_on_success() throws {
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// periphery:ignore:all
2+
import Foundation
3+
import Yosemite
4+
import Experiments
5+
6+
protocol BookingsTabEligibilityCheckerProtocol {
7+
/// Checks the initial visibility of the Bookings tab.
8+
func checkInitialVisibility() -> Bool
9+
/// Checks the final visibility of the Bookings tab.
10+
func checkVisibility() async -> Bool
11+
}
12+
13+
final class BookingsTabEligibilityChecker: BookingsTabEligibilityCheckerProtocol {
14+
private let site: Site
15+
private let stores: StoresManager
16+
private let featureFlagService: FeatureFlagService
17+
private let ciabEligibilityChecker: CIABEligibilityCheckerProtocol
18+
private let userDefaults: UserDefaults
19+
20+
init(site: Site,
21+
stores: StoresManager = ServiceLocator.stores,
22+
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
23+
ciabEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker(),
24+
userDefaults: UserDefaults = .standard) {
25+
self.site = site
26+
self.stores = stores
27+
self.featureFlagService = featureFlagService
28+
self.ciabEligibilityChecker = ciabEligibilityChecker
29+
self.userDefaults = userDefaults
30+
}
31+
32+
/// Checks the initial visibility of the Bookings tab using cached result.
33+
func checkInitialVisibility() -> Bool {
34+
userDefaults.loadCachedBookingsTabVisibility(siteID: site.siteID)
35+
}
36+
37+
/// Checks the final visibility of the Bookings tab.
38+
func checkVisibility() async -> Bool {
39+
// Check feature flag
40+
guard featureFlagService.isFeatureFlagEnabled(.ciabBookings) else {
41+
return false
42+
}
43+
44+
// Check if current site is NOT CIAB (bookings only for non-CIAB sites)
45+
guard ciabEligibilityChecker.isSiteCIAB(site) else {
46+
return false
47+
}
48+
49+
// Cache the result
50+
let isVisible = await checkIfStoreHasBookableProducts()
51+
userDefaults.cacheBookingsTabVisibility(siteID: site.siteID, isVisible: isVisible)
52+
53+
return isVisible
54+
}
55+
}
56+
57+
// MARK: Private helpers
58+
//
59+
private extension BookingsTabEligibilityChecker {
60+
@MainActor
61+
func checkIfStoreHasBookableProducts() async -> Bool {
62+
await withCheckedContinuation { continuation in
63+
stores.dispatch(ProductAction.checkIfStoreHasProducts(siteID: site.siteID, type: .booking) { result in
64+
let hasBookableProducts = (try? result.get()) ?? false
65+
continuation.resume(returning: hasBookableProducts)
66+
})
67+
}
68+
}
69+
}
70+
71+
extension UserDefaults {
72+
func loadCachedBookingsTabVisibility(siteID: Int64) -> Bool {
73+
guard let cachedValue = self[.ciabBookingsTabAvailable] as? [String: Bool],
74+
let availability = cachedValue[siteID.description] else {
75+
return false
76+
}
77+
return availability
78+
}
79+
80+
func cacheBookingsTabVisibility(siteID: Int64, isVisible: Bool) {
81+
var cache = (self[.ciabBookingsTabAvailable] as? [String: Bool]) ?? [:]
82+
cache[siteID.description] = isVisible
83+
self[.ciabBookingsTabAvailable] = cache
84+
}
85+
}

WooCommerce/Classes/Extensions/UserDefaults+Woo.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ extension UserDefaults {
7373

7474
// Application passwords experiment remote FF cached value
7575
case applicationPasswordsExperimentRemoteFFValue
76+
77+
// CIAB Bookings tab availability
78+
case ciabBookingsTabAvailable
7679
}
7780
}
7881

0 commit comments

Comments
 (0)