From f61d263aa19211f6e88ad58874a69fc5f9f469fd Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 21 Nov 2022 13:08:22 +0800 Subject: [PATCH 01/11] Add dark mode version for domain search placeholder image asset. --- .../Contents.json | 10 ++++++++++ .../domain-search-placeholder-dark.pdf | Bin 0 -> 17709 bytes 2 files changed, 10 insertions(+) create mode 100644 WooCommerce/Resources/Images.xcassets/domain-search-placeholder.imageset/domain-search-placeholder-dark.pdf diff --git a/WooCommerce/Resources/Images.xcassets/domain-search-placeholder.imageset/Contents.json b/WooCommerce/Resources/Images.xcassets/domain-search-placeholder.imageset/Contents.json index f7152ebcc14..64e502b3b04 100644 --- a/WooCommerce/Resources/Images.xcassets/domain-search-placeholder.imageset/Contents.json +++ b/WooCommerce/Resources/Images.xcassets/domain-search-placeholder.imageset/Contents.json @@ -3,6 +3,16 @@ { "filename" : "domain-search-placeholder.pdf", "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "domain-search-placeholder-dark.pdf", + "idiom" : "universal" } ], "info" : { diff --git a/WooCommerce/Resources/Images.xcassets/domain-search-placeholder.imageset/domain-search-placeholder-dark.pdf b/WooCommerce/Resources/Images.xcassets/domain-search-placeholder.imageset/domain-search-placeholder-dark.pdf new file mode 100644 index 0000000000000000000000000000000000000000..33cc40d1e746b27495251a2581b579b28d73e226 GIT binary patch literal 17709 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 literal 0 HcmV?d00001 From 1c2746d2d493e543de9d30ab5868712b5c9bfd0c Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 21 Nov 2022 13:15:48 +0800 Subject: [PATCH 02/11] Remove skip button in domain selector navigation bar. --- .../StoreCreationCoordinator.swift | 2 -- .../Settings/Domains/DomainSelectorView.swift | 18 +----------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift b/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift index 31adce44a2e..e72ec8ed37e 100644 --- a/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift +++ b/WooCommerce/Classes/Authentication/Store Creation/StoreCreationCoordinator.swift @@ -199,8 +199,6 @@ private extension StoreCreationCoordinator { await self.createStoreAndContinueToStoreSummary(from: navigationController, name: storeName, domain: domain) - }, onSkip: { - // TODO-8045: skip to the next step of store creation with an auto-generated domain. }) navigationController.pushViewController(domainSelector, animated: false) } diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index 438b2cf8e43..7379501f651 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -4,18 +4,14 @@ import SwiftUI final class DomainSelectorHostingController: UIHostingController { private let viewModel: DomainSelectorViewModel private let onDomainSelection: (String) async -> Void - private let onSkip: () -> Void /// - Parameters: /// - viewModel: View model for the domain selector. /// - onDomainSelection: Called when the user continues with a selected domain name. - /// - onSkip: Called when the user taps to skip domain selection. init(viewModel: DomainSelectorViewModel, - onDomainSelection: @escaping (String) async -> Void, - onSkip: @escaping () -> Void) { + onDomainSelection: @escaping (String) async -> Void) { self.viewModel = viewModel self.onDomainSelection = onDomainSelection - self.onSkip = onSkip super.init(rootView: DomainSelectorView(viewModel: viewModel)) rootView.onDomainSelection = { [weak self] domain in @@ -30,16 +26,11 @@ final class DomainSelectorHostingController: UIHostingController Date: Mon, 21 Nov 2022 13:23:29 +0800 Subject: [PATCH 03/11] Resign domain search text field when tapping the Continue CTA. --- .../Dashboard/Settings/Domains/DomainSelectorView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index 7379501f651..f37042ef903 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -66,6 +66,8 @@ struct DomainSelectorView: View { @State private var isWaitingForDomainSelectionCompletion: Bool = false + @FocusState private var textFieldIsFocused: Bool + init(viewModel: DomainSelectorViewModel) { self.viewModel = viewModel } @@ -87,6 +89,7 @@ struct DomainSelectorView: View { borderColor: .separator, internalHorizontalPadding: 21, internalVerticalPadding: 12)) + .focused($textFieldIsFocused) // Results header. Text(Localization.suggestionsHeader) @@ -117,6 +120,7 @@ struct DomainSelectorView: View { LazyVStack { ForEach(viewModel.domains, id: \.self) { domain in Button { + textFieldIsFocused = false selectedDomainName = domain } label: { VStack(alignment: .leading) { From 7ccc9d7e97276ced7c0f117b8cd9817b859d62fd Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 21 Nov 2022 13:25:03 +0800 Subject: [PATCH 04/11] Update suggestions header font to footnote. --- .../Dashboard/Settings/Domains/DomainSelectorView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index f37042ef903..71fe0d07670 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -94,7 +94,7 @@ struct DomainSelectorView: View { // Results header. Text(Localization.suggestionsHeader) .foregroundColor(Color(.secondaryLabel)) - .bodyStyle() + .footnoteStyle() .padding(.horizontal, Layout.defaultHorizontalPadding) if viewModel.searchTerm.isEmpty { From 469690187229a525ac87caa66db190b8e085e0bd Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 21 Nov 2022 13:36:16 +0800 Subject: [PATCH 05/11] Domain selector: move bottom Continue container to scroll view's `safeAreaInset`. --- .../Settings/Domains/DomainSelectorView.swift | 140 +++++++++--------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index 71fe0d07670..9200a51f6d1 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -73,85 +73,87 @@ struct DomainSelectorView: View { } var body: some View { - VStack(alignment: .leading, spacing: 0) { - ScrollView { - VStack(alignment: .leading) { - // Header label. - Text(Localization.subtitle) - .foregroundColor(Color(.secondaryLabel)) - .bodyStyle() - .padding(.horizontal, Layout.defaultHorizontalPadding) - - // Search text field. - SearchHeader(text: $viewModel.searchTerm, - placeholder: Localization.searchPlaceholder, - customizations: .init(backgroundColor: .clear, - borderColor: .separator, - internalHorizontalPadding: 21, - internalVerticalPadding: 12)) - .focused($textFieldIsFocused) - - // Results header. - Text(Localization.suggestionsHeader) - .foregroundColor(Color(.secondaryLabel)) - .footnoteStyle() - .padding(.horizontal, Layout.defaultHorizontalPadding) - - if viewModel.searchTerm.isEmpty { - // Placeholder image when search query is empty. - HStack { - Spacer() - Image(uiImage: .domainSearchPlaceholderImage) - Spacer() - } - } else if viewModel.isLoadingDomainSuggestions { - // Progress indicator when loading domain suggestions. - HStack { - Spacer() - ProgressView() - Spacer() - } - } else if let errorMessage = viewModel.errorMessage { - // Error message when there is an error loading domain suggestions. - Text(errorMessage) - .padding(Layout.defaultPadding) - } else { - // Domain suggestions. - LazyVStack { - ForEach(viewModel.domains, id: \.self) { domain in - Button { - textFieldIsFocused = false - selectedDomainName = domain - } label: { - VStack(alignment: .leading) { - DomainRowView(viewModel: .init(domainName: domain, - searchQuery: viewModel.searchTerm, - isSelected: domain == selectedDomainName)) - Divider() - .frame(height: Layout.dividerHeight) - .padding(.leading, Layout.defaultHorizontalPadding) - } + ScrollView { + VStack(alignment: .leading) { + // Header label. + Text(Localization.subtitle) + .foregroundColor(Color(.secondaryLabel)) + .bodyStyle() + .padding(.horizontal, Layout.defaultHorizontalPadding) + + // Search text field. + SearchHeader(text: $viewModel.searchTerm, + placeholder: Localization.searchPlaceholder, + customizations: .init(backgroundColor: .clear, + borderColor: .separator, + internalHorizontalPadding: 21, + internalVerticalPadding: 12)) + .focused($textFieldIsFocused) + + // Results header. + Text(Localization.suggestionsHeader) + .foregroundColor(Color(.secondaryLabel)) + .footnoteStyle() + .padding(.horizontal, Layout.defaultHorizontalPadding) + + if viewModel.searchTerm.isEmpty { + // Placeholder image when search query is empty. + HStack { + Spacer() + Image(uiImage: .domainSearchPlaceholderImage) + Spacer() + } + } else if viewModel.isLoadingDomainSuggestions { + // Progress indicator when loading domain suggestions. + HStack { + Spacer() + ProgressView() + Spacer() + } + } else if let errorMessage = viewModel.errorMessage { + // Error message when there is an error loading domain suggestions. + Text(errorMessage) + .padding(Layout.defaultPadding) + } else { + // Domain suggestions. + LazyVStack { + ForEach(viewModel.domains, id: \.self) { domain in + Button { + textFieldIsFocused = false + selectedDomainName = domain + } label: { + VStack(alignment: .leading) { + DomainRowView(viewModel: .init(domainName: domain, + searchQuery: viewModel.searchTerm, + isSelected: domain == selectedDomainName)) + Divider() + .frame(height: Layout.dividerHeight) + .padding(.leading, Layout.defaultHorizontalPadding) } } } } } } - + } + .safeAreaInset(edge: .bottom) { // Continue button when a domain is selected. if let selectedDomainName { - Divider() - .frame(height: Layout.dividerHeight) - .foregroundColor(Color(.separator)) - Button(Localization.continueButtonTitle) { - Task { @MainActor in - isWaitingForDomainSelectionCompletion = true - await onDomainSelection(selectedDomainName) - isWaitingForDomainSelectionCompletion = false + VStack { + Divider() + .frame(height: Layout.dividerHeight) + .foregroundColor(Color(.separator)) + Button(Localization.continueButtonTitle) { + Task { @MainActor in + isWaitingForDomainSelectionCompletion = true + await onDomainSelection(selectedDomainName) + isWaitingForDomainSelectionCompletion = false + } } + .buttonStyle(PrimaryLoadingButtonStyle(isLoading: isWaitingForDomainSelectionCompletion)) + .padding(Layout.defaultPadding) } - .buttonStyle(PrimaryLoadingButtonStyle(isLoading: isWaitingForDomainSelectionCompletion)) - .padding(Layout.defaultPadding) + .background(Color(.systemBackground)) } } .onChange(of: viewModel.isLoadingDomainSuggestions) { isLoadingDomainSuggestions in From fd89b15620d37c8792da3cafce5b41dc39255143 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 21 Nov 2022 14:13:45 +0800 Subject: [PATCH 06/11] Update spacing to match the latest design. --- .../Settings/Domains/DomainSelectorView.swift | 31 +++++++++++++------ .../SwiftUI Components/SearchHeader.swift | 8 +++-- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index 9200a51f6d1..8fb095806bb 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -74,12 +74,16 @@ struct DomainSelectorView: View { var body: some View { ScrollView { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { // Header label. Text(Localization.subtitle) .foregroundColor(Color(.secondaryLabel)) .bodyStyle() .padding(.horizontal, Layout.defaultHorizontalPadding) + .padding(.top, 16) + + Spacer() + .frame(height: 30) // Search text field. SearchHeader(text: $viewModel.searchTerm, @@ -87,17 +91,15 @@ struct DomainSelectorView: View { customizations: .init(backgroundColor: .clear, borderColor: .separator, internalHorizontalPadding: 21, - internalVerticalPadding: 12)) + internalVerticalPadding: 12, + iconSize: .init(width: 14, height: 14))) .focused($textFieldIsFocused) - // Results header. - Text(Localization.suggestionsHeader) - .foregroundColor(Color(.secondaryLabel)) - .footnoteStyle() - .padding(.horizontal, Layout.defaultHorizontalPadding) - if viewModel.searchTerm.isEmpty { // Placeholder image when search query is empty. + Spacer() + .frame(height: 30) + HStack { Spacer() Image(uiImage: .domainSearchPlaceholderImage) @@ -112,10 +114,22 @@ struct DomainSelectorView: View { } } else if let errorMessage = viewModel.errorMessage { // Error message when there is an error loading domain suggestions. + Spacer() + .frame(height: 23) + Text(errorMessage) + .foregroundColor(Color(.secondaryLabel)) + .bodyStyle() + .multilineTextAlignment(.center) .padding(Layout.defaultPadding) } else { // Domain suggestions. + Text(Localization.suggestionsHeader) + .foregroundColor(Color(.secondaryLabel)) + .footnoteStyle() + .padding(.horizontal, Layout.defaultHorizontalPadding) + .padding(.vertical, insets: .init(top: 14, leading: 0, bottom: 8, trailing: 0)) + LazyVStack { ForEach(viewModel.domains, id: \.self) { domain in Button { @@ -167,7 +181,6 @@ struct DomainSelectorView: View { private extension DomainSelectorView { enum Layout { - static let spacingBetweenTitleAndSubtitle: CGFloat = 16 static let defaultHorizontalPadding: CGFloat = 16 static let dividerHeight: CGFloat = 1 static let defaultPadding: EdgeInsets = .init(top: 10, leading: 16, bottom: 10, trailing: 16) diff --git a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/SearchHeader.swift b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/SearchHeader.swift index 1567fba39f7..7bb8bd41450 100644 --- a/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/SearchHeader.swift +++ b/WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/SearchHeader.swift @@ -9,6 +9,7 @@ struct SearchHeader: View { let borderColor: UIColor let internalHorizontalPadding: CGFloat let internalVerticalPadding: CGFloat + let iconSize: CGSize } // Tracks the scale of the view due to accessibility changes @@ -34,7 +35,8 @@ struct SearchHeader: View { backgroundColor: .searchBarBackground, borderColor: .clear, internalHorizontalPadding: Layout.internalPadding, - internalVerticalPadding: Layout.internalPadding + internalVerticalPadding: Layout.internalPadding, + iconSize: Layout.iconSize )) { self._text = text self.placeholder = placeholder @@ -47,7 +49,8 @@ struct SearchHeader: View { Image(uiImage: .searchBarButtonItemImage) .renderingMode(.template) .resizable() - .frame(width: Layout.iconSize.width * scale, height: Layout.iconSize.height * scale) + .frame(width: customizations.iconSize.width * scale, + height: customizations.iconSize.height * scale) .foregroundColor(Color(.listSmallIcon)) .padding([.leading, .trailing], customizations.internalHorizontalPadding) .accessibilityHidden(true) @@ -55,6 +58,7 @@ struct SearchHeader: View { // TextField TextField(placeholder, text: $text) .padding([.bottom, .top], customizations.internalVerticalPadding) + .padding(.trailing, customizations.internalHorizontalPadding) .accessibility(addTraits: .isSearchField) } .background(Color(customizations.backgroundColor)) From 9899adb73beae7d8dbb4e10dfe14636af77705e0 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 21 Nov 2022 14:14:17 +0800 Subject: [PATCH 07/11] Update domain search subtitle. --- .../Dashboard/Settings/Domains/DomainSelectorView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index 8fb095806bb..ee1c64a5044 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -188,7 +188,7 @@ private extension DomainSelectorView { enum Localization { static let subtitle = NSLocalizedString( - "This is where people will find you on the Internet. Don't worry, you can change it later.", + "This is where people will find you on the Internet. You can add another domain later.", comment: "Subtitle of the domain selector.") static let searchPlaceholder = NSLocalizedString("Type to get suggestions", comment: "Placeholder of the search text field on the domain selector.") static let suggestionsHeader = NSLocalizedString("SUGGESTIONS", comment: "Header label of the domain suggestions on the domain selector.") From 7ad9c54b5b49efa3203de77ed0cc5f04a6318ad2 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 21 Nov 2022 14:20:03 +0800 Subject: [PATCH 08/11] Center align domain selector error text when it is single line. --- .../Dashboard/Settings/Domains/DomainSelectorView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index ee1c64a5044..4249e4935a0 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -120,6 +120,7 @@ struct DomainSelectorView: View { Text(errorMessage) .foregroundColor(Color(.secondaryLabel)) .bodyStyle() + .frame(maxWidth: .infinity, alignment: .center) .multilineTextAlignment(.center) .padding(Layout.defaultPadding) } else { From c1807681e5e3e57912aff69444f4b55b2d997ed9 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Mon, 21 Nov 2022 14:23:21 +0800 Subject: [PATCH 09/11] Add spacing above the domain selector loading spinner. --- .../Dashboard/Settings/Domains/DomainSelectorView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index 4249e4935a0..ea8981fc95e 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -107,6 +107,9 @@ struct DomainSelectorView: View { } } else if viewModel.isLoadingDomainSuggestions { // Progress indicator when loading domain suggestions. + Spacer() + .frame(height: 23) + HStack { Spacer() ProgressView() From a04aab2248caf5dd93c962e1148d81edaeb8a690 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Tue, 22 Nov 2022 17:05:48 +0800 Subject: [PATCH 10/11] Update domain search placeholder. --- .../Dashboard/Settings/Domains/DomainSelectorView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index 254c4033704..1202289c615 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -189,7 +189,7 @@ private extension DomainSelectorView { static let subtitle = NSLocalizedString( "This is where people will find you on the Internet. You can add another domain later.", comment: "Subtitle of the domain selector.") - static let searchPlaceholder = NSLocalizedString("Type to get suggestions", comment: "Placeholder of the search text field on the domain selector.") + static let searchPlaceholder = NSLocalizedString("Type a name for your store", comment: "Placeholder of the search text field on the domain selector.") static let suggestionsHeader = NSLocalizedString("SUGGESTIONS", comment: "Header label of the domain suggestions on the domain selector.") static let continueButtonTitle = NSLocalizedString("Continue", comment: "Title of the button to continue with a selected domain.") } From 1b685f46f537ccffbc5842deb96c9ab1cf8c8acb Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Tue, 22 Nov 2022 21:44:56 +0800 Subject: [PATCH 11/11] Update domain selector view conditional logic with a state enum in the view model. --- .../Settings/Domains/DomainSelectorView.swift | 23 +++-- .../Domains/DomainSelectorViewModel.swift | 25 +++++- .../DomainSelectorViewModelTests.swift | 83 +++++++++---------- 3 files changed, 78 insertions(+), 53 deletions(-) diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift index 1202289c615..b5fcdba291c 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorView.swift @@ -45,6 +45,18 @@ private extension DomainSelectorHostingController { /// Allows the user to search for a domain and then select one to continue. struct DomainSelectorView: View { + /// The state of the main view below the fixed header. + enum ViewState: Equatable { + /// When loading domain suggestions. + case loading + /// Shown when the search query is empty. + case placeholder + /// When there is an error loading domain suggestions. + case error(message: String) + /// When domain suggestions are displayed. + case results(domains: [String]) + } + /// Set in the hosting controller. var onDomainSelection: ((String) async -> Void) = { _ in } @@ -87,7 +99,8 @@ struct DomainSelectorView: View { iconSize: .init(width: 14, height: 14))) .focused($textFieldIsFocused) - if viewModel.searchTerm.isEmpty { + switch viewModel.state { + case .placeholder: // Placeholder image when search query is empty. Spacer() .frame(height: 30) @@ -97,7 +110,7 @@ struct DomainSelectorView: View { Image(uiImage: .domainSearchPlaceholderImage) Spacer() } - } else if viewModel.isLoadingDomainSuggestions { + case .loading: // Progress indicator when loading domain suggestions. Spacer() .frame(height: 23) @@ -107,7 +120,7 @@ struct DomainSelectorView: View { ProgressView() Spacer() } - } else if let errorMessage = viewModel.errorMessage { + case .error(let errorMessage): // Error message when there is an error loading domain suggestions. Spacer() .frame(height: 23) @@ -118,7 +131,7 @@ struct DomainSelectorView: View { .frame(maxWidth: .infinity, alignment: .center) .multilineTextAlignment(.center) .padding(Layout.defaultPadding) - } else { + case .results(let domains): // Domain suggestions. Text(Localization.suggestionsHeader) .foregroundColor(Color(.secondaryLabel)) @@ -127,7 +140,7 @@ struct DomainSelectorView: View { .padding(.vertical, insets: .init(top: 14, leading: 0, bottom: 8, trailing: 0)) LazyVStack { - ForEach(viewModel.domains, id: \.self) { domain in + ForEach(domains, id: \.self) { domain in Button { textFieldIsFocused = false selectedDomainName = domain diff --git a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift index 5ebc0a47004..2325d43ca8d 100644 --- a/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift +++ b/WooCommerce/Classes/ViewRelated/Dashboard/Settings/Domains/DomainSelectorViewModel.swift @@ -10,14 +10,17 @@ final class DomainSelectorViewModel: ObservableObject { @Published var searchTerm: String = "" /// Domain names after domain suggestions are loaded remotely. - @Published private(set) var domains: [String] = [] + @Published private var domains: [String] = [] /// Error message from loading domain suggestions. - @Published private(set) var errorMessage: String? + @Published private var errorMessage: String? /// Whether domain suggestions are being loaded. @Published private(set) var isLoadingDomainSuggestions: Bool = false + /// The state of the main domain selector view based on the search query and loading state. + @Published private(set) var state: DomainSelectorView.ViewState = .placeholder + /// Subscription for search query changes for domain search. private var searchQuerySubscription: AnyCancellable? @@ -36,6 +39,8 @@ final class DomainSelectorViewModel: ObservableObject { // and thus the initial value isn't emitted in `observeDomainQuery` until setting the value afterward. observeDomainQuery() self.searchTerm = initialSearchTerm + + configureState() } } @@ -64,6 +69,22 @@ private extension DomainSelectorViewModel { } } + func configureState() { + Publishers.CombineLatest4($searchTerm, $isLoadingDomainSuggestions, $errorMessage, $domains) + .map { searchTerm, isLoadingDomainSuggestions, errorMessage, domains in + if searchTerm.isEmpty { + return .placeholder + } else if isLoadingDomainSuggestions { + return .loading + } else if let errorMessage { + return .error(message: errorMessage) + } else { + return .results(domains: domains) + } + } + .assign(to: &$state) + } + @MainActor func loadFreeDomainSuggestions(query: String) async -> Result<[FreeDomainSuggestion], Error> { await withCheckedContinuation { continuation in diff --git a/WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift b/WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift index 55db00afbb4..e9d208c6b2b 100644 --- a/WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/ViewModels/Domains/DomainSelectorViewModelTests.swift @@ -24,49 +24,61 @@ final class DomainSelectorViewModelTests: XCTestCase { func test_DomainAction_is_not_dispatched_when_searchTerm_is_empty() { // Given stores.whenReceivingAction(ofType: DomainAction.self) { action in + // Then XCTFail("Unexpected action: \(action)") } // When viewModel.searchTerm = "" viewModel.searchTerm = "" - - // Then - XCTAssertEqual(viewModel.domains, []) - XCTAssertTrue(stores.receivedActions.isEmpty) } - func test_domain_suggestions_success_returns_domain_rows_for_free_domains() { - // Given - mockDomainSuggestionsSuccess(suggestions: [ - .init(name: "free.com", isFree: true), - .init(name: "paid.com", isFree: false) - ]) + // MARK: - `isLoadingDomainSuggestions` + + func test_isLoadingDomainSuggestions_is_toggled_when_loading_suggestions() { + var loadingValues: [Bool] = [] + viewModel.$isLoadingDomainSuggestions.sink { value in + loadingValues.append(value) + }.store(in: &subscriptions) + + mockDomainSuggestionsFailure(error: SampleError.first) // When - viewModel.searchTerm = "woo" + viewModel.searchTerm = "Woo" // Then waitUntil { - self.viewModel.domains.isNotEmpty + loadingValues == [false, true, false] } - XCTAssertEqual(viewModel.domains, ["free.com"]) } - func test_domain_suggestions_failure_does_not_update_domain_rows() { + // MARK: - `state` + + func test_state_is_placeholder_when_searchTerm_is_empty() { + // When + viewModel.searchTerm = "" + + // Then + XCTAssertEqual(viewModel.state, .placeholder) + } + + func test_state_is_results_with_free_domain_only_on_domain_suggestions_success() { // Given - mockDomainSuggestionsFailure(error: SampleError.first) + mockDomainSuggestionsSuccess(suggestions: [ + .init(name: "free.com", isFree: true), + .init(name: "paid.com", isFree: false) + ]) // When viewModel.searchTerm = "woo" // Then - XCTAssertEqual(viewModel.domains, []) + waitUntil { + self.viewModel.state == .results(domains: ["free.com"]) + } } - // MARK: - `errorMessage` - - func test_domain_suggestions_failure_with_non_DotcomError_sets_default_error_message() { + func test_state_is_errorMessage_with_default_error_message_when_failure_is_not_DotcomError() { // Given mockDomainSuggestionsFailure(error: SampleError.first) @@ -75,12 +87,11 @@ final class DomainSelectorViewModelTests: XCTestCase { // Then waitUntil { - self.viewModel.errorMessage?.isNotEmpty == true + self.viewModel.state == .error(message: DomainSelectorViewModel.Localization.defaultErrorMessage) } - XCTAssertEqual(viewModel.errorMessage, DomainSelectorViewModel.Localization.defaultErrorMessage) } - func test_domain_suggestions_failure_with_DotcomError_unknown_error_sets_error_message() { + func test_state_is_errorMessage_with_DotcomError_message_when_failure_is_DotcomError() { // Given mockDomainSuggestionsFailure(error: DotcomError.unknown(code: "", message: "error message")) @@ -89,19 +100,18 @@ final class DomainSelectorViewModelTests: XCTestCase { // Then waitUntil { - self.viewModel.errorMessage?.isNotEmpty == true + self.viewModel.state == .error(message: "error message") } - XCTAssertEqual(viewModel.errorMessage, "error message") } - func test_domain_suggestions_error_message_is_reset_when_loading_domain_suggestions() { + func test_state_is_updated_from_errorMessage_to_results_when_changing_search_term_after_failure() { // Given mockDomainSuggestionsFailure(error: SampleError.first) // When viewModel.searchTerm = "woo" waitUntil { - self.viewModel.errorMessage?.isNotEmpty == true + self.viewModel.state == .error(message: DomainSelectorViewModel.Localization.defaultErrorMessage) } mockDomainSuggestionsSuccess(suggestions: []) @@ -109,26 +119,7 @@ final class DomainSelectorViewModelTests: XCTestCase { // Then waitUntil { - self.viewModel.errorMessage == nil - } - } - - // MARK: `isLoadingDomainSuggestions` - - func test_isLoadingDomainSuggestions_is_toggled_when_loading_suggestions() { - var loadingValues: [Bool] = [] - viewModel.$isLoadingDomainSuggestions.sink { value in - loadingValues.append(value) - }.store(in: &subscriptions) - - mockDomainSuggestionsFailure(error: SampleError.first) - - // When - viewModel.searchTerm = "Woo" - - // Then - waitUntil { - loadingValues == [false, true, false] + self.viewModel.state == .results(domains: []) } } }