Skip to content
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

[WooPos][Non Simple Product types] Retry UI for pagination errors on variations screens #12972

Open
wants to merge 41 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bddcae5
Add state to indicate error while paginating
AnirudhBhat Nov 21, 2024
9873533
Handle error while paginating in WooPosItemsList.kt
AnirudhBhat Nov 21, 2024
42f4e1a
Handle error while paginating in WooPosItemsScreen.kt
AnirudhBhat Nov 21, 2024
0fe310a
Remove snack bar implementation from WooPosVariationsScreen
AnirudhBhat Nov 21, 2024
d84a3a8
Override errorloadingMoreItems property in variations view state
AnirudhBhat Nov 21, 2024
455d396
Navigate to items list screen on payment success so that clicking on …
AnirudhBhat Nov 21, 2024
30b6d5d
Add tests for different state updates while paginating
AnirudhBhat Nov 21, 2024
53a1838
Remove unnecessary padding from the variations screen
AnirudhBhat Nov 21, 2024
bab7cbe
Add test to verify we navigate to the items list screen on payment su…
AnirudhBhat Nov 21, 2024
8ceaa5f
Remove unused imports
AnirudhBhat Nov 21, 2024
e5f7823
Make changes to string resource to include variable products as well …
AnirudhBhat Nov 22, 2024
a45bd6c
Refactor pagination state logic. Use sealed interface instead of bool…
AnirudhBhat Nov 25, 2024
71ee602
Yse 32 instead of 30 dp.
AnirudhBhat Nov 25, 2024
84febe1
Use MutableStateFlow's atomic operation to update the view state
AnirudhBhat Nov 25, 2024
5b60693
fix detekt error
AnirudhBhat Nov 25, 2024
20d14bd
Add string resource for pagination error messages
AnirudhBhat Nov 26, 2024
9685cd2
Add pagination error component for items list
AnirudhBhat Nov 26, 2024
0bd442d
Integrate the pagination error component in items and variations screens
AnirudhBhat Nov 26, 2024
e40c789
Fix detekt errors
AnirudhBhat Nov 26, 2024
e94d0e5
Use adaptive padding
AnirudhBhat Nov 28, 2024
3ed43d3
Move the modifier parameter near WooPosErrorScreen
AnirudhBhat Nov 28, 2024
187dfcd
Update the state to pagination error on items view model
AnirudhBhat Nov 28, 2024
6fe46f2
Refactor loadMore method for readability
AnirudhBhat Nov 28, 2024
0f5cf15
Refactor variation flow collection logic
AnirudhBhat Nov 28, 2024
d4d0060
Merge branch 'issue/12846-analytics-for-variations' into issue/12967-…
AnirudhBhat Nov 28, 2024
81cb158
Fix failing test
AnirudhBhat Nov 28, 2024
d03d6a2
Merge branch 'trunk' into issue/12967-retry-pagination-error
AnirudhBhat Nov 28, 2024
28584e8
Fix failing test
AnirudhBhat Nov 28, 2024
aa38f10
Rename WooPosPaginationErrorScreen.kt to WooPosPaginationErrorIndicator
AnirudhBhat Nov 29, 2024
6a3b777
Make the method private
AnirudhBhat Nov 29, 2024
6bb0870
Add preview for pagination error state
AnirudhBhat Nov 29, 2024
19a7ed5
Refactor WooPosPaginationErrorIndicator and add content description f…
AnirudhBhat Nov 29, 2024
ebbf621
Rename ItemList to WooPosItemList
AnirudhBhat Nov 29, 2024
623125f
Use string resource in preview
AnirudhBhat Nov 29, 2024
a9780a3
Use sealed class instead of interface for pagination state
AnirudhBhat Nov 29, 2024
45a1d7e
Fix detekt errors
AnirudhBhat Nov 29, 2024
ba90251
Revert change
AnirudhBhat Nov 29, 2024
4a920cb
Default canLoadMore value to false
AnirudhBhat Nov 29, 2024
8c3af25
Remove minOf logic
AnirudhBhat Nov 29, 2024
1eb6c32
Add list for WooPosPagina' preview
AnirudhBhat Nov 29, 2024
ef53ce9
Add margin for items loading in variations screen
AnirudhBhat Nov 29, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ interface ContentViewState {
val items: List<WooPosItem>
val loadingMore: Boolean
val reloadingProductsWithPullToRefresh: Boolean
val errorLoadingMoreItems: Boolean
Copy link
Contributor

@kidinov kidinov Nov 22, 2024

Choose a reason for hiding this comment

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

The state defined like that can contain invalidate states, e.g. loadingMore = true and errorLoadingMoreItems = true.

We probably should do something like

val paginationIndicator: PaginationIndicator

enum PaginationIndicator {
   Loading, Error, None
} 

By the way, ContentViewState does not follow the naming convention of the top-level classes. It probably should be WooPosItemsViewState. And there are a few more, e.g., ToolbarAccessibilityLabels, for instance. And some classes that are in the items package now, e.g., WooPosBanner should either contain an item in them or be placed in another package

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for raising this. I had this on my list to refactor hence I kept the PR as draft. Here's the refactored state: a45bd6c

}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fun ItemList(
listState: LazyListState,
onItemClicked: (item: WooPosItem) -> Unit,
onEndOfProductsListReached: () -> Unit,
onErrorWhilePaginating: @Composable () -> Unit,
) {
WooPosLazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
Expand Down Expand Up @@ -100,6 +101,11 @@ fun ItemList(
ItemsLoadingItem()
}
}
if (state.errorLoadingMoreItems) {
item {
onErrorWhilePaginating()
}
}
item {
Spacer(modifier = Modifier.height(104.dp))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,15 +169,22 @@ private fun MainItemsList(
listState,
onItemClicked,
onEndOfItemListReached,
)
) {
ProductsError(
modifier = Modifier.height(500.dp),
onRetryClicked = {
onEndOfItemListReached()
}
)
}
}
}

