Skip to content

Commit 9ab67ff

Browse files
authored
Merge pull request #8165 from woocommerce/feat/8045-domain-selector-updates
Store creation M2: domain selector updates
2 parents 742ca9c + 1b685f4 commit 9ab67ff

File tree

7 files changed

+178
-139
lines changed

7 files changed

+178
-139
lines changed

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,6 @@ private extension StoreCreationCoordinator {
263263
name: storeName,
264264
domain: domain,
265265
planToPurchase: planToPurchase)
266-
}, onSkip: {
267-
// TODO-8045: skip to the next step of store creation with an auto-generated domain.
268266
})
269267
navigationController.pushViewController(domainSelector, animated: false)
270268
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift

Lines changed: 101 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,14 @@ import SwiftUI
44
final class DomainSelectorHostingController: UIHostingController<DomainSelectorView> {
55
private let viewModel: DomainSelectorViewModel
66
private let onDomainSelection: (String) async -> Void
7-
private let onSkip: () -> Void
87

98
/// - Parameters:
109
/// - viewModel: View model for the domain selector.
1110
/// - onDomainSelection: Called when the user continues with a selected domain name.
12-
/// - onSkip: Called when the user taps to skip domain selection.
1311
init(viewModel: DomainSelectorViewModel,
14-
onDomainSelection: @escaping (String) async -> Void,
15-
onSkip: @escaping () -> Void) {
12+
onDomainSelection: @escaping (String) async -> Void) {
1613
self.viewModel = viewModel
1714
self.onDomainSelection = onDomainSelection
18-
self.onSkip = onSkip
1915
super.init(rootView: DomainSelectorView(viewModel: viewModel))
2016

2117
rootView.onDomainSelection = { [weak self] domain in
@@ -30,16 +26,11 @@ final class DomainSelectorHostingController: UIHostingController<DomainSelectorV
3026
override func viewDidLoad() {
3127
super.viewDidLoad()
3228

33-
configureSkipButton()
3429
configureNavigationBarAppearance()
3530
}
3631
}
3732

3833
private extension DomainSelectorHostingController {
39-
func configureSkipButton() {
40-
navigationItem.rightBarButtonItem = .init(title: Localization.skipButtonTitle, style: .plain, target: self, action: #selector(skipButtonTapped))
41-
}
42-
4334
/// Shows a transparent navigation bar without a bottom border.
4435
func configureNavigationBarAppearance() {
4536
let appearance = UINavigationBarAppearance()
@@ -52,20 +43,20 @@ private extension DomainSelectorHostingController {
5243
}
5344
}
5445

55-
private extension DomainSelectorHostingController {
56-
@objc func skipButtonTapped() {
57-
onSkip()
58-
}
59-
}
60-
61-
private extension DomainSelectorHostingController {
62-
enum Localization {
63-
static let skipButtonTitle = NSLocalizedString("Skip", comment: "Navigation bar button on the domain selector screen to skip domain selection.")
64-
}
65-
}
66-
6746
/// Allows the user to search for a domain and then select one to continue.
6847
struct DomainSelectorView: View {
48+
/// The state of the main view below the fixed header.
49+
enum ViewState: Equatable {
50+
/// When loading domain suggestions.
51+
case loading
52+
/// Shown when the search query is empty.
53+
case placeholder
54+
/// When there is an error loading domain suggestions.
55+
case error(message: String)
56+
/// When domain suggestions are displayed.
57+
case results(domains: [String])
58+
}
59+
6960
/// Set in the hosting controller.
7061
var onDomainSelection: ((String) async -> Void) = { _ in }
7162

@@ -79,88 +70,113 @@ struct DomainSelectorView: View {
7970

8071
@State private var isWaitingForDomainSelectionCompletion: Bool = false
8172

73+
@FocusState private var textFieldIsFocused: Bool
74+
8275
init(viewModel: DomainSelectorViewModel) {
8376
self.viewModel = viewModel
8477
}
8578

8679
var body: some View {
87-
VStack(alignment: .leading, spacing: 0) {
88-
ScrollView {
89-
VStack(alignment: .leading) {
90-
// Header label.
91-
Text(Localization.subtitle)
92-
.foregroundColor(Color(.secondaryLabel))
93-
.bodyStyle()
94-
.padding(.horizontal, Layout.defaultHorizontalPadding)
80+
ScrollView {
81+
VStack(alignment: .leading, spacing: 0) {
82+
// Header label.
83+
Text(Localization.subtitle)
84+
.foregroundColor(Color(.secondaryLabel))
85+
.bodyStyle()
86+
.padding(.horizontal, Layout.defaultHorizontalPadding)
87+
.padding(.top, 16)
9588

96-
// Search text field.
97-
SearchHeader(text: $viewModel.searchTerm,
98-
placeholder: Localization.searchPlaceholder,
99-
customizations: .init(backgroundColor: .clear,
100-
borderColor: .separator,
101-
internalHorizontalPadding: 21,
102-
internalVerticalPadding: 12))
89+
Spacer()
90+
.frame(height: 30)
10391

104-
// Results header.
105-
Text(Localization.suggestionsHeader)
92+
// Search text field.
93+
SearchHeader(text: $viewModel.searchTerm,
94+
placeholder: Localization.searchPlaceholder,
95+
customizations: .init(backgroundColor: .clear,
96+
borderColor: .separator,
97+
internalHorizontalPadding: 21,
98+
internalVerticalPadding: 12,
99+
iconSize: .init(width: 14, height: 14)))
100+
.focused($textFieldIsFocused)
101+
102+
switch viewModel.state {
103+
case .placeholder:
104+
// Placeholder image when search query is empty.
105+
Spacer()
106+
.frame(height: 30)
107+
108+
HStack {
109+
Spacer()
110+
Image(uiImage: .domainSearchPlaceholderImage)
111+
Spacer()
112+
}
113+
case .loading:
114+
// Progress indicator when loading domain suggestions.
115+
Spacer()
116+
.frame(height: 23)
117+
118+
HStack {
119+
Spacer()
120+
ProgressView()
121+
Spacer()
122+
}
123+
case .error(let errorMessage):
124+
// Error message when there is an error loading domain suggestions.
125+
Spacer()
126+
.frame(height: 23)
127+
128+
Text(errorMessage)
106129
.foregroundColor(Color(.secondaryLabel))
107130
.bodyStyle()
131+
.frame(maxWidth: .infinity, alignment: .center)
132+
.multilineTextAlignment(.center)
133+
.padding(Layout.defaultPadding)
134+
case .results(let domains):
135+
// Domain suggestions.
136+
Text(Localization.suggestionsHeader)
137+
.foregroundColor(Color(.secondaryLabel))
138+
.footnoteStyle()
108139
.padding(.horizontal, Layout.defaultHorizontalPadding)
140+
.padding(.vertical, insets: .init(top: 14, leading: 0, bottom: 8, trailing: 0))
109141

110-
if viewModel.searchTerm.isEmpty {
111-
// Placeholder image when search query is empty.
112-
HStack {
113-
Spacer()
114-
Image(uiImage: .domainSearchPlaceholderImage)
115-
Spacer()
116-
}
117-
} else if viewModel.isLoadingDomainSuggestions {
118-
// Progress indicator when loading domain suggestions.
119-
HStack {
120-
Spacer()
121-
ProgressView()
122-
Spacer()
123-
}
124-
} else if let errorMessage = viewModel.errorMessage {
125-
// Error message when there is an error loading domain suggestions.
126-
Text(errorMessage)
127-
.padding(Layout.defaultPadding)
128-
} else {
129-
// Domain suggestions.
130-
LazyVStack {
131-
ForEach(viewModel.domains, id: \.self) { domain in
132-
Button {
133-
selectedDomainName = domain
134-
} label: {
135-
VStack(alignment: .leading) {
136-
DomainRowView(viewModel: .init(domainName: domain,
137-
searchQuery: viewModel.searchTerm,
138-
isSelected: domain == selectedDomainName))
139-
Divider()
140-
.frame(height: Layout.dividerHeight)
141-
.padding(.leading, Layout.defaultHorizontalPadding)
142-
}
142+
LazyVStack {
143+
ForEach(domains, id: \.self) { domain in
144+
Button {
145+
textFieldIsFocused = false
146+
selectedDomainName = domain
147+
} label: {
148+
VStack(alignment: .leading) {
149+
DomainRowView(viewModel: .init(domainName: domain,
150+
searchQuery: viewModel.searchTerm,
151+
isSelected: domain == selectedDomainName))
152+
Divider()
153+
.frame(height: Layout.dividerHeight)
154+
.padding(.leading, Layout.defaultHorizontalPadding)
143155
}
144156
}
145157
}
146158
}
147159
}
148160
}
149-
161+
}
162+
.safeAreaInset(edge: .bottom) {
150163
// Continue button when a domain is selected.
151164
if let selectedDomainName {
152-
Divider()
153-
.frame(height: Layout.dividerHeight)
154-
.foregroundColor(Color(.separator))
155-
Button(Localization.continueButtonTitle) {
156-
Task { @MainActor in
157-
isWaitingForDomainSelectionCompletion = true
158-
await onDomainSelection(selectedDomainName)
159-
isWaitingForDomainSelectionCompletion = false
165+
VStack {
166+
Divider()
167+
.frame(height: Layout.dividerHeight)
168+
.foregroundColor(Color(.separator))
169+
Button(Localization.continueButtonTitle) {
170+
Task { @MainActor in
171+
isWaitingForDomainSelectionCompletion = true
172+
await onDomainSelection(selectedDomainName)
173+
isWaitingForDomainSelectionCompletion = false
174+
}
160175
}
176+
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: isWaitingForDomainSelectionCompletion))
177+
.padding(Layout.defaultPadding)
161178
}
162-
.buttonStyle(PrimaryLoadingButtonStyle(isLoading: isWaitingForDomainSelectionCompletion))
163-
.padding(Layout.defaultPadding)
179+
.background(Color(.systemBackground))
164180
}
165181
}
166182
.navigationTitle(Localization.title)
@@ -176,7 +192,6 @@ struct DomainSelectorView: View {
176192

177193
private extension DomainSelectorView {
178194
enum Layout {
179-
static let spacingBetweenTitleAndSubtitle: CGFloat = 16
180195
static let defaultHorizontalPadding: CGFloat = 16
181196
static let dividerHeight: CGFloat = 1
182197
static let defaultPadding: EdgeInsets = .init(top: 10, leading: 16, bottom: 10, trailing: 16)
@@ -185,9 +200,9 @@ private extension DomainSelectorView {
185200
enum Localization {
186201
static let title = NSLocalizedString("Choose a domain", comment: "Title of the domain selector.")
187202
static let subtitle = NSLocalizedString(
188-
"This is where people will find you on the Internet. Don't worry, you can change it later.",
203+
"This is where people will find you on the Internet. You can add another domain later.",
189204
comment: "Subtitle of the domain selector.")
190-
static let searchPlaceholder = NSLocalizedString("Type to get suggestions", comment: "Placeholder of the search text field on the domain selector.")
205+
static let searchPlaceholder = NSLocalizedString("Type a name for your store", comment: "Placeholder of the search text field on the domain selector.")
191206
static let suggestionsHeader = NSLocalizedString("SUGGESTIONS", comment: "Header label of the domain suggestions on the domain selector.")
192207
static let continueButtonTitle = NSLocalizedString("Continue", comment: "Title of the button to continue with a selected domain.")
193208
}

WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ final class DomainSelectorViewModel: ObservableObject {
1010
@Published var searchTerm: String = ""
1111

1212
/// Domain names after domain suggestions are loaded remotely.
13-
@Published private(set) var domains: [String] = []
13+
@Published private var domains: [String] = []
1414

1515
/// Error message from loading domain suggestions.
16-
@Published private(set) var errorMessage: String?
16+
@Published private var errorMessage: String?
1717

1818
/// Whether domain suggestions are being loaded.
1919
@Published private(set) var isLoadingDomainSuggestions: Bool = false
2020

21+
/// The state of the main domain selector view based on the search query and loading state.
22+
@Published private(set) var state: DomainSelectorView.ViewState = .placeholder
23+
2124
/// Subscription for search query changes for domain search.
2225
private var searchQuerySubscription: AnyCancellable?
2326

@@ -36,6 +39,8 @@ final class DomainSelectorViewModel: ObservableObject {
3639
// and thus the initial value isn't emitted in `observeDomainQuery` until setting the value afterward.
3740
observeDomainQuery()
3841
self.searchTerm = initialSearchTerm
42+
43+
configureState()
3944
}
4045
}
4146

@@ -64,6 +69,22 @@ private extension DomainSelectorViewModel {
6469
}
6570
}
6671

72+
func configureState() {
73+
Publishers.CombineLatest4($searchTerm, $isLoadingDomainSuggestions, $errorMessage, $domains)
74+
.map { searchTerm, isLoadingDomainSuggestions, errorMessage, domains in
75+
if searchTerm.isEmpty {
76+
return .placeholder
77+
} else if isLoadingDomainSuggestions {
78+
return .loading
79+
} else if let errorMessage {
80+
return .error(message: errorMessage)
81+
} else {
82+
return .results(domains: domains)
83+
}
84+
}
85+
.assign(to: &$state)
86+
}
87+
6788
@MainActor
6889
func loadFreeDomainSuggestions(query: String) async throws -> [FreeDomainSuggestion] {
6990
try await withCheckedThrowingContinuation { continuation in

WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/SearchHeader.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct SearchHeader: View {
99
let borderColor: UIColor
1010
let internalHorizontalPadding: CGFloat
1111
let internalVerticalPadding: CGFloat
12+
let iconSize: CGSize
1213
}
1314

1415
// Tracks the scale of the view due to accessibility changes
@@ -34,7 +35,8 @@ struct SearchHeader: View {
3435
backgroundColor: .searchBarBackground,
3536
borderColor: .clear,
3637
internalHorizontalPadding: Layout.internalPadding,
37-
internalVerticalPadding: Layout.internalPadding
38+
internalVerticalPadding: Layout.internalPadding,
39+
iconSize: Layout.iconSize
3840
)) {
3941
self._text = text
4042
self.placeholder = placeholder
@@ -47,14 +49,16 @@ struct SearchHeader: View {
4749
Image(uiImage: .searchBarButtonItemImage)
4850
.renderingMode(.template)
4951
.resizable()
50-
.frame(width: Layout.iconSize.width * scale, height: Layout.iconSize.height * scale)
52+
.frame(width: customizations.iconSize.width * scale,
53+
height: customizations.iconSize.height * scale)
5154
.foregroundColor(Color(.listSmallIcon))
5255
.padding([.leading, .trailing], customizations.internalHorizontalPadding)
5356
.accessibilityHidden(true)
5457

5558
// TextField
5659
TextField(placeholder, text: $text)
5760
.padding([.bottom, .top], customizations.internalVerticalPadding)
61+
.padding(.trailing, customizations.internalHorizontalPadding)
5862
.accessibility(addTraits: .isSearchField)
5963
}
6064
.background(Color(customizations.backgroundColor))
@@ -89,7 +93,7 @@ struct SearchHeaderView_Previews: PreviewProvider {
8993
customizations: .init(backgroundColor: .clear,
9094
borderColor: .separator,
9195
internalHorizontalPadding: 21,
92-
internalVerticalPadding: 12))
96+
internalVerticalPadding: 12, iconSize: .init(width: 14, height: 14)))
9397
}
9498
}
9599
}

WooCommerce/Resources/Images.xcassets/domain-search-placeholder.imageset/Contents.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
{
44
"filename" : "domain-search-placeholder.pdf",
55
"idiom" : "universal"
6+
},
7+
{
8+
"appearances" : [
9+
{
10+
"appearance" : "luminosity",
11+
"value" : "dark"
12+
}
13+
],
14+
"filename" : "domain-search-placeholder-dark.pdf",
15+
"idiom" : "universal"
616
}
717
],
818
"info" : {

0 commit comments

Comments
 (0)