From 27c6f3ec249eab0fc5cfba27b3c62dd53743c180 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Sun, 12 Oct 2025 21:42:01 -0400 Subject: [PATCH 1/9] task: iOS demo app discovers remote editors Simplify adding remote editors via discovery and authentication with wordpress-rs. --- .../Gutenberg.xcodeproj/project.pbxproj | 40 +++++ .../xcshareddata/swiftpm/Package.resolved | 9 + ios/Demo-iOS/Sources/AddSiteView.swift | 85 ++++++++++ .../Sources/AuthenticationManager.swift | 132 +++++++++++++++ ios/Demo-iOS/Sources/ConfigurationItem.swift | 51 ++++++ .../Sources/ConfigurationStorage.swift | 43 +++++ ios/Demo-iOS/Sources/ContentView.swift | 158 +++++++++++------- 7 files changed, 453 insertions(+), 65 deletions(-) create mode 100644 ios/Demo-iOS/Sources/AddSiteView.swift create mode 100644 ios/Demo-iOS/Sources/AuthenticationManager.swift create mode 100644 ios/Demo-iOS/Sources/ConfigurationItem.swift create mode 100644 ios/Demo-iOS/Sources/ConfigurationStorage.swift diff --git a/ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj b/ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj index 181892338..9f837d445 100644 --- a/ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj +++ b/ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 0C4F59A22BEFF4980028BD96 /* AddSiteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4F59A32BEFF4980028BD96 /* AddSiteView.swift */; }; + 0C4F59A42BEFF4980028BD96 /* AuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4F59A52BEFF4980028BD96 /* AuthenticationManager.swift */; }; + 0C4F59A62BEFF4980028BD96 /* ConfigurationItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4F59A72BEFF4980028BD96 /* ConfigurationItem.swift */; }; + 0C4F59A82BEFF4980028BD96 /* ConfigurationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4F59A92BEFF4980028BD96 /* ConfigurationStorage.swift */; }; 0CE8E78B2C339B0600B9DC67 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CE8E7842C339B0600B9DC67 /* Assets.xcassets */; }; 0CE8E78C2C339B0600B9DC67 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE8E7852C339B0600B9DC67 /* ContentView.swift */; }; 0CE8E78D2C339B0600B9DC67 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE8E7862C339B0600B9DC67 /* EditorView.swift */; }; @@ -17,6 +21,10 @@ /* Begin PBXFileReference section */ 0C4F598B2BEFF4970028BD96 /* Gutenberg.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Gutenberg.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0C4F59A32BEFF4980028BD96 /* AddSiteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSiteView.swift; sourceTree = ""; }; + 0C4F59A52BEFF4980028BD96 /* AuthenticationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationManager.swift; sourceTree = ""; }; + 0C4F59A72BEFF4980028BD96 /* ConfigurationItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItem.swift; sourceTree = ""; }; + 0C4F59A92BEFF4980028BD96 /* ConfigurationStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationStorage.swift; sourceTree = ""; }; 0CE8E7842C339B0600B9DC67 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 0CE8E7852C339B0600B9DC67 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 0CE8E7862C339B0600B9DC67 /* EditorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorView.swift; sourceTree = ""; }; @@ -68,6 +76,10 @@ 0CE8E7882C339B0600B9DC67 /* Sources */ = { isa = PBXGroup; children = ( + 0C4F59A32BEFF4980028BD96 /* AddSiteView.swift */, + 0C4F59A52BEFF4980028BD96 /* AuthenticationManager.swift */, + 0C4F59A72BEFF4980028BD96 /* ConfigurationItem.swift */, + 0C4F59A92BEFF4980028BD96 /* ConfigurationStorage.swift */, 0CE8E7852C339B0600B9DC67 /* ContentView.swift */, 0CE8E7862C339B0600B9DC67 /* EditorView.swift */, 0CE8E7872C339B0600B9DC67 /* GutenbergApp.swift */, @@ -108,6 +120,7 @@ name = Gutenberg; packageProductDependencies = ( 0CF6E04B2BEFF60E00EDEE8A /* GutenbergKit */, + 0C4F59A12BEFF4980028BD96 /* WordPressAPI */, ); productName = Gutenberg; productReference = 0C4F598B2BEFF4970028BD96 /* Gutenberg.app */; @@ -137,6 +150,9 @@ Base, ); mainGroup = 0C4F59822BEFF4970028BD96; + packageReferences = ( + 0C4F59A02BEFF4980028BD96 /* XCRemoteSwiftPackageReference "wordpress-rs" */, + ); productRefGroup = 0C4F598C2BEFF4970028BD96 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -163,6 +179,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0C4F59A22BEFF4980028BD96 /* AddSiteView.swift in Sources */, + 0C4F59A42BEFF4980028BD96 /* AuthenticationManager.swift in Sources */, + 0C4F59A62BEFF4980028BD96 /* ConfigurationItem.swift in Sources */, + 0C4F59A82BEFF4980028BD96 /* ConfigurationStorage.swift in Sources */, 0CE8E78E2C339B0600B9DC67 /* GutenbergApp.swift in Sources */, 0CE8E78C2C339B0600B9DC67 /* ContentView.swift in Sources */, 0CE8E78D2C339B0600B9DC67 /* EditorView.swift in Sources */, @@ -304,6 +324,8 @@ "DEVELOPMENT_TEAM[sdk=iphoneos*]" = PZYM8XX95Q; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleURLTypes = "$(INFOPLIST_KEY_CFBundleURLTypes)"; + "INFOPLIST_KEY_CFBundleURLTypes[0]" = "{ CFBundleURLSchemes = (gutenbergkit); }"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -336,6 +358,8 @@ DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleURLTypes = "$(INFOPLIST_KEY_CFBundleURLTypes)"; + "INFOPLIST_KEY_CFBundleURLTypes[0]" = "{ CFBundleURLSchemes = (gutenbergkit); }"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -379,7 +403,23 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 0C4F59A02BEFF4980028BD96 /* XCRemoteSwiftPackageReference "wordpress-rs" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Automattic/wordpress-rs"; + requirement = { + kind = revision; + revision = "alpha-20250926"; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ + 0C4F59A12BEFF4980028BD96 /* WordPressAPI */ = { + isa = XCSwiftPackageProductDependency; + package = 0C4F59A02BEFF4980028BD96 /* XCRemoteSwiftPackageReference "wordpress-rs" */; + productName = WordPressAPI; + }; 0CF6E04B2BEFF60E00EDEE8A /* GutenbergKit */ = { isa = XCSwiftPackageProductDependency; productName = GutenbergKit; diff --git a/ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3a469bac5..724d44ffe 100644 --- a/ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Demo-iOS/Gutenberg.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -8,6 +8,15 @@ "revision" : "aa85ee96017a730031bafe411cde24a08a17a9c9", "version" : "2.8.8" } + }, + { + "identity" : "wordpress-rs", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Automattic/wordpress-rs", + "state" : { + "branch" : "alpha-20250926", + "revision" : "13c6207d6beeeb66c21cd7c627e13817ca5fdcae" + } } ], "version" : 2 diff --git a/ios/Demo-iOS/Sources/AddSiteView.swift b/ios/Demo-iOS/Sources/AddSiteView.swift new file mode 100644 index 000000000..f501db987 --- /dev/null +++ b/ios/Demo-iOS/Sources/AddSiteView.swift @@ -0,0 +1,85 @@ +import SwiftUI +import AuthenticationServices + +/// View for adding a new remote editor site +struct AddSiteView: View { + @Binding var siteUrl: String + @ObservedObject var authenticationManager: AuthenticationManager + let onAdd: (RemoteEditorConfiguration) -> Void + let onCancel: () -> Void + + @State private var presentationContextProvider = WebAuthPresentationContextProvider() + + var body: some View { + NavigationStack { + Form { + Section { + TextField("Site URL", text: $siteUrl) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .keyboardType(.URL) + .onSubmit { + startAuthentication() + } + } header: { + Text("WordPress Site") + } footer: { + Text("Enter the URL of your WordPress site (e.g., https://example.com)") + } + + if let errorMessage = authenticationManager.errorMessage { + Section { + Text(errorMessage) + .foregroundColor(.red) + } + } + } + .navigationTitle("Add Remote Editor") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + onCancel() + } + .disabled(authenticationManager.isAuthenticating) + } + + ToolbarItem(placement: .confirmationAction) { + if authenticationManager.isAuthenticating { + ProgressView() + } else { + Button("Add") { + startAuthentication() + } + .disabled(siteUrl.trimmingCharacters(in: .whitespaces).isEmpty) + } + } + } + } + } + + private func startAuthentication() { + let trimmedUrl = siteUrl.trimmingCharacters(in: .whitespaces) + guard !trimmedUrl.isEmpty else { return } + + authenticationManager.startAuthentication( + siteUrl: trimmedUrl, + presentationContext: presentationContextProvider + ) { configuration in + onAdd(configuration) + } + } +} + +/// Provides the presentation context for web authentication +class WebAuthPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + // Return the first window that can be used as a presentation anchor + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + return window + } + // Fallback to a new window (shouldn't normally happen) + return ASPresentationAnchor() + } +} diff --git a/ios/Demo-iOS/Sources/AuthenticationManager.swift b/ios/Demo-iOS/Sources/AuthenticationManager.swift new file mode 100644 index 000000000..8bab00525 --- /dev/null +++ b/ios/Demo-iOS/Sources/AuthenticationManager.swift @@ -0,0 +1,132 @@ +import Foundation +import AuthenticationServices +import WordPressAPI + +/// Manages WordPress authentication flow +@MainActor +class AuthenticationManager: NSObject, ObservableObject { + @Published var isAuthenticating = false + @Published var errorMessage: String? + + private var currentApiRootUrl: String? + private var authSession: ASWebAuthenticationSession? + private var currentClient: WordPressLoginClient? + private var onAuthenticationComplete: ((RemoteEditorConfiguration) -> Void)? + + private static let appName = "GutenbergKit iOS Demo App" + private static let callbackURLScheme = "gutenbergkit" + + /// Start the authentication flow for a WordPress site + func startAuthentication( + siteUrl: String, + presentationContext: ASWebAuthenticationPresentationContextProviding, + onComplete: @escaping (RemoteEditorConfiguration) -> Void + ) { + isAuthenticating = true + errorMessage = nil + onAuthenticationComplete = onComplete + + Task { + do { + let client = WordPressLoginClient(urlSession: URLSession(configuration: .ephemeral)) + let details = try await client.details(ofSite: siteUrl) + + let apiRootUrl = details.apiRootUrl.url() + let appId = try! WpUuid.parse(input: "00000000-0000-4000-9000-000000000000") + let authUrl = details.loginURL(for: .init( + id: appId, + name: Self.appName, + callbackUrl: "\(Self.callbackURLScheme)://authorized" + )) + + currentApiRootUrl = apiRootUrl + currentClient = client + + launchAuthenticationFlow( + authenticationUrl: authUrl, + presentationContext: presentationContext + ) + } catch { + isAuthenticating = false + errorMessage = "Authentication error: \(error.localizedDescription)" + } + } + } + + /// Launch the web authentication session + private func launchAuthenticationFlow( + authenticationUrl: URL, + presentationContext: ASWebAuthenticationPresentationContextProviding + ) { + let session = ASWebAuthenticationSession( + url: authenticationUrl, + callbackURLScheme: Self.callbackURLScheme + ) { [weak self] callbackURL, error in + guard let self = self else { return } + + Task { @MainActor in + if let error = error { + if (error as? ASWebAuthenticationSessionError)?.code == .canceledLogin { + // User cancelled - just reset state + self.isAuthenticating = false + } else { + self.isAuthenticating = false + self.errorMessage = "Authentication failed: \(error.localizedDescription)" + } + return + } + + if let callbackURL = callbackURL { + self.processAuthenticationResult(callbackURL: callbackURL) + } + } + } + + session.presentationContextProvider = presentationContext + session.prefersEphemeralWebBrowserSession = false + + authSession = session + session.start() + } + + /// Process the authentication callback URL + private func processAuthenticationResult(callbackURL: URL) { + guard let client = currentClient, + let apiRootUrl = currentApiRootUrl else { + isAuthenticating = false + errorMessage = "Missing authentication parameters" + return + } + + do { + let credentials = try client.credentials(from: callbackURL) + + // Create Basic Auth header + let authString = "\(credentials.userLogin):\(credentials.password)" + guard let authData = authString.data(using: .utf8) else { + isAuthenticating = false + errorMessage = "Failed to encode credentials" + return + } + let authHeader = "Basic \(authData.base64EncodedString())" + + // Extract site name from URL + let siteName = URL(string: credentials.siteUrl)?.host ?? credentials.siteUrl + + let configuration = RemoteEditorConfiguration( + name: siteName, + siteUrl: credentials.siteUrl, + siteApiRoot: apiRootUrl, + authHeader: authHeader + ) + + isAuthenticating = false + currentApiRootUrl = nil + currentClient = nil + onAuthenticationComplete?(configuration) + } catch { + isAuthenticating = false + errorMessage = "Failed to parse credentials: \(error.localizedDescription)" + } + } +} diff --git a/ios/Demo-iOS/Sources/ConfigurationItem.swift b/ios/Demo-iOS/Sources/ConfigurationItem.swift new file mode 100644 index 000000000..e79790336 --- /dev/null +++ b/ios/Demo-iOS/Sources/ConfigurationItem.swift @@ -0,0 +1,51 @@ +import Foundation + +/// Represents a configuration item for the editor +enum ConfigurationItem: Codable, Identifiable { + case bundledEditor + case remoteEditor(RemoteEditorConfiguration) + + var id: String { + switch self { + case .bundledEditor: + return "bundled" + case .remoteEditor(let config): + return config.id + } + } + + var displayName: String { + switch self { + case .bundledEditor: + return "Bundled Editor" + case .remoteEditor(let config): + return config.name + } + } + + var subtitle: String { + switch self { + case .bundledEditor: + return "Local editor with no plugins" + case .remoteEditor(let config): + return config.siteUrl + } + } +} + +/// Configuration for a remote editor +struct RemoteEditorConfiguration: Codable, Identifiable { + let id: String + let name: String + let siteUrl: String + let siteApiRoot: String + let authHeader: String + + init(name: String, siteUrl: String, siteApiRoot: String, authHeader: String) { + self.id = UUID().uuidString + self.name = name + self.siteUrl = siteUrl + self.siteApiRoot = siteApiRoot + self.authHeader = authHeader + } +} diff --git a/ios/Demo-iOS/Sources/ConfigurationStorage.swift b/ios/Demo-iOS/Sources/ConfigurationStorage.swift new file mode 100644 index 000000000..dcc62f718 --- /dev/null +++ b/ios/Demo-iOS/Sources/ConfigurationStorage.swift @@ -0,0 +1,43 @@ +import Foundation + +/// Manages persistence of remote editor configurations +class ConfigurationStorage { + private let userDefaults: UserDefaults + private let configurationsKey = "saved_configurations" + + init(userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + /// Load saved configurations from storage + func loadConfigurations() -> [ConfigurationItem] { + guard let data = userDefaults.data(forKey: configurationsKey) else { + return [] + } + + do { + let remoteConfigs = try JSONDecoder().decode([RemoteEditorConfiguration].self, from: data) + return remoteConfigs.map { .remoteEditor($0) } + } catch { + NSLog("Failed to decode configurations: \(error)") + return [] + } + } + + /// Save configurations to storage + func saveConfigurations(_ configurations: [ConfigurationItem]) { + let remoteConfigs = configurations.compactMap { item -> RemoteEditorConfiguration? in + if case .remoteEditor(let config) = item { + return config + } + return nil + } + + do { + let data = try JSONEncoder().encode(remoteConfigs) + userDefaults.set(data, forKey: configurationsKey) + } catch { + NSLog("Failed to encode configurations: \(error)") + } + } +} diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift index 6a47966d7..c5357c073 100644 --- a/ios/Demo-iOS/Sources/ContentView.swift +++ b/ios/Demo-iOS/Sources/ContentView.swift @@ -1,39 +1,40 @@ import SwiftUI import GutenbergKit +import AuthenticationServices struct ContentView: View { - private let remoteEditors: [RemoteEditorRow] = [ - .init(id: "template", configuration: .template) - ] - - @State private var isDefaultEditorShown = false - @State private var selectedRemoteEditor: RemoteEditorRow? + @State private var selectedConfiguration: ConfigurationItem? + @State private var configurations: [ConfigurationItem] = [.bundledEditor] + @State private var showAddDialog = false + @State private var siteUrlInput = "" + @State private var authenticationManager = AuthenticationManager() + @State private var configurationStorage = ConfigurationStorage() + @State private var configurationToDelete: ConfigurationItem? @AppStorage("isNativeInserterEnabled") private var isNativeInserterEnabled = false var body: some View { List { Section { - Button("Bundled Editor") { - isDefaultEditorShown = true - } - } - - Section { - ForEach(remoteEditors) { editor in - Button(editor.title) { - selectedRemoteEditor = editor + ForEach(configurations) { config in + Button(config.displayName) { + selectedConfiguration = config + } + .swipeActions(edge: .trailing) { + if case .remoteEditor = config { + Button(role: .destructive) { + configurationToDelete = config + } label: { + Label("Delete", systemImage: "trash") + } + } } - } - - if remoteEditors.isEmpty { - Text("Add `EditorConfiguration` instances to the `remoteEditorConfigurations` array to launch remote editors here.") } } header: { - Text("Remote Editors") + Text("Editors") } footer: { if ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"] != nil { - Text("Note: The editor is backed by the dev server created by `make dev-server-remote`.") + Text("Note: The editor is backed by the dev server created by `make dev-server` and `make dev-server-remote`.") } else { Text("Note: The editor is backed by the compiled web app created by `make build`.") } @@ -43,37 +44,58 @@ struct ContentView: View { Toggle("Native Inserter", isOn: $isNativeInserterEnabled) } } - .fullScreenCover(isPresented: $isDefaultEditorShown) { - NavigationView { - EditorView(configuration: preconfigure(.default)) - } - } - .fullScreenCover(item: $selectedRemoteEditor) { editor in + .fullScreenCover(item: $selectedConfiguration) { config in NavigationView { - EditorView(configuration: preconfigure(editor.configuration)) + EditorView(configuration: preconfigure(createEditorConfiguration(for: config))) } } + .navigationTitle("GutenbergKit") .toolbar { ToolbarItem(placement: .primaryAction) { Button { - Task { - NSLog("Start to fetch assets") - for editor in remoteEditors { - let library = EditorAssetsLibrary(configuration: editor.configuration) - do { - try await library.fetchAssets() - } catch { - NSLog("Failed to fetch assets for \(editor.configuration.siteURL): \(error)") - } - } - NSLog("Done fetching assets") - } + showAddDialog = true } label: { - Image(systemName: "arrow.clockwise") + Image(systemName: "plus") } - } } + .sheet(isPresented: $showAddDialog) { + AddSiteView( + siteUrl: $siteUrlInput, + authenticationManager: authenticationManager, + onAdd: { config in + configurations.append(.remoteEditor(config)) + configurationStorage.saveConfigurations(configurations) + showAddDialog = false + siteUrlInput = "" + }, + onCancel: { + showAddDialog = false + siteUrlInput = "" + } + ) + } + .onAppear { + loadConfigurations() + } + .alert( + "Delete Remote Editor?", + isPresented: Binding( + get: { configurationToDelete != nil }, + set: { if !$0 { configurationToDelete = nil } } + ), + presenting: configurationToDelete + ) { config in + Button("Delete", role: .destructive) { + deleteConfiguration(config) + configurationToDelete = nil + } + Button("Cancel", role: .cancel) { + configurationToDelete = nil + } + } message: { config in + Text("Are you sure you want to delete \"\(config.displayName)\"?") + } } private func preconfigure(_ configuration: EditorConfiguration) -> EditorConfiguration { @@ -82,37 +104,43 @@ struct ContentView: View { .setNativeInserterEnabled(isNativeInserterEnabled) .build() } -} -private struct RemoteEditorRow: Identifiable { - let id: String - let configuration: EditorConfiguration + private func loadConfigurations() { + let saved = configurationStorage.loadConfigurations() + configurations = [.bundledEditor] + saved + } - var title: String { - URL(string: configuration.siteURL)?.host ?? configuration.siteURL + private func deleteConfiguration(_ config: ConfigurationItem) { + configurations.removeAll { $0.id == config.id } + configurationStorage.saveConfigurations(configurations) } -} -private extension EditorConfiguration { + private func createEditorConfiguration(for item: ConfigurationItem) -> EditorConfiguration { + switch item { + case .bundledEditor: + return createBundledConfiguration() + case .remoteEditor(let config): + return createRemoteConfiguration(config) + } + } - static var template: Self { - // Steps: - // 1. Update the siteURL and authHeader values below - // 2. Install the Jetpack plugin to the site - let siteUrl: String = "https://modify-me.com" - let authHeader: String = "Insert the Authorization header value here" - let siteApiRoot: String = "\(siteUrl)/wp-json/" + private func createBundledConfiguration() -> EditorConfiguration { + EditorConfigurationBuilder() + .setShouldUsePlugins(false) + .setSiteUrl("") + .setSiteApiRoot("") + .setAuthHeader("") + .build() + } - let configuration = EditorConfigurationBuilder() - .setSiteUrl(siteUrl) - .setAuthHeader(authHeader) - .setSiteApiRoot(siteApiRoot) - .setEditorAssetsEndpoint(URL(string: siteApiRoot)!.appendingPathComponent("wpcom/v2/editor-assets")) + private func createRemoteConfiguration(_ config: RemoteEditorConfiguration) -> EditorConfiguration { + EditorConfigurationBuilder() .setShouldUsePlugins(true) - - return configuration.build() + .setSiteUrl(config.siteUrl) + .setSiteApiRoot(config.siteApiRoot) + .setAuthHeader(config.authHeader) + .build() } - } #Preview { From 288249ef46efb3825eec04c859ed525e1dc1dc29 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 20 Oct 2025 16:47:45 -0400 Subject: [PATCH 2/9] task: Remove unnecessary configuration subtitle --- ios/Demo-iOS/Sources/ConfigurationItem.swift | 9 ------- ios/Demo-iOS/Sources/ContentView.swift | 25 +++++++++++++------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ios/Demo-iOS/Sources/ConfigurationItem.swift b/ios/Demo-iOS/Sources/ConfigurationItem.swift index e79790336..d89397785 100644 --- a/ios/Demo-iOS/Sources/ConfigurationItem.swift +++ b/ios/Demo-iOS/Sources/ConfigurationItem.swift @@ -22,15 +22,6 @@ enum ConfigurationItem: Codable, Identifiable { return config.name } } - - var subtitle: String { - switch self { - case .bundledEditor: - return "Local editor with no plugins" - case .remoteEditor(let config): - return config.siteUrl - } - } } /// Configuration for a remote editor diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift index c5357c073..3041c0dd5 100644 --- a/ios/Demo-iOS/Sources/ContentView.swift +++ b/ios/Demo-iOS/Sources/ContentView.swift @@ -16,22 +16,31 @@ struct ContentView: View { var body: some View { List { Section { - ForEach(configurations) { config in + Button("Bundled Editor") { + selectedConfiguration = .bundledEditor + } + } footer: { + Text("Local editor with no plugins") + } + + Section { + ForEach(configurations.filter { + if case .remoteEditor = $0 { return true } + return false + }) { config in Button(config.displayName) { selectedConfiguration = config } .swipeActions(edge: .trailing) { - if case .remoteEditor = config { - Button(role: .destructive) { - configurationToDelete = config - } label: { - Label("Delete", systemImage: "trash") - } + Button(role: .destructive) { + configurationToDelete = config + } label: { + Label("Delete", systemImage: "trash") } } } } header: { - Text("Editors") + Text("Remote Editors") } footer: { if ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"] != nil { Text("Note: The editor is backed by the dev server created by `make dev-server` and `make dev-server-remote`.") From ffa323dcbdc1c925eeb2039d22d47b8aa60c3a6b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 20 Oct 2025 16:52:25 -0400 Subject: [PATCH 3/9] task: Relocate dev server note --- ios/Demo-iOS/Sources/ContentView.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift index 3041c0dd5..af33db091 100644 --- a/ios/Demo-iOS/Sources/ContentView.swift +++ b/ios/Demo-iOS/Sources/ContentView.swift @@ -19,6 +19,18 @@ struct ContentView: View { Button("Bundled Editor") { selectedConfiguration = .bundledEditor } + } header: { + if ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"] != nil { + Text("Note: The editor is backed by the dev server created by `make dev-server` and `make dev-server-remote`.") + .textCase(nil) + .font(.footnote) + .foregroundStyle(.secondary) + } else { + Text("Note: The editor is backed by the compiled web app created by `make build`.") + .textCase(nil) + .font(.footnote) + .foregroundStyle(.secondary) + } } footer: { Text("Local editor with no plugins") } @@ -41,12 +53,6 @@ struct ContentView: View { } } header: { Text("Remote Editors") - } footer: { - if ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"] != nil { - Text("Note: The editor is backed by the dev server created by `make dev-server` and `make dev-server-remote`.") - } else { - Text("Note: The editor is backed by the compiled web app created by `make build`.") - } } Section("Configuration") { From 1d1de2a1c8a24149216743c49605861d97267f76 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 20 Oct 2025 16:55:06 -0400 Subject: [PATCH 4/9] task: Add remote editor button --- ios/Demo-iOS/Sources/ContentView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift index af33db091..0e9b93d53 100644 --- a/ios/Demo-iOS/Sources/ContentView.swift +++ b/ios/Demo-iOS/Sources/ContentView.swift @@ -51,6 +51,10 @@ struct ContentView: View { } } } + + Button("Add New Remote Editor") { + showAddDialog = true + } } header: { Text("Remote Editors") } From a51bc90f2402db8dfbcbbca4be6293449a27323a Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 20 Oct 2025 16:57:25 -0400 Subject: [PATCH 5/9] task: Add remote editor note --- ios/Demo-iOS/Sources/ContentView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift index 0e9b93d53..0701847b8 100644 --- a/ios/Demo-iOS/Sources/ContentView.swift +++ b/ios/Demo-iOS/Sources/ContentView.swift @@ -57,6 +57,8 @@ struct ContentView: View { } } header: { Text("Remote Editors") + } footer: { + Text("Site-specific editor with plugins") } Section("Configuration") { From 93aa16719c5f0c445d1a43756030cf583e6c794d Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Mon, 20 Oct 2025 17:02:42 -0400 Subject: [PATCH 6/9] task: Dev server note checks both env variables --- ios/Demo-iOS/Sources/ContentView.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift index 0701847b8..44171c201 100644 --- a/ios/Demo-iOS/Sources/ContentView.swift +++ b/ios/Demo-iOS/Sources/ContentView.swift @@ -20,7 +20,9 @@ struct ContentView: View { selectedConfiguration = .bundledEditor } } header: { - if ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"] != nil { + if ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_URL"] != nil || + ProcessInfo.processInfo.environment["GUTENBERG_EDITOR_REMOTE_URL"] != nil + { Text("Note: The editor is backed by the dev server created by `make dev-server` and `make dev-server-remote`.") .textCase(nil) .font(.footnote) From 8ec32e809526b87dcf3725300878885a8219ddfb Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 21 Oct 2025 10:03:16 -0400 Subject: [PATCH 7/9] docs: Consistently use "bundled" editor term --- CLAUDE.md | 4 ++-- android/app/src/main/res/values/strings.xml | 2 +- docs/architecture.md | 10 +++++----- ios/Demo-iOS/Sources/ContentView.swift | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8fdcb40ce..f0f3021c1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,8 +87,8 @@ npm run dev:remote The web editor is built with React and WordPress packages: - **Entry Points**: - - `src/index.jsx` - Local editor entry - - `src/remote.jsx` - Remote editor entry (for plugin support) + - `src/index.js` - Bundled editor entry + - `src/remote.js` - Remote editor entry (supports plugins) - **Core Components**: - `src/components/editor/` - Main editor component with host bridge integration - `src/components/visual-editor/` - Visual editing interface diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 636511749..6df693958 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -4,7 +4,7 @@ GutenbergKit Demo Bundled editor - Offline editor with bundled assets + Local editor without plugin support Add remote editor diff --git a/docs/architecture.md b/docs/architecture.md index 33b2abc16..b4fbdd750 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,7 +19,7 @@ GutenbergKit/ │ │ └── text-editor/ # HTML text editing interface │ ├── utils/ # Utility functions │ │ └── bridge.js # Native-to-web communication -│ ├── index.jsx # Local editor entry point +│ ├── index.jsx # Bundled editor entry point │ └── remote.jsx # Remote editor entry point ├── ios/ # iOS Swift package │ └── Sources/ @@ -90,11 +90,11 @@ The `make build` command builds both the local and remote editors by default. To Additionally, a `make dev-server-remote` command is available for serving the latest remote editor changes through a development server. To load the development server in the Demo app, add an environment variable named `GUTENBERG_EDITOR_REMOTE_URL` with the URL of the development server plus `/remote.html`—i.e., `http://:5173/remote.html`. > [!TIP] -> The remote editor redirects to the local editor when loading fails. If you need to debug the failure, disable redirects via the `?dev_mode` query parameter.. +> The remote editor redirects to the bundled editor when loading fails. If you need to debug the failure, disable redirects via the `?dev_mode` query parameter.. -### Local Editor (`index.html`) +### Bundled Editor (`index.html`) -The local editor bundles all WordPress packages and runs entirely within the WebView. This variant: +The bundled editor relies upon local `@wordpress` packages. This variant: - Provides offline capability - Has faster initial load times @@ -105,7 +105,7 @@ The local editor bundles all WordPress packages and runs entirely within the Web ### Remote Editor (`remote.html`) -The remote editor loads WordPress packages and plugins from a remote server. This variant: +The remote editor loads `@wordpress` packages and plugins from a remote server. This variant: - Supports custom blocks and plugins - Requires network connectivity diff --git a/ios/Demo-iOS/Sources/ContentView.swift b/ios/Demo-iOS/Sources/ContentView.swift index 44171c201..70488d6d5 100644 --- a/ios/Demo-iOS/Sources/ContentView.swift +++ b/ios/Demo-iOS/Sources/ContentView.swift @@ -34,7 +34,7 @@ struct ContentView: View { .foregroundStyle(.secondary) } } footer: { - Text("Local editor with no plugins") + Text("Local editor without plugin support") } Section { From 408fc2d1203b26361bf7bf1a01eb326af58eee03 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 21 Oct 2025 10:04:48 -0400 Subject: [PATCH 8/9] docs: Update stale entry path --- docs/architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index b4fbdd750..0cb380815 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,8 +19,8 @@ GutenbergKit/ │ │ └── text-editor/ # HTML text editing interface │ ├── utils/ # Utility functions │ │ └── bridge.js # Native-to-web communication -│ ├── index.jsx # Bundled editor entry point -│ └── remote.jsx # Remote editor entry point +│ ├── index.js # Bundled editor entry point +│ └── remote.js # Remote editor entry point ├── ios/ # iOS Swift package │ └── Sources/ │ └── GutenbergKit/ From 0fd2f83ccedd7a048114346f24d7c60acc419fd1 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Tue, 21 Oct 2025 10:06:07 -0400 Subject: [PATCH 9/9] docs: Update path documentation --- docs/architecture.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 0cb380815..662343323 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -101,7 +101,7 @@ The bundled editor relies upon local `@wordpress` packages. This variant: - Limited to core blocks only - No plugin support -**Entry point:** `src/index.jsx` +**Entry point:** `src/index.js` ### Remote Editor (`remote.html`) @@ -111,7 +111,7 @@ The remote editor loads `@wordpress` packages and plugins from a remote server. - Requires network connectivity - Used in production environments with custom implementations -**Entry point:** `src/remote.jsx` +**Entry point:** `src/remote.js` ## Testing