Skip to content

Commit

Permalink
Products: Grouping design for product creation type sheet (#12273)
Browse files Browse the repository at this point in the history
  • Loading branch information
hafizrahman committed Jun 11, 2024
2 parents cbab463 + f1b37ed commit 2599216
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 48 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

19.1
-----
- [*] Products: The creation sheet is now simplified and categorized better [https://github.com/woocommerce/woocommerce-ios/pull/12273]
- [*] Restore a missing navigation bar on the privacy settings screen. [https://github.com/woocommerce/woocommerce-ios/pull/13018]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,22 @@ struct AddProductWithAIActionSheet: View {

var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: Constants.margin) {
Text(Localization.title)
.subheadlineStyle()
.padding(.top, Constants.margin)
VStack(alignment: .leading) {
Group {
Text(Localization.title)
.titleStyle()
.bold()
.padding(.top, Constants.titleTopSpacing)
.padding(.bottom, Constants.verticalSpacing)

Text(Localization.subtitle)
.subheadlineStyle()
}
.padding(.horizontal, Constants.horizontalSpacing)

Divider()
.padding(.vertical, Constants.margin)


// AI option
HStack(alignment: .top, spacing: Constants.margin) {
Expand All @@ -53,6 +65,8 @@ struct AddProductWithAIActionSheet: View {
.resizable()
.foregroundColor(.accentColor)
.frame(width: Constants.sparkleIconSize * scale, height: Constants.sparkleIconSize * scale)
.padding(.top, Constants.productIconTopSpacing)

VStack(alignment: .leading, spacing: Constants.verticalSpacing) {
Text(Localization.aiTitle)
.bodyStyle()
Expand All @@ -75,15 +89,19 @@ struct AddProductWithAIActionSheet: View {
.onTapGesture {
onAIOption()
}
.padding(.horizontal, Constants.horizontalSpacing)

Divider()
.padding(.vertical, Constants.margin)

// Manual option
if !isShowingManualOptions {
HStack(alignment: .top, spacing: Constants.margin) {
Image(systemName: "plus.circle")
.font(.title3)
.foregroundColor(.secondary)
.padding(.top, Constants.productIconTopSpacing)

VStack(alignment: .leading, spacing: Constants.verticalSpacing) {
Text(Localization.manualTitle)
.bodyStyle()
Expand All @@ -92,22 +110,19 @@ struct AddProductWithAIActionSheet: View {
}
Spacer()
}
.padding(.horizontal, Constants.horizontalSpacing)
.onTapGesture {
withAnimation {
isShowingManualOptions = true
}
}
} else {
Text(Localization.manualOptionsTitle)
.subheadlineStyle()
.padding(.top, Constants.margin)

ManualProductTypeOptions(productTypes: productTypes, onOptionSelected: onProductTypeOption)
ManualProductTypeOptions(supportedProductTypes: productTypes, onOptionSelected: onProductTypeOption)
}

Spacer()
}
.padding(Constants.margin)
.padding(.vertical, Constants.margin)
.safariSheet(url: $legalURL)
}
}
Expand All @@ -117,43 +132,53 @@ private extension AddProductWithAIActionSheet {
enum Constants {
static let sparkleIconSize: CGFloat = 24
static let verticalSpacing: CGFloat = 4
static let horizontalSpacing: CGFloat = 16
static let titleTopSpacing: CGFloat = 16
static let productIconTopSpacing: CGFloat = 8
static let margin: CGFloat = 16
static let legalURL = "https://automattic.com/ai-guidelines/"
}

enum Localization {
static let title = NSLocalizedString(
"Add a product",
"addProductWithAIActionSheet.title",
value: "Create Product",
comment: "Title on the action sheet to select an option for adding new product"
)
static let manualOptionsTitle = NSLocalizedString(
"addProductWithAIActionSheet.manualOptionsTitle",
static let subtitle = NSLocalizedString(
"addProductWithAIActionSheet.subtitle",
value: "Select a product type",
comment: "Dismiss button on the alert asking to add an image for the Blaze campaign"
comment: "Subitle on the action sheet to select an option for adding new product"
)
static let aiTitle = NSLocalizedString(
"Create a product with AI",
"addProductWithAIActionSheet.aiTitle",
value: "Create a product with AI",
comment: "Title of the option to add new product with AI assistance"
)
static let aiDescription = NSLocalizedString(
"Quickly generate details for you",
"addProductWithAIActionSheet.aiDescription",
value: "Quickly generate details for you",
comment: "Description of the option to add new product with AI assistance"
)
static let legalText = NSLocalizedString(
"Powered by AI.",
"addProductWithAIActionSheet.legalText",
value: "Powered by AI.",
comment: "Label to indicate AI-generated content on the product creation action sheet."
)
static let learnMore = NSLocalizedString(
"[Learn more.](https://automattic.com/ai-guidelines/)",
"addProductWithAIActionSheet.learnMore",
value: "[Learn more.](https://automattic.com/ai-guidelines/)",
comment: "Markdown content learn more link on the product creation action sheet. " +
"Please translate the words while keeping the markdown format and URL."
)
static let manualTitle = NSLocalizedString(
"Add manually",
"addProductWithAIActionSheet.manualTitle",
value: "Add manually",
comment: "Title of the option to add new product manually"
)
static let manualDescription = NSLocalizedString(
"Add a product and the details manually",
"addProductWithAIActionSheet.manualDescription",
value: "Add a product and the details manually",
comment: "Description of the option to add new product manually"
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,62 @@ import SwiftUI
/// View to show the manual product type creation options.
///
struct ManualProductTypeOptions: View {
private let productTypes: [BottomSheetProductType]
private let supportedProductTypes: [BottomSheetProductType]
private let onOptionSelected: (BottomSheetProductType) -> Void

init(productTypes: [BottomSheetProductType],
private let supportedProductCategories: [BottomSheetProductCategory]

@ScaledMetric private var scale: CGFloat = 1.0

init(supportedProductTypes: [BottomSheetProductType],
onOptionSelected: @escaping (BottomSheetProductType) -> Void) {
self.productTypes = productTypes
self.supportedProductTypes = supportedProductTypes
self.onOptionSelected = onOptionSelected

self.supportedProductCategories = BottomSheetProductCategory.allCases.filter { category in
// Only show a product category if at least one of its product types can be found in `supportedProductTypes`
category.productTypes.first { supportedProductTypes.contains($0) } != nil
}
}

var body: some View {
ForEach(productTypes) { productType in
HStack(alignment: .top, spacing: Constants.margin) {
Image(uiImage: productType.actionSheetImage.withRenderingMode(.alwaysTemplate))
.font(.title3)
.foregroundStyle(.secondary)

VStack(alignment: .leading, spacing: Constants.verticalSpacing) {
Text(productType.actionSheetTitle)
.bodyStyle()
Text(productType.actionSheetDescription)
.subheadlineStyle()
ForEach(supportedProductCategories, id: \.self) { category in
VStack {
Text(category.label)
.subheadlineStyle()
.textCase(.uppercase)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, Constants.categoryVerticalSpacing)
.padding(.horizontal, Constants.horizontalSpacing)

ForEach(category.productTypes) { productType in
if supportedProductTypes.contains(productType) {
HStack(alignment: .top, spacing: Constants.margin) {
Image(uiImage: productType.actionSheetImage.withRenderingMode(.alwaysTemplate))
.resizable()
.frame(width: Constants.productTypeIconSize, height: Constants.productTypeIconSize)
.foregroundStyle(.primary)
.padding(.top, Constants.productIconTopSpacing)

VStack(alignment: .leading, spacing: Constants.verticalSpacing) {
Text(productType.actionSheetTitle)
.bodyStyle()
Text(productType.actionSheetDescription)
.subheadlineStyle()
}
.padding(.bottom, Constants.productBottomSpacing)
Spacer()
}
.padding(.horizontal, Constants.horizontalSpacing)
.onTapGesture {
onOptionSelected(productType)
}
}
}
if category != supportedProductCategories.last {
Divider()
.padding(.vertical, Constants.categoryVerticalSpacing)
}
Spacer()
}
.onTapGesture {
onOptionSelected(productType)
}
}
}
Expand All @@ -37,13 +67,18 @@ struct ManualProductTypeOptions: View {
private extension ManualProductTypeOptions {
enum Constants {
static let verticalSpacing: CGFloat = 4
static let horizontalSpacing: CGFloat = 16
static let categoryVerticalSpacing: CGFloat = 8
static let productBottomSpacing: CGFloat = 16
static let productIconTopSpacing: CGFloat = 4
static let margin: CGFloat = 16
static let productTypeIconSize: CGFloat = 24
}
}

#Preview {
ManualProductTypeOptions(
productTypes: [
supportedProductTypes: [
.simple(isVirtual: false),
.simple(isVirtual: true),
.subscription,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation

/// Product creation option's category.
/// Used to group the options on the product creation sheet.
///
enum BottomSheetProductCategory: CaseIterable {
case standard
case subscription
case other

/// Categorization for each available product types.
/// Depending on store configurations, some types will need to be excluded before being used in the UI.
var productTypes: [BottomSheetProductType] {
switch self {
case .standard:
return [.simple(isVirtual: false), .simple(isVirtual: true), .variable, .blank]
case .subscription:
return [.subscription, .variableSubscription]
case .other:
return [.custom("custom"), .grouped, .affiliate]
}
}

var label: String {
switch self {
case .standard:
return NSLocalizedString(
"productCreationCategory.standard",
value: "Standard",
comment: "Category label for standard product creation types"
)
case .subscription:
return NSLocalizedString(
"productCreationCategory.subscription",
value: "Subscription",
comment: "Category label for subscription product creation types"
)
case .other:
return NSLocalizedString(
"productCreationCategory.other",
value: "Other",
comment: "Category label for other product creation types"
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public enum BottomSheetProductType: Hashable, Identifiable {
}
}


/// Remote ProductType
///
var productType: ProductType {
Expand Down Expand Up @@ -77,13 +76,13 @@ public enum BottomSheetProductType: Hashable, Identifiable {
case .simple(let isVirtual):
if isVirtual {
return NSLocalizedString(
"bottomSheetProductType.simpleVirtual.title",
value: "Simple virtual product",
"bottomSheetProductType.simpleVirtualProduct.title",
value: "Virtual product",
comment: "Action sheet option when the user wants to change the Product type to simple virtual product")
} else {
return NSLocalizedString(
"bottomSheetProductType.simple.title",
value: "Simple physical product",
"bottomSheetProductType.simpleProduct.title",
value: "Physical product",
comment: "Action sheet option when the user wants to change the Product type to simple physical product")
}
case .variable:
Expand All @@ -105,13 +104,13 @@ public enum BottomSheetProductType: Hashable, Identifiable {
return title
case .subscription:
return NSLocalizedString(
"bottomSheetProductType.subscription.title",
value: "Simple subscription product",
"bottomSheetProductType.subscriptionProduct.title",
value: "Simple subscription",
comment: "Action sheet option when the user wants to change the Product type to Subscription product")
case .variableSubscription:
return NSLocalizedString(
"bottomSheetProductType.variableSubscription.title",
value: "Variable subscription product",
"bottomSheetProductType.variableSubscriptionProduct.title",
value: "Variable subscription",
comment: "Action sheet option when the user wants to change the Product type to Variable subscription product")
case .blank:
return NSLocalizedString(
Expand Down
4 changes: 4 additions & 0 deletions WooCommerce/WooCommerce.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1612,6 +1612,7 @@
867B330F2B4D39B900DCBEA6 /* BlazeAddParameterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867B330E2B4D39B900DCBEA6 /* BlazeAddParameterView.swift */; };
867EA0AC2B8F09280064BCA7 /* CustomerSelector+Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867EA0AB2B8F09280064BCA7 /* CustomerSelector+Filter.swift */; };
867EA0AF2B8F15740064BCA7 /* CustomerFilter+Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 867EA0AE2B8F15740064BCA7 /* CustomerFilter+Analytics.swift */; };
868029532C184E6C00CB64A1 /* BottomSheetProductCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 868029522C184E6C00CB64A1 /* BottomSheetProductCategory.swift */; };
86967D812B4E21C600C20CA8 /* BlazeAddParameterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86967D802B4E21C600C20CA8 /* BlazeAddParameterViewModel.swift */; };
86967D832B4E3EC300C20CA8 /* BlazeAdUrlParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86967D822B4E3EC300C20CA8 /* BlazeAdUrlParameter.swift */; };
8697AFBD2B60F56A00EFAF21 /* BlazeAdDestinationSettingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8697AFBC2B60F56A00EFAF21 /* BlazeAdDestinationSettingViewModelTests.swift */; };
Expand Down Expand Up @@ -4485,6 +4486,7 @@
867B330E2B4D39B900DCBEA6 /* BlazeAddParameterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeAddParameterView.swift; sourceTree = "<group>"; };
867EA0AB2B8F09280064BCA7 /* CustomerSelector+Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomerSelector+Filter.swift"; sourceTree = "<group>"; };
867EA0AE2B8F15740064BCA7 /* CustomerFilter+Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomerFilter+Analytics.swift"; sourceTree = "<group>"; };
868029522C184E6C00CB64A1 /* BottomSheetProductCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetProductCategory.swift; sourceTree = "<group>"; };
86967D802B4E21C600C20CA8 /* BlazeAddParameterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeAddParameterViewModel.swift; sourceTree = "<group>"; };
86967D822B4E3EC300C20CA8 /* BlazeAdUrlParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeAdUrlParameter.swift; sourceTree = "<group>"; };
8697AFBC2B60F56A00EFAF21 /* BlazeAdDestinationSettingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeAdDestinationSettingViewModelTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6041,6 +6043,7 @@
4574745C24EA84D800CF49BC /* ProductTypeBottomSheetListSelectorCommand.swift */,
2676F4CB2908284800C7A15B /* ProductCreationTypeCommand.swift */,
860476E72B6CA0FC00AF0AEB /* BottomSheetProductType.swift */,
868029522C184E6C00CB64A1 /* BottomSheetProductCategory.swift */,
);
path = BottomSheetListSelector;
sourceTree = "<group>";
Expand Down Expand Up @@ -15473,6 +15476,7 @@
AEE9A880293A3E5500227C92 /* RefreshablePlainList.swift in Sources */,
02D4472C2A4D29930040D72D /* WooAnalyticsEvent+LocalAnnouncement.swift in Sources */,
2004E2ED2C0F5DD800D62521 /* CardPresentPaymentCollectOrderPaymentUseCaseAdaptor.swift in Sources */,
868029532C184E6C00CB64A1 /* BottomSheetProductCategory.swift in Sources */,
B55BC1F121A878A30011A0C0 /* String+HTML.swift in Sources */,
B56C721221B5B44000E5E85B /* PushNotificationsConfiguration.swift in Sources */,
26FE09DF24DB871100B9BDF5 /* SurveyViewControllersFactory.swift in Sources */,
Expand Down

0 comments on commit 2599216

Please sign in to comment.