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 2/2 - Integrate status and navigation to domain details #22311

Merged
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* [**] Re-enable the support for using Security Keys as a second factor during login [#22258]
* [*] Fix crash in editor that sometimes happens after modifying tags or categories [#22265]
* [*] 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]

23.9
-----
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
@@ -1,112 +1,156 @@
import Foundation
import WordPressKit
import Combine

final class SiteDomainsViewModel: ObservableObject {
struct Section: Identifiable {
enum SectionKind {
case rows([AllDomainsListCardView.ViewModel])
case addDomain
case upgradePlan
}

let id = UUID()
let title: String?
let footer: String?
let content: SectionKind
}

private let blogService: BlogService
private let blog: Blog
private let domainsService: DomainsServiceAllDomainsFetching?

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

init(blog: Blog, blogService: BlogService) {
self.sections = Self.buildSections(from: blog)
init(blog: Blog, domainsService: DomainsServiceAllDomainsFetching?) {
self.blog = blog
self.blogService = blogService
self.domainsService = domainsService
}

func refresh() {
blogService.refreshDomains(for: blog, success: { [weak self] in
guard let self else { return }
self.sections = Self.buildSections(from: blog)
}, failure: nil)
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) -> [Section] {
return Self.buildFreeDomainSections(from: blog) + Self.buildDomainsSections(from: blog)
private static func buildSections(from blog: Blog, domains: [DomainsService.AllDomainsListItem]) -> [Section] {
var wpcomDomains: [DomainsService.AllDomainsListItem] = []
var otherDomains: [DomainsService.AllDomainsListItem] = []

for domain in domains {
if domain.wpcomDomain {
wpcomDomains.append(domain)
} else {
otherDomains.append(domain)
}
}
staskus marked this conversation as resolved.
Show resolved Hide resolved

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

private static func buildFreeDomainSections(from blog: Blog) -> [Section] {
guard let freeDomain = blog.freeDomain else { return [] }
return [Section(
title: Strings.freeDomainSectionTitle,
footer: blog.freeDomainIsPrimary ? Strings.primaryDomainDescription : nil,
content: .rows([.init(
name: blog.freeSiteAddress,
description: nil,
status: nil,
expiryDate: DomainExpiryDateFormatter.expiryDate(for: freeDomain),
isPrimary: freeDomain.isPrimaryDomain
)])
)]
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) -> [Section] {
private static func buildDomainsSections(from blog: Blog, domains: [DomainsService.AllDomainsListItem]) -> [Section] {
var sections: [Section] = []

let primaryDomain = blog.domainsList.first(where: { $0.domain.isPrimaryDomain })
let otherDomains = blog.domainsList.filter { !$0.domain.isPrimaryDomain }
let primaryDomainName = blog.domainsList.first(where: { $0.domain.isPrimaryDomain })?.domain.domainName
var primaryDomain: DomainsService.AllDomainsListItem?
var otherDomains: [DomainsService.AllDomainsListItem] = []

for domain in domains {
if domain.blogId == blog.dotComID?.intValue {
if primaryDomainName == domain.domain {
primaryDomain = domain
} else {
otherDomains.append(domain)
}
}
}
staskus marked this conversation as resolved.
Show resolved Hide resolved

if let primaryDomain {
let section = Section(
title: Strings.domainsListSectionTitle,
title: String(format: Strings.domainsListSectionTitle, blog.title ?? blog.freeSiteAddress),
footer: Strings.primaryDomainDescription,
content: .rows([.init(
name: primaryDomain.domain.domainName,
description: nil,
status: nil,
expiryDate: DomainExpiryDateFormatter.expiryDate(for: primaryDomain.domain),
isPrimary: primaryDomain.domain.isPrimaryDomain
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 {
AllDomainsListCardView.ViewModel(
name: $0.domain.domainName,
description: nil,
status: nil,
expiryDate: DomainExpiryDateFormatter.expiryDate(for: $0.domain),
isPrimary: false
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 ? Strings.domainsListSectionTitle : nil,
title: primaryDomain == nil ? String(format: Strings.domainsListSectionTitle, blog.title ?? blog.freeSiteAddress) : nil,
footer: nil,
content: .rows(domainRows)
)

sections.append(section)
}

if sections.count == 0 {
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)
}
}

private extension SiteDomainsViewModel {
extension SiteDomainsViewModel {
enum Strings {
static let freeDomainSectionTitle = NSLocalizedString("site.domains.freeDomainSection.title",
value: "Your Free WordPress.com domain",
Expand All @@ -115,7 +159,54 @@ private extension SiteDomainsViewModel {
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: "Your Site Domains",
comment: "Header of the domains list section in the Domains Dashboard.")
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
}

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

struct Button {
let title: String
let action: () -> Void
}
staskus marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading