From edc5f054c932327b7939064f958c9b2e5adb6b67 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:38:10 +1200 Subject: [PATCH 01/13] Remove `BloggingPromptsServiceRemote.fetchPrompts` --- .../BloggingPromptsServiceRemote.swift | 49 ------------ .../BloggingPromptsServiceRemoteTests.swift | 78 ------------------- 2 files changed, 127 deletions(-) diff --git a/Modules/Sources/WordPressKit/BloggingPromptsServiceRemote.swift b/Modules/Sources/WordPressKit/BloggingPromptsServiceRemote.swift index 8968a26301f5..96136291fe46 100644 --- a/Modules/Sources/WordPressKit/BloggingPromptsServiceRemote.swift +++ b/Modules/Sources/WordPressKit/BloggingPromptsServiceRemote.swift @@ -17,55 +17,6 @@ open class BloggingPromptsServiceRemote: ServiceRemoteWordPressComREST { case encodingFailure } - /// Fetches a number of blogging prompts for the specified site. - /// Note that this method hits wpcom/v2, which means the `WordPressComRestAPI` needs to be initialized with `LocaleKeyV2`. - /// - /// - Parameters: - /// - siteID: Used to check which prompts have been answered for the site with given `siteID`. - /// - number: The number of prompts to query. When not specified, this will default to remote implementation. - /// - fromDate: When specified, this will fetch prompts from the given date. When not specified, this will default to remote implementation. - /// - completion: A closure that will be called when the fetch request completes. - open func fetchPrompts(for siteID: NSNumber, - number: Int? = nil, - fromDate: Date? = nil, - completion: @escaping (Result<[RemoteBloggingPrompt], Error>) -> Void) { - let path = path(forEndpoint: "sites/\(siteID)/blogging-prompts", withVersion: ._2_0) - let requestParameter: [String: AnyHashable] = { - var params = [String: AnyHashable]() - - if let number, number > 0 { - params["number"] = number - } - - if let fromDate { - // convert to yyyy-MM-dd format, excluding the timezone information. - // the date parameter doesn't need to be timezone-accurate since prompts are grouped by date. - params["from"] = Self.dateFormatter.string(from: fromDate) - } - - return params - }() - - let decoder = JSONDecoder.apiDecoder - // our API decoder assumes that we're converting from snake case. - // revert it to default so the CodingKeys match the actual response keys. - decoder.keyDecodingStrategy = .useDefaultKeys - - Task { @MainActor in - await self.wordPressComRestApi - .perform( - .get, - URLString: path, - parameters: requestParameter as [String: AnyObject], - jsonDecoder: decoder, - type: [String: [RemoteBloggingPrompt]].self - ) - .map { $0.body.values.first ?? [] } - .mapError { error -> Error in error.asNSError() } - .execute(completion) - } - } - /// Fetches the blogging prompts settings for a given site. /// /// - Parameters: diff --git a/Tests/WordPressKitTests/WordPressKitTests/Tests/BloggingPromptsServiceRemoteTests.swift b/Tests/WordPressKitTests/WordPressKitTests/Tests/BloggingPromptsServiceRemoteTests.swift index 05f9b3b5fb16..dda49ea5d224 100644 --- a/Tests/WordPressKitTests/WordPressKitTests/Tests/BloggingPromptsServiceRemoteTests.swift +++ b/Tests/WordPressKitTests/WordPressKitTests/Tests/BloggingPromptsServiceRemoteTests.swift @@ -36,84 +36,6 @@ class BloggingPromptsServiceRemoteTests: RemoteTestCase, RESTTestable { // MARK: Tests - func test_fetchPrompts_returnsRemotePrompts() { - let expectedAvatarURLString = "https://0.gravatar.com/avatar/example?s=96&d=identicon&r=G" - stubRemoteResponse(.bloggingPromptsEndpoint, filename: .mockFileName, contentType: .ApplicationJSON) - - let expect = expectation(description: "Fetch blogging prompts succeeded") - service.fetchPrompts(for: siteID) { result in - guard case .success(let prompts) = result else { - XCTFail("Expected success result type") - return - } - - XCTAssertEqual(prompts.count, 2) - - let firstPrompt = prompts.first! - XCTAssertEqual(firstPrompt.promptID, 239) - XCTAssertEqual(firstPrompt.text, "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.") - XCTAssertEqual(firstPrompt.title, "Prompt number 1") - XCTAssertEqual(firstPrompt.content, "\n

Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.

