Skip to content

Commit 9bd75e4

Browse files
dcalhounjkmassel
andauthored
refactor: Use GutenbergKit configuration builder (#24662)
* refactor: Use GutenbergKit configuration builder Adopt the latest patterns to embrace immutable configuration. * Adopt GutenbergKit EditorConfigurationBuilder * Clean up Editor Configuration initialization * Fix a Core Data threading issue * Load the post when starting the editor * Fix JSON conversion issue * Set the postID and postType, if present * build: Update GutenbergKit version --------- Co-authored-by: Jeremy Massel <[email protected]>
1 parent 642e124 commit 9bd75e4

File tree

10 files changed

+167
-155
lines changed

10 files changed

+167
-155
lines changed

Modules/Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ let package = Package(
5353
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
5454
// We can't use wordpress-rs branches nor commits here. Only tags work.
5555
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250901"),
56-
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.8.0"),
56+
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.8.1-alpha.2"),
5757
.package(
5858
url: "https://github.com/Automattic/color-studio",
5959
revision: "bf141adc75e2769eb469a3e095bdc93dc30be8de"

Modules/Sources/WordPressKit/WordPressOrgRestApi.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ public final class WordPressOrgRestApi: NSObject {
103103
await perform(.get, path: path, parameters: parameters, options: options)
104104
}
105105

106+
public func get(
107+
path: String,
108+
parameters: [String: Any]? = nil
109+
) async -> WordPressAPIResult<Data, WordPressOrgRestApiError> {
110+
await perform(.get, path: path, parameters: parameters) { $0 }
111+
}
112+
106113
public func post<Success: Decodable>(
107114
path: String,
108115
parameters: [String: Any]? = nil,

Sources/WordPressData/Swift/AbstractPost.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,16 @@ public extension AbstractPost {
132132
]
133133
}
134134

135+
/// The post type as recorded in the `post_type` column of `wp_posts`
136+
///
137+
var wpPostType: String {
138+
return switch self {
139+
case is Post: "post"
140+
case is Page: "page"
141+
default: preconditionFailure("Unknown post type")
142+
}
143+
}
144+
135145
var analyticsPostType: String? {
136146
switch self {
137147
case is Post:

WordPress/Classes/Services/BlockEditorCache.swift

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,20 @@ final class BlockEditorCache {
2121

2222
// MARK: - Block Settings
2323

24-
func saveBlockSettings(_ settings: [String: Any], for blogID: TaggedManagedObjectID<Blog>) {
24+
func saveBlockSettings(_ settings: Data, for blogID: TaggedManagedObjectID<Blog>) throws {
2525
let fileURL = makeBlockSettingsURL(for: blogID)
26-
27-
do {
28-
let data = try JSONSerialization.data(withJSONObject: settings, options: [.prettyPrinted])
29-
try data.write(to: fileURL)
30-
} catch {
31-
DDLogError("Failed to save block editor settings: \(error)")
32-
}
26+
try settings.write(to: fileURL)
3327
}
3428

35-
func getBlockSettings(for blogID: TaggedManagedObjectID<Blog>) -> [String: Any]? {
29+
func getBlockSettings(for blogID: TaggedManagedObjectID<Blog>) -> Data? {
3630
let fileURL = makeBlockSettingsURL(for: blogID)
3731

3832
guard FileManager.default.fileExists(atPath: fileURL.path) else {
3933
return nil
4034
}
4135

4236
do {
43-
let data = try Data(contentsOf: fileURL)
44-
let object = try JSONSerialization.jsonObject(with: data, options: [])
45-
return object as? [String: Any]
37+
return try Data(contentsOf: fileURL)
4638
} catch {
4739
DDLogError("Failed to load block editor settings: \(error)")
4840
// If the file is corrupted, delete it

WordPress/Classes/Services/RawBlockEditorSettingsService.swift

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ import WordPressKit
44
import WordPressShared
55

66
final class RawBlockEditorSettingsService {
7+
78
private let blog: Blog
8-
private var refreshTask: Task<[String: Any], Error>?
9+
private var refreshTask: Task<Data, Error>?
10+
private let dotOrgRestAPI: WordPressOrgRestApi
911

1012
init(blog: Blog) {
1113
self.blog = blog
14+
self.dotOrgRestAPI = WordPressOrgRestApi(blog: blog)!
1215
}
1316

1417
private static var services: [TaggedManagedObjectID<Blog>: RawBlockEditorSettingsService] = [:]
@@ -24,27 +27,6 @@ final class RawBlockEditorSettingsService {
2427
return service
2528
}
2629

27-
@MainActor
28-
private func fetchSettingsFromAPI() async throws -> [String: Any] {
29-
guard let remoteAPI = WordPressOrgRestApi(blog: blog) else {
30-
throw URLError(.unknown) // Should not happen
31-
}
32-
let result = await remoteAPI.get(path: "/wp-block-editor/v1/settings")
33-
switch result {
34-
case .success(let response):
35-
guard let dictionary = response as? [String: Any] else {
36-
throw NSError(domain: "RawBlockEditorSettingsService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"])
37-
}
38-
let blogID = TaggedManagedObjectID(blog)
39-
Task {
40-
await saveSettingsInBackground(dictionary, for: blogID)
41-
}
42-
return dictionary
43-
case .failure(let error):
44-
throw error
45-
}
46-
}
47-
4830
/// Refreshes the editor settings in the background.
4931
func refreshSettings() {
5032
Task { @MainActor in
@@ -53,7 +35,7 @@ final class RawBlockEditorSettingsService {
5335
}
5436

5537
@MainActor
56-
private func fetchSettings() async throws -> [String: Any] {
38+
private func fetchSettings() async throws -> Data {
5739
if let task = refreshTask {
5840
return try await task.value
5941
}
@@ -70,23 +52,52 @@ final class RawBlockEditorSettingsService {
7052
return try await task.value
7153
}
7254

55+
private func fetchSettingsFromAPI() async throws -> Data {
56+
57+
let response: WordPressAPIResult<Data, WordPressOrgRestApiError> = await dotOrgRestAPI.get(
58+
path: "/wp-block-editor/v1/settings"
59+
)
60+
61+
let data = try response.get() // Unwrap the result type
62+
63+
let blogID = TaggedManagedObjectID(blog)
64+
saveSettingsInBackground(data, for: blogID)
65+
66+
return data
67+
}
68+
7369
/// Returns cached settings if available. If not, fetches the settings from
7470
/// the network.
7571
@MainActor
76-
func getSettings() async throws -> [String: Any] {
72+
func getSettings() async throws -> Data {
7773
// Return cached settings if available
7874
let blogID = TaggedManagedObjectID(blog)
7975
if let cachedSettings = await loadSettingsInBackground(for: blogID) {
8076
return cachedSettings
8177
}
8278
return try await fetchSettings()
8379
}
80+
81+
@MainActor
82+
func getSettingsString() async throws -> String {
83+
let data = try await getSettings()
84+
guard let string = String(data: data, encoding: .utf8) else {
85+
throw CocoaError(.fileReadCorruptFile)
86+
}
87+
return string
88+
}
8489
}
8590

86-
private func saveSettingsInBackground(_ settings: [String: Any], for blogID: TaggedManagedObjectID<Blog>) async {
87-
BlockEditorCache.shared.saveBlockSettings(settings, for: blogID)
91+
private func saveSettingsInBackground(_ settings: Data, for blogID: TaggedManagedObjectID<Blog>) {
92+
Task {
93+
do {
94+
try BlockEditorCache.shared.saveBlockSettings(settings, for: blogID)
95+
} catch {
96+
wpAssertionFailure("Unable to save block settings", userInfo: ["error": error])
97+
}
98+
}
8899
}
89100

90-
private func loadSettingsInBackground(for blogID: TaggedManagedObjectID<Blog>) async -> [String: Any]? {
101+
private func loadSettingsInBackground(for blogID: TaggedManagedObjectID<Blog>) async -> Data? {
91102
BlockEditorCache.shared.getBlockSettings(for: blogID)
92103
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import Foundation
2+
import GutenbergKit
3+
import WordPressData
4+
import WordPressShared
5+
6+
extension EditorConfiguration {
7+
init(blog: Blog, keychain: KeychainAccessible = KeychainUtils()) {
8+
let selfHostedApiUrl = blog.restApiRootURL ?? blog.url(withPath: "wp-json/")
9+
let applicationPassword = try? blog.getApplicationToken(using: keychain)
10+
let shouldUseWPComRestApi = applicationPassword == nil && blog.isAccessibleThroughWPCom()
11+
12+
let siteApiRoot: String?
13+
if applicationPassword != nil {
14+
siteApiRoot = selfHostedApiUrl
15+
} else {
16+
siteApiRoot = shouldUseWPComRestApi ? blog.wordPressComRestApi?.baseURL.absoluteString : selfHostedApiUrl
17+
}
18+
19+
let siteId = blog.dotComID?.stringValue
20+
let siteDomain = blog.primaryDomainAddress
21+
let authToken = blog.authToken ?? ""
22+
var authHeader = "Bearer \(authToken)"
23+
24+
if let appPassword = applicationPassword, let username = blog.username {
25+
let credentials = "\(username):\(appPassword)"
26+
if let credentialsData = credentials.data(using: .utf8) {
27+
let base64Credentials = credentialsData.base64EncodedString()
28+
authHeader = "Basic \(base64Credentials)"
29+
}
30+
}
31+
32+
// Must provide both namespace forms to detect usages of both forms in third-party code
33+
var siteApiNamespace: [String] = []
34+
if shouldUseWPComRestApi {
35+
if let siteId {
36+
siteApiNamespace.append("sites/\(siteId)/")
37+
}
38+
siteApiNamespace.append("sites/\(siteDomain)/")
39+
}
40+
41+
var builder = EditorConfigurationBuilder()
42+
.setSiteApiNamespace(siteApiNamespace)
43+
.setNamespaceExcludedPaths(["/wpcom/v2/following/recommendations", "/wpcom/v2/following/mine"])
44+
.setAuthHeader(authHeader)
45+
.setShouldUseThemeStyles(FeatureFlag.newGutenbergThemeStyles.enabled)
46+
// Limited to Jetpack-connected sites until editor assets endpoint is available in WordPress core
47+
.setShouldUsePlugins(Self.shouldEnablePlugins(for: blog, appPassword: applicationPassword))
48+
.setLocale(WordPressComLanguageDatabase().deviceLanguage.slug)
49+
50+
if let blogUrl = blog.url {
51+
builder = builder.setSiteUrl(blogUrl)
52+
}
53+
54+
if let siteApiRoot {
55+
builder = builder.setSiteApiRoot(siteApiRoot)
56+
57+
if var editorAssetsEndpoint = URL(string: siteApiRoot) {
58+
editorAssetsEndpoint.appendPathComponent("wpcom/v2/")
59+
if let namespace = siteApiNamespace.first {
60+
editorAssetsEndpoint.appendPathComponent(namespace)
61+
}
62+
63+
editorAssetsEndpoint.appendPathComponent("editor-assets")
64+
builder = builder.setEditorAssetsEndpoint(editorAssetsEndpoint)
65+
}
66+
}
67+
68+
self = builder.build()
69+
}
70+
71+
/// Returns true if the plugins should be enabled for the given blog.
72+
/// This is used to determine if the editor should load third-party
73+
/// plugins providing blocks.
74+
static func shouldEnablePlugins(for blog: Blog, appPassword: String? = nil) -> Bool {
75+
// Requires a Jetpack until editor assets endpoint is available in WordPress core.
76+
// Requires a WP.com Simple site or an application password to authenticate all REST
77+
// API requests, including those originating from non-core blocks.
78+
return RemoteFeatureFlag.newGutenbergPlugins.enabled() &&
79+
blog.isAccessibleThroughWPCom() &&
80+
(blog.isHostedAtWPcom || appPassword != nil)
81+
}
82+
}

WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite
172172

173173
if RemoteFeatureFlag.newGutenberg.enabled() {
174174
GutenbergKit.EditorViewController.warmup(
175-
configuration: blog.flatMap({ EditorConfiguration.init(blog: $0) }) ?? .default
175+
configuration: blog.flatMap { EditorConfiguration(blog: $0) } ?? .default
176176
)
177177
}
178178
}

WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ final class CommentGutenbergEditorViewController: UIViewController {
3535
override func viewDidLoad() {
3636
super.viewDidLoad()
3737

38-
var configuration = EditorConfiguration(content: initialContent ?? "")
39-
configuration.hideTitle = true
38+
let configuration = EditorConfigurationBuilder(content: initialContent ?? "")
39+
.setShouldHideTitle(true)
40+
.build()
4041

4142
let editorVC = GutenbergKit.EditorViewController(configuration: configuration)
4243
editorVC.delegate = self

0 commit comments

Comments
 (0)