Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
32eaa62
Allow storing last full sync timestamp
samiuelson Oct 2, 2025
5aec9de
Store full sync timestamp
samiuelson Oct 2, 2025
df96e15
Check if full sync is overdue
samiuelson Oct 2, 2025
7dd0994
Satisfy detekt's complaints
samiuelson Oct 2, 2025
170d746
Merge branch 'woomob-1405-woo-poslocal-catalog-run-incremental-sync-o…
samiuelson Oct 2, 2025
925d668
Clean up code
samiuelson Oct 2, 2025
f6edb88
Clean up code
samiuelson Oct 2, 2025
15df113
Add function to check if local catalog is empty
samiuelson Oct 3, 2025
144d9de
Add getProductCount method
samiuelson Oct 3, 2025
941672f
Execute one-time full sync if needed before opening POS
samiuelson Oct 3, 2025
a9eac13
Remove incremental sync from POS splash
samiuelson Oct 3, 2025
06dd9b2
Update WooPosIncrementalSyncReason
samiuelson Oct 3, 2025
2a2bab8
Update WooPosIncrementalSyncReason
samiuelson Oct 3, 2025
eba015c
Merge branch 'trunk' into woomob-1412-woo-poslocal-catalog-full-sync-…
samiuelson Oct 3, 2025
5b38e8d
Extract sync state checking into WooPosFullSyncStatusChecker
samiuelson Oct 6, 2025
4800875
Schedule full sync in home if needed
samiuelson Oct 6, 2025
e6bb3a9
Fix tests
samiuelson Oct 6, 2025
3f7fed2
Satisfy detekt's complaints
samiuelson Oct 6, 2025
2a54355
Show warning baner in case the full sync is overdue
samiuelson Oct 6, 2025
24ea772
Do not do full sync on app start
samiuelson Oct 6, 2025
f0ad8bf
Rename class
samiuelson Oct 6, 2025
a21e81d
Merge branch 'woomob-1412-woo-poslocal-catalog-full-sync-overdue-hand…
samiuelson Oct 6, 2025
03356bf
Implement warning banner
samiuelson Oct 7, 2025
5282805
Update layouts to accommodate banner
samiuelson Oct 7, 2025
215cd5a
Handle banner visibility updates
samiuelson Oct 7, 2025
82ea15b
Satisfy detekt's complaints
samiuelson Oct 7, 2025
f658137
Update FULL_SYNC_OVERDUE_THRESHOLD to 7 days
samiuelson Oct 7, 2025
29a0fea
Use getWorkInfosForUniqueWorkFlow API for worker monitoring
samiuelson Oct 7, 2025
8415c87
Update tests
samiuelson Oct 7, 2025
b5b0dd2
Handle NumberFormatException
samiuelson Oct 7, 2025
fae6e2c
Emit error in case site is null
samiuelson Oct 7, 2025
0977a06
Remove comment
samiuelson Oct 7, 2025
1c67f5b
Clean up code
samiuelson Oct 7, 2025
84eb52d
Clean up code
samiuelson Oct 7, 2025
832b108
Merge branch 'woomob-1412-woo-poslocal-catalog-full-sync-overdue-hand…
samiuelson Oct 7, 2025
3e747f4
Merge branch 'trunk' into woomob-1412-woo-poslocal-catalog-full-sync-…
samiuelson Oct 15, 2025
afed690
Move initial full sync to splash screen
samiuelson Oct 15, 2025
88fdeab
Clean up code
samiuelson Oct 16, 2025
1bed6b0
Allow entering POS with empty catalog
samiuelson Oct 16, 2025
6190d97
Improve worker completion monitoring
samiuelson Oct 16, 2025
e4e7fe6
Merge trunk into woomob-1412-woo-poslocal-catalog-full-sync-overdue-h…
samiuelson Oct 16, 2025
ec3a521
Add tests for WooPosFullSyncStatusCheckerTest
samiuelson Oct 16, 2025
fe6375a
Clean up code
samiuelson Oct 16, 2025
984b76d
Clean up code
samiuelson Oct 16, 2025
c3a3d32
Clean up code
samiuelson Oct 16, 2025
6169c46
Rename method
samiuelson Oct 16, 2025
33cfa10
Clean up code
samiuelson Oct 16, 2025
59358ad
Rename val
samiuelson Oct 17, 2025
11fac8f
Rename composable
samiuelson Oct 17, 2025
929e1d7
Improve naming of state class and vals
samiuelson Oct 17, 2025
ca7b5d6
Merge branch woomob-1412-woo-poslocal-catalog-full-sync-overdue-handling
samiuelson Oct 17, 2025
c2db87c
Update padding
samiuelson Oct 17, 2025
56bee46
Rename class
samiuelson Oct 20, 2025
e685840
Merge branch 'trunk' into woomob-1412-woo-poslocal-catalog-full-sync-…
samiuelson Oct 20, 2025
849a70f
Merge pull request #14704 from woocommerce/woomob-1412-full-sync-over…
samiuelson Oct 20, 2025
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 @@ -14,6 +14,8 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent.SearchEvent.
import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent.SearchEvent.RecentSearchSelected
import com.woocommerce.android.ui.woopos.home.WooPosHomeState.DialogState
import com.woocommerce.android.ui.woopos.home.WooPosHomeState.ScreenPositionState
import com.woocommerce.android.ui.woopos.localcatalog.WooPosIncrementalSyncReason
import com.woocommerce.android.ui.woopos.localcatalog.WooPosPerformLocalCatalogIncrementalSync
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.BackToCartTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker
Expand All @@ -31,6 +33,7 @@ class WooPosHomeViewModel @Inject constructor(
private val parentToChildrenEventSender: WooPosParentToChildrenEventSender,
private val analyticsTracker: WooPosAnalyticsTracker,
private val soundHelper: WooPosSoundHelper,
private val incrementalSync: WooPosPerformLocalCatalogIncrementalSync,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _state = savedStateHandle.getStateFlow(
Expand All @@ -54,6 +57,13 @@ class WooPosHomeViewModel @Inject constructor(
viewModelScope.launch {
soundHelper.preloadChaChing()
}
performLocalCatalogIncrementalSync()
}

private fun performLocalCatalogIncrementalSync() {
viewModelScope.launch {
incrementalSync.execute(WooPosIncrementalSyncReason.ON_POS_HOME)
}
}

override fun onCleared() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.woocommerce.android.ui.woopos.home.items

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.scaleIn
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.woocommerce.android.R
import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview
import com.woocommerce.android.ui.woopos.common.composeui.component.ShadowType
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosCard
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosText
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosCornerRadius
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosElevation
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosSpacing
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTheme
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTypography

@Composable
fun WooPosCatalogSyncOverdueBanner(
state: WooPosItemsViewModel.CatalogSyncOverdueBannerState,
modifier: Modifier = Modifier,
onDismiss: () -> Unit
) {
AnimatedVisibility(
visible = state is WooPosItemsViewModel.CatalogSyncOverdueBannerState.Visible,
enter = fadeIn(
animationSpec = tween(durationMillis = 180)
) + scaleIn(
animationSpec = tween(durationMillis = 180)
),
modifier = modifier.padding(horizontal = WooPosSpacing.Small.value)
) {
WooPosCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(WooPosCornerRadius.Medium.value),
backgroundColor = MaterialTheme.colorScheme.surfaceContainerLow,
elevation = WooPosElevation.Medium,
shadowType = ShadowType.Soft,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(WooPosSpacing.Medium.value),
modifier = Modifier
.fillMaxWidth()
.padding(WooPosSpacing.Medium.value),
) {
Icon(
painter = painterResource(id = R.drawable.ic_woo_pos_info_banner),
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null,
modifier = Modifier
.size(48.dp)
.align(Alignment.CenterVertically)
)
Column(
verticalArrangement = Arrangement.spacedBy(WooPosSpacing.Small.value),
horizontalAlignment = Alignment.Start,
modifier = Modifier.weight(1f)
) {
WooPosText(
text = stringResource(R.string.woopos_refresh_catalog_banner_title),
style = WooPosTypography.BodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
WooPosText(
text = stringResource(R.string.woopos_refresh_catalog_banner_message),
style = WooPosTypography.BodySmall,
color = MaterialTheme.colorScheme.onSurface,
)
}
IconButton(
onClick = onDismiss,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = stringResource(R.string.woopos_refresh_catalog_banner_dismiss),
tint = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.size(24.dp)
)
}
}
}
}
}

