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

Background image upload for products without attaching images to the products #15277

Open
wants to merge 31 commits into
base: trunk
Choose a base branch
from

Conversation

pmusolino
Copy link
Member

@pmusolino pmusolino commented Mar 5, 2025

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

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

Description

This pull request introduces the capability for background image uploads for products using a new background URL session. Please let me know if this PR is too big to be reviewed and if you prefer I split it into two PRs.

- Feature Introduction:

  • Adds capability for background image uploads for products.
  • Utilizes a new background URL session.

- Availability:

  • Hidden under a feature flag.
  • Accessible to users authenticated through WordPress.com stores and application passwords.

- Functionality:

  • Managed by the MediaUploadSessionManager.
  • Allows media uploads to continue even when the app is suspended or terminated.
  • Matches previous behavior when the app is in the foreground.
  • In the background, images can be uploaded without being attached to products (this will be part of future PRs).
  • Future pull requests will expand on this functionality.

- Testing:

  • Due to the size of this PR, unit tests will be included in a subsequent PR.
  • Unit tests for MediaUploadSessionManager will be added after implementation is finalized.
  • Current updates to ProductImageActionHandlerTests only address conditions when the feature flag is disabled.

Testing information

There are several scenarios with both the feature flag backgroundProductImageUpload enabled and disabled that should be tested. All of them should work as they did previously for authenticated user with WP.com but also with application password.

  • 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.

Testing information in background

Now that everything works like before, let's test the background upload:
Here you can find a detailed guide on how to send the app in background and understand the behavior.
The main points are:

  1. In Xcode, edit the scheme, and under the Launch option, choose "Wait for the executable to be launched".
Screenshot 2025-03-05 at 15 43 10
  1. In AppDelegate, under the method applicationDidEnterBackground(_ application: UIApplication), add exit(0). This will terminate the app and send it into a background state (suspended) without forcing a quit.
  2. Now run the app. After compiling, you will need to tap the app's icon manually due to point 1.
  3. Navigate to an existing product or create a new one, then upload some images. While the images are uploading, navigate outside of the app. As mentioned in point 2, the app will enter a background state (suspended).
  4. Now wait. The upload may take a few minutes, but it will complete. You can verify that the images have been uploaded correctly by checking WordPress Media on the web or through the app. However, you will not see the uploaded images attached to the product (this part is not implemented in this PR).

  • 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.

…using background URL Session (still just working on WPCom stores)
…l-background-image-upload

# Conflicts:
#	WooCommerce/Classes/ViewRelated/Products/Media/ProductImageActionHandler.swift
…s-part-5' into feat/real-background-image-upload
- Remove all print statements used for debugging
- Add DDLogError for error-related messages
- Add DDLogDebug for debug-related messages
…hod in unit tests for `ProductImageActionHandler`
@pmusolino pmusolino added the feature: add/edit products Related to adding or editing products. label Mar 5, 2025
Copy link

coderabbitai bot commented Mar 5, 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 pull request adds background media upload functionality to the project. It introduces a new file for managing media uploads in the background and updates several components to integrate this functionality. Changes include new extensions and methods for converting media types, modifications to networking classes for handling credentials, and updates in app integration, testing, and documentation. Feature flags now determine whether to use background uploads, and the media store is enhanced with a background uploader dependency.

Changes

