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

Store Product Image Upload statuses in UserDefaults #15264

Merged

Conversation

pmusolino
Copy link
Member

@pmusolino pmusolino commented Feb 27, 2025

Part 5 of the changes needed for product image upload in the background.

Before merging this PR, make sure to merge this one.

Description

In this PR:

  • I integrated the previously developed ProductImageStatusStorage into ProductImageUploader, which is the core of the uploader.
  • With the updates in ProductImageUploader, the product image statuses will be stored in User Defaults, and stored statuses will become the new source of truth.
  • All the changes are under a feature flag backgroundProductImageUpload, which currently is enabled only in debug.
  • There is also a change in ProductImage to have a custom Equatable conformance because I noticed that storing dates while encoding/decoding them could lose some precision, so ignoring fractional seconds makes this comparison accurate.
  • This PR updates and implements new unit tests for ProductImageUploader, also managing the feature flag.

After this PR, you can expect to see work on real product image uploads in the background.

Testing information

I tested several scenarios with both the feature flag enabled and disabled. All of them should work as they did previously.

  • Uploading one or multiple images to a product and saving them at the end of the upload should correctly save the product with the images included.
  • Uploading one or multiple images to a product and saving the product before the upload finishes should result in the images being loaded correctly and attached to the product, including on the web.
  • Uploading images to a new product should work as it did before.
  • Uploading images to an existing product that already has images should function as it did previously.
  • Uploading one image in a product variation should work as it did before.
  • Using Network Link Conditioner or deactivating your internet connection during the upload should generate errors, allowing you to retry the image upload.

  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if 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:

  • The PR is small and has a clear, single focus, or a valid explanation is provided in the description. If needed, please request to split it into smaller PRs.
  • Ensure Adequate Unit Test Coverage: The changes are reasonably covered by unit tests or an explanation is provided in the PR description.
  • Manual Testing: The author listed all the tests they ran, including smoke tests when needed (e.g., for refactorings). The reviewer confirmed that the PR works as expected on all devices (phone/tablet) and no regressions are added.

…ctImageStatusStorage` for storing images statuses in UserDefaults if feature flag is enabled
…s-part-4' into feat/save-product-image-upload-statuses-in-user-defaults-part-5

# Conflicts:
#	Networking/Networking/ProductImageInBackground/ProductImageStatusStorage.swift
@pmusolino pmusolino added the feature: add/edit products Related to adding or editing products. label Feb 27, 2025
@pmusolino pmusolino added this to the 21.9 milestone Feb 27, 2025
Copy link

coderabbitai bot commented Feb 27, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

📝 Walkthrough

Walkthrough

The changes refactor the ProductImage struct by moving its Equatable conformance into an extension with a custom equality operator. Additionally, the ProductImageUploader has been enhanced with a feature flag mechanism for background product image uploads by adding properties (e.g., userDefaultsStatuses and featureFlagService), modifying methods for error handling and notifications, and introducing the observeStatuses() helper. Test and mock files have been updated accordingly to validate the new behavior. A subproject commit has also been added in the woocommerce-ios repository.

Changes

File(s) Change Summary
Networking/.../Product/ProductImage.swift Moved Equatable conformance from the struct declaration to an extension; implemented a custom static == operator comparing imageID, dateCreated (as integer), src, name, and alt.
WooCommerce/Classes/ServiceLocator/ProductImageUploader.swift Integrated a feature flag for background uploads by adding userDefaultsStatuses and featureFlagService, updating the initializer, modifying methods (sendBackgroundUploadNoticeIfNeeded, scheduleUploadInProgressNotificationIfNeeded), and introducing observeStatuses().
WooCommerce/.../Mocks/MockFeatureFlagService.swift, WooCommerce/.../Mocks/MockProductImageUploader.swift,
WooCommerce/.../Tests/.../ProductImageUploaderTests.swift
Updated mocks and tests to support the feature flag: added a new boolean property (backgroundProductImageUpload) in the feature flag mock, a flag in the uploader mock to track method calls, and several new test cases verifying behavior when the feature flag is enabled.
woocommerce-ios Added a subproject commit (2244abb16a73bb0f307b44899c482a5115c89f2b) referencing a specific state of the repository.

Sequence Diagram(s)

sequenceDiagram
    participant U as ProductImageUploader
    participant F as FeatureFlagService
    participant S as ProductImageStatusStorage
    participant N as NoticePresenter

    U->>F: isFeatureFlagEnabled(backgroundProductImageUpload)
    alt Feature Enabled
        U->>S: Initialize & observe statuses
        U->>S: Retrieve active uploads & errors
        alt Active Upload Exists
            U->>N: Enqueue background upload notice
        end
    else Feature Disabled
        U->>U: Execute default upload logic
    end
Loading

Suggested labels

type: task, category: ui tests

Suggested reviewers

  • tiagomar

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Feb 27, 2025

App Icon📲 You can test the changes from this Pull Request in WooCommerce iOS Prototype by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS Prototype
Build Number29042
VersionPR #15264
Bundle IDcom.automattic.alpha.woocommerce
Commit1afd492
Installation URL07au4moobbdq8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

…s-part-4' into feat/save-product-image-upload-statuses-in-user-defaults-part-5
…roductImageUpload` feature flag, and testing that when `backgroundProductImageUpload == true`, `ProductImageStatusStorage` will be used to store image statuses in UserDefaults.
@pmusolino pmusolino marked this pull request as ready for review February 28, 2025 21:34
@pmusolino
Copy link
Member Author