@Composable
@WooPosPreview
fun WooPosRefreshCatalogBannerPreview() {
WooPosTheme {
WooPosCatalogSyncOverdueBanner(
state = WooPosItemsViewModel.CatalogSyncOverdueBannerState.Visible,
onDismiss = {}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyListState
Expand Down Expand Up @@ -45,6 +47,7 @@ fun WooPosItemsScreen(modifier: Modifier = Modifier) {
WooPosItemsScreen(
modifier = modifier,
itemsStateFlow = productsViewModel.viewState,
catalogSyncOverdueBannerStateFlow = productsViewModel.catalogSyncOverdueBannerState,
productsViewState = productsViewState,
couponsListState = couponsListState,
onUIEvent = { productsViewModel.onUIEvent(it) },
Expand All @@ -56,15 +59,18 @@ fun WooPosItemsScreen(modifier: Modifier = Modifier) {
private fun WooPosItemsScreen(
modifier: Modifier = Modifier,
itemsStateFlow: StateFlow<WooPosItemsToolbarViewState>,
catalogSyncOverdueBannerStateFlow: StateFlow<WooPosItemsViewModel.CatalogSyncOverdueBannerState>,
productsViewState: LazyListState,
couponsListState: LazyListState,
onUIEvent: (WooPosItemsUIEvent) -> Unit,
) {
val state = itemsStateFlow.collectAsState()
val catalogSyncOverdueBannerState = catalogSyncOverdueBannerStateFlow.collectAsState()

MainItemsList(
modifier = modifier,
state = state,
bannerState = catalogSyncOverdueBannerState,
productsViewState = productsViewState,
couponsListState = couponsListState,
onSearchEvent = {
Expand All @@ -86,6 +92,7 @@ private fun WooPosItemsScreen(
},
onTabClicked = { onUIEvent(WooPosItemsUIEvent.OnTabClicked(it)) },
onBackClicked = { onUIEvent(WooPosItemsUIEvent.BackFromVariationsClicked) },
onSyncWarningBannerDismissed = { onUIEvent(WooPosItemsUIEvent.SyncOverdueBannerDismissed) },
)
}

Expand All @@ -94,20 +101,17 @@ private fun WooPosItemsScreen(
private fun MainItemsList(
modifier: Modifier,
state: State<WooPosItemsToolbarViewState>,
bannerState: State<WooPosItemsViewModel.CatalogSyncOverdueBannerState>,
productsViewState: LazyListState,
couponsListState: LazyListState,
onSearchEvent: (WooPosSearchUIEvent) -> Unit,
onTabClicked: (WooPosItemsToolbarViewState.Tab) -> Unit,
onAddCouponEvent: () -> Unit,
onBackClicked: () -> Unit,
onSyncWarningBannerDismissed: () -> Unit,
) {
Box(
modifier = modifier
.fillMaxSize()
) {
Column(
modifier.fillMaxHeight()
) {
Box(modifier = modifier.fillMaxSize()) {
Column(modifier.fillMaxHeight()) {
WooPosItemsToolbar(
modifier = Modifier
.statusBarsPadding()
Expand All @@ -121,6 +125,20 @@ private fun MainItemsList(
onAddCouponEvent = onAddCouponEvent,
)

Spacer(
modifier =
Modifier
.height(WooPosSpacing.Small.value)
.padding(horizontal = WooPosSpacing.Medium.value.toAdaptivePadding())
)

WooPosCatalogSyncOverdueBanner(
state = bannerState.value,
onDismiss = onSyncWarningBannerDismissed
)

Spacer(modifier = Modifier.height(WooPosSpacing.Small.value))

val currentState = state.value

Crossfade(
Expand Down Expand Up @@ -216,10 +234,12 @@ fun WooPosItemsScreenSearchVisiblePreview(modifier: Modifier = Modifier) {
tabs = tabs()
)
)
val bannerState = MutableStateFlow(WooPosItemsViewModel.CatalogSyncOverdueBannerState.Visible)
WooPosTheme {
WooPosItemsScreen(
modifier = modifier,
itemsStateFlow = productState,
catalogSyncOverdueBannerStateFlow = bannerState,
productsViewState = rememberLazyListState(),
couponsListState = rememberLazyListState(),
onUIEvent = {},
Expand All @@ -242,10 +262,12 @@ fun WooPosItemsScreenSearchHiddenPreview(modifier: Modifier = Modifier) {
tabs = tabs()
)
)
val bannerState = MutableStateFlow(WooPosItemsViewModel.CatalogSyncOverdueBannerState.Visible)
WooPosTheme {
WooPosItemsScreen(
modifier = modifier,
itemsStateFlow = productState,
catalogSyncOverdueBannerStateFlow = bannerState,
productsViewState = rememberLazyListState(),
couponsListState = rememberLazyListState(),
onUIEvent = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ sealed class WooPosItemsUIEvent {
data object CloseSearchClicked : WooPosItemsUIEvent()
data object SearchIconClicked : WooPosItemsUIEvent()
data object AddCouponIconClicked : WooPosItemsUIEvent()
data object SyncOverdueBannerDismissed : WooPosItemsUIEvent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosItemsToolbarViewState.
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsToolbarViewState.Tab
import com.woocommerce.android.ui.woopos.home.items.coupons.creation.WooPosCouponCreationFacade
import com.woocommerce.android.ui.woopos.home.items.variations.WooPosVariationsNavigationData
import com.woocommerce.android.ui.woopos.localcatalog.WooPosFullSyncRequirement
import com.woocommerce.android.ui.woopos.localcatalog.WooPosFullSyncStatusChecker
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchButtonTapped
import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEventConstant
Expand All @@ -35,6 +37,7 @@ class WooPosItemsViewModel @Inject constructor(
private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver,
private val preferencesRepository: WooPosPreferencesRepository,
private val analyticsTracker: WooPosAnalyticsTracker,
private val syncStatusChecker: WooPosFullSyncStatusChecker,
) : ViewModel() {
private var preservedStateBeforeOpeningVariations: WooPosItemsToolbarViewState? = null
private val _viewState = MutableStateFlow<WooPosItemsToolbarViewState>(initialState())
Expand All @@ -45,6 +48,10 @@ class WooPosItemsViewModel @Inject constructor(
initialValue = _viewState.value,
)

private val _catalogSyncOverdueBannerState =
MutableStateFlow<CatalogSyncOverdueBannerState>(CatalogSyncOverdueBannerState.Hidden)
val catalogSyncOverdueBannerState: StateFlow<CatalogSyncOverdueBannerState> = _catalogSyncOverdueBannerState

init {
listenUpEvents()
searchHelper.initialize(
Expand All @@ -55,6 +62,18 @@ class WooPosItemsViewModel @Inject constructor(
viewModelScope.launch {
preferencesRepository.setWasOpenedOnce(true)
}

checkSyncStatusAndUpdateBanner()
}

private fun checkSyncStatusAndUpdateBanner() {
viewModelScope.launch {
val requirement = syncStatusChecker.checkSyncRequirement()
_catalogSyncOverdueBannerState.value = when (requirement) {
is WooPosFullSyncRequirement.Overdue -> CatalogSyncOverdueBannerState.Visible
else -> CatalogSyncOverdueBannerState.Hidden
}
}
}

fun onUIEvent(event: WooPosItemsUIEvent) {
Expand All @@ -77,6 +96,9 @@ class WooPosItemsViewModel @Inject constructor(
}

is WooPosItemsUIEvent.AddCouponIconClicked -> createAndAddCoupon()
WooPosItemsUIEvent.SyncOverdueBannerDismissed -> {
_catalogSyncOverdueBannerState.value = CatalogSyncOverdueBannerState.Hidden
}
}
}

Expand Down Expand Up @@ -241,4 +263,9 @@ class WooPosItemsViewModel @Inject constructor(
@Parcelize
data class Coupon(override val id: Long, val couponCode: String) : ItemClickedData(id), Parcelable
}

sealed class CatalogSyncOverdueBannerState {
data object Hidden : CatalogSyncOverdueBannerState()
data object Visible : CatalogSyncOverdueBannerState()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.woocommerce.android.ui.woopos.home.items.coupons

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
Expand All @@ -22,7 +21,6 @@ import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosErrorS
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosErrorScreenButtonState
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosPaginationErrorIndicator
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosIcons
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosSpacing
import com.woocommerce.android.ui.woopos.common.composeui.designsystem.WooPosTheme
import com.woocommerce.android.ui.woopos.home.items.WooPosCouponsViewState
import com.woocommerce.android.ui.woopos.home.items.WooPosItemList
Expand Down Expand Up @@ -74,7 +72,6 @@ private fun WooPosCouponsScreen(
when (val itemsState = state.value) {
is WooPosCouponsViewState.Content -> {
WooPosItemList(
modifier = Modifier.padding(top = WooPosSpacing.Large.value),
state = itemsState,
listState = listState,
onItemClicked = { item -> onUIEvent(WooPosCouponsUIEvent.CouponClicked(item.id, item.name)) },
Expand All @@ -88,9 +85,7 @@ private fun WooPosCouponsScreen(
}
}

is WooPosCouponsViewState.Loading -> WooPosItemsLoadingIndicator(
modifier = Modifier.padding(top = WooPosSpacing.Large.value)
)
is WooPosCouponsViewState.Loading -> WooPosItemsLoadingIndicator()

is WooPosCouponsViewState.Empty -> WooPosEmptyScreen(
modifier = Modifier.fillMaxSize(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ private fun Content(
onEndOfItemListReached: () -> Unit
) {
WooPosItemList(
modifier = Modifier.padding(top = WooPosSpacing.Large.value),
state = itemsState,
listState = listState,
onItemClicked = onItemClicked,
Expand Down
Loading