diff --git a/Experiments/Experiments/DefaultFeatureFlagService.swift b/Experiments/Experiments/DefaultFeatureFlagService.swift index ff222375538..43e4f96864e 100644 --- a/Experiments/Experiments/DefaultFeatureFlagService.swift +++ b/Experiments/Experiments/DefaultFeatureFlagService.swift @@ -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: diff --git a/Experiments/Experiments/FeatureFlag.swift b/Experiments/Experiments/FeatureFlag.swift index d475c5d3569..5f8c600c9e2 100644 --- a/Experiments/Experiments/FeatureFlag.swift +++ b/Experiments/Experiments/FeatureFlag.swift @@ -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 diff --git a/WooCommerce/Classes/Authentication/Store Creation/Profiler/Category/StoreCreationCategoryQuestionView.swift b/WooCommerce/Classes/Authentication/Store Creation/Profiler/Category/StoreCreationCategoryQuestionView.swift new file mode 100644 index 00000000000..30d65ad629c --- /dev/null +++ b/WooCommerce/Classes/Authentication/Store Creation/Profiler/Category/StoreCreationCategoryQuestionView.swift @@ -0,0 +1,54 @@ +import SwiftUI + +/// Hosting controller that wraps the `StoreCreationCategoryQuestionView`. +final class StoreCreationCategoryQuestionHostingController: UIHostingController { + 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: {})) + } +} diff --git a/WooCommerce/Classes/Authentication/Store Creation/Profiler/Category/StoreCreationCategoryQuestionViewModel.swift b/WooCommerce/Classes/Authentication/Store Creation/Profiler/Category/StoreCreationCategoryQuestionViewModel.swift new file mode 100644 index 00000000000..fbbc3b8d474 --- /dev/null +++ b/WooCommerce/Classes/Authentication/Store Creation/Profiler/Category/StoreCreationCategoryQuestionViewModel.swift @@ -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." + ) + } +} diff --git a/WooCommerce/Classes/Authentication/Store Creation/Profiler/OptionalStoreCreationProfilerQuestionView.swift b/WooCommerce/Classes/Authentication/Store Creation/Profiler/OptionalStoreCreationProfilerQuestionView.swift new file mode 100644 index 00000000000..0749ec4393f --- /dev/null +++ b/WooCommerce/Classes/Authentication/Store Creation/Profiler/OptionalStoreCreationProfilerQuestionView.swift @@ -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: 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(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 diff --git a/WooCommerce/Classes/Authentication/Store Creation/Profiler/StoreCreationProfilerQuestionView.swift b/WooCommerce/Classes/Authentication/Store Creation/Profiler/StoreCreationProfilerQuestionView.swift new file mode 100644 index 00000000000..2928cf87ada --- /dev/null +++ b/WooCommerce/Classes/Authentication/Store Creation/Profiler/StoreCreationProfilerQuestionView.swift @@ -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: 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 diff --git a/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift b/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift index 78d079bb47d..bbbc259cdcb 100644 --- a/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift +++ b/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift @@ -121,13 +121,22 @@ private extension StoreCreationCoordinator { presentStoreCreation(viewController: webNavigationController) } + @MainActor func startStoreCreationM2(from navigationController: UINavigationController, planToPurchase: WPComPlanProduct) { navigationController.navigationBar.prefersLargeTitles = true + // Disables interactive dismissal of the store creation modal. + navigationController.isModalInPresentation = true + let isProfilerEnabled = featureFlagService.isFeatureFlagEnabled(.storeCreationM3Profiler) let storeNameForm = StoreNameFormHostingController { [weak self] storeName in - self?.showDomainSelector(from: navigationController, - storeName: storeName, - planToPurchase: planToPurchase) + if isProfilerEnabled { + self?.showCategoryQuestion(from: navigationController, storeName: storeName, planToPurchase: planToPurchase) + } else { + self?.showDomainSelector(from: navigationController, + storeName: storeName, + categoryName: nil, + planToPurchase: planToPurchase) + } } onClose: { [weak self] in self?.showDiscardChangesAlert(flow: .native) } @@ -252,30 +261,51 @@ private extension StoreCreationCoordinator { // MARK: - Store creation M2 private extension StoreCreationCoordinator { + @MainActor + func showCategoryQuestion(from navigationController: UINavigationController, + storeName: String, + planToPurchase: WPComPlanProduct) { + let questionController = StoreCreationCategoryQuestionHostingController(viewModel: + .init(storeName: storeName) { [weak self] categoryName in + guard let self else { return } + self.showDomainSelector(from: navigationController, storeName: storeName, categoryName: categoryName, planToPurchase: planToPurchase) + } onSkip: { [weak self] in + // TODO: analytics + guard let self else { return } + self.showDomainSelector(from: navigationController, storeName: storeName, categoryName: nil, planToPurchase: planToPurchase) + }) + navigationController.pushViewController(questionController, animated: true) + // TODO: analytics + } + + @MainActor func showDomainSelector(from navigationController: UINavigationController, storeName: String, + categoryName: String?, planToPurchase: WPComPlanProduct) { let domainSelector = DomainSelectorHostingController(viewModel: .init(initialSearchTerm: storeName), onDomainSelection: { [weak self] domain in guard let self else { return } await self.createStoreAndContinueToStoreSummary(from: navigationController, name: storeName, + categoryName: categoryName, domain: domain, planToPurchase: planToPurchase) }) - navigationController.pushViewController(domainSelector, animated: false) + navigationController.pushViewController(domainSelector, animated: true) analytics.track(event: .StoreCreation.siteCreationStep(step: .domainPicker)) } @MainActor func createStoreAndContinueToStoreSummary(from navigationController: UINavigationController, name: String, + categoryName: String?, domain: String, planToPurchase: WPComPlanProduct) async { let result = await createStore(name: name, domain: domain) switch result { case .success(let siteResult): - showStoreSummary(from: navigationController, result: siteResult, planToPurchase: planToPurchase) + showStoreSummary(from: navigationController, result: siteResult, categoryName: categoryName, planToPurchase: planToPurchase) case .failure(let error): analytics.track(event: .StoreCreation.siteCreationFailed(source: source.analyticsValue, error: error, flow: .native)) showStoreCreationErrorAlert(from: navigationController, error: error) @@ -294,8 +324,9 @@ private extension StoreCreationCoordinator { @MainActor func showStoreSummary(from navigationController: UINavigationController, result: SiteCreationResult, + categoryName: String?, planToPurchase: WPComPlanProduct) { - let viewModel = StoreCreationSummaryViewModel(storeName: result.name, storeSlug: result.siteSlug) + let viewModel = StoreCreationSummaryViewModel(storeName: result.name, storeSlug: result.siteSlug, categoryName: categoryName) let storeSummary = StoreCreationSummaryHostingController(viewModel: viewModel) { [weak self] in guard let self else { return } self.showWPCOMPlan(from: navigationController, diff --git a/WooCommerce/Classes/Authentication/Store Creation/StoreCreationSummaryView.swift b/WooCommerce/Classes/Authentication/Store Creation/StoreCreationSummaryView.swift index 6384a137592..b45453dc59a 100644 --- a/WooCommerce/Classes/Authentication/Store Creation/StoreCreationSummaryView.swift +++ b/WooCommerce/Classes/Authentication/Store Creation/StoreCreationSummaryView.swift @@ -43,6 +43,8 @@ struct StoreCreationSummaryViewModel { let storeName: String /// The URL slug of the store. let storeSlug: String + /// Optional category name from the previous profiler question. + let categoryName: String? } /// Displays a summary of the store creation flow with the store information (e.g. store name, store slug). @@ -84,6 +86,12 @@ struct StoreCreationSummaryView: View { Text(viewModel.storeSlug) .foregroundColor(Color(.secondaryLabel)) .bodyStyle() + // Store category (optional). + if let categoryName = viewModel.categoryName { + Text(categoryName) + .foregroundColor(Color(.label)) + .bodyStyle() + } } } .padding(Layout.storeInfoPadding) @@ -141,9 +149,9 @@ private extension StoreCreationSummaryView { struct StoreCreationSummaryView_Previews: PreviewProvider { static var previews: some View { StoreCreationSummaryView(viewModel: - .init(storeName: "Fruity shop", storeSlug: "fruityshop.com")) + .init(storeName: "Fruity shop", storeSlug: "fruityshop.com", categoryName: "Arts and Crafts")) StoreCreationSummaryView(viewModel: - .init(storeName: "Fruity shop", storeSlug: "fruityshop.com")) + .init(storeName: "Fruity shop", storeSlug: "fruityshop.com", categoryName: "Arts and Crafts")) .preferredColorScheme(.dark) } } diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index cffeb880f17..ab84fed2592 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -21,6 +21,11 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 0201E4272945B01800C793C7 /* StoreCreationProfilerQuestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201E4262945B01800C793C7 /* StoreCreationProfilerQuestionView.swift */; }; + 0201E4292945B8B600C793C7 /* StoreCreationCategoryQuestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201E4282945B8B600C793C7 /* StoreCreationCategoryQuestionView.swift */; }; + 0201E42B2946151100C793C7 /* StoreCreationCategoryQuestionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201E42A2946151100C793C7 /* StoreCreationCategoryQuestionViewModel.swift */; }; + 0201E42D2946C23600C793C7 /* OptionalStoreCreationProfilerQuestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201E42C2946C23600C793C7 /* OptionalStoreCreationProfilerQuestionView.swift */; }; + 0201E4312946FFDB00C793C7 /* StoreCreationCategoryQuestionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0201E4302946FFDB00C793C7 /* StoreCreationCategoryQuestionViewModelTests.swift */; }; 0202B68D23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0202B68C23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift */; }; 0202B6922387AB0C00F3EBE0 /* WooTab+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0202B6912387AB0C00F3EBE0 /* WooTab+Tag.swift */; }; 0202B6952387AD1B00F3EBE0 /* UITabBar+Order.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0202B6942387AD1B00F3EBE0 /* UITabBar+Order.swift */; }; @@ -2049,6 +2054,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0201E4262945B01800C793C7 /* StoreCreationProfilerQuestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationProfilerQuestionView.swift; sourceTree = ""; }; + 0201E4282945B8B600C793C7 /* StoreCreationCategoryQuestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationCategoryQuestionView.swift; sourceTree = ""; }; + 0201E42A2946151100C793C7 /* StoreCreationCategoryQuestionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationCategoryQuestionViewModel.swift; sourceTree = ""; }; + 0201E42C2946C23600C793C7 /* OptionalStoreCreationProfilerQuestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalStoreCreationProfilerQuestionView.swift; sourceTree = ""; }; + 0201E4302946FFDB00C793C7 /* StoreCreationCategoryQuestionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationCategoryQuestionViewModelTests.swift; sourceTree = ""; }; 0202B68C23876BC100F3EBE0 /* ProductsTabProductViewModel+ProductVariation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProductsTabProductViewModel+ProductVariation.swift"; sourceTree = ""; }; 0202B6912387AB0C00F3EBE0 /* WooTab+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WooTab+Tag.swift"; sourceTree = ""; }; 0202B6942387AD1B00F3EBE0 /* UITabBar+Order.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITabBar+Order.swift"; sourceTree = ""; }; @@ -4094,6 +4104,33 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0201E4252945AFFC00C793C7 /* Profiler */ = { + isa = PBXGroup; + children = ( + 0201E4262945B01800C793C7 /* StoreCreationProfilerQuestionView.swift */, + 0201E42C2946C23600C793C7 /* OptionalStoreCreationProfilerQuestionView.swift */, + 0201E42E2946F9F400C793C7 /* Category */, + ); + path = Profiler; + sourceTree = ""; + }; + 0201E42E2946F9F400C793C7 /* Category */ = { + isa = PBXGroup; + children = ( + 0201E4282945B8B600C793C7 /* StoreCreationCategoryQuestionView.swift */, + 0201E42A2946151100C793C7 /* StoreCreationCategoryQuestionViewModel.swift */, + ); + path = Category; + sourceTree = ""; + }; + 0201E42F2946FFCA00C793C7 /* Profiler */ = { + isa = PBXGroup; + children = ( + 0201E4302946FFDB00C793C7 /* StoreCreationCategoryQuestionViewModelTests.swift */, + ); + path = Profiler; + sourceTree = ""; + }; 0202B6932387ACE000F3EBE0 /* TabBar */ = { isa = PBXGroup; children = ( @@ -4106,6 +4143,7 @@ 0203C11D2930643700EE61BF /* Store Creation */ = { isa = PBXGroup; children = ( + 0201E42F2946FFCA00C793C7 /* Profiler */, 0269A5E62913FD22003B20EB /* StoreCreationCoordinatorTests.swift */, 020D0BFE2914F6BA00BB3DCE /* LoggedOutStoreCreationCoordinatorTests.swift */, 0203C11B293058CB00EE61BF /* WebPurchasesForWPComPlansTests.swift */, @@ -4688,6 +4726,7 @@ 02759B8F28FFA06F00918176 /* Store Creation */ = { isa = PBXGroup; children = ( + 0201E4252945AFFC00C793C7 /* Profiler */, 020AF6642923C745007760E5 /* Store name */, 02EEA92929233F0F00D05F47 /* Plan */, 02063C8729260A6500130906 /* Installations */, @@ -10160,6 +10199,7 @@ D817586222BB64C300289CFE /* OrderDetailsNotices.swift in Sources */, 022F7A0324A05F6400012601 /* LinkedProductsListSelectorViewController.swift in Sources */, 2602A63F27BD880A00B347F1 /* NewOrderInitialStatusResolver.swift in Sources */, + 0201E42B2946151100C793C7 /* StoreCreationCategoryQuestionViewModel.swift in Sources */, E120F63826C26B550005A029 /* InPersonPaymentsLoadingView.swift in Sources */, 456417F4247D5434001203F6 /* UITableView+Helpers.swift in Sources */, E15FC74326BC1D2700CF83E6 /* SafariSheet.swift in Sources */, @@ -10413,6 +10453,7 @@ DE1B030D268DD01A00804330 /* ReviewOrderViewController.swift in Sources */, B5FD111621D3F13700560344 /* BordersView.swift in Sources */, 0262DA5B23A244830029AF30 /* Product+ShippingSettingsViewModels.swift in Sources */, + 0201E42D2946C23600C793C7 /* OptionalStoreCreationProfilerQuestionView.swift in Sources */, 0216272B2379662C000208D2 /* DefaultProductFormTableViewModel.swift in Sources */, 45E9A6E424DAE1EA00A600E8 /* ProductReviewsViewController.swift in Sources */, 03E471C4293A1F8D001A58AD /* BuiltInReaderConnectionAlertsProvider.swift in Sources */, @@ -10696,6 +10737,7 @@ DEE183F1292E0ED0008818AB /* LoginJetpackSetupInterruptedView.swift in Sources */, 2676F4CC2908284800C7A15B /* ProductCreationTypeCommand.swift in Sources */, 45A24E5F2451DF1A0050606B /* ProductMenuOrderViewController.swift in Sources */, + 0201E4272945B01800C793C7 /* StoreCreationProfilerQuestionView.swift in Sources */, 2667BFE1252FA117008099D4 /* RefundItemQuantityListSelectorCommand.swift in Sources */, 261AA30C2753119E009530FE /* PaymentMethodsViewModel.swift in Sources */, DE68B81F26F86B1700C86CFB /* OfflineBannerView.swift in Sources */, @@ -10780,6 +10822,7 @@ 57EBC92024EEE61800C1D45B /* WooAnalyticsEvent.swift in Sources */, 57CDABB9252E9BEB00BED88C /* ButtonTableFooterView.swift in Sources */, 021940E8291FDBF90090354E /* StoreCreationSummaryView.swift in Sources */, + 0201E4292945B8B600C793C7 /* StoreCreationCategoryQuestionView.swift in Sources */, B63D9009293E56E300BB5C9D /* AnalyticsHubQuarterToDateRangeData.swift in Sources */, 02E4FD7E2306A8180049610C /* StatsTimeRangeBarViewModel.swift in Sources */, 45D875D22611EA2100226C3F /* ListHeaderView.swift in Sources */, @@ -11501,6 +11544,7 @@ 6856DE479EC3B2265AC1F775 /* Calendar+Extensions.swift in Sources */, 4596854B254071C000D17B90 /* DownloadableFileBottomSheetListSelectorCommandTests.swift in Sources */, 26B9875F273CB6AA0090E8CA /* SimplePaymentsNoteViewModelTests.swift in Sources */, + 0201E4312946FFDB00C793C7 /* StoreCreationCategoryQuestionViewModelTests.swift in Sources */, EE8DCA8028BF964700F23B23 /* MockAuthentication.swift in Sources */, 6856D49DB7DCF4D87745C0B1 /* MockPushNotificationsManager.swift in Sources */, 4569D3C925DC065B00CDC3E2 /* SiteAddressTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Authentication/Store Creation/Profiler/StoreCreationCategoryQuestionViewModelTests.swift b/WooCommerce/WooCommerceTests/Authentication/Store Creation/Profiler/StoreCreationCategoryQuestionViewModelTests.swift new file mode 100644 index 00000000000..3e6bdb2dff9 --- /dev/null +++ b/WooCommerce/WooCommerceTests/Authentication/Store Creation/Profiler/StoreCreationCategoryQuestionViewModelTests.swift @@ -0,0 +1,65 @@ +import XCTest +@testable import WooCommerce + +@MainActor +final class StoreCreationCategoryQuestionViewModelTests: XCTestCase { + func test_selectCategory_updates_selectedCategory() throws { + // Given + let viewModel = StoreCreationCategoryQuestionViewModel(storeName: "store", + onContinue: { _ in }, + onSkip: {}) + + // When + viewModel.selectCategory(.init(name: "Cool clothing", value: "cool_clothing")) + + // Then + XCTAssertEqual(viewModel.selectedCategory, .init(name: "Cool clothing", value: "cool_clothing")) + } + + func test_continueButtonTapped_invokes_onContinue_after_selecting_a_category() throws { + waitFor { promise in + // Given + let viewModel = StoreCreationCategoryQuestionViewModel(storeName: "store", + onContinue: { _ in + // Then + promise(()) + }, + onSkip: {}) + // When + viewModel.selectCategory(.init(name: "Cool clothing", value: "cool_clothing")) + Task { @MainActor in + await viewModel.continueButtonTapped() + } + } + } + + func test_continueButtonTapped_invokes_onSkip_without_selecting_a_category() throws { + waitFor { promise in + // Given + let viewModel = StoreCreationCategoryQuestionViewModel(storeName: "store", + onContinue: { _ in }, + onSkip: { + // Then + promise(()) + }) + // When + Task { @MainActor in + await viewModel.continueButtonTapped() + } + } + } + + func test_skipButtonTapped_invokes_onSkip() throws { + waitFor { promise in + // Given + let viewModel = StoreCreationCategoryQuestionViewModel(storeName: "store", + onContinue: { _ in }, + onSkip: { + // Then + promise(()) + }) + // When + viewModel.skipButtonTapped() + } + } +}