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 10 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import Foundation
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

@Published
private(set) var sections: [Section]

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

func refresh() {
blogService.refreshDomains(for: blog, success: { [weak self] in
guard let self else { return }
self.sections = Self.buildSections(from: blog)
}, failure: nil)
}

// MARK: - Sections

private static func buildSections(from blog: Blog) -> [Section] {
return Self.buildFreeDomainSections(from: blog) + Self.buildDomainsSections(from: blog)
}

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 buildDomainsSections(from blog: Blog) -> [Section] {
var sections: [Section] = []

let primaryDomain = blog.domainsList.first(where: { $0.domain.isPrimaryDomain })
let otherDomains = blog.domainsList.filter { !$0.domain.isPrimaryDomain }

if let primaryDomain {
let section = Section(
title: Strings.domainsListSectionTitle,
footer: Strings.primaryDomainDescription,
content: .rows([.init(
name: primaryDomain.domain.domainName,
description: nil,
status: nil,
expiryDate: DomainExpiryDateFormatter.expiryDate(for: primaryDomain.domain),
isPrimary: primaryDomain.domain.isPrimaryDomain
)])
)
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
)
}

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

sections.append(section)
}

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

return sections
}
}

private 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: "Your Site Domains",
comment: "Header of the domains list section in the Domains Dashboard.")
}
}
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.")
}
}
140 changes: 46 additions & 94 deletions WordPress/Classes/ViewRelated/Domains/Views/SiteDomainsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ struct SiteDomainsView: View {

@ObservedObject var blog: Blog
@State var isShowingDomainSelectionWithType: DomainSelectionType?
@State var blogService = BlogService(coreDataStack: ContextManager.shared)
@State var domainsList: [Blog.DomainRepresentation] = []
@StateObject var viewModel: SiteDomainsViewModel

// Property observer
private func showingDomainSelectionWithType(to value: DomainSelectionType?) {
Expand All @@ -22,100 +21,83 @@ struct SiteDomainsView: View {
case .none:
break
default:
// TODO: Analytics
break
}
}

var body: some View {
List {
if blog.supports(.domains) {
makeSiteAddressSection(blog: blog)
}
makeDomainsSection(blog: blog)
.listRowInsets(Metrics.insets)
makeDomainsSections(blog: blog)
}
.listStyle(InsetGroupedListStyle())
.buttonStyle(PlainButtonStyle())
.listRowSeparator(.hidden)
//.listRowSpacing(Length.Padding.double) Re-enable when we update to Xcode 15
.onTapGesture(perform: { })
.onAppear {
updateDomainsList()

blogService.refreshDomains(for: blog, success: {
updateDomainsList()
}, failure: nil)
viewModel.refresh()
}
.sheet(item: $isShowingDomainSelectionWithType, content: { domainSelectionType in
makeDomainSearch(for: blog, domainSelectionType: domainSelectionType, onDismiss: {
isShowingDomainSelectionWithType = nil
blogService.refreshDomains(for: blog, success: {
updateDomainsList()
}, failure: nil)
viewModel.refresh()
})
.ignoresSafeArea()
})
}

@ViewBuilder
private func makeDomainsSection(blog: Blog) -> some View {
if blog.hasDomains {
makeDomainsListSection(blog: blog)
} else {
makeGetFirstDomainSection(blog: blog)
}
}
// MARK: - Domains Section

/// Builds the site address section for the given blog
private func makeSiteAddressSection(blog: Blog) -> some View {
Section(footer: Text(TextContent.primarySiteSectionFooter(blog.hasPaidPlan))) {
VStack(alignment: .leading) {
Text(TextContent.siteAddressTitle)
Text(blog.freeSiteAddress)
.bold()
if blog.freeDomainIsPrimary {
ShapeWithTextView(title: TextContent.primaryAddressLabel)
.smallRoundedRectangle()
}
/// Builds the domains list section with the` add a domain` button at the bottom, for the given blog
@ViewBuilder
private func makeDomainsSections(blog: Blog) -> some View {
ForEach(viewModel.sections, id: \.id) { section in
switch section.content {
case .rows(let rows):
makeDomainsListSection(blog: blog, section: section, rows: rows)
case .addDomain:
makeAddDomainSection(blog: blog)
case .upgradePlan:
makeGetFirstDomainSection(blog: blog)
}
}
}

@ViewBuilder
private func makeDomainCell(domain: Blog.DomainRepresentation) -> some View {
VStack(alignment: .leading) {
Text(domain.domain.domainName)
if domain.domain.isPrimaryDomain {
ShapeWithTextView(title: TextContent.primaryAddressLabel)
.smallRoundedRectangle()
private func makeDomainsListSection(blog: Blog, section: SiteDomainsViewModel.Section, rows: [AllDomainsListCardView.ViewModel]) -> some View {
Section {
ForEach(rows) { domainViewModel in
AllDomainsListCardView(viewModel: domainViewModel, padding: 0)
}
} header: {
if let title = section.title {
Text(title)
}
} footer: {
if let footer = section.footer {
Text(footer)
}
makeExpiryRenewalLabel(domain: domain)
}
}

/// Builds the domains list section with the` add a domain` button at the bottom, for the given blog
private func makeDomainsListSection(blog: Blog) -> some View {
private func makeAddDomainSection(blog: Blog) -> some View {
let destination: DomainSelectionType = blog.canRegisterDomainWithPaidPlan ? .registerWithPaidPlan : .purchaseSeparately
return Section(header: Text(TextContent.domainsListSectionHeader)) {
ForEach(domainsList) {
makeDomainCell(domain: $0)
}
if blog.supports(.domains) {
DSButton(
title: TextContent.additionalDomainTitle(blog.canRegisterDomainWithPaidPlan),
style: .init(
emphasis: .tertiary,
size: .small,
isJetpack: AppConfiguration.isJetpack
)) {
$isShowingDomainSelectionWithType.onChange(showingDomainSelectionWithType).wrappedValue = destination
}
}

return Section {
DSButton(
title: TextContent.additionalDomainTitle(blog.canRegisterDomainWithPaidPlan),
style: .init(
emphasis: .tertiary,
size: .small,
isJetpack: AppConfiguration.isJetpack
)) {
$isShowingDomainSelectionWithType.onChange(showingDomainSelectionWithType).wrappedValue = destination
}
}
}

// MARK: - First Domain Section

/// Builds the Get New Domain section when no othert domains are present for the given blog
private func makeGetFirstDomainSection(blog: Blog) -> some View {
return Section {
Section {
SiteDomainsPresentationCard(
title: TextContent.firstDomainTitle(blog.canRegisterDomainWithPaidPlan),
description: TextContent.firstDomainDescription(blog.canRegisterDomainWithPaidPlan),
Expand Down Expand Up @@ -151,18 +133,6 @@ struct SiteDomainsView: View {
return destinations
}

private var siteAddressForGetFirstDomainSection: String {
blog.canRegisterDomainWithPaidPlan ? "" : blog.freeSiteAddress
}

private func makeExpiryRenewalLabel(domain: Blog.DomainRepresentation) -> some View {
let stringForDomain = DomainExpiryDateFormatter.expiryDate(for: domain.domain)

return Text(stringForDomain)
.font(.subheadline)
.foregroundColor(domain.domain.expirySoon || domain.domain.expired ? Color(UIColor.error) : Color(UIColor.textSubtle))
}

/// Instantiates the proper search depending if it's for claiming a free domain with a paid plan or purchasing a new one
private func makeDomainSearch(for blog: Blog, domainSelectionType: DomainSelectionType, onDismiss: @escaping () -> Void) -> some View {
return DomainSuggestionViewControllerWrapper(
Expand All @@ -171,10 +141,6 @@ struct SiteDomainsView: View {
onDismiss: onDismiss
)
}

private func updateDomainsList() {
domainsList = blog.domainsList
}
}

// MARK: - Constants
Expand All @@ -184,24 +150,10 @@ private extension SiteDomainsView {
enum TextContent {
// Navigation bar
static let navigationTitle = NSLocalizedString("Site Domains", comment: "Title of the Domains Dashboard.")
// Site address section
static func primarySiteSectionFooter(_ paidPlan: Bool) -> String {
paidPlan ? "" : NSLocalizedString("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 siteAddressTitle = NSLocalizedString("Your free WordPress.com address is",
comment: "Title of the site address section in the Domains Dashboard.")
static let primaryAddressLabel = NSLocalizedString("Primary site address",
comment: "Primary site address label, used in the site address section of the Domains Dashboard.")

// Domains section
static let domainsListSectionHeader: String = NSLocalizedString("Your Site Domains",
comment: "Header of the domains list section in the Domains Dashboard.")

static let additionalRedirectedDomainTitle: String = NSLocalizedString("Add a domain",
comment: "Label of the button that starts the purchase of an additional redirected domain in the Domains Dashboard.")

static let firstFreeDomainWithPaidPlanDomainTitle: String = NSLocalizedString("site.domains.freeDomainWithPaidPlan.title",
value: "Get your domain",
comment: "Title of the card that starts the purchase of the first domain with a paid plan.")
Expand Down Expand Up @@ -253,7 +205,7 @@ final class SiteDomainsViewController: UIHostingController<SiteDomainsView> {
// MARK: - Init

init(blog: Blog) {
super.init(rootView: .init(blog: blog))
super.init(rootView: .init(blog: blog, viewModel: .init(blog: blog, blogService: BlogService(coreDataStack: ContextManager.shared))))
}

@MainActor required dynamic init?(coder aDecoder: NSCoder) {
Expand Down
Loading