File(s) Change Summary
Networking/Networking.xcodeproj/... Added file reference for MediaUploadSessionManager.swift to the project configuration.
Networking/Networking/Model/Media/WordPressMedia.swift Added extension method toMedia() and a computed property for retrieving the file extension.
Networking/Networking/Network/AlamofireNetwork.swift Introduced a new public credentials property and updated the initializer to accept and initialize credentials.
Networking/Networking/Network/MediaUploadSessionManager.swift Added a new class for managing background media uploads with delegate callbacks and URLSession handling.
Networking/Networking/Remote/MediaRemote.swift Implemented an async uploadMediaRequest(...) method to create a URLRequest for background media uploads.
WooCommerce/Classes/AppDelegate.swift,WooCommerce/Classes/ServiceLocator/ServiceLocator.swift Added background URL session handling in AppDelegate and integrated a background media uploader in ServiceLocator.
WooCommerce/Classes/ViewRelated/Products/Media/ProductImageActionHandler.swift Updated to include a FeatureFlagService dependency and feature flag–based logic to choose between background and standard media uploads, introducing an uploadID.
WooCommerce/Classes/Yosemite/AuthenticatedState.swift Updated instantiation of MediaStore to include a new backgroundUploader dependency.
Yosemite/Yosemite/Actions/MediaAction.swift,Yosemite/Yosemite/Stores/MediaStore.swift Added a new uploadMediaInBackground case in MediaAction and a corresponding method in MediaStore; initializers were updated to accept a background uploader.
WooCommerce/WooCommerceTests/...,Yosemite/YosemiteTests/... Updated test cases to inject the new background uploader and feature flag service; added handling for the background media upload action in mocks.
docs/NETWORKING.md Added a new "Background Uploads" section documenting the MediaUploadSessionManager and background upload process.

Sequence Diagram(s)

sequenceDiagram
    participant UI as ProductImageActionHandler
    participant Store as MediaStore
    participant Manager as MediaUploadSessionManager
    participant Session as URLSession
    participant Delegate as UploadDelegate

    UI->>Store: uploadMediaInBackground(siteID, productID, mediaAsset, uploadID)
    Store->>Manager: uploadMedia(request, mediaItem, uploadID, completion)
    Manager->>Session: Start background upload task
    Session-->>Manager: Upload progress/completion
    Manager->>Delegate: Notify upload completion
    Manager-->>Store: Return upload result (success/failure)
    Store-->>UI: Return upload result
Loading
sequenceDiagram
    participant Handler as ProductImageActionHandler
    participant Flag as FeatureFlagService
    participant Action as MediaAction

    Handler->>Flag: isBackgroundProductImageUploadEnabled?
    alt Enabled
        Handler->>Action: uploadMediaInBackground(..., uploadID, onCompletion)
    else Disabled
        Handler->>Action: uploadMedia(..., onCompletion)
    end
Loading

Suggested labels

type: task, feature: shipping labels


🪧 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.

@pmusolino pmusolino added this to the 21.9 milestone Mar 5, 2025
@dangermattic
Copy link
Collaborator

dangermattic commented Mar 5, 2025

1 Warning
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Mar 5, 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 Number29050
VersionPR #15277
Bundle IDcom.automattic.alpha.woocommerce
Commitf0c71cb
Installation URL3029h1ua743to
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@pmusolino
Copy link
Member Author

@coderabbitai review

Copy link

coderabbitai bot commented Mar 5, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 1

🧹 Nitpick comments (13)
Yosemite/Yosemite/Actions/MediaAction.swift (1)

43-51: Implementation looks good for background upload capability.

The addition of the uploadMediaInBackground case to the MediaAction enum provides a clear and well-structured entry point for background media uploads. It properly extends the existing upload functionality with the necessary parameters for background processing.

I'd suggest one minor adjustment to maintain consistent indentation with other enum cases:

case uploadMediaInBackground(siteID: Int64,
-                               productID: Int64,
-                               mediaAsset: ExportableAsset,
-                               altText: String?,
-                               filename: String?,
-                               uploadID: String,
-                               onCompletion: (Result<Media, Error>) -> Void)
+                             productID: Int64,
+                             mediaAsset: ExportableAsset,
+                             altText: String?,
+                             filename: String?,
+                             uploadID: String,
+                             onCompletion: (Result<Media, Error>) -> Void)
WooCommerce/Classes/AppDelegate.swift (1)

270-276: Well-implemented background session handling.

The implementation of application(_:handleEventsForBackgroundURLSession:completionHandler:) correctly handles background URL session events by checking the session identifier and delegating to the appropriate manager. This is essential for the background upload functionality to work properly when the app is relaunched after a background upload completes.

Consider adding a brief comment explaining what happens if the identifier doesn't match:

