Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site Domains: Modernize UI 1/2 #22294

Merged
merged 29 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4148601
Create SiteDomainsViewModel to build sections and rows for modern UI
staskus Dec 26, 2023
46174a0
Add domain refresh to SiteDomainsViewModel
staskus Dec 26, 2023
ba86378
Create PrimaryDomainView
staskus Dec 26, 2023
b10d656
Update AllDomainsListCardView to be reusable in SiteDomains context
staskus Dec 26, 2023
09b125f
Update SiteDomainsView to use viewModel and use same layout as AllDom…
staskus Dec 26, 2023
afc8661
Use default SwiftUI padding for cards
staskus Dec 27, 2023
5ce7bf8
Configure accessibility label for PrimaryDomainView badge
staskus Dec 27, 2023
4cfd859
Put domains in the same section to control spacing
staskus Dec 28, 2023
a800621
Update SiteDomainsView.swift
staskus Dec 28, 2023
a6749e1
Merge branch 'trunk' into task/22262-site-domains-modernize-ui
staskus Dec 28, 2023
7ab79b7
Load domains using all-domains endpoint
staskus Dec 28, 2023
8c85382
Create DomainsStateView for empty and error state
staskus Dec 29, 2023
4b472c4
Reuse error handling between AllDomainsListViewModel and SiteDomainsV…
staskus Dec 29, 2023
cbdc85c
Integrate DomainsStateView into SiteDomainsView
staskus Dec 29, 2023
d3c7b6d
Add ProgressView() to SiteDomainsView
staskus Dec 29, 2023
cc6c04a
Pass loaded domains from SiteDomains to AllDomains
staskus Dec 29, 2023
dd3bbe3
Open DomainDetails after selecting purchased domain
staskus Dec 29, 2023
14d7025
Added SiteDomainsViewModelTests
staskus Dec 29, 2023
8f4bee4
Update RELEASE-NOTES.txt
staskus Dec 29, 2023
aa0ee09
Set title to Domain Management view
staskus Jan 3, 2024
945627f
Use site name for other domains section title
staskus Jan 3, 2024
7480127
Show new domain card section also when blog has domain credit
staskus Jan 4, 2024
ea49c5e
Make add domain button touch area full row
staskus Jan 4, 2024
a4d79ec
Load free domains from allDomains endpoint
staskus Jan 4, 2024
766eb28
Remove unused MessagateStateViewModel
staskus Jan 5, 2024
44218ce
Update SiteDomainsViewModel.swift
staskus Jan 5, 2024
4b10eba
Use DomainsStateView within AllDomainsView
staskus Jan 5, 2024
d5b97ca
Site Domains: Modernize UI 2/2 - Integrate status and navigation to d…
staskus Jan 5, 2024
da9b6bc
Merge branch 'trunk' into task/22262-site-domains-modernize-ui
staskus Jan 5, 2024
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
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [*] Fix crash in editor that sometimes happens after modifying tags or categories [#22265]
* [**] Updated login screen's colors to highlight WordPress - Jetpack brand relationship
* [*] Add defensive code to make sure the retain cycles in the editor don't lead to crashes [#22252]
* [*] [Jetpack-only] Updated Site Domains screen to make domains management more convenient [#22294, #22311]
* [**] [internal] [Jetpack-only] Adds support for dynamic dashboard cards driven by the backend [#22326]
* [*] [internal] Remove personalizeHomeTab feature flag [#22280]
* [*] Fix a rare crash in post search related to tags [#22275]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import Foundation
import WordPressKit

extension DomainsService {
protocol DomainsServiceAllDomainsFetching {
func fetchAllDomains(resolveStatus: Bool, noWPCOM: Bool, completion: @escaping (DomainsServiceRemote.AllDomainsEndpointResult) -> Void)
}

extension DomainsService: DomainsServiceAllDomainsFetching {

/// Makes a GET request to `/v1.1/all-domains` endpoint and returns a list of domain objects.
///
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Foundation

struct DomainsStateViewModel {
let title: String
let description: String
let button: Button?

struct Button {
let title: String
let action: () -> Void
}
}

extension DomainsStateViewModel {
static func errorMessageViewModel(from error: Error, action: @escaping () -> Void) -> DomainsStateViewModel {
let title: String
let description: String
let button: DomainsStateViewModel.Button = .init(title: Strings.errorStateButtonTitle) {
action()
}

let nsError = error as NSError
if nsError.domain == NSURLErrorDomain, nsError.code == NSURLErrorNotConnectedToInternet {
title = Strings.offlineEmptyStateTitle
description = Strings.offlineEmptyStateDescription
} else {
title = Strings.errorEmptyStateTitle
description = Strings.errorEmptyStateDescription
}

return .init(title: title, description: description, button: button)
}
}

extension DomainsStateViewModel {
enum Strings {
static let offlineEmptyStateTitle = NSLocalizedString(
"domain.management.offline.empty.state.title",
value: "No Internet Connection",
comment: "The empty state title in All Domains screen when the user is offline"
)
static let offlineEmptyStateDescription = NSLocalizedString(
"domain.management.offline.empty.state.description",
value: "Please check your network connection and try again.",
comment: "The empty state description in All Domains screen when the user is offline"
)
static let errorEmptyStateTitle = NSLocalizedString(
"domain.management.error.empty.state.title",
value: "Something went wrong",
comment: "The empty state title in All Domains screen when an error occurs"
)

static let errorEmptyStateDescription = NSLocalizedString(
"domain.management.error.empty.state.description",
value: "We encountered an error while loading your domains. Please contact support if the issue persists.",
comment: "The empty state description in All Domains screen when an error occurs"
)
static let errorStateButtonTitle = NSLocalizedString(
"domain.management.error.state.button.title",
value: "Try again",
comment: "The empty state button title in All Domains screen when an error occurs"
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import Foundation
import WordPressKit
import Combine

final class SiteDomainsViewModel: ObservableObject {
private let blog: Blog
private let domainsService: DomainsServiceAllDomainsFetching?

@Published
private(set) var state: State = .loading
private(set) var loadedDomains: [DomainsService.AllDomainsListItem] = []

init(blog: Blog, domainsService: DomainsServiceAllDomainsFetching?) {
self.blog = blog
self.domainsService = domainsService
}

func refresh() {
domainsService?.fetchAllDomains(resolveStatus: true, noWPCOM: false, completion: { [weak self] result in
guard let self else {
return
}
switch result {
case .success(let domains):
self.loadedDomains = domains
let sections = Self.buildSections(from: blog, domains: domains)
self.state = .normal(sections)
case .failure(let error):
self.state = .message(self.errorMessageViewModel(from: error))
}
})
}

private func errorMessageViewModel(from error: Error) -> DomainsStateViewModel {
return DomainsStateViewModel.errorMessageViewModel(from: error) { [weak self] in
self?.state = .loading
self?.refresh()
}
}

// MARK: - Sections

private static func buildSections(from blog: Blog, domains: [DomainsService.AllDomainsListItem]) -> [Section] {
let wpcomDomains = domains.filter { $0.wpcomDomain }
let otherDomains = domains.filter { !$0.wpcomDomain }

return Self.buildFreeDomainSections(from: blog, wpComDomains: wpcomDomains) + Self.buildDomainsSections(from: blog, domains: otherDomains)
}

private static func buildFreeDomainSections(from blog: Blog, wpComDomains: [DomainsService.AllDomainsListItem]) -> [Section] {
let blogWpComDomains = wpComDomains.filter { $0.blogId == blog.dotComID?.intValue }
guard let freeDomain = blogWpComDomains.count > 1 ? blogWpComDomains.first(where: { $0.isWpcomStagingDomain }) : blogWpComDomains.first else {
return []
}

return [
Section(
title: Strings.freeDomainSectionTitle,
footer: blog.freeDomainIsPrimary ? Strings.primaryDomainDescription : nil,
content: .rows([.init(
viewModel: .init(
name: freeDomain.domain,
description: nil,
status: nil,
expiryDate: AllDomainsListItemViewModel.expiryDate(from: freeDomain),
isPrimary: blog.freeDomainIsPrimary
),
navigation: nil)])
)
]
}

private static func buildDomainsSections(from blog: Blog, domains: [DomainsService.AllDomainsListItem]) -> [Section] {
var sections: [Section] = []

let primaryDomainName = blog.domainsList.first(where: { $0.domain.isPrimaryDomain })?.domain.domainName
let blogDomains = domains.filter({ $0.blogId == blog.dotComID?.intValue })
let primaryDomain = blogDomains.first(where: { primaryDomainName == $0.domain })
let otherDomains = blogDomains.filter({ primaryDomainName != $0.domain })

if let primaryDomain {
let section = Section(
title: String(format: Strings.domainsListSectionTitle, blog.title ?? blog.freeSiteAddress),
footer: Strings.primaryDomainDescription,
content: .rows([.init(
viewModel: .init(
name: primaryDomain.domain,
description: nil,
status: primaryDomain.status,
expiryDate: AllDomainsListItemViewModel.expiryDate(from: primaryDomain),
isPrimary: true
),
navigation: navigation(from: primaryDomain)
)])
)
sections.append(section)
}

if otherDomains.count > 0 {
let domainRows = otherDomains.map {
SiteDomainsViewModel.Section.Row(
viewModel: .init(
name: $0.domain,
description: nil,
status: $0.status,
expiryDate: AllDomainsListItemViewModel.expiryDate(from: $0),
isPrimary: false
),
navigation: navigation(from: $0)
)
}

let section = Section(
title: primaryDomain == nil ? String(format: Strings.domainsListSectionTitle, blog.title ?? blog.freeSiteAddress) : nil,
footer: nil,
content: .rows(domainRows)
)

sections.append(section)
}

if sections.count == 0 || blog.canRegisterDomainWithPaidPlan {
sections.append(Section(title: nil, footer: nil, content: .upgradePlan))
} else {
sections.append(Section(title: nil, footer: nil, content: .addDomain))
}

return sections
}

private static func navigation(from domain: DomainsService.AllDomainsListItem) -> SiteDomainsViewModel.Section.Row.Navigation {
return .init(domain: domain.domain, siteSlug: domain.siteSlug, type: domain.type)
}
}

extension SiteDomainsViewModel {
enum Strings {
static let freeDomainSectionTitle = NSLocalizedString("site.domains.freeDomainSection.title",
value: "Your Free WordPress.com domain",
comment: "A section title which displays a row with a free WP.com domain")
static let primaryDomainDescription = NSLocalizedString("site.domains.primaryDomain",
value: "Your primary site address is what visitors will see in their address bar when visiting your website.",
comment: "Footer of the primary site section in the Domains Dashboard.")
static let domainsListSectionTitle: String = NSLocalizedString("site.domains.domainSection.title",
value: "Other domains for %1$@",
comment: "Header of the secondary domains list section in the Domains Dashboard. %1$@ is the name of the site.")
}
}

// MARK: - Types

extension SiteDomainsViewModel {
enum State {
case normal([Section])
case loading
case message(DomainsStateViewModel)
}

struct Section: Identifiable {
enum SectionKind {
case rows([Row])
case addDomain
case upgradePlan
}

struct Row: Identifiable {
struct Navigation: Hashable {
let domain: String
let siteSlug: String
let type: DomainType
let analyticsSource: String = "site_domains"
}

let id = UUID()
let viewModel: AllDomainsListCardView.ViewModel
let navigation: Navigation?
}

let id = UUID()
let title: String?
let footer: String?
let content: SectionKind
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import SwiftUI
import UIKit
import WordPressKit

/// Makes DomainDetailsWebViewController available to SwiftUI
struct DomainDetailsWebViewControllerWrapper: UIViewControllerRepresentable {
private let domain: String
private let siteSlug: String
private let type: DomainType
private let analyticsSource: String?

init(domain: String, siteSlug: String, type: DomainType, analyticsSource: String? = nil) {
self.domain = domain
self.siteSlug = siteSlug
self.type = type
self.analyticsSource = analyticsSource
}

func makeUIViewController(context: Context) -> DomainDetailsWebViewController {
DomainDetailsWebViewController(
domain: domain,
siteSlug: siteSlug,
type: type,
analyticsSource: analyticsSource
)
}

func updateUIViewController(_ uiViewController: DomainDetailsWebViewController, context: Context) { }
}
31 changes: 31 additions & 0 deletions WordPress/Classes/ViewRelated/Domains/Views/DomainsStateView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation
import SwiftUI
import DesignSystem

struct DomainsStateView: View {
private let viewModel: DomainsStateViewModel

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

var body: some View {
VStack(spacing: Length.Padding.single) {
Text(viewModel.title)
.font(Font.DS.heading3)
.multilineTextAlignment(.center)
.foregroundStyle(Color.DS.Foreground.secondary)
Text(viewModel.description)
.font(Font.DS.Body.medium)
.foregroundStyle(Color.DS.Foreground.secondary)
.multilineTextAlignment(.center)
if let button = viewModel.button {
Spacer()
.frame(height: Length.Padding.single)
DSButton(title: button.title, style: .init(emphasis: .primary, size: .medium)) {
button.action()
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import SwiftUI
import DesignSystem

struct PrimaryDomainView: View {
var body: some View {
Group {
HStack(spacing: Length.Padding.half) {
Image(systemName: "globe")
.font(.callout)
.foregroundStyle(Color.DS.Foreground.primary)
Text(Strings.primaryDomain)
.font(.callout)
.foregroundStyle(Color.DS.Foreground.primary)
}
.padding(.vertical, Length.Padding.half)
.padding(.horizontal, Length.Padding.single)
}
.background(Color.DS.Background.secondary)
.clipShape(RoundedRectangle(cornerRadius: Length.Radius.small))
.accessibilityElement(children: .ignore)
.accessibilityLabel(Strings.primaryDomain)
}
}

private extension PrimaryDomainView {
enum Strings {
static let primaryDomain = NSLocalizedString("site.domains.primaryDomain.title",
value: "Primary domain",
comment: "Primary domain label, used in the site address section of the Domains Dashboard.")
}
}
Loading