diff --git a/.gitignore b/.gitignore index 9924e15ba219..0a0b9d4bf719 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ DerivedData *.hmap *.xcscmblueprint +# Claude +.claude/settings.local.json + # Windows Thumbs.db ehthumbs.db diff --git a/CLAUDE.md b/CLAUDE.md index e69c9ec831a3..a3b75932d407 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,7 @@ WordPress-iOS uses a modular architecture with the main app and separate Swift p - Follow Swift API Design Guidelines - Use strict access control modifiers where possible - Use four spaces (not tabs) +- Use semantics text sizes like `.headline` ## Development Workflow - Branch from `trunk` (main branch) diff --git a/Modules/Sources/WordPressUI/Views/CardView.swift b/Modules/Sources/WordPressUI/Views/CardView.swift new file mode 100644 index 000000000000..34e09bf6168b --- /dev/null +++ b/Modules/Sources/WordPressUI/Views/CardView.swift @@ -0,0 +1,56 @@ +import SwiftUI + +/// A reusable card view component that provides a consistent container style +/// with optional title and customizable content. +public struct CardView: View { + let title: String? + @ViewBuilder let content: () -> Content + + public init(_ title: String? = nil, @ViewBuilder content: @escaping () -> Content) { + self.title = title + self.content = content + } + + public var body: some View { + VStack(alignment: .leading, spacing: 16) { + Group { + if let title { + Text(title.uppercased()) + .font(.caption) + .foregroundStyle(.secondary) + } + content() + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding() + .clipShape(RoundedRectangle(cornerRadius: 8)) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color(.separator), lineWidth: 0.5) + ) + } +} + +#Preview("With Title") { + CardView("Section Title") { + VStack(alignment: .leading, spacing: 12) { + Text("Card Content") + Text("More content here") + .foregroundStyle(.secondary) + } + } + .padding() +} + +#Preview("Without Title") { + CardView { + HStack { + Image(systemName: "star.fill") + .foregroundStyle(.yellow) + Text("Featured Item") + Spacer() + } + } + .padding() +} diff --git a/Modules/Sources/WordPressUI/Views/InfoRow.swift b/Modules/Sources/WordPressUI/Views/InfoRow.swift new file mode 100644 index 000000000000..a8d389e4c366 --- /dev/null +++ b/Modules/Sources/WordPressUI/Views/InfoRow.swift @@ -0,0 +1,89 @@ +import SwiftUI +import DesignSystem + +/// A reusable info row component that displays a title and customizable content. +/// Commonly used within cards or forms to display labeled information. +public struct InfoRow: View { + let title: String + @ViewBuilder let content: () -> Content + + public init(_ title: String, @ViewBuilder content: @escaping () -> Content) { + self.title = title + self.content = content + } + + public var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.subheadline.weight(.medium)) + .lineLimit(1) + content() + .font(.subheadline.weight(.regular)) + .lineLimit(1) + .textSelection(.enabled) + } + } +} + +// MARK: - Convenience Initializer + +extension InfoRow where Content == Text { + /// Convenience initializer for displaying a simple text value. + /// If the value is nil, displays a dash placeholder. + public init(_ title: String, value: String?) { + self.init(title) { + Text(value ?? "–") + .foregroundColor(AppColor.secondary) + } + } +} + +// MARK: - Previews + +#Preview("Text Value") { + VStack(spacing: 16) { + InfoRow("Email", value: "user@example.com") + InfoRow("Country", value: "United States") + InfoRow("Phone", value: nil) + } + .padding() +} + +#Preview("Custom Content") { + VStack(spacing: 16) { + InfoRow("Status") { + HStack(spacing: 4) { + Circle() + .fill(.green) + .frame(width: 8, height: 8) + Text("Active") + .foregroundStyle(.green) + } + } + + InfoRow("Website") { + Link("example.com", destination: URL(string: "https://example.com")!) + } + + InfoRow("Tags") { + HStack(spacing: 4) { + Text("Swift") + Image(systemName: "chevron.forward") + .font(.caption2) + } + .foregroundStyle(.tint) + } + } + .padding() +} + +#Preview("In Card") { + CardView("User Details") { + VStack(spacing: 16) { + InfoRow("Name", value: "John Appleseed") + InfoRow("Email", value: "john@example.com") + InfoRow("Member Since", value: "January 2024") + } + } + .padding() +} diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 77b5c4ab1c71..5903c9b2c2d6 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,8 @@ * [**] Add new “Subscribers” screen that shows both your email and Reader subscribers [#24513] * [*] Fix an issue with “Stats / Subscribers” sometimes not showing the latest email subscribers [#24513] * [*] Fix an issue with "Stats" / "Subscribers" / "Emails" showing html encoded characters [#24513] +* [*] Add search to “Jetpack Activity List” and display actors and dates [#24597] +* [*] Fix an issue with content in "Restore" and "Download Backup" flows covering the navigation bar [#24597] 25.9 ----- diff --git a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsCoordinator.swift b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsCoordinator.swift new file mode 100644 index 000000000000..b3b0320118ef --- /dev/null +++ b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsCoordinator.swift @@ -0,0 +1,52 @@ +import UIKit +import SwiftUI +import WordPressKit + +/// Coordinator to handle navigation from SwiftUI ActivityLogDetailsView to UIKit view controllers +enum ActivityLogDetailsCoordinator { + + static func presentRestore(activity: Activity, blog: Blog) { + guard let viewController = UIViewController.topViewController, + let siteRef = JetpackSiteRef(blog: blog), + activity.isRewindable, + activity.rewindID != nil else { + return + } + + // Check if the store has the credentials status cached + let store = StoreContainer.shared.activity + let isAwaitingCredentials = store.isAwaitingCredentials(site: siteRef) + + let restoreViewController = JetpackRestoreOptionsViewController( + site: siteRef, + activity: activity, + isAwaitingCredentials: isAwaitingCredentials + ) + + restoreViewController.presentedFrom = "activity_detail" + + let navigationController = UINavigationController(rootViewController: restoreViewController) + navigationController.modalPresentationStyle = .formSheet + + viewController.present(navigationController, animated: true) + } + + static func presentBackup(activity: Activity, blog: Blog) { + guard let viewController = UIViewController.topViewController, + let siteRef = JetpackSiteRef(blog: blog) else { + return + } + + let backupViewController = JetpackBackupOptionsViewController( + site: siteRef, + activity: activity + ) + + backupViewController.presentedFrom = "activity_detail" + + let navigationController = UINavigationController(rootViewController: backupViewController) + navigationController.modalPresentationStyle = .formSheet + + viewController.present(navigationController, animated: true) + } +} diff --git a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift index 8f7f5ba5e68e..80bf3d6e1d88 100644 --- a/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift +++ b/WordPress/Classes/ViewRelated/Activity/Details/ActivityLogDetailsView.swift @@ -1,10 +1,12 @@ import SwiftUI import WordPressKit import WordPressUI +import WordPressShared import Gridicons struct ActivityLogDetailsView: View { let activity: Activity + let blog: Blog @Environment(\.dismiss) var dismiss @@ -13,13 +15,71 @@ struct ActivityLogDetailsView: View { VStack(spacing: 24) { ActivityHeaderView(activity: activity) if let actor = activity.actor { - ActorCard(actor: actor) + makeActorCard(for: actor) + } + if activity.isRewindable { + restoreSiteCard } } .padding() } .navigationTitle(Strings.eventTitle) .navigationBarTitleDisplayMode(.inline) + .onAppear { + trackDetailViewed() + } + } + + private func makeActorCard(for actor: ActivityActor) -> some View { + CardView(Strings.user) { + HStack(spacing: 12) { + // Actor avatar + ActivityActorAvatarView(actor: actor, diameter: 40) + + // Actor info + VStack(alignment: .leading, spacing: 2) { + Text(actor.displayName) + .font(.headline) + + Text(actor.role.isEmpty ? actor.type.localizedCapitalized : actor.role.localizedCapitalized) + .font(.subheadline) + .foregroundStyle(.secondary) + } + + Spacer() + } + } + } + + private var restoreSiteCard: some View { + CardView(Strings.restoreSite) { + // Checkpoint date info row + InfoRow(Strings.checkpointDate) { + Text(activity.published.formatted(date: .abbreviated, time: .standard)) + } + + // Action buttons + HStack(spacing: 12) { + Button(action: { + trackRestoreTapped() + ActivityLogDetailsCoordinator.presentRestore(activity: activity, blog: blog) + }) { + Label(Strings.restore, systemImage: "arrow.counterclockwise") + .fontWeight(.medium) + } + .buttonStyle(.borderedProminent) + + Button(action: { + trackBackupTapped() + ActivityLogDetailsCoordinator.presentBackup(activity: activity, blog: blog) + }) { + Label(Strings.download, systemImage: "arrow.down.circle") + .fontWeight(.medium) + } + .buttonStyle(.bordered) + .tint(.accentColor) + } + } } } @@ -89,56 +149,7 @@ private struct ActorCard: View { let actor: ActivityActor var body: some View { - ActivityCard(Strings.user) { - HStack(spacing: 12) { - // Actor avatar - ActivityActorAvatarView(actor: actor, diameter: 40) - - // Actor info - VStack(alignment: .leading, spacing: 2) { - Text(actor.displayName) - .font(.headline) - Text(actor.role.isEmpty ? actor.type.localizedCapitalized : actor.role.localizedCapitalized) - .font(.subheadline) - .foregroundStyle(.secondary) - } - - Spacer() - } - } - } -} - -// MARK: - Shared Components - -private struct ActivityCard: View { - let title: String? - @ViewBuilder let content: () -> Content - - init(_ title: String? = nil, @ViewBuilder content: @escaping () -> Content) { - self.title = title - self.content = content - } - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - if let title { - Text(title.uppercased()) - .font(.caption) - .foregroundStyle(.secondary) - } - - content() - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding() - .background(Color(.systemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(.separator), lineWidth: 0.5) - ) } } @@ -146,19 +157,28 @@ private struct ActivityCard: View { #Preview("Backup Activity") { NavigationView { - ActivityLogDetailsView(activity: ActivityLogDetailsView.Mocks.mockBackupActivity) + ActivityLogDetailsView( + activity: ActivityLogDetailsView.Mocks.mockBackupActivity, + blog: Blog.mock + ) } } #Preview("Plugin Update") { NavigationView { - ActivityLogDetailsView(activity: ActivityLogDetailsView.Mocks.mockPluginActivity) + ActivityLogDetailsView( + activity: ActivityLogDetailsView.Mocks.mockPluginActivity, + blog: Blog.mock + ) } } #Preview("Login Succeeded") { NavigationView { - ActivityLogDetailsView(activity: ActivityLogDetailsView.Mocks.mockLoginActivity) + ActivityLogDetailsView( + activity: ActivityLogDetailsView.Mocks.mockLoginActivity, + blog: Blog.mock + ) } } @@ -176,4 +196,62 @@ private enum Strings { value: "User", comment: "Section title for user information" ) + + static let restoreSite = NSLocalizedString( + "activityDetail.section.restoreSite", + value: "Restore Site", + comment: "Section title for restore site actions" + ) + + static let checkpointDate = NSLocalizedString( + "activityDetail.checkpointDate", + value: "Checkpoint Date", + comment: "Label for the backup checkpoint date" + ) + + static let restore = NSLocalizedString( + "activityDetail.restore.button", + value: "Restore", + comment: "Button title for restoring a backup" + ) + + static let download = NSLocalizedString( + "activityDetail.download.button", + value: "Download", + comment: "Button title for downloading a backup" + ) +} + +// MARK: - Analytics + +private extension ActivityLogDetailsView { + func trackDetailViewed() { + WPAnalytics.track(.activityLogDetailViewed, withProperties: ["source": presentedFrom()]) + } + + func trackRestoreTapped() { + WPAnalytics.track(.restoreOpened, properties: ["source": "activity_detail"]) + } + + func trackBackupTapped() { + WPAnalytics.track(.backupDownloadOpened, properties: ["source": "activity_detail"]) + } + + func presentedFrom() -> String { + // Since we're in SwiftUI, we'll default to "activity_log" + // In the future, this could be passed as a parameter + return "activity_log" + } +} + +// MARK: - Preview Helpers + +#if DEBUG +extension Blog { + static var mock: Blog { + // For previews, we'll return a dummy blog object + // In real previews, this should be provided by the parent view + return Blog() + } } +#endif diff --git a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift index 0c233a8a9c3c..d7f040c79404 100644 --- a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift +++ b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogRowViewModel.swift @@ -20,7 +20,7 @@ struct ActivityLogRowViewModel: Identifiable { self.activity = activity self.id = activity.activityID if let actor = activity.actor { - if actor.role.isEmpty { + if !actor.role.isEmpty { actorSubtitle = actor.role.localizedCapitalized } else if !actor.type.isEmpty { actorSubtitle = actor.type.localizedCapitalized diff --git a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsMenu.swift b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsMenu.swift index 7b7d26780941..b059c5816844 100644 --- a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsMenu.swift +++ b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsMenu.swift @@ -1,5 +1,6 @@ import SwiftUI import WordPressKit +import WordPressShared struct ActivityLogsMenu: View { @ObservedObject var viewModel: ActivityLogsViewModel @@ -29,14 +30,16 @@ struct ActivityLogsMenu: View { DatePickerSheet( title: Strings.startDate, selection: $viewModel.parameters.startDate, - isPresented: $isShowingStartDatePicker + isPresented: $isShowingStartDatePicker, + viewModel: viewModel ) } .sheet(isPresented: $isShowingEndDatePicker) { DatePickerSheet( title: Strings.endDate, selection: $viewModel.parameters.endDate, - isPresented: $isShowingEndDatePicker + isPresented: $isShowingEndDatePicker, + viewModel: viewModel ) } } @@ -45,6 +48,8 @@ struct ActivityLogsMenu: View { Group { // Start Date Button { + // Track analytics for date filter tap + WPAnalytics.track(.activitylogFilterbarRangeButtonTapped) isShowingStartDatePicker = true } label: { Text(Strings.startDate) @@ -56,6 +61,8 @@ struct ActivityLogsMenu: View { // End Date Button { + // Track analytics for date filter tap + WPAnalytics.track(.activitylogFilterbarRangeButtonTapped) isShowingEndDatePicker = true } label: { Text(Strings.endDate) @@ -69,6 +76,7 @@ struct ActivityLogsMenu: View { private var activityTypeFilter: some View { Button { + WPAnalytics.track(.activitylogFilterbarTypeButtonTapped) isShowingActivityTypePicker = true } label: { Text(Strings.activityTypes) @@ -81,6 +89,8 @@ struct ActivityLogsMenu: View { private var resetFiltersButton: some View { Button(role: .destructive) { + WPAnalytics.track(.activitylogFilterbarResetRange) + WPAnalytics.track(.activitylogFilterbarResetType) viewModel.parameters = GetActivityLogsParameters() } label: { Label(Strings.resetFilters, systemImage: "arrow.counterclockwise") @@ -92,6 +102,7 @@ private struct DatePickerSheet: View { let title: String @Binding var selection: Date? @Binding var isPresented: Bool + var viewModel: ActivityLogsViewModel? @State private var date = Date() diff --git a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsView.swift b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsView.swift index f424e4b9cff2..c4b7f4615531 100644 --- a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsView.swift +++ b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsView.swift @@ -23,7 +23,7 @@ private struct ActivityLogsListView: View { var body: some View { List { if let response = viewModel.response { - ActivityLogsPaginatedForEach(response: response) + ActivityLogsPaginatedForEach(response: response, blog: viewModel.blog) if viewModel.isFreePlan { Text(Strings.freePlanNotice) @@ -70,13 +70,14 @@ private struct ActivityLogsSearchView: View { searchText: viewModel.searchText, search: viewModel.search ) { response in - ActivityLogsPaginatedForEach(response: response) + ActivityLogsPaginatedForEach(response: response, blog: viewModel.blog) } } } private struct ActivityLogsPaginatedForEach: View { @ObservedObject var response: ActivityLogsPaginatedResponse + let blog: Blog struct ActivityGroup: Identifiable { var id: Date { date } @@ -115,7 +116,7 @@ private struct ActivityLogsPaginatedForEach: View { .onAppear { response.onRowAppeared(item) } .background { NavigationLink { - ActivityLogDetailsView(activity: item.activity) + ActivityLogDetailsView(activity: item.activity, blog: blog) } label: { EmptyView() }.opacity(0) diff --git a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsViewModel.swift b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsViewModel.swift index 06fd2acd9095..15058bd494fd 100644 --- a/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsViewModel.swift +++ b/WordPress/Classes/ViewRelated/Activity/List/ActivityLogsViewModel.swift @@ -1,6 +1,7 @@ import Foundation import WordPressKit import WordPressUI +import WordPressShared typealias ActivityLogsPaginatedResponse = DataViewPaginatedResponse @@ -11,6 +12,7 @@ final class ActivityLogsViewModel: ObservableObject { @Published var searchText = "" @Published var parameters = GetActivityLogsParameters() { didSet { + trackParameterChanges(oldValue: oldValue, newValue: parameters) response = nil onRefreshNeeded() } @@ -103,6 +105,26 @@ final class ActivityLogsViewModel: ObservableObject { ) } } + + // MARK: - Analytics + + private func trackParameterChanges(oldValue: GetActivityLogsParameters, newValue: GetActivityLogsParameters) { + // Track date range changes + if oldValue.startDate != newValue.startDate || oldValue.endDate != newValue.endDate { + if newValue.startDate != nil || newValue.endDate != nil { + WPAnalytics.track(.activitylogFilterbarSelectRange) + } + } + + // Track activity type changes + if oldValue.activityTypes != newValue.activityTypes { + if newValue.activityTypes.isEmpty { + WPAnalytics.track(.activitylogFilterbarResetType) + } else { + WPAnalytics.track(.activitylogFilterbarSelectType, properties: ["count": newValue.activityTypes.count]) + } + } + } } private func makeViewModels(for activities: [Activity]) async -> [ActivityLogRowViewModel] { diff --git a/WordPress/Classes/ViewRelated/Blog/Subscribers/Details/SubscriberDetailsView.swift b/WordPress/Classes/ViewRelated/Blog/Subscribers/Details/SubscriberDetailsView.swift index d9a5a41bb6fe..5a3e836e02c7 100644 --- a/WordPress/Classes/ViewRelated/Blog/Subscribers/Details/SubscriberDetailsView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Subscribers/Details/SubscriberDetailsView.swift @@ -51,7 +51,7 @@ struct SubscriberDetailsView: View { SubscriberDetailsHeaderView(subscriber: info) } if let detailsError { - SubscriberDetailsCardView { + CardView { EmptyStateView.failure(error: detailsError) { Task { await refresh() } } @@ -123,8 +123,8 @@ struct SubscriberDetailsView: View { // MARK: Views private func makeNewsletterSubscriptionSection(for details: SubscribersServiceRemote.GetSubscriberDetailsResponse) -> some View { - SubscriberDetailsCardView(Strings.sectionNewsletterSubscription) { - SubscriberInfoRow(Strings.fieldSubscriptionDate, value: viewModel.formattedDateSubscribed(details.dateSubscribed)) + CardView(Strings.sectionNewsletterSubscription) { + InfoRow(Strings.fieldSubscriptionDate, value: viewModel.formattedDateSubscribed(details.dateSubscribed)) let plans = details.plans ?? [] if let plan = plans.first { NavigationLink { @@ -137,7 +137,7 @@ struct SubscriberDetailsView: View { .navigationTitle(Strings.fieldPlan) .navigationBarTitleDisplayMode(.inline) } label: { - SubscriberInfoRow(Strings.fieldPlan) { + InfoRow(Strings.fieldPlan) { HStack(spacing: 4) { Text(plan.title) Image(systemName: "chevron.forward") @@ -148,13 +148,13 @@ struct SubscriberDetailsView: View { } .buttonStyle(.plain) } else { - SubscriberInfoRow(Strings.fieldPlan, value: Strings.free) + InfoRow(Strings.fieldPlan, value: Strings.free) } } } private func makePlanView(for plan: SubscribersServiceRemote.GetSubscriberDetailsResponse.Plan) -> some View { - SubscriberDetailsCardView { + CardView { HStack { Text(plan.title) .font(.headline) @@ -166,25 +166,25 @@ struct SubscriberDetailsView: View { .foregroundStyle(.secondary) } } - SubscriberInfoRow(Strings.fieldPlanStatus, value: plan.status) + InfoRow(Strings.fieldPlanStatus, value: plan.status) if plan.renewInterval != "one-time" { - SubscriberInfoRow(Strings.fieldRenewalInterval, value: plan.renewInterval) - SubscriberInfoRow(Strings.fieldRenewalPrice, value: { + InfoRow(Strings.fieldRenewalInterval, value: plan.renewInterval) + InfoRow(Strings.fieldRenewalPrice, value: { let formatter = NumberFormatter() formatter.numberStyle = .currency formatter.currencyCode = plan.currency return formatter.string(from: plan.renewalPrice as NSNumber) }()) } - SubscriberInfoRow(Strings.fieldPlanStartDate, value: plan.startDate.formatted(date: .abbreviated, time: .shortened)) - SubscriberInfoRow(Strings.fieldPlanEndDate, value: plan.endDate.formatted(date: .abbreviated, time: .shortened)) + InfoRow(Strings.fieldPlanStartDate, value: plan.startDate.formatted(date: .abbreviated, time: .shortened)) + InfoRow(Strings.fieldPlanEndDate, value: plan.endDate.formatted(date: .abbreviated, time: .shortened)) } } @ViewBuilder private func makeSubscriberDetailsSections(for details: SubscribersServiceRemote.GetSubscriberDetailsResponse) -> some View { - SubscriberDetailsCardView(Strings.sectionSubscriberDetails) { - SubscriberInfoRow(Strings.fieldEmail) { + CardView(Strings.sectionSubscriberDetails) { + InfoRow(Strings.fieldEmail) { if let email = details.emailAddress, let url = URL(string: "mailto://\(email)") { Link(email, destination: url) } else { @@ -192,9 +192,9 @@ struct SubscriberDetailsView: View { .foregroundStyle(.secondary) } } - SubscriberInfoRow(Strings.fieldCountry, value: details.country?.name) + InfoRow(Strings.fieldCountry, value: details.country?.name) if let site = details.siteURL { - SubscriberInfoRow(Strings.fieldSite) { + InfoRow(Strings.fieldSite) { if let siteURL = URL(string: site) { Link(site, destination: siteURL) } else { @@ -235,7 +235,7 @@ private struct SubscriberStatsView: View { let stats: SubscribersServiceRemote.GetSubscriberStatsResponse var body: some View { - SubscriberDetailsCardView { + CardView { HStack { SubsciberStatsRow( systemImage: "envelope", @@ -263,37 +263,6 @@ private struct SubscriberStatsView: View { } } -private struct SubscriberInfoRow: View { - let title: String - @ViewBuilder let content: () -> Content - - init(_ title: String, @ViewBuilder content: @escaping () -> Content) { - self.title = title - self.content = content - } - - var body: some View { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.subheadline.weight(.medium)) - .lineLimit(1) - content() - .font(.subheadline.weight(.regular)) - .lineLimit(1) - .textSelection(.enabled) - } - } -} - -extension SubscriberInfoRow where Content == Text { - init(_ title: String, value: String?) { - self.init(title) { - Text(value ?? "–") - .foregroundColor(AppColor.secondary) - } - } -} - private struct SubsciberStatsRow: View { let systemImage: String let title: String @@ -314,36 +283,6 @@ private struct SubsciberStatsRow: View { } } -private struct SubscriberDetailsCardView: View { - let title: String? - @ViewBuilder let content: () -> Content - - init(_ title: String? = nil, @ViewBuilder content: @escaping () -> Content) { - self.title = title - self.content = content - } - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Group { - if let title { - Text(title.uppercased()) - .font(.caption) - .foregroundStyle(.secondary) - } - content() - } - .frame(maxWidth: .infinity, alignment: .leading) - } - .padding() - .clipShape(RoundedRectangle(cornerRadius: 8)) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color(.separator), lineWidth: 0.5) - ) - } -} - private extension SubscribersServiceRemote.GetSubscriberStatsResponse { var formattedEmailsCount: String { emailsSent.formatted(.number.notation(.compactName)) diff --git a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Complete/BaseRestoreCompleteViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Complete/BaseRestoreCompleteViewController.swift index 40f7488b9482..cab7f11339c8 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Complete/BaseRestoreCompleteViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Complete/BaseRestoreCompleteViewController.swift @@ -1,5 +1,6 @@ import Foundation import WordPressShared +import WordPressUI struct JetpackRestoreCompleteConfiguration { let title: String @@ -103,7 +104,7 @@ class BaseRestoreCompleteViewController: UIViewController { } view.addSubview(completeView) - view.pinSubviewToAllEdges(completeView) + completeView.pinEdges(to: view.safeAreaLayoutGuide) } @objc private func doneTapped() { diff --git a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Status/BaseRestoreStatusViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Status/BaseRestoreStatusViewController.swift index 7e41f7ea14f3..f03e97184f04 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Status/BaseRestoreStatusViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Status/BaseRestoreStatusViewController.swift @@ -1,4 +1,5 @@ import Foundation +import WordPressUI import WordPressShared struct JetpackRestoreStatusConfiguration { @@ -92,7 +93,7 @@ class BaseRestoreStatusViewController: UIViewController { statusView.update(progress: 0, progressTitle: configuration.placeholderProgressTitle, progressDescription: nil) view.addSubview(statusView) - view.pinSubviewToAllEdges(statusView) + statusView.pinEdges(to: view.safeAreaLayoutGuide) } @objc private func doneTapped() { diff --git a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Warning/JetpackRestoreWarningViewController.swift b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Warning/JetpackRestoreWarningViewController.swift index ad9eeeea5523..e08e32727e18 100644 --- a/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Warning/JetpackRestoreWarningViewController.swift +++ b/WordPress/Classes/ViewRelated/Jetpack/Jetpack Restore/Restore Warning/JetpackRestoreWarningViewController.swift @@ -1,6 +1,7 @@ import UIKit import WordPressFlux import WordPressShared +import WordPressUI class JetpackRestoreWarningViewController: UIViewController { @@ -80,7 +81,7 @@ class JetpackRestoreWarningViewController: UIViewController { warningView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(warningView) - view.pinSubviewToAllEdges(warningView) + warningView.pinEdges(to: view.safeAreaLayoutGuide) } }