func application(_ application: UIApplication,
                 handleEventsForBackgroundURLSession identifier: String,
                 completionHandler: @escaping () -> Void) {
    if identifier == ServiceLocator.backgroundMediaUploadSessionManager.backgroundSessionIdentifier {
        ServiceLocator.backgroundMediaUploadSessionManager.handleBackgroundSessionCompletion(completionHandler)
    }
+   // If the identifier doesn't match our background media upload session,
+   // we're not handling it since it must belong to another subsystem
}
WooCommerce/Classes/ServiceLocator/ServiceLocator.swift (2)

101-103: Consider adding documentation for the background media manager's purpose

While the property has basic documentation, consider expanding it to explain its role in the background upload process.

 /// Background image service
 ///
-private static var _backgroundMediaUploadSessionManager = MediaUploadSessionManager()
+/// Manages background media uploads that continue even when the app is suspended or terminated.
+private static var _backgroundMediaUploadSessionManager = MediaUploadSessionManager()

275-279: Missing test setter for background media manager

Unlike other services in this class, there's no test setter method for _backgroundMediaUploadSessionManager in the testability extension. This could make it harder to provide a mock for testing.

Consider adding a test setter:

static func setBackgroundMediaUploadSessionManager(_ mock: MediaUploadSessionManager) {
    guard isRunningTests() else {
        return
    }
    
    _backgroundMediaUploadSessionManager = mock
}
Yosemite/YosemiteTests/Mocks/Networking/Remote/MockMediaRemote.swift (1)

106-117: Implementation of the uploadMediaRequest method looks good

The method correctly simulates creating an upload request with appropriate multipart form data headers and records the invocation. Since this is a mock, error handling is sufficient.

One minor improvement could be to make the return value customizable, similar to how other mock methods work:

private var uploadMediaRequestResultsBySiteID = [Int64: URLRequest]()

func whenUploadingMediaRequest(siteID: Int64, thenReturn request: URLRequest) {
    uploadMediaRequestResultsBySiteID[siteID] = request
}

Then you could return the predefined request instead of constructing one in the method, offering more flexibility in tests.

Networking/Networking/Remote/MediaRemote.swift (3)

173-233: Comprehensive implementation of background media upload request creation.

The implementation constructs a proper multipart form data request for background uploading, with several positive aspects:

  • Generates a unique boundary for multipart form data
  • Properly authenticates the request when credentials are available
  • Includes appropriate error handling
  • Correctly builds the multipart form data manually

Consider potential improvements for robustness:

  1. Consider using an enumeration for HTTP methods instead of hardcoding "POST"
  2. Extract the multipart form data creation to a separate helper method for better maintainability
- request.httpMethod = "POST"
+ request.httpMethod = HTTPMethod.post.rawValue

204-207: Minor optimization opportunity in string data conversion.

The helper function for appending data uses forced unwrapping which is generally safe for UTF-8 encoding of strings, but could be improved.

Consider using a more robust approach:

-func append(_ string: String) {
-    body.append(string.data(using: .utf8)!)
-}
+func append(_ string: String) {
+    if let data = string.data(using: .utf8) {
+        body.append(data)
+    }
+}

209-213: Consider handling nil altText more explicitly.

The code uses null coalescing to set an empty string when altText is nil, which is good, but it may be clearer to handle this more explicitly.

-    ParameterKey.wordPressAltText: mediaItem.altText ?? ""
+    ParameterKey.wordPressAltText: mediaItem.altText.map { $0 } ?? ""
WooCommerce/Classes/ViewRelated/Products/Media/ProductImageActionHandler.swift (2)

229-237: Avoid code duplication between background and standard uploads.

Although the logic here is valid, there's a repeating pattern for handling phAsset under the background/foreground logic. Consider extracting a helper to reduce duplication and improve maintainability.

-                if featureFlagService.isFeatureFlagEnabled(.backgroundProductImageUpload) {
-                    action = MediaAction.uploadMediaInBackground(..., onCompletion: onCompletion)
-                } else {
-                    action = MediaAction.uploadMedia(..., onCompletion: onCompletion)
-                }
+                action = featureFlagService.isFeatureFlagEnabled(.backgroundProductImageUpload)
+                    ? MediaAction.uploadMediaInBackground(..., onCompletion: onCompletion)
+                    : MediaAction.uploadMedia(..., onCompletion: onCompletion)

246-254: Reduce repetition in .uiImage branch logic.

Similar to the .phAsset case, the .uiImage branch also has near-identical code for choosing between background and standard uploads. Extracting a shared function or using a conditional expression in a single line can simplify maintenance.

Networking/Networking/Network/MediaUploadSessionManager.swift (1)

40-74: Temp file cleanup timing.

The code removes the temp file on the main queue (DispatchQueue.main.async), which might momentarily queue up behind various UI tasks. For efficiency, consider removing the file on a background queue unless there's a specific UI-related reason to do so.

Yosemite/Yosemite/Stores/MediaStore.swift (2)

18-26: Constructor dependency injection improvements.

  1. Having multiple initializers for different requirements is flexible but can become unwieldy.
  2. Ensure that each initializer delegates to a single designated initializer to avoid code drift between them.

224-261: Background upload integration appears sound.

  1. The Task { @MainActor in ... } block for export is appropriate since it likely interacts with UI-bound code or needs main-actor constraints.
  2. Cleanup of local media on completion is well-handled.
  3. Consider adding cancellation support in the future, allowing users to halt a background upload if needed.
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2607997 and 24ed03c.

📒 Files selected for processing (17)
  • Networking/Networking.xcodeproj/project.pbxproj (4 hunks)
  • Networking/Networking/Model/Media/WordPressMedia.swift (1 hunks)
  • Networking/Networking/Network/AlamofireNetwork.swift (1 hunks)
  • Networking/Networking/Network/MediaUploadSessionManager.swift (1 hunks)
  • Networking/Networking/Remote/MediaRemote.swift (3 hunks)
  • WooCommerce/Classes/AppDelegate.swift (2 hunks)
  • WooCommerce/Classes/ServiceLocator/ServiceLocator.swift (3 hunks)
  • WooCommerce/Classes/ViewRelated/Products/Media/ProductImageActionHandler.swift (4 hunks)
  • WooCommerce/Classes/Yosemite/AuthenticatedState.swift (1 hunks)
  • WooCommerce/WooCommerceTests/Mocks/MockMediaStoresManager.swift (1 hunks)
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Edit Product/ProductFormViewModel+UpdatesTests.swift (1 hunks)
  • WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageActionHandlerTests.swift (11 hunks)
  • Yosemite/Yosemite/Actions/MediaAction.swift (1 hunks)
  • Yosemite/Yosemite/Stores/MediaStore.swift (3 hunks)
  • Yosemite/YosemiteTests/Mocks/Networking/Remote/MockMediaRemote.swift (2 hunks)
  • Yosemite/YosemiteTests/Stores/MediaStoreTests.swift (9 hunks)
  • docs/NETWORKING.md (1 hunks)
🔇 Additional comments (35)
WooCommerce/WooCommerceTests/Mocks/MockMediaStoresManager.swift (1)

59-64: The new case correctly handles background media upload mocking.

This implementation properly follows the established pattern in the class for handling media actions. It correctly checks for the presence of media and returns either a success or failure result accordingly.

Since this is a mock for testing, are there any specific error scenarios for background uploads that might need separate test cases? The current implementation only handles the basic nil media case, but background uploads might have unique failure modes (like app termination during upload or connectivity issues) that could be worth simulating in more advanced test scenarios.

Networking/Networking.xcodeproj/project.pbxproj (4)

509-509: LGTM: New file reference correctly added to the build phase.

The MediaUploadSessionManager.swift file is properly referenced in the build phase, which is a necessary step for including it in the compilation process.


1729-1729: LGTM: File reference correctly configured.

The file reference for MediaUploadSessionManager.swift is properly configured with the correct type (sourcecode.swift), which ensures Xcode will treat it as a Swift source file.


2820-2820: LGTM: Appropriate placement in project structure.

The MediaUploadSessionManager.swift file is correctly placed in the Network group alongside other networking-related files like AlamofireNetwork.swift, MockNetwork.swift, and NullNetwork.swift. This organization makes sense given its purpose of managing background media upload sessions.