(courtesy of plinky.com)
\n") - XCTAssertEqual(firstPrompt.attribution, "dayone") - - let firstDateComponents = Calendar.current.dateComponents(in: self.utcTimeZone, from: firstPrompt.date) - XCTAssertEqual(firstDateComponents.year!, 2022) - XCTAssertEqual(firstDateComponents.month!, 5) - XCTAssertEqual(firstDateComponents.day!, 3) - - XCTAssertFalse(firstPrompt.answered) - XCTAssertEqual(firstPrompt.answeredUsersCount, 0) - - let secondPrompt = prompts.last! - XCTAssertEqual(secondPrompt.answeredUsersCount, 1) - XCTAssertEqual(secondPrompt.answeredUserAvatarURLs.count, 1) - XCTAssertTrue(secondPrompt.attribution.isEmpty) - - let secondDateComponents = Calendar.current.dateComponents(in: self.utcTimeZone, from: secondPrompt.date) - XCTAssertEqual(secondDateComponents.year!, 2021) - XCTAssertEqual(secondDateComponents.month!, 9) - XCTAssertEqual(secondDateComponents.day!, 12) - - let avatarURL = secondPrompt.answeredUserAvatarURLs.first! - XCTAssertEqual(avatarURL.absoluteString, expectedAvatarURLString) - - expect.fulfill() - } - - wait(for: [expect], timeout: timeout) - } - - func test_fetchPrompts_correctlyAddsParametersToRequest() throws { - let requestReceived = expectation(description: "HTTP request is received") - var request: URLRequest? - stub(condition: isHost("public-api.wordpress.com")) { - request = $0 - requestReceived.fulfill() - return HTTPStubsResponse(error: URLError(.networkConnectionLost)) - } - - let expectedNumber = 10 - let expectedDateString = "2022-01-02" - let expectedDate = dateFormatter.date(from: expectedDateString) - service = BloggingPromptsServiceRemote(wordPressComRestApi: WordPressComRestApi()) - - // no-op; we just need to check the passed params. - service.fetchPrompts(for: siteID, number: expectedNumber, fromDate: expectedDate, completion: { _ in }) - wait(for: [requestReceived], timeout: 0.3) - - let url = try XCTUnwrap(request?.url) - let queryItems = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems) - let params = queryItems.reduce(into: [String: String]()) { result, query in - result[query.name] = query.value - } - - XCTAssertNotNil(params[.numberKey]) - XCTAssertEqual(params[.numberKey], expectedNumber.description) - - XCTAssertNotNil(params[.dateKey]) - XCTAssertEqual(params[.dateKey], expectedDateString) - } - func test_fetchSettings_returnsRemoteSettings() { stubRemoteResponse(.bloggingPromptsEndpoint, filename: .mockFetchSettingsFilename, contentType: .ApplicationJSON) From b73a13bc3d4faf1022afb7266632458ce081c2cc Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:42:20 +1200 Subject: [PATCH 02/13] Remove Bloganuary unit tests --- .../Misc/blogging-prompts-bloganuary.json | 32 ---- .../DashboardBloganuaryCardCellTests.swift | 162 ------------------ .../BloggingPromptsServiceTests.swift | 53 +----- 3 files changed, 4 insertions(+), 243 deletions(-) delete mode 100644 Tests/KeystoneTests/Resources/Mocks/Misc/blogging-prompts-bloganuary.json delete mode 100644 Tests/KeystoneTests/Tests/Features/Dashboard/DashboardBloganuaryCardCellTests.swift diff --git a/Tests/KeystoneTests/Resources/Mocks/Misc/blogging-prompts-bloganuary.json b/Tests/KeystoneTests/Resources/Mocks/Misc/blogging-prompts-bloganuary.json deleted file mode 100644 index 5c1dd4637e50..000000000000 --- a/Tests/KeystoneTests/Resources/Mocks/Misc/blogging-prompts-bloganuary.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "id": 239, - "date": "2023-12-31", - "label": "Daily writing prompt", - "text": "Was there a toy or thing you always wanted as a child, during the holidays or on your birthday, but never received? Tell us about it.", - "attribution": "dayone", - "answered": false, - "answered_users_count": 0, - "answered_users_sample": [], - "answered_link": "https:\/\/wordpress.com\/tag\/dailyprompt-239", - "answered_link_text": "View all responses", - "bloganuary_id": "" - }, - { - "id": 248, - "date": "2024-01-01", - "label": "Daily writing prompt", - "text": "Tell us about a time when you felt out of place.", - "attribution": "", - "answered": true, - "answered_users_count": 1, - "answered_users_sample": [ - { - "avatar": "https://0.gravatar.com/avatar/example?s=96&d=identicon&r=G" - } - ], - "answered_link": "https:\/\/wordpress.com\/tag\/dailyprompt-248", - "answered_link_text": "View all responses", - "bloganuary_id": "bloganuary-2024-01" - } -] diff --git a/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardBloganuaryCardCellTests.swift b/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardBloganuaryCardCellTests.swift deleted file mode 100644 index 4ddd97c9647a..000000000000 --- a/Tests/KeystoneTests/Tests/Features/Dashboard/DashboardBloganuaryCardCellTests.swift +++ /dev/null @@ -1,162 +0,0 @@ -import XCTest -@testable import WordPress -@testable import WordPressData - -final class DashboardBloganuaryCardCellTests: CoreDataTestCase { - - private static var calendar = { - Calendar(identifier: .gregorian) - }() - private let blogID = 100 - private let featureFlags = FeatureFlagOverrideStore() - - override func setUp() { - super.setUp() - featureFlags.override(RemoteFeatureFlag.bloganuaryDashboardNudge, withValue: true) - } - - override func tearDown() { - super.tearDown() - featureFlags.override(RemoteFeatureFlag.bloganuaryDashboardNudge, withValue: RemoteFeatureFlag.bloganuaryDashboardNudge.defaultValue) - } - - // MARK: - `shouldShowCard` tests - - func testCardIsNotShownWhenFlagIsDisabled() throws { - // Given - let blog = makeBlog() - makeBloggingPromptSettings() - try mainContext.save() - featureFlags.override(RemoteFeatureFlag.bloganuaryDashboardNudge, withValue: false) - - // When - let result = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInDecember) - - // Then - XCTAssertFalse(result) - } - - func testCardIsNotShownWhenSiteIsNotMarkedAsBloggingSite() throws { - // Given - let blog = makeBlog() - makeBloggingPromptSettings(markAsBloggingSite: false) - try mainContext.save() - - // When - let result = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInDecember) - - // Then - XCTAssertFalse(result) - } - - func testCardIsNotShownForEligibleSitesOutsideEligibleMonths() throws { - // Given - let blog = makeBlog() - makeBloggingPromptSettings() - try mainContext.save() - - // When - let result = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInFebruary) - - // Then - XCTAssertFalse(result) - } - - func testCardIsShownWhenSiteIsEligible() throws { - // Given - let blog = makeBlog() - makeBloggingPromptSettings() - try mainContext.save() - - // When - let resultForDecember = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInDecember) - let resultForJanuary = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInJanuary) - - // Then - XCTAssertTrue(resultForDecember) - XCTAssertTrue(resultForJanuary) - } - - func testCardIsShownForEligibleSitesThatHavePromptsDisabled() throws { - // Given - let blog = makeBlog() - makeBloggingPromptSettings(promptCardEnabled: false) - try mainContext.save() - - // When - let result = DashboardBloganuaryCardCell.shouldShowCard(for: blog, date: sometimeInDecember) - - // Then - XCTAssertTrue(result) - } -} - -// MARK: - Helpers - -private extension DashboardBloganuaryCardCellTests { - - var sometimeInDecember: Date { - let date = Date() - var components = Self.calendar.dateComponents([.year, .month, .day], from: date) - components.month = 12 - components.year = 2023 - components.day = 10 - - return Self.calendar.date(from: components) ?? date - } - - var sometimeInJanuary: Date { - let date = Date() - var components = Self.calendar.dateComponents([.year, .month, .day], from: date) - components.month = 1 - components.year = 2024 - components.day = 10 - - return Self.calendar.date(from: components) ?? date - } - - var sometimeInFebruary: Date { - let date = Date() - var components = Self.calendar.dateComponents([.year, .month, .day], from: date) - components.month = 2 - components.year = 2024 - components.day = 10 - - return Self.calendar.date(from: components) ?? date - } - - func prepareData() -> (Blog, BloggingPromptSettings) { - return (makeBlog(), makeBloggingPromptSettings()) - } - - func makeBlog() -> Blog { - let builder = BlogBuilder(mainContext) - .withAnAccount() - .with(dotComID: blogID) - - return builder.build() - } - - @discardableResult - func makeBloggingPromptSettings(markAsBloggingSite: Bool = true, promptCardEnabled: Bool = true) -> BloggingPromptSettings { - let settings = NSEntityDescription.insertNewObject(forEntityName: BloggingPromptSettings.entityName(), - into: mainContext) as! WordPress.BloggingPromptSettings - - let reminderDays = NSEntityDescription.insertNewObject(forEntityName: BloggingPromptSettingsReminderDays.entityName(), - into: mainContext) as! WordPress.BloggingPromptSettingsReminderDays - reminderDays.monday = false - reminderDays.tuesday = false - reminderDays.wednesday = false - reminderDays.thursday = false - reminderDays.friday = false - reminderDays.saturday = false - reminderDays.sunday = false - - settings.isPotentialBloggingSite = markAsBloggingSite - settings.promptCardEnabled = promptCardEnabled - settings.reminderDays = reminderDays - settings.siteID = Int32(blogID) - - return settings - } -} diff --git a/Tests/KeystoneTests/Tests/Services/BloggingPromptsServiceTests.swift b/Tests/KeystoneTests/Tests/Services/BloggingPromptsServiceTests.swift index ad60f30a9fbe..67655d4a58cc 100644 --- a/Tests/KeystoneTests/Tests/Services/BloggingPromptsServiceTests.swift +++ b/Tests/KeystoneTests/Tests/Services/BloggingPromptsServiceTests.swift @@ -9,7 +9,6 @@ final class BloggingPromptsServiceTests: CoreDataTestCase { private let siteID = 1 private let timeout: TimeInterval = 2 private let fetchPromptsResponseFileName = "blogging-prompts-fetch-success" - private let bloganuaryPromptsResponseFileName = "blogging-prompts-bloganuary" private var dateFormatter: DateFormatter = { let formatter = DateFormatter() @@ -157,10 +156,10 @@ final class BloggingPromptsServiceTests: CoreDataTestCase { let currentYear = Calendar(identifier: .gregorian).component(.year, from: Date()) XCTAssertEqual("\(currentYear)-01-02", try passedDate()) } -func test_fetchPrompts_alwaysPassesDescendingOrder() throws { - service.fetchPrompts(success: { _ in }, failure: { _ in }) - XCTAssertEqual(try passedParameter("order") as? String, "desc") -} + func test_fetchPrompts_alwaysPassesDescendingOrder() throws { + service.fetchPrompts(success: { _ in }, failure: { _ in }) + XCTAssertEqual(try passedParameter("order") as? String, "desc") + } // MARK: - Upsert Tests // new prompts should overwrite any existing prompts. @@ -285,50 +284,6 @@ func test_fetchPrompts_alwaysPassesDescendingOrder() throws { wait(for: [expectation], timeout: timeout) } - - // MARK: Bloganuary Tests - - func test_fetchPrompt_shouldParseBloganuaryPromptsCorrectly() { - // use actual remote object so the request can be intercepted by HTTPStubs. - service = BloggingPromptsService(contextManager: contextManager, blog: blog) - testPrompts = loadTestPrompts(from: bloganuaryPromptsResponseFileName) - stubFetchPromptsResponse(with: bloganuaryPromptsResponseFileName) - - let expectation = expectation(description: "Fetch prompts should succeed") - service.fetchPrompts(from: .distantPast) { [testPrompts] prompts in - XCTAssertEqual(prompts.count, testPrompts.count) - - prompts.forEach { prompt in - guard let expected = testPrompts.first(where: { $0.promptID == prompt.promptID }) else { - XCTFail("Prompt with ID: \(prompt.promptID) not found in the test data.") - return - } - - // check for Bloganuary prompts. - if let bloganuaryId = expected.bloganuaryId, !bloganuaryId.isEmpty { - // the attribution should be added client-side. - XCTAssertEqual(prompt.attribution, "bloganuary") - XCTAssertNotNil(prompt.additionalPostTags) - - let tags = prompt.additionalPostTags! - XCTAssertTrue(tags.contains("bloganuary")) - XCTAssertTrue(tags.contains(bloganuaryId)) - } else { - // otherwise, normal cards shouldn't have the bloganuary attributions. - // no additional tags should be added here. - XCTAssertNotEqual(prompt.attribution, "bloganuary") - XCTAssertTrue(prompt.additionalPostTags!.isEmpty) - } - } - - expectation.fulfill() - } failure: { _ in - XCTFail("This closure shouldn't be called.") - expectation.fulfill() - } - - wait(for: [expectation], timeout: timeout) - } } // MARK: - Helpers From d5f41e03e2cb5e516f5aab086a1c308c684aef32 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:48:28 +1200 Subject: [PATCH 03/13] Apply swift-format to dashboard card files --- .../DashboardCard+Personalization.swift | 3 +- .../Blog Dashboard/Models/DashboardCard.swift | 20 ++++---- ...logDashboardPersonalizationViewModel.swift | 50 +++++++++++++++---- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift index 9585b544b82f..5e16c074b148 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift @@ -29,7 +29,8 @@ extension DashboardCard: BlogDashboardPersonalizable { return "activity-log-card-enabled-site-settings" case .pages: return "pages-card-enabled-site-settings" - case .dynamic, .jetpackBadge, .jetpackInstall, .jetpackSocial, .failure, .ghost, .personalize, .empty, .extensiveLogging: + case .dynamic, .jetpackBadge, .jetpackInstall, .jetpackSocial, .failure, .ghost, .personalize, .empty, + .extensiveLogging: return nil } } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift index f77859b2bc19..44d433fa14be 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift @@ -137,9 +137,11 @@ enum DashboardCard: String, CaseIterable, Sendable { case .personalize: return true case .pages: - return DashboardPagesListCardCell.shouldShowCard(for: blog) && shouldShowRemoteCard(apiResponse: apiResponse) + return DashboardPagesListCardCell.shouldShowCard(for: blog) + && shouldShowRemoteCard(apiResponse: apiResponse) case .activityLog: - return DashboardActivityLogCardCell.shouldShowCard(for: blog) && shouldShowRemoteCard(apiResponse: apiResponse) + return DashboardActivityLogCardCell.shouldShowCard(for: blog) + && shouldShowRemoteCard(apiResponse: apiResponse) case .jetpackSocial: return DashboardJetpackSocialCardCell.shouldShowCard(for: blog) case .googleDomains: @@ -212,31 +214,31 @@ enum DashboardCard: String, CaseIterable, Sendable { private extension BlogDashboardRemoteEntity { var hasDrafts: Bool { - return (self.posts?.value?.draft?.count ?? 0) > 0 + (self.posts?.value?.draft?.count ?? 0) > 0 } var hasScheduled: Bool { - return (self.posts?.value?.scheduled?.count ?? 0) > 0 + (self.posts?.value?.scheduled?.count ?? 0) > 0 } var hasPages: Bool { - return self.pages?.value != nil + self.pages?.value != nil } var hasStats: Bool { - return self.todaysStats?.value != nil + self.todaysStats?.value != nil } var hasActivities: Bool { - return (self.activity?.value?.current?.orderedItems?.count ?? 0) > 0 + (self.activity?.value?.current?.orderedItems?.count ?? 0) > 0 } - } +} // MARK: - BlogDashboardAnalyticPropertiesProviding Protocol Conformance extension DashboardCard: BlogDashboardAnalyticPropertiesProviding { var analyticProperties: [AnyHashable: Any] { - return ["card": rawValue] + ["card": rawValue] } } diff --git a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift index 6ef561297a12..93219b9ac02e 100644 --- a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift @@ -71,24 +71,52 @@ private extension DashboardCard { func getLocalizedTitle() -> String { switch self { case .prompts: - return NSLocalizedString("personalizeHome.dashboardCard.prompts", value: "Blogging prompts", comment: "Card title for the pesonalization menu") + return NSLocalizedString( + "personalizeHome.dashboardCard.prompts", + value: "Blogging prompts", + comment: "Card title for the pesonalization menu" + ) case .blaze: - return NSLocalizedString("personalizeHome.dashboardCard.blaze", value: "Blaze", comment: "Card title for the pesonalization menu") + return NSLocalizedString( + "personalizeHome.dashboardCard.blaze", + value: "Blaze", + comment: "Card title for the pesonalization menu" + ) case .todaysStats: - return NSLocalizedString("personalizeHome.dashboardCard.todaysStats", value: "Today's stats", comment: "Card title for the pesonalization menu") + return NSLocalizedString( + "personalizeHome.dashboardCard.todaysStats", + value: "Today's stats", + comment: "Card title for the pesonalization menu" + ) case .draftPosts: - return NSLocalizedString("personalizeHome.dashboardCard.draftPosts", value: "Draft posts", comment: "Card title for the pesonalization menu") + return NSLocalizedString( + "personalizeHome.dashboardCard.draftPosts", + value: "Draft posts", + comment: "Card title for the pesonalization menu" + ) case .scheduledPosts: - return NSLocalizedString("personalizeHome.dashboardCard.scheduledPosts", value: "Scheduled posts", comment: "Card title for the pesonalization menu") + return NSLocalizedString( + "personalizeHome.dashboardCard.scheduledPosts", + value: "Scheduled posts", + comment: "Card title for the pesonalization menu" + ) case .activityLog: - return NSLocalizedString("personalizeHome.dashboardCard.activityLog", value: "Recent activity", comment: "Card title for the pesonalization menu") + return NSLocalizedString( + "personalizeHome.dashboardCard.activityLog", + value: "Recent activity", + comment: "Card title for the pesonalization menu" + ) case .pages: - return NSLocalizedString("personalizeHome.dashboardCard.pages", value: "Pages", comment: "Card title for the pesonalization menu") + return NSLocalizedString( + "personalizeHome.dashboardCard.pages", + value: "Pages", + comment: "Card title for the pesonalization menu" + ) case .dynamic, .ghost, - .failure, .personalize, .jetpackBadge, - .jetpackInstall, .empty, .freeToPaidPlansDashboardCard, - .domainRegistration, .jetpackSocial, .bloganuaryNudge, - .googleDomains, .extensiveLogging: + .failure, .personalize, .jetpackBadge, + .jetpackInstall, .empty, .freeToPaidPlansDashboardCard, + .domainRegistration, .jetpackSocial, .bloganuaryNudge, + .googleDomains, .extensiveLogging: assertionFailure("\(self) card should not appear in the personalization menus") return "" // These cards don't appear in the personalization menus } From 4f2720d7807981df591e64d76e3304a78a2751a2 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:50:18 +1200 Subject: [PATCH 04/13] Delete Bloganuary view controller, card cell, and tracker --- .../Prompts/DashboardBloganuaryCardCell.swift | 199 ----------- .../BloganuaryOverlayViewController.swift | 327 ------------------ .../Bloganuary/BloganuaryTracker.swift | 25 -- 3 files changed, 551 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardBloganuaryCardCell.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryOverlayViewController.swift delete mode 100644 WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryTracker.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardBloganuaryCardCell.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardBloganuaryCardCell.swift deleted file mode 100644 index 6b7d1746d1cc..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/DashboardBloganuaryCardCell.swift +++ /dev/null @@ -1,199 +0,0 @@ -import SwiftUI -import WordPressData -import WordPressShared - -class DashboardBloganuaryCardCell: DashboardCollectionViewCell { - - private var blog: Blog? { - didSet { - updateUI() - } - } - - private weak var presenterViewController: BlogDashboardViewController? - - /// Checks whether the Bloganuary nudge card should be shown on the dashboard. - /// - /// The card is only going to be shown in December, and will be hidden in January. - /// It's also going to be shown for blogs that are marked as potential blogs by the backend, regardless - /// of whether the user has manually disabled the blogging prompts. - /// - /// - Parameters: - /// - blog: The current `Blog` instance. - /// - date: The date to check. Defaults to today. - /// - Returns: `true` if the Bloganuary card should be shown. `false` otherwise. - static func shouldShowCard(for blog: Blog, date: Date = Date()) -> Bool { - guard RemoteFeatureFlag.bloganuaryDashboardNudge.enabled(), - let context = blog.managedObjectContext else { - return false - } - - // Check for date eligibility. - let isDateWithinEligibleMonths: Bool = { - let components = date.dateAndTimeComponents() - guard let month = components.month else { - return false - } - - // NOTE: For simplicity, we're going to hardcode the date check if the date is within December or January. - return Constants.eligibleMonths.contains(month) - }() - - // Check if the blog is marked as a potential blogging site. - let isPotentialBloggingSite: Bool = context.performAndWait { - return (try? BloggingPromptSettings.of(blog))?.isPotentialBloggingSite ?? false - } - - return isDateWithinEligibleMonths && isPotentialBloggingSite - } - - func configure(blog: Blog, viewController: BlogDashboardViewController?, apiResponse: BlogDashboardRemoteEntity?) { - self.blog = blog - self.presenterViewController = viewController - - BlogDashboardAnalytics.shared.track(.dashboardCardShown, - properties: [ - "type": DashboardCard.bloganuaryNudge.rawValue, - "subtype": DashboardCard.bloganuaryNudge.rawValue - ]) - } - - // MARK: Private methods - - @MainActor - private func updateUI() { - guard let blog, - let blogID = blog.dotComID?.intValue else { - return - } - - contentView.subviews.forEach { $0.removeFromSuperview() } - - let cardView = BloganuaryNudgeCardView(onLearnMoreTapped: { [weak self] in - // check if the prompts card is enabled in the dashboard. - let promptsCardEnabled = BlogDashboardPersonalizationService(siteID: blogID).isEnabled(.prompts) - let overlayView = BloganuaryOverlayViewController(blogID: blogID, promptsEnabled: promptsCardEnabled) - - let navigationController = UINavigationController(rootViewController: overlayView) - navigationController.modalPresentationStyle = .formSheet - if let sheet = navigationController.sheetPresentationController { - sheet.prefersGrabberVisible = WPDeviceIdentification.isiPhone() - } - - BloganuaryTracker.trackCardLearnMoreTapped(promptsEnabled: promptsCardEnabled) - - self?.presenterViewController?.present(navigationController, animated: true) - }) - - let hostView = UIView.embedSwiftUIView(cardView) - let frameView = makeCardFrameView() - frameView.add(subview: hostView) - - contentView.addSubview(frameView) - contentView.pinSubviewToAllEdges(frameView) - } - - private func makeCardFrameView() -> BlogDashboardCardFrameView { - let frameView = BlogDashboardCardFrameView() - frameView.translatesAutoresizingMaskIntoConstraints = false - frameView.configureButtonContainerStackView() - - // NOTE: this is intentionally called *before* configuring the ellipsis button action, - // to avoid additional trailing padding. - frameView.hideHeader() - - if let blog { - frameView.onEllipsisButtonTap = { - BlogDashboardAnalytics.trackContextualMenuAccessed(for: .bloganuaryNudge) - } - frameView.ellipsisButton.showsMenuAsPrimaryAction = true - let action = BlogDashboardHelpers.makeHideCardAction(for: .bloganuaryNudge, blog: blog) - frameView.ellipsisButton.menu = UIMenu(title: String(), options: .displayInline, children: [action]) - } - - return frameView - } - - struct Constants { - // Only show the card in December and January. - static let eligibleMonths = [1, 12] - } -} - -// MARK: - SwiftUI - -private struct BloganuaryNudgeCardView: View { - let onLearnMoreTapped: (() -> Void)? - - var body: some View { - VStack(alignment: .leading, spacing: 12.0) { - bloganuaryImage - .resizable() - .frame(width: 24.0, height: 24.0) - textContainer - Button { - onLearnMoreTapped?() - } label: { - Text(Strings.cta) - .font(.subheadline) - } - } - .padding(.top, 12.0) - .padding([.horizontal, .bottom], 16.0) - } - - var bloganuaryImage: Image { - if let uiImage = UIImage(named: "logo-bloganuary")?.withRenderingMode(.alwaysTemplate).withTintColor(.label) { - return Image(uiImage: uiImage) - } - return Image("logo-bloganuary", bundle: .main) - } - - var textContainer: some View { - VStack(alignment: .leading, spacing: 8.0) { - Text(cardTitle) - .font(.headline) - .fontWeight(.semibold) - Text(Strings.description) - .font(.subheadline) - .foregroundStyle(.secondary) - } - } - - var cardTitle: String { - let components = Date().dateAndTimeComponents() - guard let month = components.month, - DashboardBloganuaryCardCell.Constants.eligibleMonths.contains(month) else { - return Strings.title - } - - return month == 1 ? Strings.runningTitle : Strings.title - } - - struct Strings { - static let title = NSLocalizedString( - "bloganuary.dashboard.card.title", - value: "Bloganuary is coming!", - comment: "Title for the Bloganuary dashboard card." - ) - - // The card title string to be shown while Bloganuary is running - static let runningTitle = NSLocalizedString( - "bloganuary.dashboard.card.runningTitle", - value: "Bloganuary is here!", - comment: "Title for the Bloganuary dashboard card while Bloganuary is running." - ) - - static let description = NSLocalizedString( - "bloganuary.dashboard.card.description", - value: "For the month of January, blogging prompts will come from Bloganuary — our community challenge to build a blogging habit for the new year.", - comment: "Short description for the Bloganuary event, shown right below the title." - ) - - static let cta = NSLocalizedString( - "bloganuary.dashboard.card.button.learnMore", - value: "Learn more", - comment: "Title for a button that, when tapped, shows more info about participating in Bloganuary." - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryOverlayViewController.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryOverlayViewController.swift deleted file mode 100644 index 91a548eb3121..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryOverlayViewController.swift +++ /dev/null @@ -1,327 +0,0 @@ -import SwiftUI -import WordPressShared - -class BloganuaryOverlayViewController: UIViewController { - - private let blogID: Int - - private let promptsEnabled: Bool - - private lazy var viewModel: BloganuaryOverlayViewModel = { - return BloganuaryOverlayViewModel(promptsEnabled: promptsEnabled, orientation: UIDevice.current.orientation) - }() - - // MARK: Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - setupViews() - setupNavigationBar() - - // Make sure we only track this once, regardless of redraws from orientation change, etc. - BloganuaryTracker.trackModalShown(promptsEnabled: promptsEnabled) - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - - // update view model state after the device has finished the orientation change animation. - coordinator.animate(alongsideTransition: nil) { [weak self] _ in - self?.viewModel.orientation = UIDevice.current.orientation - } - } - - init(blogID: Int, promptsEnabled: Bool) { - self.blogID = blogID - self.promptsEnabled = promptsEnabled - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: Private Methods - - private func setupViews() { - view.backgroundColor = .systemBackground - - let overlayView = BloganuaryOverlayView(viewModel: viewModel, onPrimaryButtonTapped: { [weak self] in - guard let self else { - return - } - - BloganuaryTracker.trackModalActionTapped(self.promptsEnabled ? .dismiss : .turnPromptsOn) - - self.dismiss(completion: { - if self.promptsEnabled { - return - } - - Task { @MainActor in - // enable prompts card on the dashboard. - BlogDashboardPersonalizationService(siteID: self.blogID).setEnabled(true, for: .prompts) - } - }) - }) - - let swiftUIView = UIView.embedSwiftUIView(overlayView) - view.addSubview(swiftUIView) - view.pinSubviewToAllEdges(swiftUIView) - } - - private func setupNavigationBar() { - // Set up the close button in the navigation bar. - let dismissAction = UIAction { [weak self] _ in - BloganuaryTracker.trackModalDismissed() - self?.dismiss() - } - navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: dismissAction) - } - - private func dismiss(completion: (() -> Void)? = nil) { - navigationController?.dismiss(animated: true, completion: completion) - } -} - -// MARK: - SwiftUI - -class BloganuaryOverlayViewModel: ObservableObject { - let promptsEnabled: Bool - @Published var orientation: UIDeviceOrientation - - init(promptsEnabled: Bool, orientation: UIDeviceOrientation) { - self.promptsEnabled = promptsEnabled - self.orientation = orientation - } -} - -private struct BloganuaryOverlayView: View { - - @ObservedObject var viewModel: BloganuaryOverlayViewModel - - @State var scrollViewHeight: CGFloat = 0.0 - - var onPrimaryButtonTapped: (() -> Void)? - - @ScaledMetric(relativeTo: Constants.descriptionTextStyle) - private var descriptionIconSize = 24.0 - - @ScaledMetric(relativeTo: Constants.descriptionTextStyle) - private var descriptionItemHSpacing = 16.0 - - @ScaledMetric(relativeTo: Constants.descriptionTextStyle) - private var descriptionItemVSpacing = 24.0 - - var body: some View { - VStack(spacing: .zero) { - contentScrollView - footerContainer - } - } - - var contentScrollView: some View { - ScrollView { - VStack { - content - Spacer(minLength: 32.0) - Text(stringForFooter) - .font(.footnote) - .foregroundStyle(.secondary) - .padding(.horizontal, Constants.horizontalPadding) - .multilineTextAlignment(.center) - } - .padding(.vertical, 18.0) - .frame(minHeight: scrollViewHeight, maxHeight: .infinity) - } - .layoutPriority(1) // force the scroll view to fill most of the screen space. - .background { - // try to get the scrollView height and use it as the ideal height for its content view. - GeometryReader { geo in - Color.clear - .onAppear { - scrollViewHeight = geo.size.height - } - .onChange(of: viewModel.orientation) { - // since onAppear is only called once, assign the value again every time the orientation changes. - scrollViewHeight = geo.size.height - } - } - } - } - - var content: some View { - VStack(alignment: .center, spacing: 24.0) { - Image(Constants.bloganuaryImageName, bundle: nil) - .resizable() - .renderingMode(.template) - .foregroundStyle(.primary) - .scaledToFit() - .frame(width: Constants.preferredLogoWidth, height: Constants.preferredLogoHeight) - descriptionContainer - } - .padding(.horizontal, Constants.horizontalPadding) - .frame(maxWidth: .infinity, alignment: .topLeading) - } - - var descriptionContainer: some View { - VStack(alignment: .leading, spacing: 32.0) { - Text(Strings.headline) - .font(.largeTitle) - .fontWeight(.bold) - .multilineTextAlignment(.center) - .lineLimit(nil) - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity, alignment: .center) - descriptionList - } - } - - var descriptionList: some View { - HStack(spacing: .zero) { - Spacer(minLength: .zero) - VStack(alignment: .leading, spacing: descriptionItemVSpacing) { - descriptionEntry(iconName: Constants.firstDescriptionIconName, text: Strings.firstDescriptionLine) - descriptionEntry(iconName: Constants.secondDescriptionIconName, text: Strings.secondDescriptionLine) - descriptionEntry(iconName: Constants.thirdDescriptionIconName, text: Strings.thirdDescriptionLine) - } - .padding(.horizontal, 16.0) - .frame(maxWidth: 420.0) - .layoutPriority(1) - Spacer(minLength: .zero) - } - } - - func descriptionEntry(iconName: String, text: String) -> some View { - HStack(alignment: .center, spacing: descriptionItemHSpacing) { - descriptionIconView(for: iconName) - Text(text) - .font(.headline) - .fontWeight(.semibold) - .foregroundStyle(Constants.descriptionItemTextColor) - .lineLimit(nil) - .fixedSize(horizontal: false, vertical: true) - } - } - - var footerContainer: some View { - VStack(spacing: .zero) { - Divider() - .frame(maxWidth: .infinity) - Group { - ctaButton - } - .padding(WPDeviceIdentification.isiPad() ? .vertical : .top, 24.0) - .padding(.horizontal, Constants.horizontalPadding) - } - } - - var ctaButton: some View { - Button { - onPrimaryButtonTapped?() - } label: { - Text(viewModel.promptsEnabled ? Strings.buttonTitleForEnabledPrompts : Strings.buttonTitleForDisabledPrompts) - .multilineTextAlignment(.center) - .lineLimit(nil) - .frame(maxWidth: .infinity) - .fixedSize(horizontal: false, vertical: true) - } - .padding(.vertical, 14.0) - .padding(.horizontal, 20.0) - .frame(maxWidth: .infinity, alignment: .center) - .foregroundStyle(Color(.systemBackground)) - .background(Color(.label)) - .clipShape(RoundedRectangle(cornerRadius: 12.0)) - } - - var stringForFooter: String { - guard viewModel.promptsEnabled else { - return "\(Strings.footer) \(Strings.footerAddition)" - } - return Strings.footer - } - - func descriptionIconView(for iconName: String) -> some View { - Image(iconName, bundle: nil) - .resizable() - .renderingMode(.template) - .foregroundStyle(Constants.descriptionIconColor) - .flipsForRightToLeftLayoutDirection(true) - .frame(width: descriptionIconSize, height: descriptionIconSize) - .padding(12.0) - .background { - Circle().fill(Constants.descriptionIconBackgroundColor) - } - } - - // MARK: Constants - - struct Constants { - static let horizontalPadding: CGFloat = 32.0 - static let preferredLogoWidth: CGFloat = 180.0 - static let preferredLogoHeight: CGFloat = 42.0 - static let bloganuaryImageName = "logo-bloganuary-large" - static let descriptionTextStyle: Font.TextStyle = .footnote - - static let firstDescriptionIconName = "bloganuary-icon-page" - static let secondDescriptionIconName = "bloganuary-icon-verse" - static let thirdDescriptionIconName = "bloganuary-icon-people" - - static let descriptionItemTextColor = Color(.init(light: .label, dark: .secondaryLabel)) - static let descriptionIconColor = Color(.init(light: .systemBackground, dark: .label)) - static let descriptionIconBackgroundColor = Color(.init(light: .label, dark: .tertiarySystemBackground)) - } - - struct Strings { - static let headline = NSLocalizedString( - "bloganuary.learnMore.modal.headline", - value: "Join our month-long writing challenge", - comment: "The headline text of the Bloganuary modal sheet." - ) - - static let firstDescriptionLine = NSLocalizedString( - "bloganuary.learnMore.modal.descriptions.first", - value: "Receive a new prompt to inspire you each day.", - comment: "The first line of the description shown in the Bloganuary modal sheet." - ) - - static let secondDescriptionLine = NSLocalizedString( - "bloganuary.learnMore.modal.description.second", - value: "Publish your response.", - comment: "The second line of the description shown in the Bloganuary modal sheet." - ) - - static let thirdDescriptionLine = NSLocalizedString( - "bloganuary.learnMore.modal.description.third", - value: "Read other bloggers’ responses to get inspiration and make new connections.", - comment: "The third line of the description shown in the Bloganuary modal sheet." - ) - - static let footer = NSLocalizedString( - "bloganuary.learnMore.modal.footer.text", - value: "Bloganuary will use Daily Blogging Prompts to send you topics for the month of January.", - comment: "An informative excerpt shown in a subtler tone." - ) - - static let footerAddition = NSLocalizedString( - "bloganuary.learnMore.modal.footer.addition", - value: "To join Bloganuary you need to enable Blogging Prompts.", - comment: "An additional piece of information shown in case the user has the Blogging Prompts feature disabled." - ) - - static let buttonTitleForDisabledPrompts = NSLocalizedString( - "bloganuary.learnMore.modal.button.promptsDisabled", - value: "Turn on blogging prompts", - comment: "Title of a button that calls the user to enable the Blogging Prompts feature." - ) - - static let buttonTitleForEnabledPrompts = NSLocalizedString( - "bloganuary.learnMore.modal.button.promptsEnabled", - value: "Let’s go!", - comment: """ - Title of a button that will dismiss the Bloganuary modal when tapped. - Note that the word 'go' here should have a closer meaning to 'start' rather than 'move forward'. - """ - ) - } -} diff --git a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryTracker.swift b/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryTracker.swift deleted file mode 100644 index 05ca83b953be..000000000000 --- a/WordPress/Classes/ViewRelated/Blog/Blogging Prompts/Bloganuary/BloganuaryTracker.swift +++ /dev/null @@ -1,25 +0,0 @@ -import WordPressShared - -struct BloganuaryTracker { - - enum ModalAction: String { - case turnPromptsOn = "turn_prompts_on" - case dismiss - } - - static func trackCardLearnMoreTapped(promptsEnabled: Bool) { - WPAnalytics.track(.bloganuaryNudgeCardLearnMoreTapped, properties: ["prompts_enabled": promptsEnabled]) - } - - static func trackModalShown(promptsEnabled: Bool) { - WPAnalytics.track(.bloganuaryNudgeModalShown, properties: ["prompts_enabled": promptsEnabled]) - } - - static func trackModalDismissed() { - WPAnalytics.track(.bloganuaryNudgeModalDismissed) - } - - static func trackModalActionTapped(_ action: ModalAction) { - WPAnalytics.track(.bloganuaryNudgeModalActionTapped, properties: ["action": action.rawValue]) - } -} From cb473417b895b270312a20eeba72b69ae669d5f0 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:50:24 +1200 Subject: [PATCH 05/13] Remove Bloganuary dashboard card wiring --- .../Models/DashboardCard+Personalization.swift | 2 -- .../Blog/Blog Dashboard/Models/DashboardCard.swift | 5 ----- .../BlogDashboardPersonalizationViewModel.swift | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift index 5e16c074b148..b9e1e93f9903 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard+Personalization.swift @@ -12,8 +12,6 @@ extension DashboardCard: BlogDashboardPersonalizable { return "scheduled-posts-card-enabled-site-settings" case .blaze: return "blaze-card-enabled-site-settings" - case .bloganuaryNudge: - return "bloganuary-nudge-card-enabled-site-settings" case .prompts: // Warning: there is an irregularity with the prompts key that doesn't // have a "-card" component in the key name. Keeping it like this to diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift index 44d433fa14be..7d2c725e4eb3 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Models/DashboardCard.swift @@ -11,7 +11,6 @@ import Support enum DashboardCard: String, CaseIterable, Sendable { case dynamic case jetpackInstall - case bloganuaryNudge = "bloganuary_nudge" case prompts case extensiveLogging case googleDomains @@ -47,8 +46,6 @@ enum DashboardCard: String, CaseIterable, Sendable { return DashboardScheduledPostsCardCell.self case .todaysStats: return DashboardStatsCardCell.self - case .bloganuaryNudge: - return DashboardBloganuaryCardCell.self case .prompts: return DashboardPromptsCardCell.self case .ghost: @@ -110,8 +107,6 @@ enum DashboardCard: String, CaseIterable, Sendable { return shouldShowRemoteCard(apiResponse: apiResponse) case .todaysStats: return DashboardStatsCardCell.shouldShowCard(for: blog) && shouldShowRemoteCard(apiResponse: apiResponse) - case .bloganuaryNudge: - return DashboardBloganuaryCardCell.shouldShowCard(for: blog) case .prompts: return DashboardPromptsCardCell.shouldShowCard(for: blog) case .extensiveLogging: diff --git a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift index 93219b9ac02e..3bb1714b0e49 100644 --- a/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift +++ b/WordPress/Classes/ViewRelated/Blog/BlogPersonalization/BlogDashboardPersonalizationViewModel.swift @@ -115,7 +115,7 @@ private extension DashboardCard { case .dynamic, .ghost, .failure, .personalize, .jetpackBadge, .jetpackInstall, .empty, .freeToPaidPlansDashboardCard, - .domainRegistration, .jetpackSocial, .bloganuaryNudge, + .domainRegistration, .jetpackSocial, .googleDomains, .extensiveLogging: assertionFailure("\(self) card should not appear in the personalization menus") return "" // These cards don't appear in the personalization menus From 6cccac9dcdf9f9db366e205b3bb30f5f6d783eaa Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:53:45 +1200 Subject: [PATCH 06/13] Apply swift-format to WPAnalyticsEvent.swift --- .../Classes/Utility/Analytics/WPAnalyticsEvent.swift | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index ed90a13240e2..bbd9ab40c082 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -742,7 +742,7 @@ import WordPressShared return "media_library_photo_added" case .editorAddedPhotoViaTenor: return "editor_photo_added" - // Media + // Media case .siteMediaShareTapped: return "site_media_shared_tapped" case .mediaStorageDetailsViewed: @@ -1909,7 +1909,7 @@ import WordPressShared case .jetpackConnectStepRetried: return "jetpack_rest_connect_step_retried" - // Intelligence + // Intelligence case .intelligenceExcerptGeneratorOpened: return "intelligence_excerpt_generator_opened" case .intelligenceExcerptSelected: @@ -2009,7 +2009,8 @@ extension WPAnalytics { static func track(_ event: WPAnalyticsEvent, properties: [AnyHashable: Any], blog: Blog) { var props = properties props[WPAppAnalyticsKeyBlogID] = blog.dotComID - props[WPAppAnalyticsKeySiteType] = blog.isWPForTeams ? WPAppAnalyticsValueSiteTypeP2 : WPAppAnalyticsValueSiteTypeBlog + props[WPAppAnalyticsKeySiteType] = + blog.isWPForTeams ? WPAppAnalyticsValueSiteTypeP2 : WPAppAnalyticsValueSiteTypeBlog WPAnalytics.track(event, properties: props) } @@ -2105,7 +2106,9 @@ extension WPAnalytics { } if event == nil { - print("🟡 Not Tracked: \"\(eventName)\" Block Editor event ignored as it was not found in the `trackBlockEditorEvent` conversion cases.") + print( + "🟡 Not Tracked: \"\(eventName)\" Block Editor event ignored as it was not found in the `trackBlockEditorEvent` conversion cases." + ) } else { WPAnalytics.track(event!, properties: properties, blog: blog) } From c638e34fa536a6bbaca87c2bc84b3618252a56c6 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:55:00 +1200 Subject: [PATCH 07/13] Remove Bloganuary analytics events --- .../Utility/Analytics/WPAnalyticsEvent.swift | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index bbd9ab40c082..a8557cefc55b 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -480,12 +480,6 @@ import WordPressShared case promptsOtherAnswersTapped case promptsSettingsShowPromptsTapped - // Bloganuary Nudges - case bloganuaryNudgeCardLearnMoreTapped - case bloganuaryNudgeModalShown - case bloganuaryNudgeModalDismissed - case bloganuaryNudgeModalActionTapped - // Jetpack branding case jetpackPoweredBadgeTapped case jetpackPoweredBannerTapped @@ -1543,16 +1537,6 @@ import WordPressShared case .promptsSettingsShowPromptsTapped: return "blogging_prompts_settings_show_prompts_tapped" - // Bloganuary Nudges - case .bloganuaryNudgeCardLearnMoreTapped: - return "bloganuary_nudge_my_site_card_learn_more_tapped" - case .bloganuaryNudgeModalShown: - return "bloganuary_nudge_learn_more_modal_shown" - case .bloganuaryNudgeModalDismissed: - return "bloganuary_nudge_learn_more_modal_dismissed" - case .bloganuaryNudgeModalActionTapped: - return "bloganuary_nudge_learn_more_modal_action_tapped" - // Jetpack branding case .jetpackPoweredBadgeTapped: return "jetpack_powered_badge_tapped" From 72b73f7ae5080935520a6d35102a4e84eb5a346d Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:55:19 +1200 Subject: [PATCH 08/13] Apply swift-format to RemoteFeatureFlag.swift --- .../Utility/BuildInformation/RemoteFeatureFlag.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index c151374c2a97..82df956f7d10 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -228,8 +228,10 @@ public enum RemoteFeatureFlag: Int, CaseIterable { /// If the flag is overridden, the overridden value is returned. /// If the flag exists in the local cache, the current value will be returned. /// If the flag is not overridden and does not exist in the local cache, the compile-time default will be returned. - func enabled(using remoteStore: RemoteFeatureFlagStore = RemoteFeatureFlagStore(), - overrideStore: FeatureFlagOverrideStore = FeatureFlagOverrideStore()) -> Bool { + func enabled( + using remoteStore: RemoteFeatureFlagStore = RemoteFeatureFlagStore(), + overrideStore: FeatureFlagOverrideStore = FeatureFlagOverrideStore() + ) -> Bool { if let overriddenValue = overrideStore.overriddenValue(for: self) { return overriddenValue } @@ -243,11 +245,11 @@ public enum RemoteFeatureFlag: Int, CaseIterable { extension RemoteFeatureFlag: OverridableFlag { var key: String { - return "ff-override-\(String(describing: self))" + "ff-override-\(String(describing: self))" } var originalValue: Bool { - return enabled() + enabled() } var canOverride: Bool { From 931cc59024be6a9bfed7cd15b1b8c50301027e15 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:56:37 +1200 Subject: [PATCH 09/13] Remove bloganuaryDashboardNudge remote feature flag --- .../Utility/BuildInformation/RemoteFeatureFlag.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift index 82df956f7d10..68d0e5c1431c 100644 --- a/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/RemoteFeatureFlag.swift @@ -23,7 +23,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { case domainManagement case dynamicDashboardCards case plansInSiteCreation - case bloganuaryDashboardNudge // pcdRpT-4FE-p2 case inAppRating case siteMonitoring case inAppUpdates @@ -77,8 +76,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return false case .plansInSiteCreation: return false - case .bloganuaryDashboardNudge: - return AppConfiguration.isJetpack case .inAppRating: return false case .siteMonitoring: @@ -141,8 +138,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return "dynamic_dashboard_cards" case .plansInSiteCreation: return "plans_in_site_creation" - case .bloganuaryDashboardNudge: - return "bloganuary_dashboard_nudge" case .inAppRating: return "in_app_rating_and_feedback" case .siteMonitoring: @@ -204,8 +199,6 @@ public enum RemoteFeatureFlag: Int, CaseIterable { return "Dynamic Dashboard Cards" case .plansInSiteCreation: return "Plans in Site Creation" - case .bloganuaryDashboardNudge: - return "Bloganuary Dashboard Nudge" case .inAppRating: return "In-App Rating and Feedback" case .siteMonitoring: From a08beaa9a1ef4935e4774431f6a47a883fd8a122 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 13:58:07 +1200 Subject: [PATCH 10/13] Delete unused Bloganuary image assets --- .../bloganuary-icon-page.imageset/Contents.json | 15 --------------- .../bloganuary-icon-page.pdf | Bin 1770 -> 0 bytes .../Contents.json | 15 --------------- .../blognuary-icon-people.pdf | Bin 3651 -> 0 bytes .../Contents.json | 15 --------------- .../bloganuary-icon-verse.pdf | Bin 2281 -> 0 bytes .../Contents.json | 12 ------------ .../logo-bloganuary-large.pdf | Bin 97896 -> 0 bytes 8 files changed, 57 deletions(-) delete mode 100644 WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/Contents.json delete mode 100644 WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/bloganuary-icon-page.pdf delete mode 100644 WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/Contents.json delete mode 100644 WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/blognuary-icon-people.pdf delete mode 100644 WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/Contents.json delete mode 100644 WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/bloganuary-icon-verse.pdf delete mode 100644 WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/Contents.json delete mode 100644 WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/logo-bloganuary-large.pdf diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/Contents.json deleted file mode 100644 index b948e1f83a2e..000000000000 --- a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "bloganuary-icon-page.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/bloganuary-icon-page.pdf b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-page.imageset/bloganuary-icon-page.pdf deleted file mode 100644 index de83d3086a4e639d06fa11d795a588f76be8a7cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1770 zcmaKt-*3|}5XayBSKLdb4QWZ7#7!$rlUU0T0z}Dpn|KJR*9Nr-BpDj~^_(61&K(tb zsGN`QzW2kQFu?#1IP(TdN)j3qa7{D3mEXc}Bnrc0VzIbob<@$0tj#sMC_ z;(Ws={_^{WhImu$v;!aYwyAErXL#V7Mt3QtgXfi3D)rDk^|cQk$H!H#2JH3!`b zb9(RA*p7>#JL*fgXttMAQ{uAgMlc222FLf!AC1}Df2FD)Z8=iq3o)C$`E>Ue!>Mlh diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/Contents.json deleted file mode 100644 index 34fb916a0055..000000000000 --- a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "blognuary-icon-people.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/blognuary-icon-people.pdf b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-people.imageset/blognuary-icon-people.pdf deleted file mode 100644 index bd3dbfb06a95f5e61a50c137bbe78cea2cca16f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3651 zcmZu!U60!~5Pava;7fp{KxFYlq6h>5n!Bbb+M-vdZ$TeiopR^GAFb^aX@7l(6gi~j z7(q@Rle;^+%N_B>!~Of$s&PUn*~;S|e+ntzypeC;is|qez6xFAtDmOB)8$zjfa|d8 zd^t^%uDO@}hBHc5)dX&f`HS zM4mUrzpca`-L$IH%E3|s0qNd%P66uM&U8)!vH;ovkn?)*Mvh@?Z!P`-^h0Y+8`dQk zL!*s#gSmo|gSeIJTp!&RYaS87eYBRm`bM{K?`E$lI^P%%_t8b-;K_kp3Bjvs>=1(@ z2uPb4da;J2*+{YW+BQQMQ&FO83)jq{f(q9P%sHX8k~w;wKg7=pHVTHIFun?u%E|B9iDIdl?>3XJP=MM`so&YC1xtHUFv3*?b8tb9~n~w|#&N(A83eFBvD3RAaPKg94JQ<1n?I?fNFPY%*rs9)u?A3IN z2*Mub-=a+bayxqhAlxik48bh>5YTPAf^+QZq4Y{g1DmCWQi>5Y(^VzmS#+5nkN>{L zk_aG{pq{3vXE%#%kYb8#H*0Ya`lQ9kaS-etNa{=GfygM3k05nHo4G0}9WB``bd^7o zb&kr!bW_qRB-tKCg(WehB`PNUVUA=uhX^e6myxq?$&bj0e5NcAW2UI&%xC3~S!YyL zwNm62LVmYcI7FD-aRugJTDdP62YsJaje{P2b0ejg(F?^z{0muTCOFVTrI%uW={!pr z6+>qwtAs^oA!Af{@0-~$aw6mx#&lXmxuYdYBK_6M$*_uMGxj3Sq*$O(?Hg%lx{5Vs zwf4&7GdLEF1c+2LZ?RDDPv3}x_*I1W{q!=Q-~7W%{(K`a5OZD#EMZ&}R^J{D$IDrM z{2dnud{)2x``08_51Xec0Y6WV+s(V<7x}Tg{&N&-q$_@8dCHd7)A{)MG)`w3t|seb z?(TTFOb2)j_mT`gY)-&rm!RtQ5+L4cRzGeZ5vmLf)-wCKJATGqbrb&?NM)(NvJ?oO z>97#L+gvug_OA79N$S*?%9iwuNgf>$55|4oQKUiUVq QOQd70_Tt5h?|ymzKWiq~5dZ)H diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/Contents.json deleted file mode 100644 index 849eed1d57f2..000000000000 --- a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "bloganuary-icon-verse.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/bloganuary-icon-verse.pdf b/WordPress/Resources/AppImages.xcassets/bloganuary-icon-verse.imageset/bloganuary-icon-verse.pdf deleted file mode 100644 index 28cfb92eb06788e876d021d379b042d10b867ada..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2281 zcmZuzO>fjN5WV|X%%xI$XkzDcBUP2?mLdd*Wy`JN5VCH&D4Rf%qQb9d9M6oiEjd)P zFZ1O$&zYWVu5ZpwR4QdaL;wAU0=T$<%S&auo%^YhB_4mY-NW<FcU`|KJV_rzAtJZH?a%xYoKQn6PR_$K1rjOq9bV^)~~nw^1? z3yMgbrs&$0JUgOC2X~RK$jxb(WKgmUGB!kIDc7SSPK(kz@pNqk&rbbgL2)x0$cTn9~a_Pi~|z<5^Dn{(sUm*^7|hqFI|;;)u}Ja zZ6#Vcyr0}E@_Wk^nU=Mc35@G3e$#dRG{W6CTyOZqU;q5H5O3-SOTZ_)+t*k9Gu(wG zCG`{e{s)UQZSB}U4UHYaEzm8cTld{$JFMuoCxe@MKn!w%7CY8KvU}qX`yI9l?gU%l zI_v%kW6i7nM?@Bxila>KfTI))2Vd7y-S!X1q0fW8S14rVN8_a+2-9OU%ocP4 zrP!x?1Ttm%(#w=Nj_KH|O0d@>$mvm_X{h(D9h95i`)j1*bnFlI3{LmEGfzsq?fVJb efoFo_+x=gQ-OvA&)Wb9%C((JOPEOu_zWE1Xam|td diff --git a/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/Contents.json deleted file mode 100644 index 30a38a27b61a..000000000000 --- a/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "logo-bloganuary-large.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/logo-bloganuary-large.pdf b/WordPress/Resources/AppImages.xcassets/logo-bloganuary-large.imageset/logo-bloganuary-large.pdf deleted file mode 100644 index d083793dc79b4ed22f48fa082f4fcc1fc2a5f26d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97896 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 Date: Fri, 22 May 2026 14:11:59 +1200 Subject: [PATCH 11/13] Apply swift-format to BloggingPrompt and attribution files --- .../Swift/BloggingPrompt+CoreDataClass.swift | 11 ++++++----- .../Swift/BloggingPromptRemoteObject.swift | 6 ++++-- .../BloggingPromptsAttribution+Strings.swift | 14 ++++++++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Sources/WordPressData/Swift/BloggingPrompt+CoreDataClass.swift b/Sources/WordPressData/Swift/BloggingPrompt+CoreDataClass.swift index 8a103f8df53b..b5bb9c7ba236 100644 --- a/Sources/WordPressData/Swift/BloggingPrompt+CoreDataClass.swift +++ b/Sources/WordPressData/Swift/BloggingPrompt+CoreDataClass.swift @@ -5,11 +5,11 @@ import CoreData public class BloggingPrompt: NSManagedObject { @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: Self.classNameWithoutNamespaces()) + NSFetchRequest(entityName: Self.classNameWithoutNamespaces()) } @nonobjc public class func newObject(in context: NSManagedObjectContext) -> BloggingPrompt? { - return NSEntityDescription.insertNewObject(forEntityName: Self.entityName(), into: context) as? BloggingPrompt + NSEntityDescription.insertNewObject(forEntityName: Self.entityName(), into: context) as? BloggingPrompt } public override func awakeFromInsert() { @@ -43,7 +43,7 @@ public class BloggingPrompt: NSManagedObject { } public func textForDisplay() -> String { - return text.stringByDecodingXMLCharacters().trim() + text.stringByDecodingXMLCharacters().trim() } /// Convenience method that checks if the given date is within the same day of the prompt's date without considering the timezone information. @@ -55,7 +55,7 @@ public class BloggingPrompt: NSManagedObject { /// - localDate: The date to compare against in local timezone. /// - Returns: True if the year, month, and day components of the `localDate` matches the prompt's localized date. public func inSameDay(as dateToCompare: Date) -> Bool { - return DateFormatters.utc.string(from: date) == DateFormatters.local.string(from: dateToCompare) + DateFormatters.utc.string(from: date) == DateFormatters.local.string(from: dateToCompare) } /// Used for comparison on upsert – there can't be two `BloggingPrompt` objects with the same date, so we can use it as a unique identifier @@ -89,7 +89,8 @@ private extension BloggingPrompt { init?(with remotePrompt: BloggingPromptRemoteObject) { // Bloganuary context if let bloganuaryId = remotePrompt.bloganuaryId, - bloganuaryId.contains(Constants.bloganuaryTag) { + bloganuaryId.contains(Constants.bloganuaryTag) + { self = .bloganuary(bloganuaryId) return } diff --git a/Sources/WordPressData/Swift/BloggingPromptRemoteObject.swift b/Sources/WordPressData/Swift/BloggingPromptRemoteObject.swift index d0c43500efc7..eb3e203bfb0c 100644 --- a/Sources/WordPressData/Swift/BloggingPromptRemoteObject.swift +++ b/Sources/WordPressData/Swift/BloggingPromptRemoteObject.swift @@ -65,7 +65,8 @@ extension BloggingPromptRemoteObject: Decodable { self.answeredLink = { guard let linkURLString = try? container.decode(String.self, forKey: .answeredLink), - let answeredLinkURL = URL(string: linkURLString) else { + let answeredLinkURL = URL(string: linkURLString) + else { return nil } return answeredLinkURL @@ -75,7 +76,8 @@ extension BloggingPromptRemoteObject: Decodable { self.bloganuaryId = { guard let remoteBloganuaryId = try? container.decode(String.self, forKey: .bloganuaryId), - !remoteBloganuaryId.isEmpty else { + !remoteBloganuaryId.isEmpty + else { return nil } return remoteBloganuaryId diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution+Strings.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution+Strings.swift index 0bbbafbbea7e..8ce3347578c4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution+Strings.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution+Strings.swift @@ -48,7 +48,10 @@ extension BloggingPromptsAttribution { } private struct Strings { - static let fromTextFormat = NSLocalizedString("From %1$@", comment: "Format for blogging prompts attribution. %1$@ is the attribution source.") + static let fromTextFormat = NSLocalizedString( + "From %1$@", + comment: "Format for blogging prompts attribution. %1$@ is the attribution source." + ) static let dayOne = "Day One" static let bloganuary = "Bloganuary" } @@ -56,18 +59,21 @@ extension BloggingPromptsAttribution { private struct Constants { static let baseAttributes: [NSAttributedString.Key: Any] = [ .font: WPStyleGuide.fontForTextStyle(.caption1), - .foregroundColor: UIColor.secondaryLabel, + .foregroundColor: UIColor.secondaryLabel ] static let sourceAttributes: [NSAttributedString.Key: Any] = [ .font: WPStyleGuide.fontForTextStyle(.caption1, fontWeight: .medium), - .foregroundColor: UIColor.label, + .foregroundColor: UIColor.label ] static let dayOneIconSize = CGSize(width: 18, height: 18) static let dayOneIcon = UIImage(named: "logo-dayone")?.resized(to: Constants.dayOneIconSize) static let dayOneURL = URL(string: "https://dayoneapp.com/?utm_source=jetpack&utm_medium=prompts") static let linkIconSize = CGFloat(10) - static let linkIcon = UIImage(systemName: "link", withConfiguration: UIImage.SymbolConfiguration(pointSize: linkIconSize)) + static let linkIcon = UIImage( + systemName: "link", + withConfiguration: UIImage.SymbolConfiguration(pointSize: linkIconSize) + ) /// This is computed so it can react accordingly on color scheme changes. static var bloganuaryIcon: UIImage? { From 8ef39aa3225fb2df2f95da188a05668d2122ac05 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 14:16:18 +1200 Subject: [PATCH 12/13] Remove Bloganuary prompt attribution and remote parsing --- .../Swift/BloggingPrompt+CoreDataClass.swift | 35 ------------------- .../Swift/BloggingPromptRemoteObject.swift | 11 ------ .../Swift/BloggingPromptsAttribution.swift | 1 - .../BloggingPromptsAttribution+Strings.swift | 16 --------- 4 files changed, 63 deletions(-) diff --git a/Sources/WordPressData/Swift/BloggingPrompt+CoreDataClass.swift b/Sources/WordPressData/Swift/BloggingPrompt+CoreDataClass.swift index b5bb9c7ba236..4f443f4a18b8 100644 --- a/Sources/WordPressData/Swift/BloggingPrompt+CoreDataClass.swift +++ b/Sources/WordPressData/Swift/BloggingPrompt+CoreDataClass.swift @@ -36,10 +36,6 @@ public class BloggingPrompt: NSManagedObject { self.answerCount = Int32(remotePrompt.answeredUsersCount) self.displayAvatarURLs = remotePrompt.answeredUserAvatarURLs self.additionalPostTags = [String]() // reset previously additional tags. - - if let brandContext = BrandContext(with: remotePrompt) { - brandContext.configure(self) - } } public func textForDisplay() -> String { @@ -79,37 +75,6 @@ public extension BloggingPrompt { private extension BloggingPrompt { - enum Constants { - static let bloganuaryTag = "bloganuary" - } - - enum BrandContext { - case bloganuary(String) - - init?(with remotePrompt: BloggingPromptRemoteObject) { - // Bloganuary context - if let bloganuaryId = remotePrompt.bloganuaryId, - bloganuaryId.contains(Constants.bloganuaryTag) - { - self = .bloganuary(bloganuaryId) - return - } - - return nil - } - - /// Configures the given prompt with additional data based on the brand context. - /// - /// - Parameter prompt: The `BloggingPrompt` instance to configure. - func configure(_ prompt: BloggingPrompt) { - switch self { - case .bloganuary(let id): - prompt.additionalPostTags = [Constants.bloganuaryTag, id] - prompt.attribution = BloggingPromptsAttribution.bloganuary.rawValue - } - } - } - struct DateFormatters { static let local: DateFormatter = { let formatter = DateFormatter() diff --git a/Sources/WordPressData/Swift/BloggingPromptRemoteObject.swift b/Sources/WordPressData/Swift/BloggingPromptRemoteObject.swift index eb3e203bfb0c..211e847c525e 100644 --- a/Sources/WordPressData/Swift/BloggingPromptRemoteObject.swift +++ b/Sources/WordPressData/Swift/BloggingPromptRemoteObject.swift @@ -11,7 +11,6 @@ public struct BloggingPromptRemoteObject { let answeredUserAvatarURLs: [URL] let answeredLink: URL? let answeredLinkText: String - let bloganuaryId: String? /// Used for comparison on import public var dateString: String { @@ -32,7 +31,6 @@ extension BloggingPromptRemoteObject: Decodable { case answeredUserAvatarURLs = "answered_users_sample" case answeredLink = "answered_link" case answeredLinkText = "answered_link_text" - case bloganuaryId = "bloganuary_id" } /// meta structure to simplify decoding logic for user avatar objects. @@ -73,14 +71,5 @@ extension BloggingPromptRemoteObject: Decodable { }() self.answeredLinkText = try container.decode(String.self, forKey: .answeredLinkText) - - self.bloganuaryId = { - guard let remoteBloganuaryId = try? container.decode(String.self, forKey: .bloganuaryId), - !remoteBloganuaryId.isEmpty - else { - return nil - } - return remoteBloganuaryId - }() } } diff --git a/Sources/WordPressData/Swift/BloggingPromptsAttribution.swift b/Sources/WordPressData/Swift/BloggingPromptsAttribution.swift index 2f580a809ed5..c4c0d48634a7 100644 --- a/Sources/WordPressData/Swift/BloggingPromptsAttribution.swift +++ b/Sources/WordPressData/Swift/BloggingPromptsAttribution.swift @@ -1,4 +1,3 @@ public enum BloggingPromptsAttribution: String { case dayone - case bloganuary } diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution+Strings.swift b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution+Strings.swift index 8ce3347578c4..9c51ea377f2d 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution+Strings.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Dashboard/Cards/Prompts/BloggingPromptsAttribution+Strings.swift @@ -21,21 +21,18 @@ extension BloggingPromptsAttribution { var source: String { switch self { case .dayone: return Strings.dayOne - case .bloganuary: return Strings.bloganuary } } var iconImage: UIImage? { switch self { case .dayone: return Constants.dayOneIcon - case .bloganuary: return Constants.bloganuaryIcon } } var externalURL: URL? { switch self { case .dayone: return Constants.dayOneURL - case .bloganuary: return nil } } @@ -53,7 +50,6 @@ extension BloggingPromptsAttribution { comment: "Format for blogging prompts attribution. %1$@ is the attribution source." ) static let dayOne = "Day One" - static let bloganuary = "Bloganuary" } private struct Constants { @@ -74,17 +70,5 @@ extension BloggingPromptsAttribution { systemName: "link", withConfiguration: UIImage.SymbolConfiguration(pointSize: linkIconSize) ) - - /// This is computed so it can react accordingly on color scheme changes. - static var bloganuaryIcon: UIImage? { - UIImage(named: "logo-bloganuary")? - .withRenderingMode(.alwaysTemplate) - .resized(to: Constants.bloganuaryIconSize) - .withAlignmentRectInsets(UIEdgeInsets(.all, -6.0)) - .withTintColor(.label) - } - - /// Unlike the dayOne icon, the bloganuary icon has no implicit 6px padding surrounding the icon. - static let bloganuaryIconSize = CGSize(width: 12, height: 12) } } From bd26aca41bc1093ba0e7f3bc4cd96371ac3d0c12 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 22 May 2026 14:16:24 +1200 Subject: [PATCH 13/13] Delete logo-bloganuary image asset --- .../logo-bloganuary.imageset/Contents.json | 15 --------------- .../logo-bloganuary.imageset/logo-bloganuary.svg | 3 --- 2 files changed, 18 deletions(-) delete mode 100644 WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/Contents.json delete mode 100644 WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/logo-bloganuary.svg diff --git a/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/Contents.json b/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/Contents.json deleted file mode 100644 index 4d3ca8dcd5a0..000000000000 --- a/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "logo-bloganuary.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/logo-bloganuary.svg b/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/logo-bloganuary.svg deleted file mode 100644 index 87debb9a3124..000000000000 --- a/WordPress/Resources/AppImages.xcassets/logo-bloganuary.imageset/logo-bloganuary.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -