Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,6 @@ private extension StoreCreationCoordinator {
name: storeName,
domain: domain,
planToPurchase: planToPurchase)
}, onSkip: {
// TODO-8045: skip to the next step of store creation with an auto-generated domain.
})
navigationController.pushViewController(domainSelector, animated: false)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@ import SwiftUI
final class DomainSelectorHostingController: UIHostingController<DomainSelectorView> {
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
Expand All @@ -30,16 +26,11 @@ final class DomainSelectorHostingController: UIHostingController<DomainSelectorV
override func viewDidLoad() {
super.viewDidLoad()

configureSkipButton()
configureNavigationBarAppearance()
}
}

private extension DomainSelectorHostingController {
func configureSkipButton() {
navigationItem.rightBarButtonItem = .init(title: Localization.skipButtonTitle, style: .plain, target: self, action: #selector(skipButtonTapped))
}

/// Shows a transparent navigation bar without a bottom border.
func configureNavigationBarAppearance() {
let appearance = UINavigationBarAppearance()
Expand All @@ -52,20 +43,20 @@ private extension DomainSelectorHostingController {
}
}

private extension DomainSelectorHostingController {
@objc func skipButtonTapped() {
onSkip()
}
}

private extension DomainSelectorHostingController {
enum Localization {
static let skipButtonTitle = NSLocalizedString("Skip", comment: "Navigation bar button on the domain selector screen to skip domain selection.")
}
}

/// 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 }

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

@State private var isWaitingForDomainSelectionCompletion: Bool = false

@FocusState private var textFieldIsFocused: Bool

init(viewModel: DomainSelectorViewModel) {
self.viewModel = viewModel
}

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)
ScrollView {
VStack(alignment: .leading, spacing: 0) {
// Header label.
Text(Localization.subtitle)
.foregroundColor(Color(.secondaryLabel))
.bodyStyle()
.padding(.horizontal, Layout.defaultHorizontalPadding)
.padding(.top, 16)

// Search text field.
SearchHeader(text: $viewModel.searchTerm,
placeholder: Localization.searchPlaceholder,
customizations: .init(backgroundColor: .clear,
borderColor: .separator,
internalHorizontalPadding: 21,
internalVerticalPadding: 12))
Spacer()
.frame(height: 30)

// Results header.
Text(Localization.suggestionsHeader)
// Search text field.
SearchHeader(text: $viewModel.searchTerm,
placeholder: Localization.searchPlaceholder,
customizations: .init(backgroundColor: .clear,
borderColor: .separator,
internalHorizontalPadding: 21,
internalVerticalPadding: 12,
iconSize: .init(width: 14, height: 14)))
.focused($textFieldIsFocused)

switch viewModel.state {
case .placeholder:
// Placeholder image when search query is empty.
Spacer()
.frame(height: 30)

HStack {
Spacer()
Image(uiImage: .domainSearchPlaceholderImage)
Spacer()
}
case .loading:
// Progress indicator when loading domain suggestions.
Spacer()
.frame(height: 23)

HStack {
Spacer()
ProgressView()
Spacer()
}
case .error(let errorMessage):
// Error message when there is an error loading domain suggestions.
Spacer()
.frame(height: 23)

Text(errorMessage)
.foregroundColor(Color(.secondaryLabel))
.bodyStyle()
.frame(maxWidth: .infinity, alignment: .center)
.multilineTextAlignment(.center)
.padding(Layout.defaultPadding)
case .results(let domains):
// 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))

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 {
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)
}
LazyVStack {
ForEach(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))
}
}
.navigationTitle(Localization.title)
Expand All @@ -176,7 +192,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)
Expand All @@ -185,9 +200,9 @@ private extension DomainSelectorView {
enum Localization {
static let title = NSLocalizedString("Choose a domain", comment: "Title of the domain selector.")
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 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.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -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()
}
}

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -47,14 +49,16 @@ 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)

// TextField
TextField(placeholder, text: $text)
.padding([.bottom, .top], customizations.internalVerticalPadding)
.padding(.trailing, customizations.internalHorizontalPadding)
.accessibility(addTraits: .isSearchField)
}
.background(Color(customizations.backgroundColor))
Expand Down Expand Up @@ -89,7 +93,7 @@ struct SearchHeaderView_Previews: PreviewProvider {
customizations: .init(backgroundColor: .clear,
borderColor: .separator,
internalHorizontalPadding: 21,
internalVerticalPadding: 12))
internalVerticalPadding: 12, iconSize: .init(width: 14, height: 14)))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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" : {
Expand Down
Binary file not shown.
Loading