Skip to content

Fix: Users Unable to Upload Product Images if the store is private #12824

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

Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
95f7966
fix: spaces in ProductFormTableViewDataSource
pmusolino May 20, 2024
07c0544
fix: space ProductFormTableViewDataSource
pmusolino May 20, 2024
c0d2eb5
feat: introduced dynamic variable isPrivateWPCOMSite in `Site` model
pmusolino May 20, 2024
8ba173c
feat: logic to understand is a wpcom store is private, passing the in…
pmusolino May 21, 2024
fd046bf
feat: added titleTintColor into ImageAndTitleAndTextTableViewCell
pmusolino May 23, 2024
5b059b4
- Introduced the presentURL(_:) method to handle URL presentation mod…
pmusolino May 23, 2024
54649ae
update: ProductFormActionsFactoryTests
pmusolino May 23, 2024
cf953f6
update: tests for `isStorePublic`
pmusolino May 23, 2024
6577162
update: ProductFormActionsFactory_ReadonlyProductTests
pmusolino May 23, 2024
e631fd7
fix: line lenght
pmusolino May 23, 2024
5fa6f8d
feat: tests clean up
pmusolino May 23, 2024
059db02
fix: lines should be 163 characters or less
pmusolino May 24, 2024
60a6927
update: RELEASE-NOTES
pmusolino May 24, 2024
a3fd3bd
fix: indentation
pmusolino May 27, 2024
445234a
fix: typo
pmusolino May 27, 2024
c2255b7
feat: removed unused StoresManager from DefaultProductFormTableViewModel
pmusolino May 27, 2024
7223f38
Merge branch 'trunk' into issue/12646-Users-Unable-to-Upload-Product-…
pmusolino May 27, 2024
2fac786
Merge commit 'b3894996b5a6d897fbe75c8e93efdc8a84f1fa86' into issue/12…
pmusolino May 30, 2024
a91a6fc
feat: migration of `isPublic` to -> `visibility` in the `Site` model,…
pmusolino May 30, 2024
44036af
Set default visibility for self-hosted sites to be public
itsmeichigo May 31, 2024
b21ed6e
update: release notes
pmusolino May 31, 2024
a11871a
update: MIGRATIONS.md
pmusolino May 31, 2024
408181a
feat: implemented test method `test_migrating_from_111_to_112_updates…
pmusolino May 31, 2024
fde919c
Merge branch 'trunk' into issue/12646-Users-Unable-to-Upload-Product-…
pmusolino Jun 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Networking/Networking/Model/Site.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,11 @@ public extension Site {
return TimeZone(secondsFromGMT: secondsFromGMT) ?? .current
}

/// Returns true only if the site is hosted on WP.com and is private.
///
var isPrivateWPCOMSite: Bool {
return isWordPressComStore && !isPublic
Copy link
Contributor

Choose a reason for hiding this comment

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

This logic includes only public sites. Is it possible to access images for sites with Coming soon status?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not expected, unfortunately. As you can see here C06NC969ZF0/p1715793466695389?thread_ts=1715193928.110329&cid=C06NC969ZF0 I was expecting that parsing blog_public limiting it to Atomic websites return private = true only if the store is private and not coming soon or public.

It seems btw that the app parse isPublic alias blog_public as a boolean, while it's an integer. From my tests, the values should be private = -1, coming soon = 0, public = 1. When it's -1, it's automatically false, that's why it works as expected. When it's coming soon, it's also false (0), and that's why you see the banner.

I'm going to address this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@itsmeichigo this is ready for another review!
Here are all the changes: a91a6fc

Below is the list of changes:

  • I removed the isPublic property from the Site model, which has been replaced with visibility, a property of type SiteVisibility. SiteVisibility is an enum that contains the values private = -1, coming soon = 0, public = 1.
  • Following the above change, I created a new Core Data model (version 112). As there is a data migration involved from the old Boolean value to the new Int64 in the database, there is also a new mapping model that converts the old Boolean value from model version 111 to the new Int64 in model version 112.
  • I updated all the code to use the new visibility property.

}
}

private extension Site {
Expand Down
4 changes: 4 additions & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
*** PLEASE FOLLOW THIS FORMAT: [<priority indicator, more stars = higher priority>] <description> [<PR URL>]

18.9
-----
- [internal] Resolved an issue where users were unable to upload product images when the store is set to private on WP.com. [https://github.com/woocommerce/woocommerce-ios/issues/12646]

18.8
-----
- [internal] Optimize API calls sent for Dashboard screen. [https://github.com/woocommerce/woocommerce-ios/pull/12775]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class ImageAndTitleAndTextTableViewCell: UITableViewCell {
struct DataConfiguration {
let title: String?
let titleFontStyle: FontStyle
let titleTintColor: UIColor?
let text: String?
let textTintColor: UIColor?
let image: UIImage?
Expand All @@ -36,6 +37,7 @@ final class ImageAndTitleAndTextTableViewCell: UITableViewCell {

init(title: String?,
titleFontStyle: FontStyle = .body,
titleTintColor: UIColor? = nil,
text: String? = nil,
textTintColor: UIColor? = nil,
image: UIImage? = nil,
Expand All @@ -46,6 +48,7 @@ final class ImageAndTitleAndTextTableViewCell: UITableViewCell {
showsSeparator: Bool = true) {
self.title = title
self.titleFontStyle = titleFontStyle
self.titleTintColor = titleTintColor
self.text = text
self.textTintColor = textTintColor
self.image = image
Expand All @@ -60,6 +63,7 @@ final class ImageAndTitleAndTextTableViewCell: UITableViewCell {
struct ViewModel: Equatable {
let title: String?
let titleFontStyle: FontStyle
let titleTintColor: UIColor?
let text: String?
let textTintColor: UIColor?
let image: UIImage?
Expand All @@ -73,6 +77,7 @@ final class ImageAndTitleAndTextTableViewCell: UITableViewCell {

init(title: String?,
titleFontStyle: FontStyle = .body,
titleTintColor: UIColor? = nil,
text: String?,
textTintColor: UIColor? = nil,
image: UIImage? = nil,
Expand All @@ -85,6 +90,7 @@ final class ImageAndTitleAndTextTableViewCell: UITableViewCell {
showsSeparator: Bool = true) {
self.title = title
self.titleFontStyle = titleFontStyle
self.titleTintColor = titleTintColor
self.text = text
self.textTintColor = textTintColor
self.image = image
Expand Down Expand Up @@ -183,8 +189,11 @@ extension ImageAndTitleAndTextTableViewCell {
selectionStyle = viewModel.isActionable ? .default: .none
accessoryView = nil

if let titleTintColor = viewModel.titleTintColor {
titleLabel.textColor = titleTintColor
}

if let textTintColor = viewModel.textTintColor {
titleLabel.textColor = textTintColor
descriptionLabel.textColor = textTintColor
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ struct DefaultProductFormTableViewModel: ProductFormTableViewModel {

private let isDescriptionAIEnabled: Bool
private let featureFlagService: FeatureFlagService
private let stores: StoresManager


init(product: ProductFormDataModel,
actionsFactory: ProductFormActionsFactoryProtocol,
Expand All @@ -33,14 +35,16 @@ struct DefaultProductFormTableViewModel: ProductFormTableViewModel {
weightUnit: String? = ServiceLocator.shippingSettingsService.weightUnit,
dimensionUnit: String? = ServiceLocator.shippingSettingsService.dimensionUnit,
isDescriptionAIEnabled: Bool,
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
stores: StoresManager = ServiceLocator.stores) {
self.currency = currency
self.currencyFormatter = currencyFormatter
self.shippingValueLocalizer = shippingValueLocalizer
self.weightUnit = weightUnit
self.dimensionUnit = dimensionUnit
self.isDescriptionAIEnabled = isDescriptionAIEnabled
self.featureFlagService = featureFlagService
self.stores = stores
configureSections(product: product, actionsFactory: actionsFactory)
}
}
Expand All @@ -55,8 +59,11 @@ private extension DefaultProductFormTableViewModel {
func primaryFieldRows(product: ProductFormDataModel, actions: [ProductFormEditAction]) -> [ProductFormSection.PrimaryFieldRow] {
actions.map { action -> [ProductFormSection.PrimaryFieldRow] in
switch action {
case .images(let editable):
return [.images(isEditable: editable, allowsMultiple: product.allowsMultipleImages(), isVariation: product is EditableProductVariationModel)]
case .images(let editable, let isStorePublic):
return [.images(isEditable: editable,
isStorePublic: isStorePublic,
allowsMultiple: product.allowsMultipleImages(),
isVariation: product is EditableProductVariationModel)]
case .linkedProductsPromo(let viewModel):
return [.linkedProductsPromo(viewModel: viewModel)]
case .name(let editable):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class DownloadableFileBottomSheetListSelectorCommand: BottomSheetListSelec
cell.selectionStyle = .none
cell.accessoryType = .disclosureIndicator
let viewModel = ImageAndTitleAndTextTableViewCell.ViewModel(title: model.title,
titleTintColor: .text,
text: nil,
textTintColor: .text,
image: model.image,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import protocol Experiments.FeatureFlagService

/// Edit actions in the product form. Each action allows the user to edit a subset of product properties.
enum ProductFormEditAction: Equatable {
case images(editable: Bool)
case images(editable: Bool, isStorePublic: Bool)
case linkedProductsPromo(viewModel: FeatureAnnouncementCardViewModel)
case name(editable: Bool)
case description(editable: Bool)
Expand Down Expand Up @@ -62,6 +62,8 @@ struct ProductFormActionsFactory: ProductFormActionsFactoryProtocol {
private let addOnsFeatureEnabled: Bool
private let variationsPrice: VariationsPrice

private let stores: StoresManager

private let isLinkedProductsPromoEnabled: Bool
private let linkedProductsPromoCampaign = LinkedProductsPromoCampaign()
private var linkedProductsPromoViewModel: FeatureAnnouncementCardViewModel {
Expand All @@ -81,7 +83,8 @@ struct ProductFormActionsFactory: ProductFormActionsFactoryProtocol {
isBundledProductsEnabled: Bool = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.productBundles),
isCompositeProductsEnabled: Bool = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.compositeProducts),
isMinMaxQuantitiesEnabled: Bool = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.readOnlyMinMaxQuantities),
variationsPrice: VariationsPrice = .unknown) {
variationsPrice: VariationsPrice = .unknown,
stores: StoresManager = ServiceLocator.stores) {
self.product = product
self.formType = formType
self.canPromoteWithBlaze = canPromoteWithBlaze
Expand All @@ -92,6 +95,7 @@ struct ProductFormActionsFactory: ProductFormActionsFactoryProtocol {
self.isBundledProductsEnabled = isBundledProductsEnabled
self.isCompositeProductsEnabled = isCompositeProductsEnabled
self.isMinMaxQuantitiesEnabled = isMinMaxQuantitiesEnabled
self.stores = stores
}

/// Returns an array of actions that are visible in the product form primary section.
Expand All @@ -107,8 +111,14 @@ struct ProductFormActionsFactory: ProductFormActionsFactoryProtocol {

let shouldShowPromoteWithBlaze = canPromoteWithBlaze

var isStorePublic = true

if let site = stores.sessionManager.defaultSite {
isStorePublic = !site.isPrivateWPCOMSite
}

let actions: [ProductFormEditAction?] = [
shouldShowImagesRow ? .images(editable: editable): nil,
shouldShowImagesRow ? .images(editable: editable, isStorePublic: isStorePublic): nil,
shouldShowLinkedProductsPromo ? .linkedProductsPromo(viewModel: newLinkedProductsPromoViewModel) : nil,
.name(editable: editable),
shouldShowDescriptionRow ? .description(editable: editable): nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ extension ProductFormSection.PrimaryFieldRow: ReusableTableRow {
var cellTypes: [UITableViewCell.Type] {
switch self {
case .images:
return [ProductImagesHeaderTableViewCell.self]
return [ProductImagesHeaderTableViewCell.self, ImageAndTitleAndTextTableViewCell.self]
case .linkedProductsPromo:
return [cellType]
case .name:
Expand All @@ -51,8 +51,8 @@ extension ProductFormSection.PrimaryFieldRow: ReusableTableRow {

private var cellType: UITableViewCell.Type {
switch self {
case .images:
return ProductImagesHeaderTableViewCell.self
case .images(_, let isStorePublic, _, _):
return isStorePublic ? ProductImagesHeaderTableViewCell.self : ImageAndTitleAndTextTableViewCell.self
case .linkedProductsPromo:
return FeatureAnnouncementCardCell.self
case .name(_, let editable, _):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Yosemite
private extension ProductFormSection.SettingsRow.ViewModel {
func toCellViewModel() -> ImageAndTitleAndTextTableViewCell.ViewModel {
return ImageAndTitleAndTextTableViewCell.ViewModel(title: title,
titleTintColor: tintColor,
text: details,
textTintColor: tintColor,
image: icon,
Expand Down Expand Up @@ -87,8 +88,12 @@ private extension ProductFormTableViewDataSource {
private extension ProductFormTableViewDataSource {
func configureCellInPrimaryFieldsSection(_ cell: UITableViewCell, row: ProductFormSection.PrimaryFieldRow) {
switch row {
case .images(let editable, let allowsMultipleImages, let isVariation):
configureImages(cell: cell, isEditable: editable, allowsMultipleImages: allowsMultipleImages, isVariation: isVariation)
case .images(let editable, let isStorePublic, let allowsMultipleImages, let isVariation):
configureImages(cell: cell,
isEditable: editable,
isStorePublic: isStorePublic,
allowsMultipleImages: allowsMultipleImages,
isVariation: isVariation)
case .linkedProductsPromo(let viewModel):
configureLinkedProductsPromo(cell: cell, viewModel: viewModel)
case .name(let name, let editable, let productStatus):
Expand All @@ -107,9 +112,36 @@ private extension ProductFormTableViewDataSource {
configurePromoteWithBlaze(cell: cell)
}
}
func configureImages(cell: UITableViewCell, isEditable: Bool, allowsMultipleImages: Bool, isVariation: Bool) {

func configureImages(cell: UITableViewCell, isEditable: Bool, isStorePublic: Bool, allowsMultipleImages: Bool, isVariation: Bool) {
// only show images row if it's a .org site or a public .com site. Private .com site will see a banner.
guard isStorePublic else {
guard let cell = cell as? ImageAndTitleAndTextTableViewCell else {
fatalError("Expected cell to be of type ImageAndTitleAndTextTableViewCell for the product images cell when store is private on WP.com")
}
let viewModel = ImageAndTitleAndTextTableViewCell.ViewModel(
title: Localization.wpComStoreNotPublicTitle,
titleFontStyle: .body,
titleTintColor: .text,
text: Localization.wpComStoreNotPublicText,
textTintColor: .textLink,
image: .infoOutlineImage,
imageTintColor: .text,
numberOfLinesForTitle: 0,
numberOfLinesForText: 0,
isActionable: true,
showsDisclosureIndicator: false,
showsSeparator: true
)

cell.updateUI(viewModel: viewModel)
cell.contentView.backgroundColor = .warningBackground
return
}

// display product images
guard let cell = cell as? ProductImagesHeaderTableViewCell else {
fatalError()
fatalError("Expected cell to be of type ProductImagesHeaderTableViewCell for displaying product images")
}

defer {
Expand Down Expand Up @@ -144,6 +176,7 @@ private extension ProductFormTableViewDataSource {
self?.onAddImage?()
}
}

func configureName(cell: UITableViewCell, name: String?, isEditable: Bool, productStatus: ProductStatus) {
if isEditable {
configureEditableName(cell: cell, name: name, productStatus: productStatus)
Expand Down Expand Up @@ -430,5 +463,15 @@ private extension ProductFormTableViewDataSource {
"Learn more",
comment: "Title for the link to open the legal URL for AI-generated content in the product detail screen"
)
static let wpComStoreNotPublicTitle = NSLocalizedString(
"productFormTableViewDataSource.wpComStoreNotPublicTitle",
value: "The images are unavailable because your site is marked Private. You can change this by switching to Coming Soon mode.",
comment: "Title message indicating that images are unavailable because the site is private."
)
static let wpComStoreNotPublicText = NSLocalizedString(
"productFormTableViewDataSource.wpComStoreNotPublicText",
value: "Tap to learn more.",
comment: "Text message prompting the user to tap to learn more about changing the privacy setting."
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ enum ProductFormSection: Equatable {
}

enum PrimaryFieldRow: Equatable {
case images(isEditable: Bool, allowsMultiple: Bool, isVariation: Bool)
case images(isEditable: Bool, isStorePublic: Bool, allowsMultiple: Bool, isVariation: Bool)
case linkedProductsPromo(viewModel: FeatureAnnouncementCardViewModel)
case name(name: String?, isEditable: Bool, productStatus: ProductStatus)
case variationName(name: String)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Combine
import SafariServices
import Photos
import UIKit
import WordPressUI
Expand Down Expand Up @@ -400,6 +401,11 @@ final class ProductFormViewController<ViewModel: ProductFormViewModelProtocol>:
case .primaryFields(let rows):
let row = rows[indexPath.row]
switch row {
case .images(_, let isStorePublic, _, _):
guard isStorePublic else {
presentURL(URLs.wpComPrivacySettings)
return
}
case .description(_, let isEditable, _):
guard isEditable else {
return
Expand Down Expand Up @@ -1284,6 +1290,18 @@ extension ProductFormViewController: KeyboardScrollable {
}
}

// MARK: - Helper Methods

private extension ProductFormViewController {

/// Presents a URL modally.
///
func presentURL(_ url: URL) {
let safariViewController = SFSafariViewController(url: url)
present(safariViewController, animated: true)
}
}

// MARK: - Navigation actions handling
//
private extension ProductFormViewController {
Expand Down Expand Up @@ -2039,6 +2057,10 @@ private enum Localization {
}
}

private enum URLs {
static let wpComPrivacySettings = URL(string: "https://wordpress.com/support/privacy-settings/")!
}

private enum ActionSheetStrings {
static let saveProductAsDraft = NSLocalizedString("Save as draft",
comment: "Button title to save a product as draft in Product More Options Action Sheet")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,31 @@ struct ProductVariationFormActionsFactory: ProductFormActionsFactoryProtocol {

private let isMinMaxQuantitiesEnabled: Bool

private let stores: StoresManager

init(productVariation: EditableProductVariationModel,
editable: Bool,
isMinMaxQuantitiesEnabled: Bool = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.readOnlyMinMaxQuantities)) {
isMinMaxQuantitiesEnabled: Bool = ServiceLocator.featureFlagService.isFeatureFlagEnabled(.readOnlyMinMaxQuantities),
stores: StoresManager = ServiceLocator.stores) {
self.productVariation = productVariation
self.editable = editable
self.isMinMaxQuantitiesEnabled = isMinMaxQuantitiesEnabled
self.stores = stores
}

/// Returns an array of actions that are visible in the product form primary section.
func primarySectionActions() -> [ProductFormEditAction] {

var isStorePublic = true

if let site = stores.sessionManager.defaultSite {
isStorePublic = !site.isPrivateWPCOMSite
}

let shouldShowImagesRow = editable || productVariation.images.isNotEmpty
let shouldShowDescriptionRow = editable || productVariation.description?.isNotEmpty == true
let actions: [ProductFormEditAction?] = [
shouldShowImagesRow ? .images(editable: editable): nil,
shouldShowImagesRow ? .images(editable: editable, isStorePublic: isStorePublic): nil,
.variationName,
shouldShowDescriptionRow ? .description(editable: editable): nil
]
Expand Down
Loading