-
Notifications
You must be signed in to change notification settings - Fork 118
[Woo POS][Local Catalog] Add incremental sync triggers #16264
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
base: trunk
Are you sure you want to change the base?
[Woo POS][Local Catalog] Add incremental sync triggers #16264
Conversation
Catalog size limit check can fail due to network issues and get unreported up the chain. Throw an error when that happens.
Set refreshError when syncing fails. Most often it will be an inline error, showed together with the content. Also, if error is set, refresh items when loadItems is called, which happens when retry is tapped on the error.
…ental-sync-triggers-pull-to
|
|
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.
However, the error handling was more trickier part. I had to introduce refreshState to track when the refresh happens or fails.
Would this be needed if we didn't have to wrap the refresh task in a Task block? Since it's not required in the existing implementation, I assume not?
It seems like it would be a lot simpler if we didn't have that extra state...
Potentially it can be removed. The important thing is not to change the view while there's a refresh happening. Perhaps that's unavoidable while we're observing the underlying database... but I don't think it should be changing except when the refresh task completes... and maybe that would be fine?
I've not had time to dig deep, can sync on it tomorrow if you like. We did face similar issues with the original implementation and managed to fix them all so that we didn't have to swallow the errors with that Task block.
There's a few questions and suggestions in line too.
It does work as expected and test well, so I'm cautiously approving it, but it'd be really good to explore further whether we can remove refreshState before merging. Let me know what you think...
| await Task { | ||
| await itemsController.refreshItems(base: node) | ||
| }.value |
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.
I thought that this is only needed because we set the state to loading on a refresh, which isn't really needed – the refreshing spinner that iOS puts up for us is the indicator that something's happening. However, removing the task shows that some explicitlyCancelled errors are coming through in that case.
I remember writing some code to swallow these – they're not real errors, it's an AlamoFire thing when a previous request gets cancelled, I think. Perhaps we need the same in the incremental sync service somewhere? With that, and removing the call to always set a loading state, I think we could get rid of the task.
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.
I noticed it happening even before I had a loading state, and only had an error state. It was enough to set the error to nil before the incremental sync to cancel it.
In short, if I made any view updates while the refreshItems was still ongoing, the SwiftUI would kill the refreshable task, which caused an explicitlyCancelled error to happen. Even if we swallow the error not to be displayed, the refresh task remains canceled.
I think the problem may be with the view structure, since it doesn't happen on the ItemListView. However, I wasn't able to find a structure that wouldn't produce this cancellation...
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.
Can you reproduce it with the existing item controller? If not, perhaps that rules out the view structure?
In short, if I made any view updates while the refreshItems was still ongoing, the SwiftUI would kill the refreshable task, which caused an explicitlyCancelled error to happen. Even if we swallow the error not to be displayed, the refresh task remains canceled.
Yes, that's unavoidable unfortunately. I'm confusing two memories, we had explicitly cancelled errors from elsewhere for something else. The key is that we can't change the view while we're refreshing.
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.
Can you reproduce it with the existing item controller? If not, perhaps that rules out the view structure?
I'll try!
| // Loading state - preserve existing items (both for data loading and refresh) | ||
| if dataSource.isLoadingProducts || refreshState == .loading { |
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.
| // Loading state - preserve existing items (both for data loading and refresh) | |
| if dataSource.isLoadingProducts || refreshState == .loading { | |
| // Loading state - preserve existing items (only use for data loading, refresh has a PTR spinner) | |
| if dataSource.isLoadingProducts && refreshState != .loading { |
We shouldn't show the loading cell if we're refreshing – the PTR indicator is sufficient, and changing the state breaks the refresh task anyway.
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.
I implemented it to support the error "retry" case, but we could live without it. It's a shame there's no way to manually show a PTR loading animation. I remember trying to replicate it without success.
| // Loading state - preserve existing items (both for data loading and refresh) | ||
| if dataSource.isLoadingVariations || refreshState == .loading { |
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.
| // Loading state - preserve existing items (both for data loading and refresh) | |
| if dataSource.isLoadingVariations || refreshState == .loading { | |
| // Loading state - preserve existing items (only use for data loading, refresh has a PTR spinner) | |
| if dataSource.isLoadingProducts && refreshState != .loading { |
|
|
||
| // Track current parent for variation state mapping | ||
| private var currentParentItem: POSItem? | ||
| private var refreshState: RefreshState = .idle |
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.
What do you think about this being shared between root and variations? It feels like an opportunity to leak state between views by accident...
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.
Well, I made it shared since the incremental sync itself is shared. There's no separate sync for specific parent views. How do you think I should structure it?
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.
Ah yes, I see. Makes sense.
The one niggle I have is that this can be out of sync with the view state, because Apple's code's in control of which PTR indicator is shown. If you PTR on a variation screen, then go back to a parent screen and scroll down to load more items, we'd be loading and refreshing at the same time. Which is kind of OK... but if we use the refresh state to decide to show a loading indicator, we could finish one load without finishing the incremental sync, and still show a loading indicator for the refresh.
If we remove the loading cell for refresh tasks it's less of an issue... and we deliberately don't show that for PTR in the existing implementation, as the spinner is enough.
…ental-sync-triggers-pull-to
|
@joshheald, thanks for testing!
True, we could avoid that if we wouldn't be able to set The best thing would be to figure out why SwiftUI redraws the ChildList and kills the refreshable task when the error changes but it doesn't do that for the ItemListView.
I'm not sure I can remove |
Could we do that and indicate progress for the retry using a different indicator? Perhaps a spinner on the retry button? I think the UX makes sense – you PTR, it fails, we show an error. You then tap a button to retry, it's not a PTR any more, so an indicator on the button is perhaps clearer. |
@joshheald thanks, I'll try that. I think we have a button loading state developed. |
|
@joshheald I also remembered one more case for the loading state. When we have an empty view and we want to show a loading view when the refresh is tapped. However, we could only affect this particular state. |
Yeah. I think the key is: don't change the state when doing a Pull to Refresh, let Apple handle it. |
Additionally, remove the business logic duplications from the controller
They can happen when PTR is started in one view and we move to another. No need to show an error.
Generated by 🚫 Danger |
This hack was required to support child view changes while refresh is happening. However, we avoid changing the state now while refresh is happening so it's no longer necessary.
Retry.animation.mov |
I wasn't able to remove refreshState, so I'm not forcing a merge for now, especially if there are some unexplored ways 🤔 |
|
OK, I'll take it over and try the other ways early next week. I'll probably end up merging as is, but at least we'll be sure |
|
Thanks! @joshheald. I wanted to get it over the line but better be safe with this core functionality. |

WOOMOB-1098
Done based on the project test plan: pdfdoF-7RD-p2
Description
Add incremental sync triggers:
Solution
Pull to Refresh
For PTR, I needed to update
PointOfSaleObservableItemsControllerto supportrefreshItemsmethod. Within it, I addedcoordinator.performIncrementalSynccall. However, the error handling was more trickier part. I had to introducerefreshStateto track when the refresh happens or fails.ChildItemListwas also updating every time anything was changing in observable, therefore, the.refreshablewould get canceled immediately afterrefreshStatechanged. I had to wrap it in another task to make sure it doesn't get canceled automatically.refreshStateneeds to be included in multiple checks to handle error and empty list cases, to make sure the refresh is triggered at the right times.Payment Completion
For payment completion, I'm using
Observationframework observation tracking to check for cash and card payment success to trigger incremental syncPOS Opened
The most straightforward place was to trigger sync within aggregate model loading.
App Opened
It was already implemented, but I updated
performSmartSyncto not do incremental sync if it has happened within an hour.Steps to reproduce
Launch POS
PTR on Parent and Child view
PTR + Errors + Recovery
Payment completion
App open + incremental sync skipped if 1h not passed
Testing information
Tested on iPad Air 26 simulator, new and established sites.
Screenshots
Incremental sync on: Open POS, PTR on parent and child, payment completion, skipped on app launch
incremental.sync.2.mov
Empty State
Simulator.Screen.Recording.-.iPad.Air.11-inch.M3.-.2025-10-21.at.12.37.46.mov
RELEASE-NOTES.txtif necessary.