5502-5502: Don't forget to include unit tests in the follow-up PR.

The MediaUploadSessionManager.swift file is properly included in the source compilation. As mentioned in the PR description, unit tests for this new functionality will be added in a follow-up PR. Please ensure these tests cover all critical paths of the background upload functionality, including:

  1. Session initialization
  2. Background upload handling
  3. Error cases and recovery
  4. Interaction with other components (ServiceLocator, MediaStore)
Networking/Networking/Model/Media/WordPressMedia.swift (2)

129-143: Extension provides a clean conversion method.

The toMedia() method is well-implemented and handles data mapping between related types efficiently. The code properly deals with optional values and provides sensible fallbacks for the filename.


145-150: File extension handling is clean and robust.

The fileExtension property safely extracts the extension from a filename and handles the null case appropriately. Using URL(fileURLWithPath:).pathExtension is an elegant approach to extract the file extension.

docs/NETWORKING.md (1)

40-48: Clear documentation for the new background upload functionality.

The added documentation clearly explains the purpose and features of the MediaUploadSessionManager. This will be valuable for developers working with this component. The documentation is concise and follows the established style of the existing NETWORKING.md file.

WooCommerce/Classes/AppDelegate.swift (2)

5-5: Import is correctly added for the new functionality.

The import of MediaUploadSessionManager is properly placed at the top of the file with other imports.


270-276:

❓ Verification inconclusive

Verify integration with other background URL sessions.

The current implementation only handles sessions for the backgroundMediaUploadSessionManager. If the app uses other background URL sessions, you should ensure the method properly handles those as well or allows for extensibility.


🏁 Script executed:

#!/bin/bash
# Search for other URLSession background configurations in the codebase

echo "Searching for background URLSession configurations..."
rg -i "\.background|urlsessionconfiguration.*background" --type swift

Length of output: 66136


Action Required: Confirm Background Session Handling

The implementation in WooCommerce/Classes/AppDelegate.swift (lines 270–276) only handles the background session for backgroundMediaUploadSessionManager. However, our code search reveals that other modules—including the networking layer and WordPressAuthenticator components—also configure background URL sessions (for example, in Networking/Networking/Network/MediaUploadSessionManager.swift and various files under WooCommerce/WordPressAuthenticator/WordPressKit/).

  • Review Required: Verify whether these additional background sessions are managed via separate mechanisms. If they need to be routed through the AppDelegate, consider extending this handler (or implementing a more generic dispatcher) to cover all background sessions.
  • Documentation Note: If multiple background session handlers are intentional, please add clarifying comments to ensure future maintainers understand the design.
Yosemite/YosemiteTests/Stores/MediaStoreTests.swift (3)

53-54: Consistent integration of background uploader parameter

The backgroundUploader parameter has been correctly added to the MediaStore initializer to support the new background upload functionality.


80-81: Consistent application of new dependency across test cases

The backgroundUploader parameter is consistently applied to ensure test cases continue to work with the updated MediaStore API.


233-234: Thorough update of all MediaStore instances

All initializations of the MediaStore class have been properly updated with the new backgroundUploader parameter, ensuring test coverage for the new functionality.

Also applies to: 261-262, 291-292, 322-323, 654-655, 751-752, 757-758

WooCommerce/Classes/ServiceLocator/ServiceLocator.swift (1)

7-7: LGTM: Clear import declaration for the new class dependency

The import statement correctly imports just the specific class needed rather than the entire module.

WooCommerce/WooCommerceTests/ViewRelated/Products/Edit Product/ProductFormViewModel+UpdatesTests.swift (1)

361-367: Well-designed feature flag integration in the convenience initializer

The initializer properly handles both cases (with and without feature flag), ensuring backward compatibility while allowing for new functionality.

Yosemite/YosemiteTests/Mocks/Networking/Remote/MockMediaRemote.swift (1)

52-52: Good addition of a new invocation case for uploadMediaRequest

This new enum case allows for tracking calls to the new method, which is essential for test verification.

Networking/Networking/Network/AlamofireNetwork.swift (2)

