-
Notifications
You must be signed in to change notification settings - Fork 121
[Woo POS] Coupons: Sync from remote #15423
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
Conversation
I'm not sure but I think we might have discussed to do the opposite? Storage first and remote later? |
|
|
@iamgabrielma Yes, I imagined we could do local first so the results would appear immediately, but update them with remote ones. How does the current IPP implementation work? As I understand, it's storage-first. My idea was to keep the same strategy as IPP to save time on coming up with alternatives.
A full-screen loading doesn't work here. A good point is that we need to ask a designer to consider how we display a loading state. Asked here p1742974197820449-slack-C070SJRA8DP |
|
This is how IPP works. I added a new coupon and returned to the screen. After a moment, a new coupon appeared. We could sync with the design on this as well. Maybe we could show a small loading indicator somewhere as well while the network request is ongoing. Wdyt? ScreenRecording_03-26-2025.09-36-12_1.mov |
That sounds good, I'll take a deeper look of how we do it in coupons in the app and make the changes to do local first -> then update with remote results. I'll see if I can change the loading indicator on this PR as well or add the "ghost rows" separately 👍 |
|
I've spent quite a bit of time looking at the IPP implementation and I'm no sure we can do the same, or at least not right away, this is mostly because the current design forces us to perform an action in the UI (load, reload, scroll, ... ) when calling the provider for new items, while IPP plays with a SyncCoordinator delegate that listens and acts upon changes in storage/entities. The sync + upsert handling also relies on calling the Coupons store, which forces us to do the same if we want to reuse what we have rather than reimplementing it. I'm still unsure if makes more sense to add it in the controller or the service, but for the moment I think the service makes more sense, as all of these are in Yosemite and will be called every time we fetch items from any action, but we can switch them over as needed, perhaps once we implement PTR and UI state changes this will become clearer.
Screen.Recording.2025-03-31.at.12.30.31.mov
Screen.Recording.2025-03-31.at.13.01.20.mov |
|
Thanks, @iamgabrielma!
Yes, it looks like it's not possible to copy the entire solution and we need to add our details. Do I understand correctly that the challenge is combining local and remote updates so the POS would be aware when both happen?
I think it's fine. Let's reuse the store. 👍
Sounds good! If the main challenge is communicating UI updates, I think it should be achievable.
We'll probably need some additional flags within What do you think? What are your main challenges? Which state do you want to reach in the scope of this PR? |
| } | ||
|
|
||
| extension CouponsRemote { | ||
| public func loadAllCoupons(for siteID: Int64, |
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.
super nit; not loading all coupons here
| public func loadAllCoupons(for siteID: Int64, | |
| public func loadCoupons(for siteID: Int64, |
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'm not sure to understand: loadAllCoupons loads all while loadCoupons loads a subset which we know the couponIDs of, right? "makes a single request for a list of coupons" but we don't have that list so we have to fetch them all.
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 just meant from a readability point of view. loadAllCoupons sounds like we're loading ALL coupons, rather than specific page. But I see now that CouponsRemoteProtocol has loadAllCoupons and loadCoupons which is different. 👍 So we can keep it as it is. 👍
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 gotcha, I agree the names are a bit confusing thought, I'll see if I can sneak a task separately to rename these better.
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.
It's fine, not necessary 👍
| @MainActor | ||
| public func providePointOfSaleItems(pageNumber: Int) async throws -> PagedItems<POSItem> { | ||
| let coupons = providePointOfSaleCoupons() | ||
| let coupons = await providePointOfSaleCoupons() |
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.
Idea: If coupons is empty, we could wait for syncCouponsFromRemote to complete before returning PagedItems.
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.
That sounds good, updated here: 1a23559 . I was a bit wary of using a detached Task attached to a dispatcher action, but from testing seems to work fine, let me know if raises any red flag:
- If non empty, we return storage and fire a sync task which will complete at some point
- If empty, we wait for the sync both remote and storage, then return that
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 was a bit wary of using a detached Task attached to a dispatcher action, but from testing seems to work fine
I think it's a right usage of a detached Task. We want the storage to be updated with the newest coupons independent from the providePointOfSaleItems.
Yes, the question depends a bit on another one: Do we want the UI to be aware of this changes? Or we want to rely on an user action to communicate those (as we do for products currently)? Let me explain: The current interface for products allows us to fetch items via calling the provider explicitly, and we do so through either:
But we don't update the existing list automatically if the remote is updated, it needs to navigate back, to PTR, or to trigger scrolling, so we fetch/sync on demand. With coupons we're introducing a different approach: We have something in storage, maybe something different in remote. We fetch and display storage while simultaneously updating the data with the latest remote results, and making the UI update automatically if there are changes. Cue to:
In both cases (if we want the UI to update automatically or upon an action) I'd say the scope for this PR could be limited to what's now:
I'd leave the UI loading state, and pagination for separate PRs, specially since the current IPP coupons implementation relies heavily on pagination to interact with storage. What is your position on the UI update for coupons? It feels nicer if we update them automatically if these changes, but at the same time it's a different behaviour that the one we have now 🤔 |
|
@iamgabrielma, appreciate the explanation!
Well, I think we should update it to the newest state once it's loaded. We could try keeping it simple at first and update the controller itemListState when the service communicates that the remote was refreshed. I don't know how it all connects to pagination and PTR, you're aware of details better. But yes, updating list when remote updates would be good 👍 |
staskus
left a comment
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.
Looks good 👍 made minor comments
We can add testing, handling remote updates, and improve loading state if another PR if you prefer.
If you make larger changes within this PR, please re-request the review.
| private let currencyFormatter: CurrencyFormatter | ||
| private let productsRemote: ProductsRemote | ||
| private let variationRemote: ProductVariationsRemoteProtocol | ||
| private let couponsRemote: CouponsRemote |
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.
Should we use a CouponsRemoteProtocol here to support mocking the remote in unit tests?
Or is it preferred to mock Network instead in the project?
| private let couponsRemote: CouponsRemote | |
| private let couponsRemote: CouponsRemoteProtocol |
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.
Good question, I'm not sure what we'd want to test better right now: Passing a predefined response from mocking the network, or mocking the remote logic, since a lot of it should be covered by pre-existing tests. Perhaps both! I'll take a look into it asap separately with some unit tests.
| @MainActor | ||
| public func providePointOfSaleItems(pageNumber: Int) async throws -> PagedItems<POSItem> { | ||
| let coupons = providePointOfSaleCoupons() | ||
| let coupons = await providePointOfSaleCoupons() |
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 was a bit wary of using a detached Task attached to a dispatcher action, but from testing seems to work fine
I think it's a right usage of a detached Task. We want the storage to be updated with the newest coupons independent from the providePointOfSaleItems.
| import class WooFoundation.CurrencySettings | ||
| import Storage | ||
|
|
||
| public final class PointOfSaleCouponService: PointOfSaleItemServiceProtocol { |
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.
Theoretically, PointOfSaleCouponService doesn't need or perhaps even shouldn't be PointOfSaleItemServiceProtocol. It's an independent service with its own logic. It doesn't have to fit the same protocol defined for items.
Having a separate protocol allows us to define an interface that would support our needs. E.g., notifying a controller when remote updates.
public func providePointOfSaleItems(pageNumber: Int) could maybe even be AsyncSequence that returns first round of items from storage and another round when storage is updated with remote. I don't know if we have used it anywhere else yet but maybe could be interesting to try out.
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.
That's a very good point, I initially stuck with conforming to the PointOfSaleItemServiceProtocol to follow the same design as products, but its making things more rigid.
As long as we keep the same protocol for the controller, we can switch them in the dashboard to switch between products and coupons (currentController), but the service that feeds it can be independent, which should give us more flexibility. I'll do that as a next step after merging this one just to keep changes separate, but as you mention, this should clarify/simplify the path for the rest of changes.
| } | ||
|
|
||
| extension CouponsRemote { | ||
| public func loadAllCoupons(for siteID: Int64, |
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.
It's fine, not necessary 👍
Closes: #15324
Description
This PR adds the ability to fetch coupons from remote to the coupons service. First, we attempt to fetch from the remote. If this fails, we'll attempt to fetch from storage. If fails, we just return an empty list of coupons (for the moment)
We might change the fetching strategy, for now it seemed adequate to attempt remote first and local storage later.
Screen.Recording.2025-03-26.at.13.58.58.mov
Testing information
couponsbuttonCouponsRemote.loadAllCoupons():RELEASE-NOTES.txtif necessary.Reviewer (or Author, in the case of optional code reviews):
Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement: