Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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
15 changes: 10 additions & 5 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,24 @@ 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 {
} else if let productType, products.contains(where: { $0.productTypeKey == productType.rawValue}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Should we also update StorageType.loadProducts() and include product type into query instead of doing local filtering after a storage fetch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thinking! I optimized the code in bd9d56d, let me know what you think @RafaelKayumov.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx! :shipit:

return onCompletion(.success(true))
} else if status == nil && productType == nil {
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