30-30: Credentials property exposed in a controlled way, good implementation.

Adding the public credentials property allows the network layer to share authentication information with other components that need it for background processing.


36-36: Properly initialized credentials field.

The credentials property is correctly initialized from the constructor parameter, maintaining the object's integrity.

WooCommerce/Classes/Yosemite/AuthenticatedState.swift (1)

49-52: Dependency injection of backgroundUploader properly implemented.

The MediaStore initialization has been updated to include the backgroundUploader dependency, which enables the background image upload functionality. This follows good dependency injection practices and maintains separation of concerns.

Networking/Networking/Remote/MediaRemote.swift (4)

2-2: Import for Alamofire correctly added.

The Alamofire import is necessary as the updated code relies on Alamofire types.


24-28: Well-defined protocol extension for background upload support.

The function signature properly includes async/throws for error handling and returns a URLRequest that can be used for background uploads.


186-188: Proper type checking and error handling.

The code correctly verifies that the network is an AlamofireNetwork before proceeding, which prevents runtime errors.


194-198: Good authentication and error handling.

The implementation properly authenticates the request when credentials are available and throws an appropriate error when they're not.

WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageActionHandlerTests.swift (5)

14-15: Necessary test dependencies added.

The addition of mockFeatureFlagService and storage as private properties allows for proper testing of the new feature flag functionality.


17-22: Well-structured test setup method.

The setup method properly initializes the test dependencies, including removing any existing test data to ensure a clean state.


24-29: Thorough test teardown to prevent test pollution.

The teardown method correctly cleans up all test resources, ensuring that tests don't interfere with each other.


50-52: Test updated to accommodate new dependency requirement.

The ProductImageActionHandler instantiation now includes the mockFeatureFlagService parameter to match the updated constructor signature.


108-109: Consistent constructor parameter updates across all test cases.

All instances of ProductImageActionHandler instantiation have been updated to include the new featureFlag parameter, maintaining test consistency.

Also applies to: 151-152, 179-180, 208-209, 250-251, 301-302, 339-340, 385-386, 435-436

WooCommerce/Classes/ViewRelated/Products/Media/ProductImageActionHandler.swift (3)

4-4: Good addition for feature-flag-driven logic.

Importing the FeatureFlagService protocol at this location is clear and satisfies the need to decouple feature-flag logic from the handler.


49-50: Great approach to dependency injection.

Using a dedicated property for featureFlagService helps keep feature-flag checks explicit, and makes it easier to mock during tests.


79-80: Constructor design is consistent and test-friendly.

  1. Providing default parameters from ServiceLocator is perfectly valid, and it avoids propagation of global singletons beyond this type.
  2. Having an explicit parameter for featureFlagService also ensures that tests can inject a mock or stub.

Also applies to: 85-85

Networking/Networking/Network/MediaUploadSessionManager.swift (2)

3-7: Interface is clearly defined.

The use of a protocol for delegate callbacks is commendable. This design cleanly separates upload logic from the consumer and makes testing more straightforward.


81-163: Robust error handling and success callbacks.

  1. Detailed logging of success/failure states is helpful for troubleshooting.
  2. The fallback for non-2xx status codes is correct, returning a failure.
  3. Consider clarifying which thread the didReceive data method runs on. If it isn't always the main thread, ensure dictionary writes remain thread-safe.
Yosemite/Yosemite/Stores/MediaStore.swift (2)

9-9: Injecting a dedicated background uploader is a welcome extension.

This approach avoids monolithic solutions and maintains the single-responsibility principle by delegating background upload tasks to a specialized component.


14-15: Convenience initializer keeps initialization consistent.

It’s good to see that you’re using a convenience initializer to cleanly inject the necessary dependencies in one place.

Comment on lines 30 to 34
private var completionHandlers: [String: (Result<Media, Error>) -> Void] = [:]
private var taskResponseData: [Int: Data] = [:]
private var backgroundCompletionHandler: (() -> Void)?
weak var delegate: MediaUploadSessionManagerDelegate?

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential concurrent access to dictionaries.

