Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions Modules/Sources/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return buildConfig == .localDeveloper || buildConfig == .alpha
case .pointOfSaleLocalCatalogi1:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .ciabBookings:
return buildConfig == .localDeveloper || buildConfig == .alpha
default:
return true
}
Expand Down
4 changes: 4 additions & 0 deletions Modules/Sources/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,8 @@ public enum FeatureFlag: Int {
/// It syncs products and variations to local storage and display them in POS for quick access.
///
case pointOfSaleLocalCatalogi1

/// Enables a new Bookings tab for CIAB sites
///
case ciabBookings
}
5 changes: 4 additions & 1 deletion Modules/Sources/Networking/Remote/ProductsRemote.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public protocol ProductsRemoteProtocol {
pageNumber: Int,
pageSize: Int,
productStatus: ProductStatus?,
productType: ProductType?,
completion: @escaping (Result<[Int64], Error>) -> Void)
func loadNumberOfProducts(siteID: Int64) async throws -> Int64

Expand Down Expand Up @@ -593,12 +594,14 @@ public final class ProductsRemote: Remote, ProductsRemoteProtocol {
pageNumber: Int = Default.pageNumber,
pageSize: Int = Default.pageSize,
productStatus: ProductStatus? = nil,
productType: ProductType? = nil,
completion: @escaping (Result<[Int64], Error>) -> Void) {
let parameters = [
ParameterKey.page: String(pageNumber),
ParameterKey.perPage: String(pageSize),
ParameterKey.fields: ParameterKey.id,
ParameterKey.productStatus: productStatus?.rawValue ?? ""
ParameterKey.productStatus: productStatus?.rawValue ?? "",
ParameterKey.productType: productType?.rawValue ?? ""
].filter({ $0.value.isEmpty == false })

let path = Path.products
Expand Down
14 changes: 14 additions & 0 deletions Modules/Sources/Storage/Tools/StorageType+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,20 @@ public extension StorageType {
return allObjects(ofType: Product.self, matching: predicate, sortedBy: [descriptor])
}

/// Has stored Products for the provided siteID and optional requirements.
///
func hasProducts(siteID: Int64, status: String?, type: String?) -> Bool {
var predicates: [NSPredicate] = [\Product.siteID == siteID]
if let status {
predicates.append(\Product.statusKey == status)
}
if let type {
predicates.append(\Product.productTypeKey == type)
}
let combinedPredicate = NSCompoundPredicate(type: .and, subpredicates: predicates)
return firstObject(ofType: Product.self, matching: combinedPredicate) != nil
}

/// Retrieves all of the stored Products matching the provided array products ids from the provided SiteID
///
func loadProducts(siteID: Int64, productsIDs: [Int64]) -> [Product] {
Expand Down
1 change: 1 addition & 0 deletions Modules/Sources/Yosemite/Actions/ProductAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public enum ProductAction: Action {
///
case checkIfStoreHasProducts(siteID: Int64,
status: ProductStatus? = nil,
type: ProductType? = nil,
onCompletion: (Result<Bool, Error>) -> Void)

/// Identifies the language from the given string
Expand Down
19 changes: 9 additions & 10 deletions Modules/Sources/Yosemite/Stores/ProductStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ public class ProductStore: Store {
validateProductSKU(sku, siteID: siteID, onCompletion: onCompletion)
case let .replaceProductLocally(product, onCompletion):
replaceProductLocally(product: product, onCompletion: onCompletion)
case let .checkIfStoreHasProducts(siteID, status, onCompletion):
checkIfStoreHasProducts(siteID: siteID, status: status, onCompletion: onCompletion)
case let .checkIfStoreHasProducts(siteID, status, productType, onCompletion):
checkIfStoreHasProducts(siteID: siteID, status: status, productType: productType, onCompletion: onCompletion)
case let .identifyLanguage(siteID, string, feature, completion):
identifyLanguage(siteID: siteID,
string: string, feature: feature,
Expand Down Expand Up @@ -562,19 +562,18 @@ private extension ProductStore {
/// Checks if the store already has any products with the given status.
/// Returns `false` if the store has no products.
///
func checkIfStoreHasProducts(siteID: Int64, status: ProductStatus?, onCompletion: @escaping (Result<Bool, Error>) -> Void) {
func checkIfStoreHasProducts(siteID: Int64,
status: ProductStatus?,
productType: ProductType?,
onCompletion: @escaping (Result<Bool, Error>) -> Void) {
// Check for locally stored products first.
let storage = storageManager.viewStorage
if let products = storage.loadProducts(siteID: siteID), !products.isEmpty {
if let status, (products.filter { $0.statusKey == status.rawValue }.isEmpty) == false {
return onCompletion(.success(true))
} else if status == nil {
return onCompletion(.success(true))
}
if storage.hasProducts(siteID: siteID, status: status?.rawValue, type: productType?.rawValue) {
return onCompletion(.success(true))
}

// If there are no locally stored products, then check remote.
remote.loadProductIDs(for: siteID, pageNumber: 1, pageSize: 1, productStatus: status) { result in
remote.loadProductIDs(for: siteID, pageNumber: 1, pageSize: 1, productStatus: status, productType: productType) { result in
switch result {
case .success(let ids):
onCompletion(.success(ids.isEmpty == false))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ extension MockProductsRemote: ProductsRemoteProtocol {
pageNumber: Int,
pageSize: Int,
productStatus: ProductStatus?,
productType: ProductType?,
completion: @escaping (Result<[Int64], Error>) -> Void) {
// no-op
}
Expand Down
20 changes: 20 additions & 0 deletions Modules/Tests/YosemiteTests/Stores/ProductStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1898,6 +1898,26 @@ final class ProductStoreTests: XCTestCase {
XCTAssertFalse(hasPublishedProducts)
}

func test_checkIfStoreHasProducts_returns_expected_result_when_local_storage_has_no_product_of_given_product_type_and_remote_returns_empty_array() throws {
// Given
storageManager.insertSampleProduct(readOnlyProduct: Product.fake().copy(siteID: sampleSiteID, productTypeKey: "simple"))
let productStore = ProductStore(dispatcher: dispatcher, storageManager: storageManager, network: network)
network.simulateResponse(requestUrlSuffix: "products", filename: "products-ids-only-empty")

// When
let result: Result<Bool, Error> = waitFor { promise in
let action = ProductAction.checkIfStoreHasProducts(siteID: self.sampleSiteID, type: .booking) { result in
promise(result)
}
productStore.onAction(action)
}

// Then
XCTAssertTrue(result.isSuccess)
let hasBookableProducts = try XCTUnwrap(result.get())
XCTAssertFalse(hasBookableProducts)
}

// MARK: - ProductAction.generateProductDescription

func test_generateProductDescription_returns_text_on_success() throws {
Expand Down
85 changes: 85 additions & 0 deletions WooCommerce/Classes/Bookings/BookingsTabEligibilityChecker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// periphery:ignore:all
import Foundation
import Yosemite
import Experiments

protocol BookingsTabEligibilityCheckerProtocol {
/// Checks the initial visibility of the Bookings tab.
func checkInitialVisibility() -> Bool
/// Checks the final visibility of the Bookings tab.
func checkVisibility() async -> Bool
}

final class BookingsTabEligibilityChecker: BookingsTabEligibilityCheckerProtocol {
private let site: Site
private let stores: StoresManager
private let featureFlagService: FeatureFlagService
private let ciabEligibilityChecker: CIABEligibilityCheckerProtocol
private let userDefaults: UserDefaults

init(site: Site,
stores: StoresManager = ServiceLocator.stores,
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
ciabEligibilityChecker: CIABEligibilityCheckerProtocol = CIABEligibilityChecker(),
userDefaults: UserDefaults = .standard) {
self.site = site
self.stores = stores
self.featureFlagService = featureFlagService
self.ciabEligibilityChecker = ciabEligibilityChecker
self.userDefaults = userDefaults
}

/// Checks the initial visibility of the Bookings tab using cached result.
func checkInitialVisibility() -> Bool {
userDefaults.loadCachedBookingsTabVisibility(siteID: site.siteID)
}

/// Checks the final visibility of the Bookings tab.
func checkVisibility() async -> Bool {
// Check feature flag
guard featureFlagService.isFeatureFlagEnabled(.ciabBookings) else {
return false
}

// Check if current site is NOT CIAB (bookings only for non-CIAB sites)
guard ciabEligibilityChecker.isSiteCIAB(site) else {
return false
}

// Cache the result
let isVisible = await checkIfStoreHasBookableProducts()
userDefaults.cacheBookingsTabVisibility(siteID: site.siteID, isVisible: isVisible)

return isVisible
}
}

// MARK: Private helpers
//
private extension BookingsTabEligibilityChecker {
@MainActor
func checkIfStoreHasBookableProducts() async -> Bool {
await withCheckedContinuation { continuation in
stores.dispatch(ProductAction.checkIfStoreHasProducts(siteID: site.siteID, type: .booking) { result in
let hasBookableProducts = (try? result.get()) ?? false
continuation.resume(returning: hasBookableProducts)
})
}
}
}

extension UserDefaults {
func loadCachedBookingsTabVisibility(siteID: Int64) -> Bool {
guard let cachedValue = self[.ciabBookingsTabAvailable] as? [String: Bool],
let availability = cachedValue[siteID.description] else {
return false
}
return availability
}

func cacheBookingsTabVisibility(siteID: Int64, isVisible: Bool) {
var cache = (self[.ciabBookingsTabAvailable] as? [String: Bool]) ?? [:]
cache[siteID.description] = isVisible
self[.ciabBookingsTabAvailable] = cache
}
}
3 changes: 3 additions & 0 deletions WooCommerce/Classes/Extensions/UserDefaults+Woo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ extension UserDefaults {

// Application passwords experiment remote FF cached value
case applicationPasswordsExperimentRemoteFFValue

// CIAB Bookings tab availability
case ciabBookingsTabAvailable
}
}

Expand Down
1 change: 1 addition & 0 deletions WooCommerce/Classes/System/SessionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ final class SessionManager: SessionManagerProtocol {
defaults[.tapToPayAwarenessMomentFirstLaunchCompleted] = nil
defaults[.applicationPasswordUnsupportedList] = nil
defaults[.applicationPasswordsExperimentRemoteFFValue] = nil
defaults[.ciabBookingsTabAvailable] = nil
resetTimestampsValues()
imageCache.clearCache()
}
Expand Down
15 changes: 15 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2862,6 +2862,7 @@
DEDA8DBE2B19952B0076BF0F /* ThemeSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDA8DBD2B19952B0076BF0F /* ThemeSettingViewModel.swift */; };
DEDA8DC02B19CDC50076BF0F /* ThemeSettingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDA8DBF2B19CDC50076BF0F /* ThemeSettingViewModelTests.swift */; };
DEDB2D262845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDB2D252845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift */; };
DEDB5D392E7AC0670022E5A1 /* BookingsTabEligibilityCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDB5D372E7AC0670022E5A1 /* BookingsTabEligibilityCheckerTests.swift */; };
DEDB886B26E8531E00981595 /* ShippingLabelPackageAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDB886A26E8531E00981595 /* ShippingLabelPackageAttributes.swift */; };
DEDC5F5E2D9E7CD5005E38BD /* WooShippingShipmentDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDC5F5D2D9E7CD5005E38BD /* WooShippingShipmentDetailsView.swift */; };
DEDC5F602D9E7CEA005E38BD /* WooShippingShipmentDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDC5F5F2D9E7CDD005E38BD /* WooShippingShipmentDetailsViewModel.swift */; };
Expand Down Expand Up @@ -6099,6 +6100,7 @@
DEDA8DBD2B19952B0076BF0F /* ThemeSettingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingViewModel.swift; sourceTree = "<group>"; };
DEDA8DBF2B19CDC50076BF0F /* ThemeSettingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSettingViewModelTests.swift; sourceTree = "<group>"; };
DEDB2D252845D31900CE7D35 /* CouponAllowedEmailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CouponAllowedEmailsViewModel.swift; sourceTree = "<group>"; };
DEDB5D372E7AC0670022E5A1 /* BookingsTabEligibilityCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookingsTabEligibilityCheckerTests.swift; sourceTree = "<group>"; };
DEDB886A26E8531E00981595 /* ShippingLabelPackageAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelPackageAttributes.swift; sourceTree = "<group>"; };
DEDC5F5D2D9E7CD5005E38BD /* WooShippingShipmentDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingShipmentDetailsView.swift; sourceTree = "<group>"; };
DEDC5F5F2D9E7CDD005E38BD /* WooShippingShipmentDetailsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WooShippingShipmentDetailsViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6406,6 +6408,7 @@
3F0904022D26A40800D8ACCE /* WordPressAuthenticator */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3F09041C2D26A40800D8ACCE /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WordPressAuthenticator; sourceTree = "<group>"; };
3F09040E2D26A40800D8ACCE /* WordPressAuthenticatorTests */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3FD28D5E2D271391002EBB3D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WordPressAuthenticatorTests; sourceTree = "<group>"; };
3FD9BFBE2E0A2533004A8DC8 /* WooCommerceScreenshots */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3FD9BFC42E0A2534004A8DC8 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = WooCommerceScreenshots; sourceTree = "<group>"; };
DEDB5D342E7A68950022E5A1 /* Bookings */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Bookings; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -10845,6 +10848,7 @@
B958A7CF28B527FB00823EEF /* Universal Links */,
D8F01DD125DEDC0100CE70BE /* Stripe Integration Tests */,
5791FB4024EC833200117FD6 /* ViewModels */,
DEDB5D382E7AC0670022E5A1 /* Bookings */,
57C2F6E324C27B0C00131012 /* Authentication */,
CCDC49F1240060F3003166BA /* UnitTests.xctestplan */,
D816DDBA22265D8000903E59 /* ViewRelated */,
Expand Down Expand Up @@ -10919,6 +10923,7 @@
B56DB3F12049C0B800D4AA8E /* Classes */ = {
isa = PBXGroup;
children = (
DEDB5D342E7A68950022E5A1 /* Bookings */,
2DCB54F82E6AE8C900621F90 /* CIAB */,
DEB387932C2E7A540025256E /* GoogleAds */,
20AE33C32B0510AD00527B60 /* Destinations */,
Expand Down Expand Up @@ -13755,6 +13760,14 @@
path = Themes;
sourceTree = "<group>";
};
DEDB5D382E7AC0670022E5A1 /* Bookings */ = {
isa = PBXGroup;
children = (
DEDB5D372E7AC0670022E5A1 /* BookingsTabEligibilityCheckerTests.swift */,
);
path = Bookings;
sourceTree = "<group>";
};
DEDC5F5C2D9E7CB8005E38BD /* ShipmentDetails */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -14466,6 +14479,7 @@
);
fileSystemSynchronizedGroups = (
2D33E6B02DD1453E000C7198 /* WooShippingPaymentMethod */,
DEDB5D342E7A68950022E5A1 /* Bookings */,
);
name = WooCommerce;
packageProductDependencies = (
Expand Down Expand Up @@ -17735,6 +17749,7 @@
EE57C121297E76E000BC31E7 /* TrackEventRequestNotificationHandlerTests.swift in Sources */,
D8A8C4F32268288F001C72BF /* AddManualCustomTrackingViewModelTests.swift in Sources */,
269B46642A16D6ED00ADA872 /* UpdateAnalyticsSettingsUseCaseTests.swift in Sources */,
DEDB5D392E7AC0670022E5A1 /* BookingsTabEligibilityCheckerTests.swift in Sources */,
AEFF77A829786A2900667F7A /* PriceInputViewControllerTests.swift in Sources */,
02C2756F24F5F5EE00286C04 /* ProductShippingSettingsViewModel+ProductVariationTests.swift in Sources */,
EEBB9B402D8FE5B6008D6CE5 /* WooShippingSplitShipmentsViewModelTests.swift in Sources */,
Expand Down
Loading