@coderabbitai full review

Copy link

coderabbitai bot commented Mar 2, 2025

✅ Actions performed

Full review triggered.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
WooCommerce/Classes/ServiceLocator/ProductImageUploader.swift (1)

362-379: Monitoring statuses from storage is a good addition.
Consider whether multiple failures should be communicated rather than just the first one, if that is ever a requirement.

WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift (2)

794-794: Unused CurrentValueSubject.

There's an unused CurrentValueSubject that doesn't appear to serve any purpose in this test.

-    _ = CurrentValueSubject<[ProductImageUploaderKey], Never>([])

834-834: Consider verifying notice content.

While the test verifies that sendBackgroundUploadNoticeIfNeeded is called, it doesn't verify the content of the notice. Consider capturing the notice and verifying its content to ensure it contains the expected message.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acb6300 and 3540180.

📒 Files selected for processing (6)
  • Networking/Networking/Model/Product/ProductImage.swift (2 hunks)
  • WooCommerce/Classes/ServiceLocator/ProductImageUploader.swift (7 hunks)
  • WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift (4 hunks)
  • WooCommerce/WooCommerceTests/Mocks/MockProductImageUploader.swift (2 hunks)
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift (18 hunks)
  • woocommerce-ios (1 hunks)
🔇 Additional comments (28)
woocommerce-ios (1)

1-1: Subproject Commit Reference Update Approved

The commit reference "2244abb16a73bb0f307b44899c482a5115c89f2b" correctly points to the desired state of the subproject. This update is standard for managing subproject dependencies and ensures version consistency.

WooCommerce/WooCommerceTests/Mocks/MockProductImageUploader.swift (2)

18-18: Good addition for test validation.
This property is neatly introduced to help confirm that the sendBackgroundUploadNoticeIfNeeded method was invoked.


68-73: Implementation aligns with intended test behavior.
Using the mock to enqueue a notice only if activeUploadsKeys contains the given key is a clean way to simulate real upload behavior in tests.

Networking/Networking/Model/Product/ProductImage.swift (2)

6-6: Separation of concerns for Equatable conformance is good.
Removing Equatable from the struct declaration and placing it in an extension keeps the struct declaration lean.


58-71: Confirm the handling of dateModified.
The extension currently compares only dateCreated (ignoring milliseconds) but not dateModified. Verify if excluding dateModified from equality checks is intentional.

WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift (4)

26-26: Feature flag property looks correct.
Declaring backgroundProductImageUpload is a straightforward addition for toggling behavior.


48-49: Constructor parameters updated seamlessly.
The new boolean parameter maintains backward compatibility with a default of false.


71-71: Assignment logic is straightforward.
Storing the constructor parameter into the property is correctly handled.


118-119: Adds explicit case for the new feature flag.
The switch statement properly returns the new property for .backgroundProductImageUpload.

WooCommerce/Classes/ServiceLocator/ProductImageUploader.swift (13)

10-11: New imports are appropriate for the feature flag and storage.
No issues with adding these imports.


93-94: Optional storage reference is a sensible approach.
Having userDefaultsStatuses optional simplifies logic when the flag is disabled.


95-119: Error publisher logic is correctly feature-flagged.
Splitting the error pipeline based on the feature flag is a clean solution.


122-137: Active uploads pipeline follows the same pattern as errors.
This maintains consistency between the two streams.


153-153: Feature flag service injection is clear.
Storing a reference to featureFlagService ensures maintainability and clarity.


156-157: Combine cancellables property is necessary.
Tracking subscriptions with Set<AnyCancellable> is the standard approach.


165-170: Conditional initialization of userDefaultsStatuses is correct.
Observing user defaults statuses only if the feature flag is enabled avoids overhead otherwise.