completionHandlers and taskResponseData may be accessed from different threads (e.g., during didReceive data callbacks vs. the main thread). Swift dictionaries are not inherently thread-safe. Consider using a dedicated serial queue or locks to avoid data races.

-class MediaUploadSessionManager: NSObject {
-    private var completionHandlers: [String: (Result<Media, Error>) -> Void] = [:]
-    private var taskResponseData: [Int: Data] = [:]
+class MediaUploadSessionManager: NSObject {
+    private let syncQueue = DispatchQueue(label: "com.woocommerce.MediaUploadSessionManager.sync")
+    private var completionHandlers: [String: (Result<Media, Error>) -> Void] = [:]
+    private var taskResponseData: [Int: Data] = [:]
+
+    private func setCompletionHandler(_ handler: @escaping (Result<Media, Error>) -> Void, for uploadID: String) {
+        syncQueue.async {
+            self.completionHandlers[uploadID] = handler
+        }
+    }
+
+    // ... similarly wrap dictionary read/writes for 'taskResponseData'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private var completionHandlers: [String: (Result<Media, Error>) -> Void] = [:]
private var taskResponseData: [Int: Data] = [:]
private var backgroundCompletionHandler: (() -> Void)?
weak var delegate: MediaUploadSessionManagerDelegate?
class MediaUploadSessionManager: NSObject {
private let syncQueue = DispatchQueue(label: "com.woocommerce.MediaUploadSessionManager.sync")
private var completionHandlers: [String: (Result<Media, Error>) -> Void] = [:]
private var taskResponseData: [Int: Data] = [:]
private var backgroundCompletionHandler: (() -> Void)?
weak var delegate: MediaUploadSessionManagerDelegate?
private func setCompletionHandler(_ handler: @escaping (Result<Media, Error>) -> Void, for uploadID: String) {
syncQueue.async {
self.completionHandlers[uploadID] = handler
}
}
// TODO: Similarly wrap dictionary read/writes for 'taskResponseData'
}

@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.

@pmusolino pmusolino changed the title Implement background image upload for products without attaching images to the products Background image upload for products without attaching images to the products Mar 7, 2025
…s-part-5' into feat/real-background-image-upload
@selanthiraiyan selanthiraiyan self-assigned this Mar 10, 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! Background testing worked as described. 👏

I left a few questions about the code to get a better understanding.


I noticed the following issue while testing background image uploads.

After testing the background upload case when I open the app I see the loading indicator over the product image in the product list screen.

ScreenRecording_03-11-2025.16-48-21_1.MP4

@@ -46,7 +46,10 @@ class AuthenticatedState: StoresManagerState {
InboxNotesStore(dispatcher: dispatcher, storageManager: storageManager, network: network),
JetpackSettingsStore(dispatcher: dispatcher, storageManager: storageManager, network: network),
JustInTimeMessageStore(dispatcher: dispatcher, storageManager: storageManager, network: network),
MediaStore(dispatcher: dispatcher, storageManager: storageManager, network: network),
MediaStore(dispatcher: dispatcher,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we inject the credentials into MediaStore here instead of making it a public property in AlamofireNetwork? We can pass the credentials to Remote later to create the request.

availableAsRESTRequest: true)

guard let network = network as? AlamofireNetwork else {
throw NetworkError.unacceptableStatusCode(statusCode: 500, response: nil)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you kindly explain why we have to throw 500 error code?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good question. I identified it as a generic error, but the HTTP error 500 is more for server-side errors, while for a client error, it's probably best to use a generic error code like 400 (Bad Request). Wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

while for a client error, it's probably best to use a generic error code like 400 (Bad Request).

Thanks! This sounds good to me.

var modifiedRequest = request
modifiedRequest.httpBody = nil

let task = backgroundSession.uploadTask(with: modifiedRequest, fromFile: tempFileURL)
Copy link
Contributor

Choose a reason for hiding this comment

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

I see that we have already appended the data of the media item to the request body in this line. Do we still have to create a temporary file and pass it to this uploadTask method?

Is this the reason for using a temp file? If yes, can you add a comment to explain?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's exactly the reason. I will add a comment to clarify that for the future 👍

Comment on lines +66 to +69
// Cleanup temp file
DispatchQueue.main.async {
try? FileManager.default.removeItem(at: tempFileURL)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand why we removed the temporary file here. Shouldn't we wait for the upload task to finish/fail?

Copy link
Member Author

Choose a reason for hiding this comment

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

So, as you understand from reading that article, it is necessary to have the file on disk. However, when you create and start an upload task using a file URL, URLSession immediately begins accessing the file to prepare for the upload. The system reads the file data synchronously or copies the file data into its own buffer to manage the upload independently of the app's process. At that moment, it is no longer needed.

Since the upload task has already been started and the system has a reference to the file, deleting it from the app's sandboxed file system doesn't interrupt the upload.

There's a slight chance that if the file is deleted before URLSession has begun reading it, the upload could fail. That's why I use DispatchQueue.main.async, which helps mitigate this risk by ensuring the deletion happens after the current code execution block.

Thinking aloud about it, for maximum safety, I tried to defer deleting the temp file until we receive confirmation that the upload task has successfully started or even until the upload completes. However, this might not be practical because:

  • Waiting until the upload completes could mean the temp file remains on disk for an extended period, especially if the upload takes a long time.
  • If the app is terminated (not background suspended) before we get a chance to delete the file (e.g., user force-quits the app), the temp file might remain on disk indefinitely.

So I think the current solution offers a good balance between cleanup and reliability.

Copy link
Contributor

Choose a reason for hiding this comment

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

As it is a temporary directory, we shouldn't have to worry about file remaining in disk for extended period.

We can delete the file once the upload completes and let the system deal with cleaning it up in case the app is force-quit.

What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

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

If we delete it right away, we somehow prevent the system from cleaning up the directory at its discretion if the device has little available disk space. Also, I believe it will be easy to test in the future, right?

}

// Use MediaMapper to parse response
if let jsonString = String(data: data, encoding: .utf8) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I see that jsonString is unused. I believe we will use this JSON response in a future PR.

private var backgroundCompletionHandler: (() -> Void)?
weak var delegate: MediaUploadSessionManagerDelegate?

public init(sessionIdentifier: String = "com.automattic.woocommerce.background.upload") {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use bundle based identifiers as suggested here?

…s-part-5' into feat/real-background-image-upload
@pmusolino
Copy link
Member Author

Thanks @selanthiraiyan for the review!

After testing the background upload case when I open the app I see the loading indicator over the product image in the product list screen.

Yes, this is completely normal in this PR. When the feature flag is disabled, this issue will not occur. The problem arises because the upload statuses are not yet managed in the current PR. You begin uploading the images while the app is running, and the statuses are properly managed in UserDefaults as uploading. However, if you suspend the app, there is currently no code in the MediaUploadSessionManager to manage those statuses. This will be addressed in future PRs. 👍

…s-part-5' into feat/real-background-image-upload
@selanthiraiyan
Copy link
Contributor

Thanks for the explanations, Paolo! I believe you are working on addressing this comment.

Kindly let me know by tapping the "Request Re-review" button once this PR is ready for another round of review.

Screenshot 2025-03-17 at 11 03 07 AM

@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.

…l-background-image-upload

# Conflicts:
#	WooCommerce/WooCommerceTests/Mocks/MockFeatureFlagService.swift
Base automatically changed from feat/save-product-image-upload-statuses-in-user-defaults-part-5 to trunk March 28, 2025 11:14
@pmusolino
Copy link
Member Author

@selanthiraiyan this is ready for another review. Changes applied here: 0c7be43

I also disabled the feature flag backgroundProductImageUpload in debug mode 3fcb776. Since this PR does not manage the status still and we don't know when this project will be wrapped up, someone might think that the image upload handling is not functioning properly in the near future. However, the issue is that the status is not currently managed, as this is a work in progress in a branch, so better to disable it also in debug (remember to re-enable it during your tests). More here: p91TBi-d1B#comment-14130

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, Paolo! Could you kindly add your thoughts about my previous comment about injecting credentials in MediaStore?

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