Skip to content

[Dynamic dashboard] Improve "has orders" logic #11469

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
merged 3 commits into from
May 9, 2024
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
52 changes: 42 additions & 10 deletions WooCommerce/src/main/kotlin/com/woocommerce/android/SiteObserver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ package com.woocommerce.android
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.ui.common.environment.EnvironmentRepository
import com.woocommerce.android.util.WooLog
import com.woocommerce.android.util.WooLog.T.UTILS
import com.woocommerce.android.util.dispatchAndAwait
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.generated.WCOrderActionBuilder
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrderStatusOptionsPayload
import org.wordpress.android.fluxc.store.WCOrderStore.OnOrderStatusOptionsChanged
import org.wordpress.android.fluxc.store.WooCommerceStore
import javax.inject.Inject

Expand All @@ -18,22 +28,44 @@ import javax.inject.Inject
class SiteObserver @Inject constructor(
private val selectedSite: SelectedSite,
private val wooCommerceStore: WooCommerceStore,
private val environmentRepository: EnvironmentRepository
private val environmentRepository: EnvironmentRepository,
private val dispatcher: Dispatcher
) {
suspend fun observeAndUpdateSelectedSiteData() {
selectedSite.observe()
.filterNotNull()
.distinctUntilChanged { old, new -> new.id == old.id }
.collect { site ->
WooLog.d(WooLog.T.UTILS, "Fetch plugins for site ${site.name}")
wooCommerceStore.fetchSitePlugins(site)
.collectLatest { site ->
coroutineScope {
launch { fetchPlugins(site) }
Comment on lines +39 to +40
Copy link
Member Author

Choose a reason for hiding this comment

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

The change to use coroutineScope with launch builders is to allow parallel requests.

Copy link
Contributor

Choose a reason for hiding this comment

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

Clever!


// Makes sure the store ID is fetched for the site.
environmentRepository.fetchOrGetStoreID(site)
.takeIf { result -> result.isError.not() }
?.model?.let { storeID ->
WooLog.d(WooLog.T.UTILS, "Fetched StoreID $storeID for site ${site.name}")
}
launch { fetchStoreId(site) }

launch { fetchOrderStatusOptions(site) }
}
}
}

private suspend fun fetchPlugins(site: SiteModel) {
WooLog.d(WooLog.T.UTILS, "Fetch plugins for site ${site.name}")
wooCommerceStore.fetchSitePlugins(site)
}

private suspend fun fetchStoreId(site: SiteModel) {
// Makes sure the store ID is fetched for the site.
environmentRepository.fetchOrGetStoreID(site)
.takeIf { result -> result.isError.not() }
?.model?.let { storeID ->
WooLog.d(UTILS, "Fetched StoreID $storeID for site ${site.name}")
}
}

private suspend fun fetchOrderStatusOptions(site: SiteModel) {
WooLog.d(WooLog.T.UTILS, "Fetch status options for site ${site.name}")
dispatcher.dispatchAndAwait<FetchOrderStatusOptionsPayload, OnOrderStatusOptionsChanged>(
WCOrderActionBuilder.newFetchOrderStatusOptionsAction(
FetchOrderStatusOptionsPayload(site)
)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,34 @@ package com.woocommerce.android.ui.dashboard.data
import com.woocommerce.android.R
import com.woocommerce.android.model.DashboardWidget
import com.woocommerce.android.tools.SelectedSite
import com.woocommerce.android.util.CoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.withContext
import org.wordpress.android.fluxc.store.WCOrderStore
import org.wordpress.android.fluxc.store.WCOrderStore.HasOrdersResult
import javax.inject.Inject

@OptIn(ExperimentalCoroutinesApi::class)
class ObserveStatsWidgetsStatus @Inject constructor(
private val selectedSite: SelectedSite,
private val orderStore: WCOrderStore
private val orderStore: WCOrderStore,
private val coroutineDispatchers: CoroutineDispatchers
) {
operator fun invoke() = selectedSite.observe()
.filterNotNull()
.flatMapLatest { orderStore.observeOrderCountForSite(it) }
.map { count -> count != 0 }
.distinctUntilChanged()
.transform { hasOrders ->
.transformLatest { hasOrders ->
if (!hasOrders) {
val fetchResult = orderStore.fetchHasOrders(selectedSite.get(), null).let {
when (it) {
is HasOrdersResult.Success -> it.hasOrders
// Default to true if we can't determine if there are orders
is HasOrdersResult.Failure -> true
}
}
emit(fetchResult)
// This means either the store doesn't have orders, or no orders are cached yet
// Use other approaches to determine if the store has orders
emit(getHasOrdersFromOrderStatusOptions() ?: fetchHasOrdersFromApi())
} else {
emit(true)
}
Expand All @@ -45,4 +43,22 @@ class ObserveStatsWidgetsStatus @Inject constructor(
)
}
}

private suspend fun getHasOrdersFromOrderStatusOptions() = withContext(coroutineDispatchers.io) {
orderStore.getOrderStatusOptionsForSite(selectedSite.get())
.filter { it.statusKey != "checkout-draft" }
.takeIf { it.isNotEmpty() }
?.sumOf { it.statusCount }
?.let { it != 0 }
}

private suspend fun fetchHasOrdersFromApi(): Boolean {
return orderStore.fetchHasOrders(selectedSite.get(), null).let {
when (it) {
is HasOrdersResult.Success -> it.hasOrders
// Default to true if we can't determine if there are orders
is HasOrdersResult.Failure -> true
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@ import org.greenrobot.eventbus.ThreadMode
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.action.AccountAction
import org.wordpress.android.fluxc.generated.AccountActionBuilder
import org.wordpress.android.fluxc.generated.WCOrderActionBuilder
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.AccountStore.AuthenticationErrorType
import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged
import org.wordpress.android.fluxc.store.AccountStore.OnAuthenticationChanged
import org.wordpress.android.fluxc.store.AccountStore.UpdateTokenPayload
import org.wordpress.android.fluxc.store.WCOrderStore.FetchOrderStatusOptionsPayload
import org.wordpress.android.fluxc.store.WooCommerceStore
import javax.inject.Inject

Expand Down Expand Up @@ -82,11 +80,6 @@ class MainPresenter @Inject constructor(
override fun selectedSiteChanged(site: SiteModel) {
productImageMap.reset()

// Fetch a fresh list of order status options
dispatcher.dispatch(
WCOrderActionBuilder
.newFetchOrderStatusOptionsAction(FetchOrderStatusOptionsPayload(site))
)
Comment on lines -85 to -89
Copy link
Member Author

Choose a reason for hiding this comment

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

This was moved to the SiteObserver class to ensure it's called both when the app is launched, and when the selected site is changed.

coroutineScope.launch { clearCardReaderDataAction() }

updateStatsWidgets()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,8 @@ class ShouldShowOnboarding @Inject constructor(
if (appPrefsWrapper.getStoreOnboardingShown(siteId) && !appPrefsWrapper.isOnboardingCompleted(siteId)) {
analyticsTrackerWrapper.track(stat = STORE_ONBOARDING_COMPLETED)
}
appPrefsWrapper.updateOnboardingCompletedStatus(siteId, true)
true
} else {
if (appPrefsWrapper.isOnboardingCompleted(siteId)) {
// Reset the onboarding completed status if there are still pending tasks
appPrefsWrapper.updateOnboardingCompletedStatus(siteId, false)
}
false
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.woocommerce.android.ui.onboarding

import com.woocommerce.android.AppPrefsWrapper
import com.woocommerce.android.WooException
import com.woocommerce.android.extensions.isFreeTrial
import com.woocommerce.android.tools.SelectedSite
Expand All @@ -25,7 +26,8 @@ import javax.inject.Singleton
class StoreOnboardingRepository @Inject constructor(
private val onboardingStore: OnboardingStore,
private val selectedSite: SelectedSite,
private val siteStore: SiteStore
private val siteStore: SiteStore,
private val appPrefs: AppPrefsWrapper
) {
private val onboardingTasksCacheFlow: MutableSharedFlow<OnboardingTasksEvent> = MutableSharedFlow(replay = 1)
val hasCachedTasks
Expand Down Expand Up @@ -60,6 +62,13 @@ class StoreOnboardingRepository @Inject constructor(
?.sortedBy { it.isComplete }
?: emptyList()

// Update onboarding completed status based on the tasks completion status
if (mobileSupportedTasks.all { it.isComplete }) {
appPrefs.updateOnboardingCompletedStatus(selectedSite.getSelectedSiteId(), true)
} else if (appPrefs.isOnboardingCompleted(selectedSite.getSelectedSiteId())) {
appPrefs.updateOnboardingCompletedStatus(selectedSite.getSelectedSiteId(), false)
}
Comment on lines +65 to +70
Copy link
Member Author

Choose a reason for hiding this comment

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

Moving this logic here instead of ShouldShowOnboarding was to make sure we don't miss updating the status when the onboarding tasks list is empty, this happened to me on one of my sites, and the onboarding card kept being shown then dismissed on each launch.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nice! I saw this as well on my site and planned to look into it.


onboardingTasksCacheFlow.emit(OnboardingTasksEvent(selectedSite.get().id, mobileSupportedTasks))
Result.success(Unit)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,6 @@ internal class ShouldShowOnboardingTest : BaseUnitTest() {
verify(analyticsTrackerWrapper).track(STORE_ONBOARDING_COMPLETED)
}

@Test
fun `when all tasks are completed and onboarding, then mark onboarding completed locally`() {
givenStoreOnboardingHasBeenShownAtLeastOnce(shown = true)

sut.showForTasks(ONBOARDING_TASK_COMPLETED_LIST)

verify(appPrefsWrapper).updateOnboardingCompletedStatus(CURRENT_SITE_ID, true)
}

@Test
fun `given onboarding is enabled from settings, when at least one task is incomplete, then return true`() {
val show = sut.showForTasks(ONBOARDING_TASK_INCOMPLETED_LIST)
Expand Down
Loading