Skip to content

Commit 8f37e46

Browse files
authored
Background image upload: Show a notice when the user leaves product details while uploads are pending (#15134)
2 parents 3fe7850 + 1d9201a commit 8f37e46

File tree

6 files changed

+87
-10
lines changed

6 files changed

+87
-10
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- [*] Product List: Display syncing animation on items with image upload in progress [https://github.com/woocommerce/woocommerce-ios/pull/15052]
1010
- [*] Background image upload: Fix issue showing uploaded images while saving is in progress [https://github.com/woocommerce/woocommerce-ios/pull/15107]
1111
- [*] Background image upload: Fix missing error notice in iPhones [https://github.com/woocommerce/woocommerce-ios/pull/15117]
12+
- [*] Background image upload: Show a notice when the user leaves product details while uploads are pending [https://github.com/woocommerce/woocommerce-ios/pull/15134]
1213
- [*] Filters applied in product selector no longer affect the main product list screen. [https://github.com/woocommerce/woocommerce-ios/pull/14764]
1314

1415
21.7

WooCommerce/Classes/ServiceLocator/ProductImageUploader.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Combine
2+
import Foundation
23
import struct Yosemite.ProductImage
34
import enum Yosemite.ProductAction
45
import protocol Yosemite.StoresManager
@@ -79,6 +80,11 @@ protocol ProductImageUploaderProtocol {
7980
/// - key: identifiable information about the product.
8081
func startEmittingErrors(key: ProductImageUploaderKey)
8182

83+
/// Triggers a notice about background image upload for a product if needed.
84+
/// - Parameter key: identifiable information about the product.
85+
///
86+
func sendBackgroundUploadNoticeIfNeeded(key: ProductImageUploaderKey, using noticePresenter: NoticePresenter)
87+
8288
/// Determines whether there are unsaved changes on a product's images.
8389
/// If the product had any save request before, it checks whether the image statuses to save match the latest image statuses.
8490
/// Otherwise, it checks whether there is any pending upload or the image statuses match the given original image statuses.
@@ -165,6 +171,13 @@ final class ProductImageUploader: ProductImageUploaderProtocol {
165171
statusUpdatesExcludedProductKeys.remove(key)
166172
}
167173

174+
func sendBackgroundUploadNoticeIfNeeded(key: ProductImageUploaderKey, using noticePresenter: NoticePresenter) {
175+
if activeUploadsPublisher.contains(key) {
176+
let notice = Notice(title: Localization.backgroundUploadNoticeTitle)
177+
noticePresenter.enqueue(notice: notice)
178+
}
179+
}
180+
168181
func hasUnsavedChangesOnImages(key: ProductImageUploaderKey, originalImages: [ProductImage]) -> Bool {
169182
guard let handler = actionHandlersByProduct[key] else {
170183
return false
@@ -298,3 +311,11 @@ enum ProductImageUploaderError: Error {
298311
case failedSavingProductAfterImageUpload(error: Error)
299312
case failedUploadingImage(error: Error)
300313
}
314+
315+
private enum Localization {
316+
static let backgroundUploadNoticeTitle = NSLocalizedString(
317+
"productImageUploader.backgroundUploadNotice.title",
318+
value: "Image uploading will continue in the background",
319+
comment: ""
320+
)
321+
}

WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,12 +194,7 @@ final class ProductFormViewController<ViewModel: ProductFormViewModelProtocol>:
194194
super.viewWillDisappear(animated)
195195

196196
view.endEditing(true)
197-
198-
if isBeingDismissedInAnyWay {
199-
productImageUploader.startEmittingErrors(key: .init(siteID: viewModel.productModel.siteID,
200-
productOrVariationID: productOrVariationID,
201-
isLocalID: !viewModel.productModel.existsRemotely))
202-
}
197+
prepareForBackgroundUploadsUponDismissal()
203198
}
204199

205200
override var shouldShowOfflineBanner: Bool {
@@ -1337,6 +1332,16 @@ private extension ProductFormViewController {
13371332
// MARK: - Navigation actions handling
13381333
//
13391334
private extension ProductFormViewController {
1335+
func prepareForBackgroundUploadsUponDismissal() {
1336+
guard isBeingDismissedInAnyWay else { return }
1337+
1338+
let key = ProductImageUploaderKey(siteID: viewModel.productModel.siteID,
1339+
productOrVariationID: productOrVariationID,
1340+
isLocalID: !viewModel.productModel.existsRemotely)
1341+
productImageUploader.startEmittingErrors(key: key)
1342+
productImageUploader.sendBackgroundUploadNoticeIfNeeded(key: key, using: ServiceLocator.noticePresenter)
1343+
}
1344+
13401345
func presentBackNavigationActionSheet(onDiscard: @escaping () -> Void = {}, onCancel: @escaping () -> Void = {}) {
13411346
let exitForm: () -> Void = {
13421347
presentationStyle.createExitForm(viewController: navigationController ?? self, completion: onDiscard)

WooCommerce/Classes/ViewRelated/Products/ProductsSplitViewCoordinator.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,7 @@ extension ProductsSplitViewCoordinator: UINavigationControllerDelegate {
231231
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
232232
if didNavigateFromTheLastSecondaryViewControllerToProductListInCollapsedMode(navigationController, didShow: viewController) {
233233
if let contentType = contentTypes.last, case let .productForm(product) = contentType, let product {
234-
ServiceLocator.productImageUploader.startEmittingErrors(
235-
key: .init(siteID: product.siteID,
236-
productOrVariationID: .product(id: product.productID),
237-
isLocalID: false))
234+
didDismissProductForm(product: product)
238235
}
239236
contentTypes = []
240237
secondaryNavigationController.viewControllers = []
@@ -275,6 +272,15 @@ private extension ProductsSplitViewCoordinator {
275272
return splitViewController.isCollapsed && navigationController == primaryNavigationController
276273
&& contentTypes.isNotEmpty && isNavigatingToProductList
277274
}
275+
276+
func didDismissProductForm(product: Product) {
277+
let uploader = ServiceLocator.productImageUploader
278+
let key = ProductImageUploaderKey(siteID: product.siteID,
279+
productOrVariationID: .product(id: product.productID),
280+
isLocalID: false)
281+
uploader.startEmittingErrors(key: key)
282+
uploader.sendBackgroundUploadNoticeIfNeeded(key: key, using: ServiceLocator.noticePresenter)
283+
}
278284
}
279285

280286
private extension ProductsSplitViewCoordinator {

WooCommerce/WooCommerceTests/Mocks/MockProductImageUploader.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ extension MockProductImageUploader: ProductImageUploaderProtocol {
6363
hasUnsavedChangesOnImages
6464
}
6565

66+
func sendBackgroundUploadNoticeIfNeeded(key: ProductImageUploaderKey, using noticePresenter: NoticePresenter) {
67+
// no-op
68+
}
69+
6670
func reset() {
6771
resetWasCalled = true
6872
}

WooCommerce/WooCommerceTests/ViewRelated/Products/Media/ProductImageUploaderTests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ final class ProductImageUploaderTests: XCTestCase {
607607
}
608608

609609
func test_product_is_removed_from_activeUploads_when_upload_is_cancelled() {
610+
// Given
610611
let stores = MockStoresManager(sessionManager: .testingInstance)
611612
let imageUploader = ProductImageUploader(stores: stores)
612613
let key = ProductImageUploaderKey(siteID: siteID,
@@ -638,6 +639,45 @@ final class ProductImageUploaderTests: XCTestCase {
638639
activeUploads == []
639640
}
640641
}
642+
643+
func test_background_upload_notice_is_sent_when_there_are_active_uploads() {
644+
// Given
645+
let stores = MockStoresManager(sessionManager: .testingInstance)
646+
let imageUploader = ProductImageUploader(stores: stores)
647+
let key = ProductImageUploaderKey(siteID: siteID,
648+
productOrVariationID: .product(id: productID),
649+
isLocalID: false)
650+
let actionHandler = imageUploader.actionHandler(key: key, originalStatuses: [])
651+
652+
let noticePresenter = MockNoticePresenter()
653+
var isNoticeTriggered = false
654+
noticePresenter.onNoticeQueued = { _ in
655+
isNoticeTriggered = true
656+
}
657+
658+
var activeUploads: [ProductImageUploaderKey] = []
659+
activeUploadsSubscription = imageUploader.activeUploads
660+
.sink { keys in
661+
activeUploads = keys
662+
}
663+
664+
// When
665+
imageUploader.sendBackgroundUploadNoticeIfNeeded(key: key, using: noticePresenter)
666+
667+
// Then
668+
XCTAssertFalse(isNoticeTriggered)
669+
670+
// When
671+
let asset = PHAsset()
672+
actionHandler.uploadMediaAssetToSiteMediaLibrary(asset: .phAsset(asset: asset))
673+
waitUntil {
674+
activeUploads == [key]
675+
}
676+
677+
// Then
678+
imageUploader.sendBackgroundUploadNoticeIfNeeded(key: key, using: noticePresenter)
679+
XCTAssertTrue(isNoticeTriggered)
680+
}
641681
}
642682

643683
extension ProductImageUploadErrorInfo: @retroactive Equatable {

0 commit comments

Comments
 (0)