-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
3af3d19
Add a feature flag for store creation M3 profiler questions.
jaclync a9d722f
Add generic profiler question views for the first profiler question a…
jaclync f6bd19d
Update store creation navigation to show category profiler question u…
jaclync e695bf8
Pass the view model directly to `StoreCreationCategoryQuestionView` a…
jaclync a3574d5
Add test cases for `StoreCreationCategoryQuestionViewModel`.
jaclync 180fcbf
Merge branch 'trunk' into feat/profiler-questions
jaclync 7142d88
Show the top header in uppercase to match design.
jaclync 87be413
Use transparent navigation bar helper.
jaclync 8a9ce6b
Disable interactive dismissal of the store creation flow.
jaclync File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
...s/Authentication/Store Creation/Profiler/Category/StoreCreationCategoryQuestionView.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: {})) | ||
| } | ||
| } |
104 changes: 104 additions & 0 deletions
104
...hentication/Store Creation/Profiler/Category/StoreCreationCategoryQuestionViewModel.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| 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." | ||
| ) | ||
| } | ||
| } | ||
88 changes: 88 additions & 0 deletions
88
...es/Authentication/Store Creation/Profiler/OptionalStoreCreationProfilerQuestionView.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
67 changes: 67 additions & 0 deletions
67
...ce/Classes/Authentication/Store Creation/Profiler/StoreCreationProfilerQuestionView.swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Is this a typo? 😅
Uh oh!
There was an error while loading. Please reload this page.
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.
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