Skip to content
Merged
2 changes: 2 additions & 0 deletions Experiments/Experiments/DefaultFeatureFlagService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
return true
case .storeCreationM2WithInAppPurchasesEnabled:
return false
case .storeCreationM3Profiler:
return buildConfig == .localDeveloper || buildConfig == .alpha
case .justInTimeMessagesOnDashboard:
return true
case .systemStatusReportInSupportRequest:
Expand Down
4 changes: 4 additions & 0 deletions Experiments/Experiments/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ public enum FeatureFlag: Int {
///
case storeCreationM2WithInAppPurchasesEnabled

/// Store creation milestone 3 - profiler questions
///
case storeCreationM3Profiler

/// Just In Time Messages on Dashboard
///
case justInTimeMessagesOnDashboard
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import SwiftUI

/// Hosting controller that wraps the `StoreCreationCategoryQuestionView`.
final class StoreCreationCategoryQuestionHostingController: UIHostingController<StoreCreationCategoryQuestionView> {
init(viewModel: StoreCreationCategoryQuestionViewModel) {
super.init(rootView: StoreCreationCategoryQuestionView(viewModel: viewModel))
}

@available(*, unavailable)
required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

configureTransparentNavigationBar()
}
}

/// Shows the store category question in the store creation flow.
struct StoreCreationCategoryQuestionView: View {
@ObservedObject private var viewModel: StoreCreationCategoryQuestionViewModel

init(viewModel: StoreCreationCategoryQuestionViewModel) {
self.viewModel = viewModel
}

var body: some View {
OptionalStoreCreationProfilerQuestionView(viewModel: viewModel) {
VStack(spacing: 16) {
ForEach(viewModel.categories, id: \.name) { category in
Button(action: {
viewModel.selectCategory(category)
}, label: {
HStack {
Text(category.name)
Spacer()
}
})
.buttonStyle(SelectableSecondaryButtonStyle(isSelected: viewModel.selectedCategory == category))
}
}
}
}
}

