Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
24ea772
Do not do full sync on app start
samiuelson Oct 6, 2025
f0ad8bf
Rename class
samiuelson Oct 6, 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
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 @@ -116,6 +116,7 @@ private fun WooPosHomeScreen(
cartWidthDp = cartWidthDp,
totalsWidthDp = totalsWidthAnimatedDp,
onHomeUIEvent = onHomeUIEvent,
onRetryCatalogSyncClicked = { onHomeUIEvent(WooPosHomeUIEvent.RetryCatalogSyncClicked) },
)
}

Expand All @@ -127,6 +128,7 @@ private fun WooPosHomeScreen(
cartWidthDp: Dp,
totalsWidthDp: Dp,
onHomeUIEvent: (WooPosHomeUIEvent) -> Unit,
onRetryCatalogSyncClicked: () -> Unit = {},
) {
Box(
modifier = Modifier
Expand All @@ -149,7 +151,9 @@ private fun WooPosHomeScreen(
) {
WooPosHomeScreenProducts(
modifier = Modifier
.width(productsWidthDp)
.width(productsWidthDp),
catalogSyncState = state.catalogSyncState,
onRetryCatalogSyncClicked = onRetryCatalogSyncClicked
)
WooPosHomeScreenCart(
modifier = Modifier
Expand Down Expand Up @@ -195,11 +199,19 @@ private fun Dialogs(
}

@Composable
private fun WooPosHomeScreenProducts(modifier: Modifier) {
private fun WooPosHomeScreenProducts(
modifier: Modifier,
catalogSyncState: WooPosHomeState.CatalogSyncState = WooPosHomeState.CatalogSyncState.Idle,
onRetryCatalogSyncClicked: () -> Unit = {}
) {
if (isPreviewMode()) {
WooPosItemsScreenPreview(modifier)
} else {
WooPosItemsScreen(modifier = modifier)
WooPosItemsScreen(
modifier = modifier,
catalogSyncState = catalogSyncState,
onRetryCatalogSyncClicked = onRetryCatalogSyncClicked
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import kotlinx.parcelize.Parcelize
data class WooPosHomeState(
val screenPositionState: ScreenPositionState,
val dialogState: DialogState = DialogState.Hidden,
val catalogSyncState: CatalogSyncState = CatalogSyncState.Idle,
) : Parcelable {
@Parcelize
sealed class ScreenPositionState : Parcelable {
Expand Down Expand Up @@ -45,4 +46,19 @@ data class WooPosHomeState(
val confirmButton: Int = R.string.woopos_exit_dialog_confirmation_confirm_button
}
}

@Parcelize
sealed class CatalogSyncState : Parcelable {
@Parcelize
data object Idle : CatalogSyncState()

@Parcelize
data object Syncing : CatalogSyncState()

@Parcelize
data object Success : CatalogSyncState()

@Parcelize
data class Failed(val error: String) : CatalogSyncState()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ sealed class WooPosHomeUIEvent {
data object DismissScanningSetupDialog : WooPosHomeUIEvent()
data object OnPaymentCompletedViaCash : WooPosHomeUIEvent()
data object ExitPosClicked : WooPosHomeUIEvent()
data object RetryCatalogSyncClicked : WooPosHomeUIEvent()
data class OnBarcodeEvent(
val result: BarcodeInputDetector.BarcodeResult
) : WooPosHomeUIEvent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ 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.WooPosFullSyncRequirement
import com.woocommerce.android.ui.woopos.localcatalog.WooPosFullSyncState
import com.woocommerce.android.ui.woopos.localcatalog.WooPosFullSyncStatusChecker
import com.woocommerce.android.ui.woopos.localcatalog.WooPosIncrementalSyncReason
import com.woocommerce.android.ui.woopos.localcatalog.WooPosPerformInstantCatalogFullSync
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 +37,9 @@ class WooPosHomeViewModel @Inject constructor(
private val parentToChildrenEventSender: WooPosParentToChildrenEventSender,
private val analyticsTracker: WooPosAnalyticsTracker,
private val soundHelper: WooPosSoundHelper,
private val syncStatusChecker: WooPosFullSyncStatusChecker,
private val performInitialFullSync: WooPosPerformInstantCatalogFullSync,
private val incrementalSync: WooPosPerformLocalCatalogIncrementalSync,
Copy link
Contributor

Choose a reason for hiding this comment

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

🔍 We might want to add perform prefix for consistency.

savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _state = savedStateHandle.getStateFlow(
Expand All @@ -54,6 +63,49 @@ class WooPosHomeViewModel @Inject constructor(
viewModelScope.launch {
soundHelper.preloadChaChing()
}
startCatalogSyncIfNeeded()
}

private fun startCatalogSyncIfNeeded() {
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 I'd consider extracting this into a separate class - feels like an implementation detail the VM shouldn't know about, otherwise, it might grow a lot overtime.

viewModelScope.launch {
val requirement = syncStatusChecker.checkSyncRequirement()

when (requirement) {
is WooPosFullSyncRequirement.NotRequired,
is WooPosFullSyncRequirement.Overdue -> {
_state.value = _state.value.copy(
catalogSyncState = WooPosHomeState.CatalogSyncState.Idle
)
incrementalSync.execute(WooPosIncrementalSyncReason.ON_POS_HOME)
}
is WooPosFullSyncRequirement.BlockingRequired -> {
performInitialFullSync().collect { syncStatus ->
when (syncStatus) {
is WooPosFullSyncState.InProgress -> {
_state.value = _state.value.copy(
catalogSyncState = WooPosHomeState.CatalogSyncState.Syncing
)
}
is WooPosFullSyncState.Success -> {
_state.value = _state.value.copy(
catalogSyncState = WooPosHomeState.CatalogSyncState.Success
)
}
is WooPosFullSyncState.Failed -> {
_state.value = _state.value.copy(
catalogSyncState = WooPosHomeState.CatalogSyncState.Failed(syncStatus.error)
)
}
}
}
}
is WooPosFullSyncRequirement.Error -> {
_state.value = _state.value.copy(
catalogSyncState = WooPosHomeState.CatalogSyncState.Failed(requirement.message)
)
}
}
}
}

override fun onCleared() {
Expand Down Expand Up @@ -88,6 +140,10 @@ class WooPosHomeViewModel @Inject constructor(
}
}

WooPosHomeUIEvent.RetryCatalogSyncClicked -> {
startCatalogSyncIfNeeded()
}

is WooPosHomeUIEvent.OnBarcodeEvent -> {
sendEventToChildren(ParentToChildrenEvent.BarcodeEvent(event.result))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,42 @@ package com.woocommerce.android.ui.woopos.home.items
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
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.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import com.woocommerce.android.R
import com.woocommerce.android.ui.woopos.common.composeui.WooPosPreview
import com.woocommerce.android.ui.woopos.common.composeui.component.Button
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosCircularLoadingIndicator
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosErrorScreen
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchInputState
import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearchUIEvent
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.toAdaptivePadding
import com.woocommerce.android.ui.woopos.home.WooPosHomeState.CatalogSyncState
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsToolbarViewState.Tab.Coupons
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsToolbarViewState.Tab.HighlightLevel
import com.woocommerce.android.ui.woopos.home.items.WooPosItemsToolbarViewState.Tab.Products
Expand All @@ -38,7 +54,11 @@ import kotlinx.coroutines.flow.StateFlow

@OptIn(ExperimentalMaterialApi::class)
@Composable
fun WooPosItemsScreen(modifier: Modifier = Modifier) {
fun WooPosItemsScreen(
modifier: Modifier = Modifier,
catalogSyncState: CatalogSyncState = CatalogSyncState.Idle,
onRetryCatalogSyncClicked: () -> Unit = {}
) {
val productsViewState = rememberLazyListState()
val couponsListState = rememberLazyListState()
val productsViewModel: WooPosItemsViewModel = hiltViewModel()
Expand All @@ -47,6 +67,8 @@ fun WooPosItemsScreen(modifier: Modifier = Modifier) {
itemsStateFlow = productsViewModel.viewState,
productsViewState = productsViewState,
couponsListState = couponsListState,
catalogSyncState = catalogSyncState,
onRetryCatalogSync = onRetryCatalogSyncClicked,
onUIEvent = { productsViewModel.onUIEvent(it) },
)
}
Expand All @@ -58,6 +80,8 @@ private fun WooPosItemsScreen(
itemsStateFlow: StateFlow<WooPosItemsToolbarViewState>,
productsViewState: LazyListState,
couponsListState: LazyListState,
catalogSyncState: CatalogSyncState,
onRetryCatalogSync: () -> Unit,
onUIEvent: (WooPosItemsUIEvent) -> Unit,
) {
val state = itemsStateFlow.collectAsState()
Expand All @@ -67,6 +91,8 @@ private fun WooPosItemsScreen(
state = state,
productsViewState = productsViewState,
couponsListState = couponsListState,
catalogSyncState = catalogSyncState,
onRetryCatalogSync = onRetryCatalogSync,
onSearchEvent = {
when (it) {
WooPosSearchUIEvent.Clear -> onUIEvent(WooPosItemsUIEvent.ClearSearchClicked)
Expand Down Expand Up @@ -96,10 +122,12 @@ private fun MainItemsList(
state: State<WooPosItemsToolbarViewState>,
productsViewState: LazyListState,
couponsListState: LazyListState,
catalogSyncState: CatalogSyncState,
onSearchEvent: (WooPosSearchUIEvent) -> Unit,
onTabClicked: (WooPosItemsToolbarViewState.Tab) -> Unit,
onAddCouponEvent: () -> Unit,
onBackClicked: () -> Unit,
onRetryCatalogSync: () -> Unit = {},
) {
Box(
modifier = modifier
Expand Down Expand Up @@ -159,6 +187,15 @@ private fun MainItemsList(
}
}
}

if (catalogSyncState is CatalogSyncState.Syncing ||
catalogSyncState is CatalogSyncState.Failed
) {
CatalogSyncOverlay(
catalogSyncState = catalogSyncState,
onRetryClicked = onRetryCatalogSync
)
}
}
}

Expand Down Expand Up @@ -201,6 +238,61 @@ private fun getScreenState(state: WooPosItemsToolbarViewState): ScreenState {
}
}

@Composable
private fun CatalogSyncOverlay(
catalogSyncState: CatalogSyncState,
onRetryClicked: () -> Unit = {}
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.95f)),
contentAlignment = Alignment.Center
) {
when (catalogSyncState) {
is CatalogSyncState.Syncing -> {
SyncingCatalogContent()
}
is CatalogSyncState.Failed -> {
SyncFailedContent(onRetryClicked = onRetryClicked)
}
else -> {
// Should not happen, but handle gracefully
}
}
}
}

@Suppress("WooPosDesignSystemSpacingUsageRule", "WooPosDesignSystemTextUsageRule")
@Composable
private fun SyncingCatalogContent() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
WooPosCircularLoadingIndicator(modifier = Modifier.size(160.dp))
Spacer(modifier = Modifier.height(32.dp))
Text(
text = stringResource(R.string.woopos_home_syncing_catalog_title),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
}
}

@Composable
private fun SyncFailedContent(onRetryClicked: () -> Unit) {
WooPosErrorScreen(
message = stringResource(R.string.woopos_home_sync_failed_title),
reason = stringResource(R.string.woopos_home_sync_failed_message),
primaryButton = Button(
text = stringResource(R.string.woopos_home_sync_failed_retry_button),
click = onRetryClicked
)
)
}

@OptIn(ExperimentalMaterialApi::class)
@Composable
@WooPosPreview
Expand All @@ -222,6 +314,8 @@ fun WooPosItemsScreenSearchVisiblePreview(modifier: Modifier = Modifier) {
itemsStateFlow = productState,
productsViewState = rememberLazyListState(),
couponsListState = rememberLazyListState(),
catalogSyncState = CatalogSyncState.Idle,
onRetryCatalogSync = {},
onUIEvent = {},
)
}
Expand All @@ -248,6 +342,8 @@ fun WooPosItemsScreenSearchHiddenPreview(modifier: Modifier = Modifier) {
itemsStateFlow = productState,
productsViewState = rememberLazyListState(),
couponsListState = rememberLazyListState(),
catalogSyncState = CatalogSyncState.Idle,
onRetryCatalogSync = {},
onUIEvent = {},
)
}
Expand Down
Loading