Skip to content

Commit cddcea4

Browse files
authored
Merge pull request #8379 from woocommerce/feat/profiler-questions
Store creation M3: profiler question - optional category selector
2 parents e81d612 + 8a9ce6b commit cddcea4

File tree

10 files changed

+475
-8
lines changed

10 files changed

+475
-8
lines changed

Experiments/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
3939
return true
4040
case .storeCreationM2WithInAppPurchasesEnabled:
4141
return false
42+
case .storeCreationM3Profiler:
43+
return buildConfig == .localDeveloper || buildConfig == .alpha
4244
case .justInTimeMessagesOnDashboard:
4345
return true
4446
case .systemStatusReportInSupportRequest:

Experiments/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ public enum FeatureFlag: Int {
8787
///
8888
case storeCreationM2WithInAppPurchasesEnabled
8989

90+
/// Store creation milestone 3 - profiler questions
91+
///
92+
case storeCreationM3Profiler
93+
9094
/// Just In Time Messages on Dashboard
9195
///
9296
case justInTimeMessagesOnDashboard
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import SwiftUI
2+
3+
/// Hosting controller that wraps the `StoreCreationCategoryQuestionView`.
4+
final class StoreCreationCategoryQuestionHostingController: UIHostingController<StoreCreationCategoryQuestionView> {
5+
init(viewModel: StoreCreationCategoryQuestionViewModel) {
6+
super.init(rootView: StoreCreationCategoryQuestionView(viewModel: viewModel))
7+
}
8+
9+
@available(*, unavailable)
10+
required dynamic init?(coder aDecoder: NSCoder) {
11+
fatalError("init(coder:) has not been implemented")
12+
}
13+
14+
override func viewDidLoad() {
15+
super.viewDidLoad()
16+
17+
configureTransparentNavigationBar()
18+
}
19+
}
20+
21+
/// Shows the store category question in the store creation flow.
22+
struct StoreCreationCategoryQuestionView: View {
23+
@ObservedObject private var viewModel: StoreCreationCategoryQuestionViewModel
24+
25+
init(viewModel: StoreCreationCategoryQuestionViewModel) {
26+
self.viewModel = viewModel
27+
}
28+
29+
var body: some View {
30+
OptionalStoreCreationProfilerQuestionView(viewModel: viewModel) {
31+
VStack(spacing: 16) {
32+
ForEach(viewModel.categories, id: \.name) { category in
33+
Button(action: {
34+
viewModel.selectCategory(category)
35+
}, label: {
36+
HStack {
37+
Text(category.name)
38+
Spacer()
39+
}
40+
})
41+
.buttonStyle(SelectableSecondaryButtonStyle(isSelected: viewModel.selectedCategory == category))
42+
}
43+
}
44+
}
45+
}
46+
}
47+
48+
struct StoreCreationCategoryQuestionView_Previews: PreviewProvider {
49+
static var previews: some View {
50+
StoreCreationCategoryQuestionView(viewModel: .init(storeName: "Holiday store",
51+
onContinue: { _ in },
52+
onSkip: {}))
53+
}
54+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import Combine
2+
import Foundation
3+
4+
/// View model for `StoreCreationCategoryQuestionView`, an optional profiler question about store category in the store creation flow.
5+
@MainActor
6+
final class StoreCreationCategoryQuestionViewModel: StoreCreationProfilerQuestionViewModel, ObservableObject {
7+
/// Contains necessary information about a category.
8+
struct Category: Equatable {
9+
/// Display name for the category.
10+
let name: String
11+
/// Value that is sent to the API.
12+
let value: String
13+
}
14+
15+
let topHeader: String
16+
17+
let title: String = Localization.title
18+
19+
let subtitle: String = Localization.subtitle
20+
21+
/// Question content.
22+
/// TODO: 8376 - update values when API is ready.
23+
let categories: [Category] = [
24+
.init(name: NSLocalizedString("Art & Photography",
25+
comment: "Option in the store creation category question."),
26+
value: ""),
27+
.init(name: NSLocalizedString("Books & Magazines",
28+
comment: "Option in the store creation category question."),
29+
value: ""),
30+
.init(name: NSLocalizedString("Electronics and Software",
31+
comment: "Option in the store creation category question."),
32+
value: ""),
33+
.init(name: NSLocalizedString("Construction & Industrial",
34+
comment: "Option in the store creation category question."),
35+
value: ""),
36+
.init(name: NSLocalizedString("Design & Marketing",
37+
comment: "Option in the store creation category question."),
38+
value: ""),
39+
.init(name: NSLocalizedString("Fashion and Apparel",
40+
comment: "Option in the store creation category question."),
41+
value: ""),
42+
.init(name: NSLocalizedString("Food and Drink",
43+
comment: "Option in the store creation category question."),
44+
value: ""),
45+
.init(name: NSLocalizedString("Arts and Crafts",
46+
comment: "Option in the store creation category question."),
47+
value: ""),
48+
.init(name: NSLocalizedString("Health and Beauty",
49+
comment: "Option in the store creation category question."),
50+
value: ""),
51+
.init(name: NSLocalizedString("Pets Pet Care",
52+
comment: "Option in the store creation category question."),
53+
value: ""),
54+
.init(name: NSLocalizedString("Sports and Recreation",
55+
comment: "Option in the store creation category question."),
56+
value: "")
57+
]
58+
59+
@Published private(set) var selectedCategory: Category?
60+
61+
private let onContinue: (String) -> Void
62+
private let onSkip: () -> Void
63+
64+
init(storeName: String,
65+
onContinue: @escaping (String) -> Void,
66+
onSkip: @escaping () -> Void) {
67+
self.topHeader = storeName
68+
self.onContinue = onContinue
69+
self.onSkip = onSkip
70+
}
71+
}
72+
73+
extension StoreCreationCategoryQuestionViewModel: OptionalStoreCreationProfilerQuestionViewModel {
74+
func continueButtonTapped() async {
75+
guard let selectedCategory else {
76+
return onSkip()
77+
}
78+
79+
onContinue(selectedCategory.name)
80+
}
81+
82+
func skipButtonTapped() {
83+
onSkip()
84+
}
85+
}
86+
87+
extension StoreCreationCategoryQuestionViewModel {
88+
func selectCategory(_ category: Category) {
89+
selectedCategory = category
90+
}
91+
}
92+
93+
private extension StoreCreationCategoryQuestionViewModel {
94+
enum Localization {
95+
static let title = NSLocalizedString(
96+
"What’s your business about?",
97+
comment: "Title of the store creation profiler question about the store category."
98+
)
99+
static let subtitle = NSLocalizedString(
100+
"Choose a category that defines your business the best.",
101+
comment: "Subtitle of the store creation profiler question about the store category."
102+
)
103+
}
104+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import SwiftUI
2+
3+
/// Handles the navigation actions in an optional profiler question view during store creation.
4+
/// The question is skippable.
5+
protocol OptionalStoreCreationProfilerQuestionViewModel {
6+
func continueButtonTapped() async
7+
func skipButtonTapped()
8+
}
9+
10+
/// Shows an optional profiler question in the store creation flow.
11+
/// The user can choose to skip the question or continue with an optional answer.
12+
struct OptionalStoreCreationProfilerQuestionView<QuestionContent: View>: View {
13+
private let viewModel: StoreCreationProfilerQuestionViewModel & OptionalStoreCreationProfilerQuestionViewModel
14+
@ViewBuilder private let questionContent: () -> QuestionContent
15+
@State private var isWaitingForCompletion: Bool = false
16+
17+
init(viewModel: StoreCreationProfilerQuestionViewModel & OptionalStoreCreationProfilerQuestionViewModel,
18+
@ViewBuilder questionContent: @escaping () -> QuestionContent) {
19+
self.viewModel = viewModel
20+
self.questionContent = questionContent
21+
}
22+
23+
var body: some View {
24+
ScrollView {
25+
StoreCreationProfilerQuestionView<QuestionContent>(viewModel: viewModel, questionContent: questionContent)
26+
}
27+
.safeAreaInset(edge: .bottom) {
28+
VStack {
29+
Divider()
30+
.frame(height: Layout.dividerHeight)
31+
.foregroundColor(Color(.separator))
32+
Button(Localization.continueButtonTitle) {
33+
Task { @MainActor in
34+
isWaitingForCompletion = true
35+
await viewModel.continueButtonTapped()
36+
isWaitingForCompletion = false
37+
}
38+
}
39+
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: isWaitingForCompletion))
40+
.padding(Layout.defaultPadding)
41+
}
42+
.background(Color(.systemBackground))
43+
}
44+
.toolbar {
45+
ToolbarItem(placement: .navigationBarTrailing) {
46+
Button(Localization.skipButtonTitle) {
47+
viewModel.skipButtonTapped()
48+
}
49+
.buttonStyle(LinkButtonStyle())
50+
}
51+
}
52+
// Disables large title to avoid a large gap below the navigation bar.
53+
.navigationBarTitleDisplayMode(.inline)
54+
}
55+
}
56+
57+
private enum Layout {
58+
static let dividerHeight: CGFloat = 1
59+
static let defaultPadding: EdgeInsets = .init(top: 10, leading: 16, bottom: 10, trailing: 16)
60+
}
61+
62+
private enum Localization {
63+
static let continueButtonTitle = NSLocalizedString("Continue", comment: "Title of the button to continue with a profiler question.")
64+
static let skipButtonTitle = NSLocalizedString("Skip", comment: "Title of the button to skip a profiler question.")
65+
}
66+
67+
#if DEBUG
68+
69+
private struct StoreCreationQuestionPreviewViewModel: StoreCreationProfilerQuestionViewModel, OptionalStoreCreationProfilerQuestionViewModel {
70+
let topHeader: String = "Store name"
71+
let title: String = "Which of these best describes you?"
72+
let subtitle: String = "Choose a category that defines your business the best."
73+
74+
func continueButtonTapped() async {}
75+
func skipButtonTapped() {}
76+
}
77+
78+
struct OptionalStoreCreationProfilerQuestionView_Previews: PreviewProvider {
79+
static var previews: some View {
80+
NavigationView {
81+
OptionalStoreCreationProfilerQuestionView(viewModel: StoreCreationQuestionPreviewViewModel()) {
82+
Text("question content")
83+
}
84+
}
85+
}
86+
}
87+
88+
#endif
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import SwiftUI
2+
3+
/// Provides the copy for labels in the store creation profiler question view above the content.
4+
protocol StoreCreationProfilerQuestionViewModel {
5+
var topHeader: String { get }
6+
var title: String { get }
7+
var subtitle: String { get }
8+
}
9+
10+
/// Shows a profiler question in the store creation flow.
11+
struct StoreCreationProfilerQuestionView<QuestionContent: View>: View {
12+
private let viewModel: StoreCreationProfilerQuestionViewModel
13+
private let questionContent: QuestionContent
14+
15+
init(viewModel: StoreCreationProfilerQuestionViewModel,
16+
@ViewBuilder questionContent: () -> QuestionContent) {
17+
self.viewModel = viewModel
18+
self.questionContent = questionContent()
19+
}
20+
21+
var body: some View {
22+
VStack(alignment: .leading, spacing: 40) {
23+
VStack(alignment: .leading, spacing: 16) {
24+
// Top header label.
25+
Text(viewModel.topHeader.uppercased())
26+
.foregroundColor(Color(.secondaryLabel))
27+
.footnoteStyle()
28+
29+
// Title label.
30+
Text(viewModel.title)
31+
.fontWeight(.bold)
32+
.titleStyle()
33+
34+
// Subtitle label.
35+
Text(viewModel.subtitle)
36+
.foregroundColor(Color(.secondaryLabel))
37+
.bodyStyle()
38+
}
39+
40+
// Content of the profiler question.
41+
questionContent
42+
}
43+
.padding(Layout.contentPadding)
44+
}
45+
}
46+
47+
private enum Layout {
48+
static let contentPadding: EdgeInsets = .init(top: 38, leading: 16, bottom: 16, trailing: 16)
49+
}
50+
51+
#if DEBUG
52+
53+
private struct StoreCreationQuestionPreviewViewModel: StoreCreationProfilerQuestionViewModel {
54+
let topHeader: String = "Store name"
55+
let title: String = "Which of these best describes you?"
56+
let subtitle: String = "Choose a category that defines your business the best."
57+
}
58+
59+
struct StoreCreationProfilerQuestionView_Previews: PreviewProvider {
60+
static var previews: some View {
61+
StoreCreationProfilerQuestionView(viewModel: StoreCreationQuestionPreviewViewModel()) {
62+
Text("question content")
63+
}
64+
}
65+
}
66+
67+
#endif

0 commit comments

Comments
 (0)