-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10389 from woocommerce/feat/10376-features-view
[Profiler] Features question screen UI
- Loading branch information
Showing
7 changed files
with
296 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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
42 changes: 42 additions & 0 deletions
42
...uthentication/Store Creation/Profiler/Features/StoreCreationFeaturesQuestionOptions.swift
This file contains 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,42 @@ | ||
import Foundation | ||
|
||
extension StoreCreationFeaturesQuestionViewModel { | ||
// TODO: 10386 Align with Android and send these via tracks | ||
enum Feature: String, CaseIterable { | ||
case salesAndAnalyticsReports = "sales-and-analytics-reports" | ||
case productManagementAndInventoryTracking = "product-management-and-inventory-tracking" | ||
case flexibleAndSecurePaymentOptions = "flexible-and-secure-payment-options" | ||
case inPersonPayment = "in-person-payment" | ||
case abilityToScaleAsBusinessGrows = "ability-to-scale-as-business-grows" | ||
case customizationOptionForStoreDesign = "customization-options-for-my-store-design" | ||
case wideRangeOfPluginsAndExtensions = "wide-range-of-plugins-and-extensions" | ||
case others = "cthers" | ||
} | ||
|
||
var features: [Feature] { | ||
Feature.allCases | ||
} | ||
} | ||
|
||
extension StoreCreationFeaturesQuestionViewModel.Feature { | ||
var name: String { | ||
switch self { | ||
case .salesAndAnalyticsReports: | ||
return NSLocalizedString("Comprehensive sales and analytics reports", comment: "Feature option in the store creation features question.") | ||
case .productManagementAndInventoryTracking: | ||
return NSLocalizedString("Easy product management and inventory tracking", comment: "Feature option in the store creation features question.") | ||
case .flexibleAndSecurePaymentOptions: | ||
return NSLocalizedString("Flexible and secure payment options", comment: "Feature option in the store creation features question.") | ||
case .inPersonPayment: | ||
return NSLocalizedString("In-person payment", comment: "Feature option in the store creation features question.") | ||
case .abilityToScaleAsBusinessGrows: | ||
return NSLocalizedString("Ability to scale as my business grows", comment: "Feature option in the store creation features question.") | ||
case .customizationOptionForStoreDesign: | ||
return NSLocalizedString("Customization options for my store design", comment: "Feature option in the store creation features question.") | ||
case .wideRangeOfPluginsAndExtensions: | ||
return NSLocalizedString("Access to a wide range of plugins and extensions", comment: "Feature option in the store creation features question.") | ||
case .others: | ||
return NSLocalizedString("Others", comment: "Feature option in the store creation features question.") | ||
} | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
...s/Authentication/Store Creation/Profiler/Features/StoreCreationFeaturesQuestionView.swift
This file contains 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,53 @@ | ||
import SwiftUI | ||
|
||
/// Hosting controller that wraps the `StoreCreationFeaturesQuestionView`. | ||
final class StoreCreationFeaturesQuestionHostingController: UIHostingController<StoreCreationFeaturesQuestionView> { | ||
init(viewModel: StoreCreationFeaturesQuestionViewModel) { | ||
super.init(rootView: StoreCreationFeaturesQuestionView(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 features question in the store creation flow. | ||
struct StoreCreationFeaturesQuestionView: View { | ||
@ObservedObject private var viewModel: StoreCreationFeaturesQuestionViewModel | ||
|
||
init(viewModel: StoreCreationFeaturesQuestionViewModel) { | ||
self.viewModel = viewModel | ||
} | ||
|
||
var body: some View { | ||
OptionalStoreCreationProfilerQuestionView(viewModel: viewModel) { | ||
VStack(alignment: .leading, spacing: 16) { | ||
ForEach(viewModel.features, id: \.self) { feature in | ||
Button(action: { | ||
viewModel.didTapFeature(feature) | ||
}, label: { | ||
HStack { | ||
Text(feature.name) | ||
Spacer() | ||
} | ||
}) | ||
.buttonStyle(SelectableSecondaryButtonStyle(isSelected: viewModel.selectedFeatures.contains(where: { $0 == feature }))) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
struct StoreCreationFeaturesQuestionView_Previews: PreviewProvider { | ||
static var previews: some View { | ||
StoreCreationFeaturesQuestionView(viewModel: .init(onContinue: { _ in }, | ||
onSkip: {})) | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
...hentication/Store Creation/Profiler/Features/StoreCreationFeaturesQuestionViewModel.swift
This file contains 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,74 @@ | ||
import Combine | ||
import Foundation | ||
|
||
/// Necessary data from the answer of the store creation features question. | ||
struct StoreCreationFeaturesAnswer: Equatable { | ||
/// Display name of the selected feature. | ||
let name: String | ||
/// Raw value of the feature to be sent to the backend. | ||
let value: String | ||
} | ||
|
||
/// View model for `StoreCreationFeaturesQuestionView`, an optional profiler question about features in the store creation flow. | ||
@MainActor | ||
final class StoreCreationFeaturesQuestionViewModel: StoreCreationProfilerQuestionViewModel, ObservableObject { | ||
typealias Answer = StoreCreationFeaturesAnswer | ||
|
||
let topHeader = Localization.topHeader | ||
|
||
let title = Localization.title | ||
|
||
let subtitle = Localization.subtitle | ||
|
||
@Published private(set) var selectedFeatures: [Feature] = [] | ||
|
||
private let onContinue: ([Answer]) -> Void | ||
private let onSkip: () -> Void | ||
|
||
init(onContinue: @escaping ([Answer]) -> Void, | ||
onSkip: @escaping () -> Void) { | ||
self.onContinue = onContinue | ||
self.onSkip = onSkip | ||
} | ||
} | ||
|
||
extension StoreCreationFeaturesQuestionViewModel: OptionalStoreCreationProfilerQuestionViewModel { | ||
func continueButtonTapped() async { | ||
guard selectedFeatures.isNotEmpty else { | ||
return onSkip() | ||
} | ||
|
||
onContinue(selectedFeatures.map { .init(name: $0.name, value: $0.rawValue) }) | ||
} | ||
|
||
func skipButtonTapped() { | ||
onSkip() | ||
} | ||
} | ||
|
||
extension StoreCreationFeaturesQuestionViewModel { | ||
func didTapFeature(_ feature: Feature) { | ||
if let alreadySelectedIndex = selectedFeatures.firstIndex(of: feature) { | ||
selectedFeatures.remove(at: alreadySelectedIndex) | ||
} else { | ||
selectedFeatures.append(feature) | ||
} | ||
} | ||
} | ||
|
||
private extension StoreCreationFeaturesQuestionViewModel { | ||
enum Localization { | ||
static let topHeader = NSLocalizedString( | ||
"About you", | ||
comment: "Top header text of the store creation profiler question about the features." | ||
) | ||
static let title = NSLocalizedString( | ||
"Which features are you most interested in?", | ||
comment: "Title of the store creation profiler question about the features." | ||
) | ||
static let subtitle = NSLocalizedString( | ||
"Let us know what you are looking forward to using in our app.", | ||
comment: "Subtitle of the store creation profiler question about the features." | ||
) | ||
} | ||
} |
This file contains 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 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
88 changes: 88 additions & 0 deletions
88
.../Authentication/Store Creation/Profiler/StoreCreationFeaturesQuestionViewModelTests.swift
This file contains 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 XCTest | ||
@testable import WooCommerce | ||
|
||
@MainActor | ||
final class StoreCreationFeaturesQuestionViewModelTests: XCTestCase { | ||
func test_didTapFeature_adds_feature_to_selectedFeatures() throws { | ||
// Given | ||
let viewModel = StoreCreationFeaturesQuestionViewModel(onContinue: { _ in }, | ||
onSkip: {}) | ||
|
||
// When | ||
viewModel.didTapFeature(.productManagementAndInventoryTracking) | ||
viewModel.didTapFeature(.abilityToScaleAsBusinessGrows) | ||
|
||
// Then | ||
XCTAssertEqual(viewModel.selectedFeatures, [.productManagementAndInventoryTracking, .abilityToScaleAsBusinessGrows]) | ||
} | ||
|
||
func test_didTapFeature_removes_feature_from_selectedFeatures_if_already_selected() throws { | ||
// Given | ||
let viewModel = StoreCreationFeaturesQuestionViewModel(onContinue: { _ in }, | ||
onSkip: {}) | ||
|
||
// When | ||
viewModel.didTapFeature(.productManagementAndInventoryTracking) | ||
viewModel.didTapFeature(.abilityToScaleAsBusinessGrows) | ||
|
||
// Then | ||
XCTAssertEqual(viewModel.selectedFeatures, [.productManagementAndInventoryTracking, .abilityToScaleAsBusinessGrows]) | ||
|
||
// When | ||
viewModel.didTapFeature(.productManagementAndInventoryTracking) | ||
|
||
// Then | ||
XCTAssertEqual(viewModel.selectedFeatures, [.abilityToScaleAsBusinessGrows]) | ||
} | ||
|
||
func test_continueButtonTapped_invokes_onContinue_after_selecting_features() throws { | ||
let answer = waitFor { promise in | ||
// Given | ||
let viewModel = StoreCreationFeaturesQuestionViewModel(onContinue: { answer in | ||
promise(answer) | ||
}, | ||
onSkip: {}) | ||
// When | ||
viewModel.didTapFeature(.productManagementAndInventoryTracking) | ||
viewModel.didTapFeature(.abilityToScaleAsBusinessGrows) | ||
|
||
Task { @MainActor in | ||
await viewModel.continueButtonTapped() | ||
} | ||
} | ||
|
||
// Then | ||
XCTAssertEqual(answer, [.init(name: StoreCreationFeaturesQuestionViewModel.Feature.productManagementAndInventoryTracking.name, | ||
value: "product-management-and-inventory-tracking"), | ||
.init(name: StoreCreationFeaturesQuestionViewModel.Feature.abilityToScaleAsBusinessGrows.name, | ||
value: "ability-to-scale-as-business-grows")]) | ||
} | ||
|
||
func test_continueButtonTapped_invokes_onSkip_without_selecting_a_feature() throws { | ||
waitFor { promise in | ||
// Given | ||
let viewModel = StoreCreationFeaturesQuestionViewModel( onContinue: { _ in }, | ||
onSkip: { | ||
// Then | ||
promise(()) | ||
}) | ||
// When | ||
Task { @MainActor in | ||
await viewModel.continueButtonTapped() | ||
} | ||
} | ||
} | ||
|
||
func test_skipButtonTapped_invokes_onSkip() throws { | ||
waitFor { promise in | ||
// Given | ||
let viewModel = StoreCreationFeaturesQuestionViewModel( onContinue: { _ in }, | ||
onSkip: { | ||
// Then | ||
promise(()) | ||
}) | ||
// When | ||
viewModel.skipButtonTapped() | ||
} | ||
} | ||
} |