-
Notifications
You must be signed in to change notification settings - Fork 121
Store creation M3: profiler question - optional category selector #8379
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
Changes from 5 commits
3af3d19
a9d722f
f6bd19d
e695bf8
a3574d5
180fcbf
7142d88
87be413
8a9ce6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| 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() | ||
|
|
||
| configureNavigationBarAppearance() | ||
| } | ||
|
|
||
| /// Shows a transparent navigation bar without a bottom border and with a close button to dismiss. | ||
| func configureNavigationBarAppearance() { | ||
| let appearance = UINavigationBarAppearance() | ||
| appearance.configureWithTransparentBackground() | ||
| appearance.backgroundColor = .systemBackground | ||
|
|
||
| navigationItem.standardAppearance = appearance | ||
| navigationItem.scrollEdgeAppearance = appearance | ||
| navigationItem.compactAppearance = appearance | ||
| } | ||
| } | ||
|
|
||
| /// 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", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a typo? 😅
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| .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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that we're using this style a lot so I added an extension for this here. Maybe we should use it instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup thanks for creating that extension, updated in 87be413