struct StoreCreationCategoryQuestionView_Previews: PreviewProvider {
static var previews: some View {
StoreCreationCategoryQuestionView(viewModel: .init(storeName: "Holiday store",
onContinue: { _ in },
onSkip: {}))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Combine
import Foundation

/// View model for `StoreCreationCategoryQuestionView`, an optional profiler question about store category in the store creation flow.
@MainActor
final class StoreCreationCategoryQuestionViewModel: StoreCreationProfilerQuestionViewModel, ObservableObject {
/// Contains necessary information about a category.
struct Category: Equatable {
/// Display name for the category.
let name: String
/// Value that is sent to the API.
let value: String
}

let topHeader: String

let title: String = Localization.title

let subtitle: String = Localization.subtitle

/// Question content.
/// TODO: 8376 - update values when API is ready.
let categories: [Category] = [
.init(name: NSLocalizedString("Art & Photography",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Books & Magazines",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Electronics and Software",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Construction & Industrial",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Design & Marketing",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Fashion and Apparel",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Food and Drink",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Arts and Crafts",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Health and Beauty",
comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Pets Pet Care",
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a typo? 😅

Copy link
Contributor Author

@jaclync jaclync Dec 14, 2022

Choose a reason for hiding this comment

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

This was in the design and I had the same question, but we're also updating the list and separating it into two sections as the latest design after syncing with the Ghidorah team. There's a subtask to update the design in #8376, where the categories will be updated

comment: "Option in the store creation category question."),
value: ""),
.init(name: NSLocalizedString("Sports and Recreation",
comment: "Option in the store creation category question."),
value: "")
]

@Published private(set) var selectedCategory: Category?

private let onContinue: (String) -> Void
private let onSkip: () -> Void

init(storeName: String,
onContinue: @escaping (String) -> Void,
onSkip: @escaping () -> Void) {
self.topHeader = storeName
self.onContinue = onContinue
self.onSkip = onSkip
}
}

extension StoreCreationCategoryQuestionViewModel: OptionalStoreCreationProfilerQuestionViewModel {
func continueButtonTapped() async {
guard let selectedCategory else {
return onSkip()
}

onContinue(selectedCategory.name)
}

func skipButtonTapped() {
onSkip()
}
}

extension StoreCreationCategoryQuestionViewModel {
func selectCategory(_ category: Category) {
selectedCategory = category
}
}

private extension StoreCreationCategoryQuestionViewModel {
enum Localization {
static let title = NSLocalizedString(
"What’s your business about?",
comment: "Title of the store creation profiler question about the store category."
)
static let subtitle = NSLocalizedString(
"Choose a category that defines your business the best.",
comment: "Subtitle of the store creation profiler question about the store category."
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import SwiftUI

/// Handles the navigation actions in an optional profiler question view during store creation.
/// The question is skippable.
protocol OptionalStoreCreationProfilerQuestionViewModel {
func continueButtonTapped() async
func skipButtonTapped()
}

/// Shows an optional profiler question in the store creation flow.
/// The user can choose to skip the question or continue with an optional answer.
struct OptionalStoreCreationProfilerQuestionView<QuestionContent: View>: View {
private let viewModel: StoreCreationProfilerQuestionViewModel & OptionalStoreCreationProfilerQuestionViewModel
@ViewBuilder private let questionContent: () -> QuestionContent
@State private var isWaitingForCompletion: Bool = false

init(viewModel: StoreCreationProfilerQuestionViewModel & OptionalStoreCreationProfilerQuestionViewModel,
@ViewBuilder questionContent: @escaping () -> QuestionContent) {
self.viewModel = viewModel
self.questionContent = questionContent
}

var body: some View {
ScrollView {
StoreCreationProfilerQuestionView<QuestionContent>(viewModel: viewModel, questionContent: questionContent)
}
.safeAreaInset(edge: .bottom) {
VStack {
Divider()
.frame(height: Layout.dividerHeight)
.foregroundColor(Color(.separator))
Button(Localization.continueButtonTitle) {
Task { @MainActor in
isWaitingForCompletion = true
await viewModel.continueButtonTapped()
isWaitingForCompletion = false
}
}
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: isWaitingForCompletion))
.padding(Layout.defaultPadding)
}
.background(Color(.systemBackground))
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(Localization.skipButtonTitle) {
viewModel.skipButtonTapped()
}
.buttonStyle(LinkButtonStyle())
}
}
// Disables large title to avoid a large gap below the navigation bar.
.navigationBarTitleDisplayMode(.inline)
}
}

private enum Layout {
static let dividerHeight: CGFloat = 1
static let defaultPadding: EdgeInsets = .init(top: 10, leading: 16, bottom: 10, trailing: 16)
}

private enum Localization {
static let continueButtonTitle = NSLocalizedString("Continue", comment: "Title of the button to continue with a profiler question.")
static let skipButtonTitle = NSLocalizedString("Skip", comment: "Title of the button to skip a profiler question.")
}

#if DEBUG

private struct StoreCreationQuestionPreviewViewModel: StoreCreationProfilerQuestionViewModel, OptionalStoreCreationProfilerQuestionViewModel {
let topHeader: String = "Store name"
let title: String = "Which of these best describes you?"
let subtitle: String = "Choose a category that defines your business the best."

func continueButtonTapped() async {}
func skipButtonTapped() {}
}

struct OptionalStoreCreationProfilerQuestionView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
OptionalStoreCreationProfilerQuestionView(viewModel: StoreCreationQuestionPreviewViewModel()) {
Text("question content")
}
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import SwiftUI

/// Provides the copy for labels in the store creation profiler question view above the content.
protocol StoreCreationProfilerQuestionViewModel {
var topHeader: String { get }
var title: String { get }
var subtitle: String { get }
}

/// Shows a profiler question in the store creation flow.
struct StoreCreationProfilerQuestionView<QuestionContent: View>: View {
private let viewModel: StoreCreationProfilerQuestionViewModel
private let questionContent: QuestionContent

init(viewModel: StoreCreationProfilerQuestionViewModel,
@ViewBuilder questionContent: () -> QuestionContent) {
self.viewModel = viewModel
self.questionContent = questionContent()
}

var body: some View {
VStack(alignment: .leading, spacing: 40) {
VStack(alignment: .leading, spacing: 16) {
// Top header label.
Text(viewModel.topHeader.uppercased())
.foregroundColor(Color(.secondaryLabel))
.footnoteStyle()

// Title label.
Text(viewModel.title)
.fontWeight(.bold)
.titleStyle()

// Subtitle label.
Text(viewModel.subtitle)
.foregroundColor(Color(.secondaryLabel))
.bodyStyle()
}

// Content of the profiler question.
questionContent
}
.padding(Layout.contentPadding)
}
}

private enum Layout {
static let contentPadding: EdgeInsets = .init(top: 38, leading: 16, bottom: 16, trailing: 16)
}

#if DEBUG

private struct StoreCreationQuestionPreviewViewModel: StoreCreationProfilerQuestionViewModel {
let topHeader: String = "Store name"
let title: String = "Which of these best describes you?"
let subtitle: String = "Choose a category that defines your business the best."
}

struct StoreCreationProfilerQuestionView_Previews: PreviewProvider {
static var previews: some View {
StoreCreationProfilerQuestionView(viewModel: StoreCreationQuestionPreviewViewModel()) {
Text("question content")
}
}
}

#endif
Loading