228-239: Conditional path for background upload notice.
Both branches handle the notice consistently with the rest of the feature-flag logic.


313-330: Logic for scheduling local notifications is consistent.
The approach of splitting by feature flag usage is cohesive with the rest of the code.


385-389: Status updates push changes to user defaults.
This ensures the new approach remains the source of truth when the flag is on.


390-397: Falling back to local property updates is straightforward.
The code path is consistent with non–feature-flag scenarios.


411-420: Error reporting remains split by feature flag.
This method seamlessly updates user defaults or emits errors directly.


427-436: Method cleanly removes active upload references.
By removing the relevant statuses or active keys, the two approaches remain parallel.

WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift (6)

6-6: Import added for Networking module.

The new Networking import appears to be needed for the feature flag implementation. This is appropriate as it helps organize module dependencies.


14-16: Properties added for feature flag testing.

These new properties will be used to support the feature flag testing. The naming is clear and follows Swift conventions, while maintaining appropriate access control levels.


18-24: Well-structured setUp method.

The setUp method properly initializes the test environment by creating the mock feature flag service and ensuring UserDefaults is clean for each test. Following best practices by calling super.setUp() first.


26-31: Proper cleanup in tearDown method.

Good practice to clean up resources in tearDown, including setting properties to nil and removing UserDefaults entries. Following best practices by calling super.tearDown() last.


33-33: Good use of MARK for test organization.

Adding a MARK comment to distinguish between feature flag disabled and enabled tests improves code readability and organization.


735-881: Comprehensive tests for feature flag functionality.

The new test cases thoroughly cover the functionality when the feature flag is enabled, testing:

  • Change tracking
  • Error publishing
  • Active uploads tracking
  • Background upload notices
  • Reset behavior
  • Local ID replacement

All tests follow the Given-When-Then pattern and have clear assertions. Good use of mocks to isolate the code under test.

@selanthiraiyan selanthiraiyan self-assigned this Mar 3, 2025
Copy link
Contributor

@selanthiraiyan selanthiraiyan left a comment

Choose a reason for hiding this comment

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

Thanks for working on this, Paolo!

I left a few questions about the unit tests. Please take a look when you can.

woocommerce-ios Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is an accidental addition and should be removed.

Copy link
Member Author

Choose a reason for hiding this comment

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

To what are you referring?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I see a new folder named woocommerce-ios added here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, good catch! I think it was generated by mistake while moving all this code in this PR, as it was initially part of a larger branch.

…eUploader`, considering all the errors and not just the first one.
@pmusolino
Copy link
Member Author

Thank you for the review, @selanthiraiyan! 🙌 I will respond to your feedback step by step.

I noticed an issue that was addressed in another class and in a previous PR, so I fixed it here as well. I ensured that all error statuses are processed and reported, not just the first one. 3a14b53

Copy link
Contributor

@itsmeichigo itsmeichigo left a comment

Choose a reason for hiding this comment

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

I took a quick look at the updates in ProductImageUploader and have some comments too:

…uctImageUploader` and always instantiate `ProductImageStatusStorage` in `ProductImageUploader`
- Refactor `ProductImageUploaderTests.swift` to use actual `ProductImageUploader` instead of mock
- Update test logic to check actual upload status and actions
- Remove redundant test storage key and update UserDefaults handling in `ProductImageUploaderTests`
@pmusolino
Copy link
Member Author

@selanthiraiyan thanks for all the suggestions! I updated the tests you mentioned since you were right, also removing the unnecessary code in MockProductImageUploader, and you can find them in this commit 927e199. I also removed the folder you mentioned (in reality Github doesn't show it correctly, it looks like a folder but in Sourcetree it's a submodule), reverting it here a3f7fd4, so it's ready for another look.

