-
Notifications
You must be signed in to change notification settings - Fork 136
Issue/woomob 1351 add logic to show or hide the booking tab dynamically #14640
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
Merged
JorgeMucientes
merged 23 commits into
trunk
from
issue/woomob-1351-add-logic-to-show-or-hide-the-booking-tab-dynamically
Sep 24, 2025
Merged
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
c38a541
Add logs to error case when failing to check Bookings tab visibility
JorgeMucientes c89ba2d
Add new extension function to check site is CIAB
JorgeMucientes 84ff7b1
Check if current site is CIAB before displaying bookings tab
JorgeMucientes f99c836
Refactor extension function to apply only to SiteModel
JorgeMucientes 838ea0f
Link Bookings tab visibility to Bookable published bookable products
JorgeMucientes aed415a
Add logs when refreshing bookable products fails
JorgeMucientes ccf36cd
Rename class
JorgeMucientes cab6f52
Add tests
JorgeMucientes f3b0973
Add unit tests
JorgeMucientes 802ffd6
Ensure site is selected before observing bookings count
JorgeMucientes a120eed
Fix unit tests after refactor
JorgeMucientes eb13d9e
Move new is isCIABSite to the correct file
JorgeMucientes 6c84c24
Use filterNotNull function
JorgeMucientes 5610a84
Remove unneeded nullable expression and simplified boolean condition
JorgeMucientes 2ad102a
Avoid fetching bookings over the network if site is not CIAB
JorgeMucientes 41a3fc6
Make onStart logic async to avoid delaying the emission of first event
JorgeMucientes a3e6f53
Reactive implementation enables removing lifecycle observer
JorgeMucientes 9e03aae
Fix unit tests
JorgeMucientes f05fba9
Remove nesting flow collection to avoid potential issues
JorgeMucientes 7a5b2c0
Fix detekt issue
JorgeMucientes e1227bc
Moves logic to observe site changes to ObserveBookingsTabVisibility
JorgeMucientes 81a856c
Fix unit tests
JorgeMucientes 34a3bb1
Fix detekt indentation issue
JorgeMucientes File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
...e/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| package com.woocommerce.android.ui.bookings.tab | ||
|
|
||
| import androidx.annotation.VisibleForTesting | ||
| import com.woocommerce.android.di.AppCoroutineScope | ||
| import com.woocommerce.android.extensions.isCIABSite | ||
| import com.woocommerce.android.tools.SelectedSite | ||
| import com.woocommerce.android.ui.products.ProductStatus | ||
| import com.woocommerce.android.ui.products.list.ProductListRepository | ||
| import com.woocommerce.android.util.FeatureFlag | ||
| import com.woocommerce.android.util.WooLog | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||
| import kotlinx.coroutines.flow.emitAll | ||
| import kotlinx.coroutines.flow.filterNotNull | ||
| import kotlinx.coroutines.flow.flatMapLatest | ||
| import kotlinx.coroutines.flow.flow | ||
| import kotlinx.coroutines.flow.map | ||
| import kotlinx.coroutines.flow.onStart | ||
| import kotlinx.coroutines.launch | ||
| import org.wordpress.android.fluxc.model.SiteModel | ||
| import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption | ||
| import javax.inject.Inject | ||
|
|
||
| class ObserveBookingsTabVisibility @Inject constructor( | ||
| private val productListRepository: ProductListRepository, | ||
| private val selectedSite: SelectedSite, | ||
| @AppCoroutineScope private val appCoroutineScope: CoroutineScope, | ||
| ) { | ||
|
|
||
| @OptIn(ExperimentalCoroutinesApi::class) | ||
| operator fun invoke(): Flow<Boolean> = | ||
| selectedSite.observe() | ||
| .filterNotNull() | ||
| .flatMapLatest { siteModel -> | ||
| observeBookingTabVisibility(siteModel) | ||
| } | ||
|
|
||
| private fun observeBookingTabVisibility(siteModel: SiteModel): Flow<Boolean> = flow { | ||
| val isCIABSite = FeatureFlag.BOOKINGS_MVP.isEnabled() && siteModel.isCIABSite() | ||
| if (!isCIABSite) { | ||
| emit(false) | ||
| } else { | ||
| emitAll( | ||
| productListRepository.observeProductsCount( | ||
| filterOptions = mapOf( | ||
| ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, | ||
| ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE | ||
| ), | ||
| excludeSampleProducts = true | ||
| ) | ||
| .map { count -> count > 0 } | ||
| .onStart { | ||
| appCoroutineScope.launch { | ||
| productListRepository.fetchProductList( | ||
| productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) | ||
| ).onFailure { | ||
| WooLog.w(WooLog.T.BOOKINGS, "Failed to fetch bookable products") | ||
| } | ||
| } | ||
| }.distinctUntilChanged() | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| companion object { | ||
| @VisibleForTesting | ||
| const val BOOKING_PRODUCT_TYPE = "booking" | ||
| } | ||
| } |
13 changes: 0 additions & 13 deletions
13
WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
...c/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| package com.woocommerce.android.ui.bookings.tab | ||
|
|
||
| import app.cash.turbine.test | ||
| import com.woocommerce.android.tools.SelectedSite | ||
| import com.woocommerce.android.ui.bookings.tab.ObserveBookingsTabVisibility.Companion.BOOKING_PRODUCT_TYPE | ||
| import com.woocommerce.android.ui.products.ProductStatus | ||
| import com.woocommerce.android.ui.products.list.ProductListRepository | ||
| import com.woocommerce.android.viewmodel.BaseUnitTest | ||
| import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
| import kotlinx.coroutines.flow.MutableStateFlow | ||
| import kotlinx.coroutines.test.TestScope | ||
| import org.junit.Assert.assertFalse | ||
| import org.junit.Assert.assertTrue | ||
| import org.junit.Test | ||
| import org.mockito.kotlin.any | ||
| import org.mockito.kotlin.anyOrNull | ||
| import org.mockito.kotlin.eq | ||
| import org.mockito.kotlin.mock | ||
| import org.mockito.kotlin.verify | ||
| import org.mockito.kotlin.whenever | ||
| import org.wordpress.android.fluxc.model.SiteModel | ||
| import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption | ||
|
|
||
| @OptIn(ExperimentalCoroutinesApi::class) | ||
| class ObserveBookingsTabVisibilityTest : BaseUnitTest() { | ||
| private val testScope = TestScope(coroutinesTestRule.testDispatcher) | ||
| private val selectedSite: SelectedSite = mock() | ||
| private val selectedSiteFlow = MutableStateFlow<SiteModel?>(null) | ||
| private val bookableProdsFilterOptions = mapOf( | ||
| ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, | ||
| ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE | ||
| ) | ||
| private val bookableProdsCountFlow = MutableStateFlow(0L) | ||
|
|
||
| private val productListRepository: ProductListRepository = mock { | ||
| on { | ||
| observeProductsCount( | ||
| filterOptions = bookableProdsFilterOptions, | ||
| excludeSampleProducts = true | ||
| ) | ||
| }.thenReturn(bookableProdsCountFlow) | ||
| } | ||
|
|
||
| private lateinit var sut: ObserveBookingsTabVisibility | ||
|
|
||
| suspend fun setup(prepareMocks: suspend () -> Unit = {}) { | ||
| prepareMocks() | ||
| whenever(selectedSite.observe()).thenReturn(selectedSiteFlow) | ||
| sut = ObserveBookingsTabVisibility( | ||
| productListRepository = productListRepository, | ||
| selectedSite = selectedSite, | ||
| appCoroutineScope = testScope, | ||
| ) | ||
| } | ||
|
|
||
| @Test | ||
| fun `given site is CIAB, when invoke is called, then bookable products are fetched`() = testBlocking { | ||
| bookableProdsCountFlow.value = 2 | ||
| selectedSiteFlow.value = ciabSite() | ||
| setup() | ||
|
|
||
| sut().test { | ||
| verify(productListRepository).fetchProductList( | ||
| loadMore = any(), | ||
| productFilterOptions = eq(mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE)), | ||
| excludedProductIds = any(), | ||
| sortType = anyOrNull() | ||
| ) | ||
| cancelAndIgnoreRemainingEvents() | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `given CIAB site and bookings products published, when invoke, then emits true`() = testBlocking { | ||
| bookableProdsCountFlow.value = 1 | ||
| selectedSiteFlow.value = ciabSite() | ||
| setup() | ||
|
|
||
| sut().test { | ||
| val showBookingTabValue = awaitItem() | ||
| assertTrue(showBookingTabValue) | ||
| cancelAndIgnoreRemainingEvents() | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `given CIAB site with zero booking products, when invoke, then emits false`() = testBlocking { | ||
| bookableProdsCountFlow.value = 0 | ||
| selectedSiteFlow.value = ciabSite() | ||
| setup() | ||
|
|
||
| sut().test { | ||
| val showBookingTabValue = awaitItem() | ||
| assertFalse(showBookingTabValue) | ||
| cancelAndIgnoreRemainingEvents() | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `given non-CIAB site, when invoke, then emits false`() = testBlocking { | ||
| bookableProdsCountFlow.value = 10 | ||
| selectedSiteFlow.value = nonCiabSite() | ||
| setup() | ||
|
|
||
| sut().test { | ||
| val showBookingTabValue = awaitItem() | ||
| assertFalse(showBookingTabValue) | ||
| cancelAndIgnoreRemainingEvents() | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `given non-Commerce Garden CIAB site, when invoke, then emits false`() = testBlocking { | ||
| bookableProdsCountFlow.value = 10 | ||
| selectedSiteFlow.value = nonCommerceGardenSite() | ||
| setup() | ||
|
|
||
| sut().test { | ||
| val showBookingTabValue = awaitItem() | ||
| assertFalse(showBookingTabValue) | ||
| cancelAndIgnoreRemainingEvents() | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `given same inputs produce same result, when values update, then only emits once`() = testBlocking { | ||
| bookableProdsCountFlow.value = 2 | ||
| selectedSiteFlow.value = ciabSite() | ||
| setup() | ||
|
|
||
| sut().test { | ||
| val firstEmission = awaitItem() | ||
| assertTrue(firstEmission) | ||
| // When inputs change but computed value remains true | ||
| bookableProdsCountFlow.value = 3 // still true | ||
| // Then no new emissions due to distinctUntilChanged | ||
| expectNoEvents() | ||
|
|
||
| // Now change to false | ||
| bookableProdsCountFlow.value = 0 | ||
| val secondEmission = awaitItem() | ||
| assertFalse(secondEmission) | ||
| cancelAndIgnoreRemainingEvents() | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| fun `given bookable products fetch fails onStart, when invoke, then emits based on persisted values`() = | ||
| testBlocking { | ||
| bookableProdsCountFlow.value = 1 | ||
| selectedSiteFlow.value = ciabSite() | ||
| setup() | ||
|
|
||
| sut().test { | ||
| assert(awaitItem()) | ||
| cancelAndIgnoreRemainingEvents() | ||
| } | ||
| } | ||
|
|
||
| private fun ciabSite(): SiteModel = SiteModel().apply { | ||
| setIsGardenSite(true) | ||
| gardenName = "commerce" | ||
| } | ||
|
|
||
| private fun nonCiabSite(): SiteModel = SiteModel().apply { | ||
| setIsGardenSite(false) | ||
| gardenName = "commerce" | ||
| } | ||
|
|
||
| private fun nonCommerceGardenSite(): SiteModel = SiteModel().apply { | ||
| setIsGardenSite(true) | ||
| gardenName = "other" | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not directly related to this PR, but just sharing an observation here, I think that given we use
activity.lifecycleScope.launchin this function, it's safe to call it directly frominitwithout making this class aLifecycleObserver,lifecycleScopealready takes care of this, and won't start the Coroutine until theActivityis started.