Skip to content

Commit 22e5450

Browse files
authored
Merge pull request #8481 from woocommerce/feat/8377-store-creation-selling-status
Store creation M3: profiler question - optional selling status
2 parents fa067b4 + 845bcaa commit 22e5450

File tree

5 files changed

+243
-2
lines changed

5 files changed

+243
-2
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import SwiftUI
2+
3+
/// Hosting controller that wraps the `StoreCreationSellingStatusQuestionView`.
4+
final class StoreCreationSellingStatusQuestionHostingController: UIHostingController<StoreCreationSellingStatusQuestionView> {
5+
init(storeName: String, onContinue: @escaping () -> Void, onSkip: @escaping () -> Void) {
6+
super.init(rootView: StoreCreationSellingStatusQuestionView(storeName: storeName, onContinue: onContinue, onSkip: onSkip))
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 selling status question in the store creation flow.
22+
struct StoreCreationSellingStatusQuestionView: View {
23+
@ObservedObject private var viewModel: StoreCreationSellingStatusQuestionViewModel
24+
25+
init(storeName: String, onContinue: @escaping () -> Void, onSkip: @escaping () -> Void) {
26+
self.viewModel = StoreCreationSellingStatusQuestionViewModel(storeName: storeName, onContinue: onContinue, onSkip: onSkip)
27+
}
28+
29+
var body: some View {
30+
OptionalStoreCreationProfilerQuestionView(viewModel: viewModel) {
31+
VStack(spacing: 16) {
32+
ForEach(viewModel.sellingStatuses, id: \.self) { sellingStatus in
33+
Button(action: {
34+
viewModel.selectStatus(sellingStatus)
35+
}, label: {
36+
HStack {
37+
Text(sellingStatus.description)
38+
Spacer()
39+
}
40+
})
41+
.buttonStyle(SelectableSecondaryButtonStyle(isSelected: viewModel.selectedStatus == sellingStatus))
42+
}
43+
}
44+
}
45+
}
46+
}
47+
48+
struct StoreCreationSellingStatusQuestionView_Previews: PreviewProvider {
49+
static var previews: some View {
50+
StoreCreationSellingStatusQuestionView(storeName: "New Year Store", onContinue: {}, onSkip: {})
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Combine
2+
import Foundation
3+
4+
/// View model for `StoreCreationSellingStatusQuestionView`, an optional profiler question about store selling status in the store creation flow.
5+
@MainActor
6+
final class StoreCreationSellingStatusQuestionViewModel: StoreCreationProfilerQuestionViewModel, ObservableObject {
7+
/// Selling status options.
8+
/// https://github.com/Automattic/woocommerce.com/blob/trunk/themes/woo/start/config/options.json
9+
enum SellingStatus: Equatable {
10+
/// Just starting my business.
11+
case justStarting
12+
/// Already selling, but not online.
13+
case alreadySellingButNotOnline
14+
/// Already selling online.
15+
case alreadySellingOnline
16+
}
17+
18+
let topHeader: String
19+
20+
let title: String = Localization.title
21+
22+
let subtitle: String = Localization.subtitle
23+
24+
/// Question content.
25+
/// TODO: 8376 - update values when API is ready.
26+
let sellingStatuses: [SellingStatus] = [.justStarting, .alreadySellingButNotOnline, .alreadySellingOnline]
27+
28+
@Published private(set) var selectedStatus: SellingStatus?
29+
30+
private let onContinue: () -> Void
31+
private let onSkip: () -> Void
32+
33+
init(storeName: String,
34+
onContinue: @escaping () -> Void,
35+
onSkip: @escaping () -> Void) {
36+
self.topHeader = storeName
37+
self.onContinue = onContinue
38+
self.onSkip = onSkip
39+
}
40+
}
41+
42+
extension StoreCreationSellingStatusQuestionViewModel: OptionalStoreCreationProfilerQuestionViewModel {
43+
func continueButtonTapped() async {
44+
guard selectedStatus != nil else {
45+
return onSkip()
46+
}
47+
onContinue()
48+
}
49+
50+
func skipButtonTapped() {
51+
onSkip()
52+
}
53+
}
54+
55+
extension StoreCreationSellingStatusQuestionViewModel {
56+
/// Called when a selling status option is selected.
57+
func selectStatus(_ status: SellingStatus) {
58+
selectedStatus = status
59+
}
60+
}
61+
62+
extension StoreCreationSellingStatusQuestionViewModel.SellingStatus {
63+
var description: String {
64+
switch self {
65+
case .justStarting:
66+
return NSLocalizedString(
67+
"I am just starting to sell",
68+
comment: "Option in the store creation selling status question."
69+
)
70+
case .alreadySellingButNotOnline:
71+
return NSLocalizedString(
72+
"I am selling offline",
73+
comment: "Option in the store creation selling status question."
74+
)
75+
case .alreadySellingOnline:
76+
return NSLocalizedString(
77+
"I am already selling online",
78+
comment: "Option in the store creation selling status question."
79+
)
80+
}
81+
}
82+
}
83+
84+
private extension StoreCreationSellingStatusQuestionViewModel {
85+
enum Localization {
86+
static let title = NSLocalizedString(
87+
"Where are you on your commerce journey?",
88+
comment: "Title of the store creation profiler question about the store selling status."
89+
)
90+
static let subtitle = NSLocalizedString(
91+
"To speed things up, we’ll tailor your WooCommerce experience for you based on your response.",
92+
comment: "Subtitle of the store creation profiler question about the store selling status."
93+
)
94+
}
95+
}

WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,16 +308,33 @@ private extension StoreCreationCoordinator {
308308
let questionController = StoreCreationCategoryQuestionHostingController(viewModel:
309309
.init(storeName: storeName) { [weak self] categoryName in
310310
guard let self else { return }
311-
self.showDomainSelector(from: navigationController, storeName: storeName, categoryName: categoryName, planToPurchase: planToPurchase)
311+
self.showSellingStatusQuestion(from: navigationController, storeName: storeName, categoryName: categoryName, planToPurchase: planToPurchase)
312312
} onSkip: { [weak self] in
313313
// TODO: analytics
314314
guard let self else { return }
315-
self.showDomainSelector(from: navigationController, storeName: storeName, categoryName: nil, planToPurchase: planToPurchase)
315+
self.showSellingStatusQuestion(from: navigationController, storeName: storeName, categoryName: nil, planToPurchase: planToPurchase)
316316
})
317317
navigationController.pushViewController(questionController, animated: true)
318318
// TODO: analytics
319319
}
320320

321+
@MainActor
322+
func showSellingStatusQuestion(from navigationController: UINavigationController,
323+
storeName: String,
324+
categoryName: String?,
325+
planToPurchase: WPComPlanProduct) {
326+
let questionController = StoreCreationSellingStatusQuestionHostingController(storeName: storeName) { [weak self] in
327+
guard let self else { return }
328+
self.showDomainSelector(from: navigationController, storeName: storeName, categoryName: categoryName, planToPurchase: planToPurchase)
329+
} onSkip: { [weak self] in
330+
// TODO: analytics
331+
guard let self else { return }
332+
self.showDomainSelector(from: navigationController, storeName: storeName, categoryName: nil, planToPurchase: planToPurchase)
333+
}
334+
navigationController.pushViewController(questionController, animated: true)
335+
// TODO: analytics
336+
}
337+
321338
@MainActor
322339
func showDomainSelector(from navigationController: UINavigationController,
323340
storeName: String,

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
020C908424C84652001E2BEB /* ProductListMultiSelectorSearchUICommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020C908324C84652001E2BEB /* ProductListMultiSelectorSearchUICommandTests.swift */; };
5959
020D0BFD2914E92800BB3DCE /* StorePickerCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020D0BFC2914E92800BB3DCE /* StorePickerCoordinatorTests.swift */; };
6060
020D0BFF2914F6BA00BB3DCE /* LoggedOutStoreCreationCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020D0BFE2914F6BA00BB3DCE /* LoggedOutStoreCreationCoordinatorTests.swift */; };
61+
020DD0AF294A06C400727BEF /* StoreCreationSellingStatusQuestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020DD0AE294A06C400727BEF /* StoreCreationSellingStatusQuestionView.swift */; };
62+
020DD0B1294A071600727BEF /* StoreCreationSellingStatusQuestionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020DD0B0294A071600727BEF /* StoreCreationSellingStatusQuestionViewModel.swift */; };
6163
020DD48A23229495005822B1 /* ProductsTabProductTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020DD48923229495005822B1 /* ProductsTabProductTableViewCell.swift */; };
6264
020DD48D2322A617005822B1 /* ProductsTabProductViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020DD48C2322A617005822B1 /* ProductsTabProductViewModel.swift */; };
6365
020DD48F232392C9005822B1 /* UIViewController+AppReview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020DD48E232392C9005822B1 /* UIViewController+AppReview.swift */; };
@@ -295,6 +297,7 @@
295297
0286B27C23C7051F003D784B /* ProductImagesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0286B27823C7051F003D784B /* ProductImagesViewController.xib */; };
296298
0286B27D23C7051F003D784B /* ProductImagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0286B27923C7051F003D784B /* ProductImagesViewController.swift */; };
297299
0286B27F23C70557003D784B /* ColumnFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0286B27E23C70557003D784B /* ColumnFlowLayout.swift */; };
300+
028A4655295AD2DA001CF6CE /* StoreCreationSellingStatusQuestionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028A4654295AD2DA001CF6CE /* StoreCreationSellingStatusQuestionViewModelTests.swift */; };
298301
028AFFB32484ED2800693C09 /* Dictionary+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028AFFB22484ED2800693C09 /* Dictionary+Logging.swift */; };
299302
028AFFB62484EDA000693C09 /* Dictionary+LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028AFFB52484EDA000693C09 /* Dictionary+LoggingTests.swift */; };
300303
028BAC3D22F2DECE008BB4AF /* StoreStatsAndTopPerformersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028BAC3C22F2DECE008BB4AF /* StoreStatsAndTopPerformersViewController.swift */; };
@@ -2094,6 +2097,8 @@
20942097
020C908324C84652001E2BEB /* ProductListMultiSelectorSearchUICommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListMultiSelectorSearchUICommandTests.swift; sourceTree = "<group>"; };
20952098
020D0BFC2914E92800BB3DCE /* StorePickerCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePickerCoordinatorTests.swift; sourceTree = "<group>"; };
20962099
020D0BFE2914F6BA00BB3DCE /* LoggedOutStoreCreationCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedOutStoreCreationCoordinatorTests.swift; sourceTree = "<group>"; };
2100+
020DD0AE294A06C400727BEF /* StoreCreationSellingStatusQuestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationSellingStatusQuestionView.swift; sourceTree = "<group>"; };
2101+
020DD0B0294A071600727BEF /* StoreCreationSellingStatusQuestionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationSellingStatusQuestionViewModel.swift; sourceTree = "<group>"; };
20972102
020DD48923229495005822B1 /* ProductsTabProductTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsTabProductTableViewCell.swift; sourceTree = "<group>"; };
20982103
020DD48C2322A617005822B1 /* ProductsTabProductViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsTabProductViewModel.swift; sourceTree = "<group>"; };
20992104
020DD48E232392C9005822B1 /* UIViewController+AppReview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+AppReview.swift"; sourceTree = "<group>"; };
@@ -2332,6 +2337,7 @@
23322337
0286B27823C7051F003D784B /* ProductImagesViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ProductImagesViewController.xib; sourceTree = "<group>"; };
23332338
0286B27923C7051F003D784B /* ProductImagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductImagesViewController.swift; sourceTree = "<group>"; };
23342339
0286B27E23C70557003D784B /* ColumnFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnFlowLayout.swift; sourceTree = "<group>"; };
2340+
028A4654295AD2DA001CF6CE /* StoreCreationSellingStatusQuestionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreCreationSellingStatusQuestionViewModelTests.swift; sourceTree = "<group>"; };
23352341
028AFFB22484ED2800693C09 /* Dictionary+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Logging.swift"; sourceTree = "<group>"; };
23362342
028AFFB52484EDA000693C09 /* Dictionary+LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+LoggingTests.swift"; sourceTree = "<group>"; };
23372343
028BAC3C22F2DECE008BB4AF /* StoreStatsAndTopPerformersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsAndTopPerformersViewController.swift; sourceTree = "<group>"; };
@@ -4116,6 +4122,7 @@
41164122
0201E4262945B01800C793C7 /* StoreCreationProfilerQuestionView.swift */,
41174123
0201E42C2946C23600C793C7 /* OptionalStoreCreationProfilerQuestionView.swift */,
41184124
0201E42E2946F9F400C793C7 /* Category */,
4125+
020DD0AD294A069500727BEF /* Selling Status */,
41194126
);
41204127
path = Profiler;
41214128
sourceTree = "<group>";
@@ -4133,6 +4140,7 @@
41334140
isa = PBXGroup;
41344141
children = (
41354142
0201E4302946FFDB00C793C7 /* StoreCreationCategoryQuestionViewModelTests.swift */,
4143+
028A4654295AD2DA001CF6CE /* StoreCreationSellingStatusQuestionViewModelTests.swift */,
41364144
);
41374145
path = Profiler;
41384146
sourceTree = "<group>";
@@ -4252,6 +4260,15 @@
42524260
path = Product;
42534261
sourceTree = "<group>";
42544262
};
4263+
020DD0AD294A069500727BEF /* Selling Status */ = {
4264+
isa = PBXGroup;
4265+
children = (
4266+
020DD0AE294A06C400727BEF /* StoreCreationSellingStatusQuestionView.swift */,
4267+
020DD0B0294A071600727BEF /* StoreCreationSellingStatusQuestionViewModel.swift */,
4268+
);
4269+
path = "Selling Status";
4270+
sourceTree = "<group>";
4271+
};
42554272
020DD48B2322A5F9005822B1 /* View Models */ = {
42564273
isa = PBXGroup;
42574274
children = (
@@ -10518,6 +10535,7 @@
1051810535
CC254F3026C2A53D005F3C82 /* ShippingLabelAddNewPackage.swift in Sources */,
1051910536
7441E1D221503F77004E6ECE /* IntrinsicTableView.swift in Sources */,
1052010537
B517EA1D218B41F200730EC4 /* String+Woo.swift in Sources */,
10538+
020DD0B1294A071600727BEF /* StoreCreationSellingStatusQuestionViewModel.swift in Sources */,
1052110539
26B3D8A0252235C50054C319 /* RefundShippingDetailsViewModel.swift in Sources */,
1052210540
B958A7CD28B3DD9100823EEF /* OrderDetailsRoute.swift in Sources */,
1052310541
45DB7040261209B10064A6CF /* ItemToFulfillRow.swift in Sources */,
@@ -11058,6 +11076,7 @@
1105811076
E16715CB26663B0B00326230 /* CardPresentModalSuccessWithoutEmail.swift in Sources */,
1105911077
028CB70F290138EF00331C09 /* Publisher+Concurrency.swift in Sources */,
1106011078
311F827426CD897900DF5BAD /* CardReaderSettingsAlertsProvider.swift in Sources */,
11079+
020DD0AF294A06C400727BEF /* StoreCreationSellingStatusQuestionView.swift in Sources */,
1106111080
CC4D1D8625E6CDDE00B6E4E7 /* RenameAttributesViewModel.swift in Sources */,
1106211081
DEFA3D932897D8930076FAE1 /* NoWooErrorViewModel.swift in Sources */,
1106311082
020A55F127F6C605007843F0 /* CardReaderConnectionAnalyticsTracker.swift in Sources */,
@@ -11523,6 +11542,7 @@
1152311542
26F94E34267AA42F00DB6CCF /* ProductAddOnViewModelTests.swift in Sources */,
1152411543
AE7C957F27C417FA007E8E12 /* FeeLineDetailsViewModelTests.swift in Sources */,
1152511544
0290E27E238E5B5C00B5C466 /* ProductStockStatusListSelectorCommandTests.swift in Sources */,
11545+
028A4655295AD2DA001CF6CE /* StoreCreationSellingStatusQuestionViewModelTests.swift in Sources */,
1152611546
EE81B1382865BB0B0032E0D4 /* ProductImagesProductIDUpdaterTests.swift in Sources */,
1152711547
7E7C5F8B2719AEDA00315B61 /* EditProductCategoryListViewModelTests.swift in Sources */,
1152811548
0247F510286E7D26009C177E /* ProductVariationFormViewModel+ImageUploaderTests.swift in Sources */,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import XCTest
2+
@testable import WooCommerce
3+
4+
@MainActor
5+
final class StoreCreationSellingStatusQuestionViewModelTests: XCTestCase {
6+
func test_selectCategory_updates_selectedStatus() throws {
7+
// Given
8+
let viewModel = StoreCreationSellingStatusQuestionViewModel(storeName: "store") {} onSkip: {}
9+
10+
// When
11+
viewModel.selectStatus(.alreadySellingOnline)
12+
13+
// Then
14+
XCTAssertEqual(viewModel.selectedStatus, .alreadySellingOnline)
15+
}
16+
17+
func test_continueButtonTapped_invokes_onContinue_after_selecting_a_status() throws {
18+
waitFor { promise in
19+
// Given
20+
let viewModel = StoreCreationSellingStatusQuestionViewModel(storeName: "store") {
21+
// Then
22+
promise(())
23+
} onSkip: {}
24+
// When
25+
viewModel.selectStatus(.alreadySellingButNotOnline)
26+
Task { @MainActor in
27+
await viewModel.continueButtonTapped()
28+
}
29+
}
30+
}
31+
32+
func test_continueButtonTapped_invokes_onSkip_without_selecting_a_category() throws {
33+
waitFor { promise in
34+
// Given
35+
let viewModel = StoreCreationSellingStatusQuestionViewModel(storeName: "store") {} onSkip: {
36+
// Then
37+
promise(())
38+
}
39+
// When
40+
Task { @MainActor in
41+
await viewModel.continueButtonTapped()
42+
}
43+
}
44+
}
45+
46+
func test_skipButtonTapped_invokes_onSkip() throws {
47+
waitFor { promise in
48+
// Given
49+
let viewModel = StoreCreationSellingStatusQuestionViewModel(storeName: "store") {} onSkip: {
50+
// Then
51+
promise(())
52+
}
53+
// When
54+
viewModel.skipButtonTapped()
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)