…ked_through_storage_with_flag_enabled` using real `ProductImageUploader` instead of a mock.
…torage as a parameter, and avoid using UserDefaults.standard using mock instances. Additionally, address the failing unit test `test_activeUploads_are_tracked_through_storage_with_flag_enabled`, which has been inconsistent when run alongside other tests.
@selanthiraiyan selanthiraiyan removed their assignment Mar 7, 2025
@wpmobilebot wpmobilebot modified the milestones: 21.9, 22.0 Mar 7, 2025
@wpmobilebot
Copy link
Collaborator

Version 21.9 has now entered code-freeze, so the milestone of this PR has been updated to 22.0.

Copy link
Contributor

@itsmeichigo itsmeichigo left a comment

Choose a reason for hiding this comment

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

I tested the app with the changes and found that uploading still works as expected. I saw an issue with the error handling though, as described in the comment below.

The PR description didn't mention testing the upload in the background. Are there any more PR coming to support that or can we test background upload in this PR already?

}
}

func test_replaceLocalID_is_called_when_flag_enabled() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I read through the implementation of replaceLocalID again and still don't see any connection between this method and the status storage. It works solely with actionHandlersByProduct, an in-memory dictionary that holds the action handlers by product upload key. Do you think this implementation still works when the app is in the background?

If you see a connection between replaceLocalID and the status storage, please show me where the code that indicates that, because I cannot see it.

Comment on lines 99 to 108
errorItems.publisher
.compactMap { errorItem in
guard let productOrVariationID = errorItem.productOrVariationID,
let assetType = errorItem.assetType else { return nil }
return ProductImageUploadErrorInfo(
siteID: errorItem.siteID,
productOrVariationID: productOrVariationID,
error: .failedUploadingImage(asset: assetType, error: errorItem.error)
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I also missed in the previous reviews that the stream should exclude any keys listed in statusUpdatesExcludedProductKeys. That way, we would not display the error notice when an upload fails while the user is on the same product form.

Notice above that both the error notice and alert are displayed.

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice catch Huong, I fixed it here 5b719e4 can you please take another look?

@itsmeichigo
Copy link
Contributor

I'm also seeing that test_reset_clears_storage_state_with_flag_enabled() in ProductImageUploaderTests is failing consistently. Please check the test again, thanks!

@pmusolino
Copy link
Member Author

The PR description didn't mention testing the upload in the background. Are there any more PR coming to support that or can we test background upload in this PR already?

This is part of following PRs (one is still currently in draft), nothing to test in background here, but thanks for asking!

I'm also seeing that test_reset_clears_storage_state_with_flag_enabled() in ProductImageUploaderTests is failing consistently. Please check the test again, thanks!

It seems that the test is unreliable: it passes when run alone but fails when executed alongside the other tests. I will investigate to determine what is causing the issue. 👍

@pmusolino pmusolino requested a review from itsmeichigo March 7, 2025 15:16
Copy link
Contributor

@itsmeichigo itsmeichigo left a comment

Choose a reason for hiding this comment

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

Thanks Paolo for the updates and explanations! I left my last feedback on the threads, please check them out. I'm pre-approving this as the PR should be ready after those last issues are addressed (other than the failing unit test).

Comment on lines +124 to +129
.filter { info in
let key = Key(siteID: info.siteID,
productOrVariationID: info.productOrVariationID,
isLocalID: info.productOrVariationID.id == 0)
return !self.statusUpdatesExcludedProductKeys.contains(key)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This filter is redundant because we already check for this in observeImageUploads.

Copy link
Member Author

Choose a reason for hiding this comment

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

I do not think it's redundant because errorsSubject can receive error events from other places in ProductImageUploader besides observeImageUploads. For instance, if you look at the saveProductImagesWhenNoneIsPendingUploadAnymore method, it calls errorsSubject.send(...) directly when saving fails, without checking statusUpdatesExcludedProductKeys. Because of that, the filter on errorSubject acts as a "final gatekeeper" ensuring that errors tied to excluded keys are reliably discarded no matter where in ProductImageUploader they're sent.
If the only place errors were published was observeImageUploads and it always checked for excluded keys, then the filter might be redundant, but given errors can come from more than one place, I believe the filter is necessary to ensure excluded keys never emit errors. Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it was intentional that the save product errors were not filtered out. We were only filtering out upload errors because we also listened to the product form errors. The background saving is handled separately from the product form, so any failures would still be displayed. But this code exists before I joined the project so I'm not entirely sure.

If you feel strongly about hiding the background saving failures when the product form is present, please keep the current change.

}
}

func test_replaceLocalID_is_called_when_flag_enabled() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we have existing tests for replaceLocalID already, I think it's better to remove this test when it's unnecessary.

…s-part-4' into feat/save-product-image-upload-statuses-in-user-defaults-part-5
Base automatically changed from feat/save-product-image-upload-statuses-in-user-defaults-part-4 to trunk March 11, 2025 13:09
@wpmobilebot wpmobilebot modified the milestones: 22.0, 22.1 Mar 21, 2025
@wpmobilebot
Copy link
Collaborator

Version 22.0 has now entered code-freeze, so the milestone of this PR has been updated to 22.1.

@pmusolino pmusolino enabled auto-merge March 28, 2025 10:54
@pmusolino pmusolino merged commit 2a0cb60 into trunk Mar 28, 2025
13 checks passed
@pmusolino pmusolino deleted the feat/save-product-image-upload-statuses-in-user-defaults-part-5 branch March 28, 2025 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature: add/edit products Related to adding or editing products.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants