diff --git a/Modules/Sources/Yosemite/Actions/BookingAction.swift b/Modules/Sources/Yosemite/Actions/BookingAction.swift index 5f710654e74..f386d962beb 100644 --- a/Modules/Sources/Yosemite/Actions/BookingAction.swift +++ b/Modules/Sources/Yosemite/Actions/BookingAction.swift @@ -103,4 +103,12 @@ public enum BookingAction: Action { bookingID: Int64, note: String, onCompletion: (Error?) -> Void) + + + /// Clears the booking cache. + /// + /// - Parameter siteID: The site ID of the booking. + /// - Parameter onCompletion: Called when clear completes. + case clearBookingsCache(siteID: Int64, + onCompletion: () -> Void) } diff --git a/Modules/Sources/Yosemite/Stores/BookingStore.swift b/Modules/Sources/Yosemite/Stores/BookingStore.swift index 4753075c4b2..78d1fff21e0 100644 --- a/Modules/Sources/Yosemite/Stores/BookingStore.swift +++ b/Modules/Sources/Yosemite/Stores/BookingStore.swift @@ -89,6 +89,8 @@ public class BookingStore: Store { note: note, onCompletion: onCompletion ) + case .clearBookingsCache(siteID: let siteID, onCompletion: let onCompletion): + clearBookingsCache(siteID: siteID, onCompletion: onCompletion) } } } @@ -205,12 +207,12 @@ private extension BookingStore { /// Returns results immediately without saving to storage. /// func searchBookings(siteID: Int64, - searchQuery: String, - pageNumber: Int, - pageSize: Int, - filters: BookingFilters?, - order: BookingsRemote.Order, - onCompletion: @escaping (Result<[Booking], Error>) -> Void) { + searchQuery: String, + pageNumber: Int, + pageSize: Int, + filters: BookingFilters?, + order: BookingsRemote.Order, + onCompletion: @escaping (Result<[Booking], Error>) -> Void) { Task { @MainActor in do { let bookings = try await remote.loadAllBookings(for: siteID, @@ -271,9 +273,9 @@ private extension BookingStore { /// Synchronizes booking resources for the specified site. /// func synchronizeResources(siteID: Int64, - pageNumber: Int, - pageSize: Int, - onCompletion: @escaping (Result) -> Void) { + pageNumber: Int, + pageSize: Int, + onCompletion: @escaping (Result) -> Void) { Task { @MainActor in do { let resources = try await remote.fetchResources( @@ -461,6 +463,12 @@ private extension BookingStore { } } } + + func clearBookingsCache(siteID: Int64, onCompletion: @escaping () -> Void) { + storageManager.performAndSave({ storage in + storage.deleteBookings(siteID: siteID) + }, completion: onCompletion, on: .main) + } } diff --git a/WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift b/WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift index a5bf4ca30f9..f221cab485f 100644 --- a/WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift +++ b/WooCommerce/Classes/Bookings/BookingList/BookingListContainerViewModel.swift @@ -24,6 +24,11 @@ final class BookingListContainerViewModel: ObservableObject { private var searchQuerySubscription: AnyCancellable? private var sortBySubscription: AnyCancellable? + private lazy var allTabViewModels: [BookingListViewModel] = [ + todayListViewModel, + upcomingListViewModel, + allListViewModel + ] private var filters = BookingFiltersViewModel.Filters() @@ -87,6 +92,10 @@ final class BookingListContainerViewModel: ObservableObject { } restorePersistedFilters() + + todayListViewModel.refreshCoordinator = self + upcomingListViewModel.refreshCoordinator = self + allListViewModel.refreshCoordinator = self } func listViewModel(for tab: BookingListTab) -> BookingListViewModel { @@ -181,6 +190,29 @@ private extension BookingListContainerViewModel { } } +extension BookingListContainerViewModel: BookingListsRefreshCoordinating { + @MainActor + func refreshAllLists() async { + await withCheckedContinuation { continuation in + let action = BookingAction.clearBookingsCache(siteID: siteID) { + continuation.resume() + } + stores.dispatch(action) + } + + // Launch all tab refreshes in parallel and wait for all to complete + await withTaskGroup(of: Void.self) { group in + for viewModel in allTabViewModels { + group.addTask { @MainActor in + await viewModel.reloadData( + reason: BookingListViewModel.siblingRefreshReason + ) + } + } + } + } +} + private extension BookingListContainerViewModel { enum Localization { static let filter = NSLocalizedString( diff --git a/WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift b/WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift index a071bf2d506..f32a5b9b736 100644 --- a/WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift +++ b/WooCommerce/Classes/Bookings/BookingList/BookingListViewModel.swift @@ -5,6 +5,10 @@ import Combine import protocol Storage.StorageManagerType import class Networking.BookingsRemote +protocol BookingListsRefreshCoordinating: AnyObject { + func refreshAllLists() async +} + /// View model for `BookingListView` final class BookingListViewModel: ObservableObject { @@ -14,6 +18,8 @@ final class BookingListViewModel: ObservableObject { @Published private(set) var hasFilters = false + weak var refreshCoordinator: BookingListsRefreshCoordinating? + var emptyStateTitle: String { type.emptyStateTitle(hasFilters: hasFilters) } @@ -32,6 +38,7 @@ final class BookingListViewModel: ObservableObject { private static let refreshCacheReason = "refresh-cache" private static let reorderReason = "reorder" + static let siblingRefreshReason = "sibling-refresh" /// Keeps track of the current state of the syncing @Published private(set) var syncState: SyncState = .empty @@ -58,6 +65,7 @@ final class BookingListViewModel: ObservableObject { init(siteID: Int64, type: BookingListTab, + parent: BookingListContainerViewModel? = nil, stores: StoresManager = ServiceLocator.stores, storage: StorageManagerType = ServiceLocator.storageManager, currentDate: Date = Date()) { @@ -96,10 +104,14 @@ final class BookingListViewModel: ObservableObject { } /// Called when the user pulls down the list to refresh. - @MainActor func onRefreshAction() async { + await refreshCoordinator?.refreshAllLists() + } + + @MainActor + func reloadData(reason: String = BookingListViewModel.refreshCacheReason) async { await withCheckedContinuation { continuation in - paginationTracker.resync(reason: Self.refreshCacheReason) { + paginationTracker.resync(reason: reason) { continuation.resume(returning: ()) } } diff --git a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingListViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingListViewModelTests.swift index c294f081b43..834c61052fa 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingListViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/Bookings/BookingListViewModelTests.swift @@ -307,7 +307,7 @@ struct BookingListViewModelTests { let viewModel = BookingListViewModel(siteID: sampleSiteID, type: .all, stores: stores) // When - await viewModel.onRefreshAction() + await viewModel.reloadData() // Then #expect(skip == 0) @@ -441,26 +441,18 @@ struct BookingListViewModelTests { #expect(actionCallCount == 2, "Should have made two API calls") } - @Test func on_refresh_action_clears_cache() async { + @Test func on_refresh_action_calls_refreshcoordiantor() async { // Given let stores = MockStoresManager(sessionManager: .testingInstance) - var capturedShouldClearCache: Bool? - - stores.whenReceivingAction(ofType: BookingAction.self) { action in - guard case let .synchronizeBookings(_, _, _, _, _, shouldClearCache, onCompletion) = action else { - return - } - capturedShouldClearCache = shouldClearCache - onCompletion(.success(false)) - } - + let mockRefresher = MockBookingListsRefreshCoordinating() let viewModel = BookingListViewModel(siteID: sampleSiteID, type: .all, stores: stores) + viewModel.refreshCoordinator = mockRefresher // When await viewModel.onRefreshAction() // Then - #expect(capturedShouldClearCache == true, "Refresh action should clear cache") + #expect(mockRefresher.refreshAllListsCalled) } // MARK: - Local storage filtering @@ -719,3 +711,12 @@ private extension BookingListViewModelTests { return Booking.fake().copy(siteID: siteID ?? self.sampleSiteID, bookingID: id, startDate: startDate) } } + + +class MockBookingListsRefreshCoordinating: BookingListsRefreshCoordinating { + private(set) var refreshAllListsCalled = false + + func refreshAllLists() async { + refreshAllListsCalled = true + } +}