is WooPosItemsViewState.Loading -> ItemsLoadingIndicator()

is WooPosItemsViewState.Empty -> ProductsEmptyList()

is WooPosItemsViewState.Error -> ProductsError { onRetryClicked() }
is WooPosItemsViewState.Error -> ProductsError(modifier = Modifier.width(640.dp)) { onRetryClicked() }
Copy link
Contributor

Choose a reason for hiding this comment

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

Where does 640 come from? How does this look on a screen of 800 or whatever min we support now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code was already present in the trunk. I just re-arranged it.

Copy link
Contributor

@kidinov kidinov Nov 28, 2024

Choose a reason for hiding this comment

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

it's a bug I beleave, we should fix that probably not expand it to another screen

Copy link
Contributor Author

@AnirudhBhat AnirudhBhat Nov 29, 2024

Choose a reason for hiding this comment

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

Just to understand more. What problem do you see using width of 640? We could use fillMaxWidth() instead.

}
}
PullRefreshIndicator(
Expand Down Expand Up @@ -300,13 +307,13 @@ fun ProductsEmptyList() {
}

@Composable
fun ProductsError(onRetryClicked: () -> Unit) {
fun ProductsError(modifier: Modifier, onRetryClicked: () -> Unit) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
WooPosErrorScreen(
modifier = Modifier.width(640.dp),
modifier = modifier,
message = stringResource(id = R.string.woopos_products_loading_error_title),
reason = stringResource(id = R.string.woopos_products_loading_error_message),
primaryButton = Button(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sealed class WooPosItemsViewState(
override val items: List<WooPosItem>,
override val loadingMore: Boolean,
val bannerState: BannerState,
override val errorLoadingMoreItems: Boolean = false,
override val reloadingProductsWithPullToRefresh: Boolean = false
) : WooPosItemsViewState(reloadingProductsWithPullToRefresh), ContentViewState {
data class BannerState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ sealed class WooPosVariationsViewState(
override val items: List<WooPosItem.Variation>,
override val loadingMore: Boolean,
override val reloadingProductsWithPullToRefresh: Boolean = false,
override val errorLoadingMoreItems: Boolean = false,
) : WooPosVariationsViewState(reloadingProductsWithPullToRefresh), ContentViewState

data class Loading(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarDuration
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
Expand All @@ -29,17 +25,13 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import com.woocommerce.android.R
import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview
import com.woocommerce.android.ui.woopos.common.composeui.WooPosTheme
Expand All @@ -61,27 +53,10 @@ fun WooPosVariationsScreen(
onBackClicked: () -> Unit
) {
val viewModel: WooPosVariationsViewModel = hiltViewModel()
val snackbarHostState = remember { SnackbarHostState() }
val lifecycleOwner = LocalLifecycleOwner.current
val paginationErrorMessage = stringResource(id = R.string.woopos_variations_screen_pagination_error)

LaunchedEffect(variableProductData.id) {
viewModel.init(variableProductData.id)
}
LaunchedEffect(Unit) {
viewModel.events
.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { event ->
when (event) {
is WooPosVariationsViewModel.WooPosVariationEvents.PaginationError -> {
snackbarHostState.showSnackbar(
message = paginationErrorMessage,
duration = SnackbarDuration.Short
)
}
}
}
}
val state = viewModel.viewState
WooPosVariationsScreens(
modifier,
Expand All @@ -102,7 +77,6 @@ fun WooPosVariationsScreen(
},
variableProductData,
state,
snackbarHostState,
)
}

Expand All @@ -118,93 +92,82 @@ private fun WooPosVariationsScreens(
onRetryClicked: () -> Unit,
variableProductData: VariableProductData,
state: StateFlow<WooPosVariationsViewState>,
snackbarHostState: SnackbarHostState,
) {
val itemState = state.collectAsState()
val pullToRefreshState = rememberPullRefreshState(
itemState.value.reloadingProductsWithPullToRefresh,
onPullToRefresh
)
Scaffold(
snackbarHost = {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 80.dp)
) {
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(Alignment.Center)
)
}
}
Box(
modifier = modifier
.fillMaxSize()
.pullRefresh(pullToRefreshState)
.padding(
start = 16.dp.toAdaptivePadding(),
end = 16.dp.toAdaptivePadding(),
top = 30.dp.toAdaptivePadding(),
Copy link
Contributor

@kidinov kidinov Nov 22, 2024

Choose a reason for hiding this comment

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

I think 30 can not be in 4dp grid. 28 or 32 should be

https://m2.material.io/design/layout/spacing-methods.html#baseline-grid

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done: 71ee602

bottom = 0.dp.toAdaptivePadding(),
)
) {
Box(
modifier = modifier
.fillMaxSize()
.pullRefresh(pullToRefreshState)
.padding(
start = 16.dp.toAdaptivePadding(),
end = 16.dp.toAdaptivePadding(),
top = 30.dp.toAdaptivePadding(),
bottom = 0.dp.toAdaptivePadding(),
)
BackHandler(onBack = onBackClicked)
Column(
modifier = modifier.fillMaxHeight()
) {
BackHandler(onBack = onBackClicked)
Column(
modifier = modifier.fillMaxHeight()
) {
VariationsToolbar(
variableProductData = variableProductData,
onBackClicked = onBackClicked
)
when (val itemsState = itemState.value) {
is WooPosVariationsViewState.Content -> {
Spacer(modifier = Modifier.height(16.dp))
ItemList(
state = itemsState,
listState = rememberLazyListState(),
onItemClicked = {
onItemClicked(
(it as WooPosItem.Variation).productId,
it.id
)
VariationsToolbar(
variableProductData = variableProductData,
onBackClicked = onBackClicked
)
when (val itemsState = itemState.value) {
is WooPosVariationsViewState.Content -> {
val lazyListState = rememberLazyListState()
Spacer(modifier = Modifier.height(16.dp))
ItemList(
state = itemsState,
listState = lazyListState,
onItemClicked = {
onItemClicked(
(it as WooPosItem.Variation).productId,
it.id
)
},
onEndOfProductsListReached = onEndOfItemListReached,
onErrorWhilePaginating = {
VariationsError(modifier = Modifier.height(500.dp)) {
onEndOfItemListReached()
}
) {
onEndOfItemListReached()
}
}

is WooPosVariationsViewState.Loading -> ItemsLoadingIndicator(
minOf(10, variableProductData.numOfVariations)
)
}

is WooPosVariationsViewState.Error -> {
VariationsError {
onRetryClicked()
}
}
is WooPosVariationsViewState.Loading -> ItemsLoadingIndicator(
minOf(10, variableProductData.numOfVariations)
Copy link
Contributor

Choose a reason for hiding this comment

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

If the goal is to show the exact amount of variations as loading items, then I don't understand why we need to take min from it or 10?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The goal is not to show the exact amount of loading items as variations. The goal is to show the exact amount of loading items as variations only if the variation is <= 10.

Copy link
Contributor

Choose a reason for hiding this comment

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

Then, I don't understand the logic behind that. I think we either want to indicate the number of items we load or don't want to do it and just show a generic loading screen similar to the products. Imo this just creates confusion for both us and the users on top of that it leaks logic to the view layer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright, fixed here: 8c3af25

)

else -> {}
is WooPosVariationsViewState.Error -> {
VariationsError(modifier = Modifier.width(640.dp)) {
onRetryClicked()
}
}

else -> {}
}
PullRefreshIndicator(
modifier = Modifier.align(Alignment.TopCenter),
refreshing = itemState.value.reloadingProductsWithPullToRefresh,
state = pullToRefreshState
)
}
PullRefreshIndicator(
modifier = Modifier.align(Alignment.TopCenter),
refreshing = itemState.value.reloadingProductsWithPullToRefresh,
state = pullToRefreshState
)
}
}

@Composable
fun VariationsError(onRetryClicked: () -> Unit) {
fun VariationsError(modifier: Modifier, onRetryClicked: () -> Unit) {
Box(
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
WooPosErrorScreen(
modifier = Modifier.width(640.dp),
modifier = modifier,
message = stringResource(id = R.string.woopos_variations_loading_error_title),
reason = stringResource(id = R.string.woopos_products_loading_error_message),
primaryButton = Button(
Expand Down Expand Up @@ -316,7 +279,6 @@ fun WooPosVariationsScreenPreview() {
numOfVariations = 20,
),
state = productState,
snackbarHostState = SnackbarHostState()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosVariationsViewState
import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
Expand All @@ -37,11 +35,6 @@ class WooPosVariationsViewModel @Inject constructor(
initialValue = _viewState.value,
)

private val _events: MutableSharedFlow<WooPosVariationEvents> = MutableSharedFlow(
extraBufferCapacity = 1
)
val events = _events.asSharedFlow()

private var fetchJob: Job? = null
private var loadMoreJob: Job? = null

Expand Down Expand Up @@ -100,18 +93,15 @@ class WooPosVariationsViewModel @Inject constructor(
if (!variationsDataSource.canLoadMore()) {
return
}
_viewState.value = currentState.copy(loadingMore = true)
_viewState.value = currentState.copy(loadingMore = true, errorLoadingMoreItems = false)
loadMoreJob?.cancel()
loadMoreJob = viewModelScope.launch {
val result = variationsDataSource.loadMore(productId)
if (result.isSuccess) {
Copy link
Contributor

Choose a reason for hiding this comment

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

np:

            _viewState.value = if (result.isSuccess) {
                currentState.copy(loadingMore = false, errorLoadingMoreItems = false)
            } else {
                currentState.copy(loadingMore = false, errorLoadingMoreItems = true)
            }

Result.success(Unit)
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this line do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is no longer relevant as result of this change: a45bd6c

if (!variationsDataSource.canLoadMore()) {
_viewState.value = currentState.copy(loadingMore = false)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you use currentState here? _viewState.value can be modified by the time the request comes back but you copying based on the old state

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is no longer relevant as result of this change: a45bd6c

}
_viewState.value = currentState.copy(loadingMore = false, errorLoadingMoreItems = false)
} else {
_events.tryEmit(WooPosVariationEvents.PaginationError)
_viewState.value = currentState.copy(loadingMore = false)
_viewState.value = currentState.copy(loadingMore = false, errorLoadingMoreItems = true)
}
}
}
Expand Down Expand Up @@ -151,8 +141,4 @@ class WooPosVariationsViewModel @Inject constructor(
private fun onEndOfVariationsListReached(productId: Long) {
loadMore(productId)
}

sealed class WooPosVariationEvents {
data object PaginationError : WooPosVariationEvents()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent
import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender
import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel
import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator
import com.woocommerce.android.ui.woopos.util.WooPosNetworkStatus
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
Expand All @@ -38,6 +39,7 @@ class WooPosTotalsViewModel @Inject constructor(
private val priceFormat: WooPosFormatPrice,
private val analyticsTracker: WooPosAnalyticsTracker,
private val networkStatus: WooPosNetworkStatus,
private val wooPosItemsNavigator: WooPosItemsNavigator,
savedState: SavedStateHandle,
) : ViewModel() {

Expand Down Expand Up @@ -120,6 +122,9 @@ class WooPosTotalsViewModel @Inject constructor(
when (status) {
is WooPosCardReaderPaymentStatus.Success -> {
val state = uiState.value
wooPosItemsNavigator.sendNavigationEvent(
WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateBackToItemListScreen
)
check(state is WooPosTotalsViewState.Totals)
uiState.value = WooPosTotalsViewState.PaymentSuccess(orderTotalText = state.orderTotalText)
childrenToParentEventSender.sendToParent(ChildToParentEvent.OrderSuccessfullyPaid)
Expand Down
Loading
Loading